@@ -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, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) ;
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