(d, fromHover = false)
| 278 | } |
| 279 | |
| 280 | export function showSidebarContent(d, fromHover = false) { |
| 281 | const sidebarContent = document.getElementById('sidebar-content'); |
| 282 | if (!sidebarContent) return; |
| 283 | if (fromHover && sidebarSticky) return; |
| 284 | if (!d) { |
| 285 | sidebarContent.innerHTML = ''; |
| 286 | return; |
| 287 | } |
| 288 | let starHtml = ''; |
| 289 | if (archiveProgramIds && archiveProgramIds.includes(d.id)) { |
| 290 | starHtml = '<span style="position:relative;top:0.05em;left:0.15em;font-size:1.6em;color:#FFD600;z-index:10;" title="MAP-elites member" aria-label="MAP-elites member">★</span>'; |
| 291 | } |
| 292 | let locatorBtn = '<button id="sidebar-locator-btn" title="Locate selected node" aria-label="Locate selected node" style="position:absolute;top:0.05em;right:2.5em;font-size:1.5em;background:none;border:none;color:#FFD600;cursor:pointer;z-index:10;line-height:1;filter:drop-shadow(0 0 2px #FFD600);">⦿</button>'; |
| 293 | let closeBtn = '<button id="sidebar-close-btn" style="position:absolute;top:0.05em;right:0.15em;font-size:1.6em;background:none;border:none;color:#888;cursor:pointer;z-index:10;line-height:1;">×</button>'; |
| 294 | let openLink = '<div style="text-align:center;margin:-1em 0 1.2em 0;"><a href="/program/' + d.id + '" target="_blank" class="open-in-new" style="font-size:0.95em;">[open in new window]</a></div>'; |
| 295 | let tabHtml = ''; |
| 296 | let tabContentHtml = ''; |
| 297 | let tabNames = []; |
| 298 | if (d.code && typeof d.code === 'string' && d.code.trim() !== '') tabNames.push('Code'); |
| 299 | if ((d.prompts && typeof d.prompts === 'object' && Object.keys(d.prompts).length > 0) || (d.artifacts_json && typeof d.artifacts_json === 'object' && Object.keys(d.artifacts_json).length > 0)) tabNames.push('Prompts'); |
| 300 | const children = allNodeData.filter(n => n.parent_id === d.id); |
| 301 | if (children.length > 0) tabNames.push('Children'); |
| 302 | |
| 303 | // Handle nodes with "-copyN" IDs |
| 304 | function getBaseId(id) { |
| 305 | return id.includes('-copy') ? id.split('-copy')[0] : id; |
| 306 | } |
| 307 | const baseId = getBaseId(d.id); |
| 308 | const clones = allNodeData.filter(n => getBaseId(n.id) === baseId && n.id !== d.id); |
| 309 | if (clones.length > 0) tabNames.push('Clones'); |
| 310 | |
| 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]); |
| 331 | } |
| 332 | } |
| 333 | // backtrack |
| 334 | let i = 0, j = 0; |
| 335 | const parts = []; |
| 336 | while (i < m && j < n) { |
| 337 | if (a[i] === b[j]) { |
no test coverage detected