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}!`;', '};']
}
]
}
};