Skip to content

Commit 9f70b35

Browse files
committed
add diff to parent node
1 parent 25884ed commit 9f70b35

File tree

1 file changed

+131
-72
lines changed

1 file changed

+131
-72
lines changed

scripts/static/js/sidebar.js

Lines changed: 131 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -308,84 +308,143 @@ export function showSidebarContent(d, fromHover = false) {
308308
const clones = allNodeData.filter(n => getBaseId(n.id) === baseId && n.id !== d.id);
309309
if (clones.length > 0) tabNames.push('Clones');
310310

311-
let activeTab = lastSidebarTab && tabNames.includes(lastSidebarTab) ? lastSidebarTab : tabNames[0];
312-
313-
// Helper to render tab content
314-
function renderSidebarTabContent(tabName, d, children) {
315-
if (tabName === 'Code') {
316-
return `<pre class="sidebar-code-pre">${escapeHtml(d.code)}</pre>`;
317-
}
318-
if (tabName === 'Prompts') {
319-
// Prompt select logic
320-
let promptOptions = [];
321-
let promptMap = {};
322-
if (d.prompts && typeof d.prompts === 'object') {
323-
for (const [k, v] of Object.entries(d.prompts)) {
324-
if (v && typeof v === 'object' && !Array.isArray(v)) {
325-
for (const [subKey, subVal] of Object.entries(v)) {
326-
const optLabel = `${k} - ${subKey}`;
327-
promptOptions.push(optLabel);
328-
promptMap[optLabel] = subVal;
329-
}
330-
} else {
331-
const optLabel = `${k}`;
332-
promptOptions.push(optLabel);
333-
promptMap[optLabel] = v;
334-
}
311+
// Add a Diff tab when a parent exists with code to compare against
312+
const parentNodeForDiff = d.parent_id && d.parent_id !== 'None' ? allNodeData.find(n => n.id == d.parent_id) : null;
313+
if (parentNodeForDiff && parentNodeForDiff.code && parentNodeForDiff.code.trim() !== '') {
314+
tabNames.push('Diff');
315+
}
316+
317+
let activeTab = lastSidebarTab && tabNames.includes(lastSidebarTab) ? lastSidebarTab : tabNames[0];
318+
319+
// Helper to render tab content
320+
// Simple line-level LCS diff renderer between two code strings
321+
function renderCodeDiff(aCode, bCode) {
322+
const a = (aCode || '').split('\n');
323+
const b = (bCode || '').split('\n');
324+
const m = a.length, n = b.length;
325+
// build LCS table
326+
const dp = Array.from({length: m+1}, () => new Array(n+1).fill(0));
327+
for (let ii = m-1; ii >= 0; --ii) {
328+
for (let jj = n-1; jj >= 0; --jj) {
329+
if (a[ii] === b[jj]) dp[ii][jj] = dp[ii+1][jj+1] + 1;
330+
else dp[ii][jj] = Math.max(dp[ii+1][jj], dp[ii][jj+1]);
335331
}
336332
}
337-
// Artifacts
338-
if (d.artifacts_json) {
339-
const optLabel = `artifacts`;
340-
promptOptions.push(optLabel);
341-
promptMap[optLabel] = d.artifacts_json;
342-
}
343-
// Get last selected prompt from localStorage, or default to first
344-
let lastPromptKey = localStorage.getItem('sidebarPromptSelect') || promptOptions[0] || '';
345-
if (!promptMap[lastPromptKey]) lastPromptKey = promptOptions[0] || '';
346-
// Build select box
347-
let selectHtml = '';
348-
if (promptOptions.length > 1) {
349-
selectHtml = `<select id="sidebar-prompt-select" style="margin-bottom:0.7em;max-width:100%;font-size:1em;">
350-
${promptOptions.map(opt => `<option value="${opt}"${opt===lastPromptKey?' selected':''}>${opt}</option>`).join('')}
351-
</select>`;
352-
}
353-
// Show only the selected prompt
354-
let promptVal = promptMap[lastPromptKey];
355-
let promptHtml = `<pre class="sidebar-pre">${promptVal ?? ''}</pre>`;
356-
return selectHtml + promptHtml;
357-
}
358-
if (tabName === 'Children') {
359-
const metric = (document.getElementById('metric-select') && document.getElementById('metric-select').value) || 'combined_score';
360-
let min = 0, max = 1;
361-
const vals = children.map(child => (child.metrics && typeof child.metrics[metric] === 'number') ? child.metrics[metric] : null).filter(x => x !== null);
362-
if (vals.length > 0) {
363-
min = Math.min(...vals);
364-
max = Math.max(...vals);
333+
// backtrack
334+
let i = 0, j = 0;
335+
const parts = [];
336+
while (i < m && j < n) {
337+
if (a[i] === b[j]) {
338+
parts.push({type: 'eq', line: a[i]});
339+
i++; j++;
340+
} else if (dp[i+1][j] >= dp[i][j+1]) {
341+
parts.push({type: 'del', line: a[i]});
342+
i++;
343+
} else {
344+
parts.push({type: 'ins', line: b[j]});
345+
j++;
346+
}
365347
}
366-
return `<div><ul style='margin:0.5em 0 0 1em;padding:0;'>` +
367-
children.map(child => {
368-
let val = (child.metrics && typeof child.metrics[metric] === 'number') ? child.metrics[metric].toFixed(4) : '(no value)';
369-
let bar = (child.metrics && typeof child.metrics[metric] === 'number') ? renderMetricBar(child.metrics[metric], min, max) : '';
370-
return `<li style='margin-bottom:0.3em;'><a href="#" class="child-link" data-child="${child.id}">${child.id}</a><br /><br /> <span style='margin-left:0.5em;'>${val}</span> ${bar}</li>`;
371-
}).join('') +
372-
`</ul></div>`;
348+
while (i < m) { parts.push({type: 'del', line: a[i++]}); }
349+
while (j < n) { parts.push({type: 'ins', line: b[j++]}); }
350+
351+
// Render HTML with inline styles
352+
const htmlLines = parts.map(function(p) {
353+
if (p.type === 'eq') return '<div style="white-space:pre-wrap;">' + escapeHtml(p.line) + '</div>';
354+
if (p.type === 'del') return '<div style="background:#fff0f0;color:#8b1a1a;padding:0.08em 0.3em;border-left:3px solid #f26;white-space:pre-wrap;">- ' + escapeHtml(p.line) + '</div>';
355+
return '<div style="background:#f2fff2;color:#116611;padding:0.08em 0.3em;border-left:3px solid #2a8;white-space:pre-wrap;">+ ' + escapeHtml(p.line) + '</div>';
356+
});
357+
return '<div style="font-family: \'Fira Mono\', monospace; font-size:0.95em; line-height:1.35;">' +
358+
'<div style="margin-bottom:0.4em;color:#666;">Showing diff between program and its parent (parent id: ' + (parentNodeForDiff ? parentNodeForDiff.id : 'N/A') + ')</div>' +
359+
htmlLines.join('') + '</div>';
373360
}
374-
if (tabName === 'Clones') {
375-
return `<div><ul style='margin:0.5em 0 0 1em;padding:0;'>` +
376-
clones.map(clone =>
377-
`<li style='margin-bottom:0.3em;'><a href="#" class="clone-link" data-clone="${clone.id}">${clone.id}</a></li>`
378-
).join('') +
379-
`</ul></div>`;
361+
362+
// small helper to escape HTML
363+
function escapeHtml(s) {
364+
return (s+'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
380365
}
381-
return '';
382-
}
383366

384-
if (tabNames.length > 0) {
385-
tabHtml = '<div id="sidebar-tab-bar" style="display:flex;gap:0.7em;margin-bottom:0.7em;">' +
386-
tabNames.map((name) => `<span class="sidebar-tab${name===activeTab?' active':''}" data-tab="${name}">${name}</span>`).join('') + '</div>';
387-
tabContentHtml = `<div id="sidebar-tab-content">${renderSidebarTabContent(activeTab, d, children)}</div>`;
388-
}
367+
function renderSidebarTabContent(tabName, d, children) {
368+
if (tabName === 'Code') {
369+
return `<pre class="sidebar-code-pre">${d.code}</pre>`;
370+
}
371+
if (tabName === 'Prompts') {
372+
// Prompt select logic
373+
let promptOptions = [];
374+
let promptMap = {};
375+
if (d.prompts && typeof d.prompts === 'object') {
376+
for (const [k, v] of Object.entries(d.prompts)) {
377+
if (v && typeof v === 'object' && !Array.isArray(v)) {
378+
for (const [subKey, subVal] of Object.entries(v)) {
379+
const optLabel = `${k} - ${subKey}`;
380+
promptOptions.push(optLabel);
381+
promptMap[optLabel] = subVal;
382+
}
383+
} else {
384+
const optLabel = `${k}`;
385+
promptOptions.push(optLabel);
386+
promptMap[optLabel] = v;
387+
}
388+
}
389+
}
390+
// Artifacts
391+
if (d.artifacts_json) {
392+
const optLabel = `artifacts`;
393+
promptOptions.push(optLabel);
394+
promptMap[optLabel] = d.artifacts_json;
395+
}
396+
// Get last selected prompt from localStorage, or default to first
397+
let lastPromptKey = localStorage.getItem('sidebarPromptSelect') || promptOptions[0] || '';
398+
if (!promptMap[lastPromptKey]) lastPromptKey = promptOptions[0] || '';
399+
// Build select box
400+
let selectHtml = '';
401+
if (promptOptions.length > 1) {
402+
selectHtml = `<select id="sidebar-prompt-select" style="margin-bottom:0.7em;max-width:100%;font-size:1em;">
403+
${promptOptions.map(opt => `<option value="${opt}"${opt===lastPromptKey?' selected':''}>${opt}</option>`).join('')}
404+
</select>`;
405+
}
406+
// Show only the selected prompt
407+
let promptVal = promptMap[lastPromptKey];
408+
let promptHtml = `<pre class="sidebar-pre">${promptVal ?? ''}</pre>`;
409+
return selectHtml + promptHtml;
410+
}
411+
if (tabName === 'Children') {
412+
const metric = (document.getElementById('metric-select') && document.getElementById('metric-select').value) || 'combined_score';
413+
let min = 0, max = 1;
414+
const vals = children.map(child => (child.metrics && typeof child.metrics[metric] === 'number') ? child.metrics[metric] : null).filter(x => x !== null);
415+
if (vals.length > 0) {
416+
min = Math.min(...vals);
417+
max = Math.max(...vals);
418+
}
419+
return `<div><ul style='margin:0.5em 0 0 1em;padding:0;'>` +
420+
children.map(child => {
421+
let val = (child.metrics && typeof child.metrics[metric] === 'number') ? child.metrics[metric].toFixed(4) : '(no value)';
422+
let bar = (child.metrics && typeof child.metrics[metric] === 'number') ? renderMetricBar(child.metrics[metric], min, max) : '';
423+
return `<li style='margin-bottom:0.3em;'><a href="#" class="child-link" data-child="${child.id}">${child.id}</a><br /><br /> <span style='margin-left:0.5em;'>${val}</span> ${bar}</li>`;
424+
}).join('') +
425+
`</ul></div>`;
426+
}
427+
if (tabName === 'Clones') {
428+
return `<div><ul style='margin:0.5em 0 0 1em;padding:0;'>` +
429+
clones.map(clone =>
430+
`<li style='margin-bottom:0.3em;'><a href="#" class="clone-link" data-clone="${clone.id}">${clone.id}</a></li>`
431+
).join('') +
432+
`</ul></div>`;
433+
}
434+
if (tabName === 'Diff') {
435+
const parentNode = parentNodeForDiff;
436+
const parentCode = parentNode ? parentNode.code || '' : '';
437+
const curCode = d.code || '';
438+
return renderCodeDiff(parentCode, curCode);
439+
}
440+
return '';
441+
}
442+
443+
if (tabNames.length > 0) {
444+
tabHtml = '<div id="sidebar-tab-bar" style="display:flex;gap:0.7em;margin-bottom:0.7em;">' +
445+
tabNames.map((name) => `<span class="sidebar-tab${name===activeTab?' active':''}" data-tab="${name}">${name}</span>`).join('') + '</div>';
446+
tabContentHtml = `<div id="sidebar-tab-content">${renderSidebarTabContent(activeTab, d, children)}</div>`;
447+
}
389448
let parentIslandHtml = '';
390449
if (d.parent_id && d.parent_id !== 'None') {
391450
const parent = allNodeData.find(n => n.id == d.parent_id);

0 commit comments

Comments
 (0)