AkurAI Build
Menu

Public repository / master

AkurAI-Framework / site/frontend/ai/components/artifacts/code_diff.js

Latest change 8847e6f4a315fb27eba8cb69b87397dd08a068eb - feat(ai/artifacts): add Chart, DataTable, CodeBlock, CodeDiff ES modules by Ólafur Búi Ólafsson

/**
 * Artifact: Code Diff
 * Payload (either form):
 *   { filename?: string, hunks: [{ old: string[], new: string[] }] }
 *   { filename?: string, diff: string }   — unified diff string
 *
 * Renders a red/green line-level diff. Lines are set via textContent — XSS-safe.
 */

/**
 * Parse a unified diff string into typed line objects.
 * Skips the file-header lines (--- / +++).
 */
function parseUnified(diff) {
  const lines = [];
  for (const raw of diff.split('\n')) {
    if (raw.startsWith('--- ') || raw.startsWith('+++ ')) continue;
    if (raw.startsWith('@@')) {
      lines.push({ type: 'hunk', text: raw });
    } else if (raw.startsWith('-')) {
      lines.push({ type: 'del', text: raw.slice(1) });
    } else if (raw.startsWith('+')) {
      lines.push({ type: 'add', text: raw.slice(1) });
    } else {
      // context line: leading space or bare (end-of-file marker)
      lines.push({ type: 'ctx', text: raw.startsWith(' ') ? raw.slice(1) : raw });
    }
  }
  return lines;
}

/**
 * Convert structured hunks ({old, new}) into typed line objects.
 * Outputs a separator before each hunk, then del lines then add lines.
 */
function parseHunks(hunks) {
  const lines = [];
  for (const hunk of hunks) {
    lines.push({ type: 'hunk', text: '' });
    for (const line of (hunk.old ?? [])) lines.push({ type: 'del', text: String(line) });
    for (const line of (hunk.new ?? [])) lines.push({ type: 'add', text: String(line) });
  }
  return lines;
}

const LINE_STYLES = {
  del:  { bg: 'color-mix(in srgb,#ff6b6b 14%,transparent)', sigColor: '#ff6b6b', sig: '−', textColor: '#ffb3b3' },
  add:  { bg: 'color-mix(in srgb,#3ddc84 12%,transparent)', sigColor: '#3ddc84', sig: '+', textColor: '#b3f0d0' },
  hunk: { bg: 'color-mix(in srgb,#5b8cff 10%,transparent)', sigColor: 'transparent', sig: '', textColor: '#5c647a' },
  ctx:  { bg: 'transparent',                                  sigColor: 'transparent', sig: '', textColor: '#8a93a8' }
};

export const def = {
  name: 'code_diff',
  category: 'Artifacts',
  title: 'Code Diff',

  match(p) {
    return p != null && (typeof p.diff === 'string' || Array.isArray(p.hunks));
  },

  render(payload) {
    const lines = typeof payload.diff === 'string'
      ? parseUnified(payload.diff)
      : parseHunks(payload.hunks ?? []);

    const wrap = document.createElement('div');
    wrap.style.cssText = 'border:1px solid var(--border);border-radius:12px;overflow:hidden;background:#0c0f16';

    /* ── Optional filename header ── */
    if (payload.filename) {
      const header = document.createElement('div');
      header.style.cssText = 'display:flex;align-items:center;padding:8px 14px;border-bottom:1px solid var(--border);background:var(--panel-2)';
      const fn = document.createElement('span');
      fn.style.cssText = 'font-family:var(--mono);font-size:12px;color:var(--muted)';
      fn.textContent = String(payload.filename);
      header.appendChild(fn);
      wrap.appendChild(header);
    }

    /* ── Diff body ── */
    const body = document.createElement('div');
    body.style.cssText = 'overflow-x:auto;font-family:var(--mono);font-size:13px;line-height:1.7';

    if (lines.length === 0) {
      const empty = document.createElement('div');
      empty.style.cssText = 'padding:16px;color:var(--dim)';
      empty.textContent = 'No changes.';
      body.appendChild(empty);
    }

    for (const line of lines) {
      const style = LINE_STYLES[line.type] ?? LINE_STYLES.ctx;

      const row = document.createElement('div');
      row.style.cssText = `display:flex;background:${style.bg};min-width:0`;

      /* Gutter sign (+/−) */
      const sig = document.createElement('span');
      sig.setAttribute('aria-hidden', 'true');
      sig.style.cssText = `width:2em;flex-shrink:0;text-align:center;user-select:none;color:${style.sigColor}`;
      sig.textContent = style.sig;
      row.appendChild(sig);

      /* Line text */
      const txt = document.createElement('span');
      txt.style.cssText = `flex:1;min-width:0;white-space:pre;padding-right:16px;color:${style.textColor}`;
      txt.textContent = line.type === 'hunk' && !line.text ? '─────' : line.text;
      if (line.type === 'hunk') txt.style.fontStyle = 'italic';
      row.appendChild(txt);

      body.appendChild(row);
    }

    wrap.appendChild(body);
    return wrap;
  },

  example: {
    filename: 'src/greet.js',
    hunks: [
      {
        old: ["function greet(name) {", "  return 'Hello ' + name;", '}'],
        new: ['const greet = (name) => {', '  return `Hello, ${name}!`;', '};']
      }
    ]
  }
};