`; }); html += ''; } else { html += '

No symbols available

'; } document.getElementById('details-content').innerHTML = html; // Add breadcrumb click handler for module name const breadcrumbModule = document.getElementById('breadcrumb-module'); if (breadcrumbModule) { breadcrumbModule.addEventListener('click', function() { const moduleNode = MODULES_GRAPH.nodes.find(n => n.name === moduleName); if (moduleNode) { viewStack.pop(); showModuleDetails(moduleNode); } }); } // Bind focus-file button via data attribute (safe from quote injection) const focusFileBtn = document.querySelector('.focus-file-btn'); if (focusFileBtn) { focusFileBtn.addEventListener('click', function() { focusFileGraph(this.getAttribute('data-file-path')); }); } // Bind focus-func buttons via data attributes document.querySelectorAll('.focus-func-btn').forEach(btn => { btn.addEventListener('click', function(e) { e.stopPropagation(); focusFunctionGraph(this.getAttribute('data-func-key')); }); }); // Bind clickable symbol rows (function/method) for sidebar drill-down document.querySelectorAll('.symbol-row-clickable').forEach(row => { row.addEventListener('mouseenter', function() { this.style.background = '#e0f2fe'; this.style.borderLeftColor = '#0ea5e9'; }); row.addEventListener('mouseleave', function() { this.style.background = '#f9fafb'; this.style.borderLeftColor = ''; }); row.addEventListener('click', function(e) { // Don't trigger if the 🕸️ icon was clicked if (e.target.closest('.focus-func-btn')) return; const idx = parseInt(this.getAttribute('data-sig-index'), 10); if (isNaN(idx)) return; // Look up full metadata by index (handles duplicate names) const sig = fileData.signatures[idx]; if (!sig) return; const funcKey = `${fileData.path}:${sig.name}`; const funcDataObj = { id: funcKey, name: sig.name, file: fileData.path, module: (INDEX_DATA.files[fileData.path] || {}).module || 'unknown', kind: sig.kind || '', line: sig.line, desc: sig.desc || '' }; showFunctionDetails(funcDataObj, fileData, moduleName); }); }); } // Show function details function showFunctionDetails(funcData, parentFileData, parentModuleName) { // Push file onto viewStack when drilling down from file details if (parentFileData) { viewStack.push({ type: 'file', fileData: parentFileData, moduleName: parentModuleName }); } const funcKey = funcData.id; const funcDeps = INDEX_DATA.function_dependencies || {}; const myDeps = funcDeps[funcKey] || []; // Resolve module name for breadcrumb const rawModule = parentModuleName || funcData.module || 'unknown'; const displayModule = rawModule.replace('mod:', ''); // Build breadcrumb: Home › Module › File › Function (full) or Home › Module › Function (from graph) let breadcrumbHtml = `
🏠 Home ${displayModule} `; if (parentFileData) { breadcrumbHtml += ` ${parentFileData.name} `; } breadcrumbHtml += ` ${funcData.name}
`; let html = breadcrumbHtml; html += `

⚡ ${funcData.name}

`; // Kind badge const kind = funcData.kind || ''; if (kind) { const kindColor = {'function': '#8b5cf6', 'method': '#ec4899', 'class': '#3b82f6'}[kind] || '#6b7280'; html += `
${kind}
`; } // Description const desc = funcData.desc || ''; if (desc) { html += `

${desc}

`; } html += `
File
${funcData.file}
Module
${displayModule}
`; if (funcData.line) { html += `
Line
${funcData.line}
`; } // Show function calls if (myDeps.length > 0) { html += `

Calls (${myDeps.length})

`; myDeps.forEach(dep => { const targetParts = dep.target.split(':'); const targetFunc = targetParts[1] || dep.target; const targetFile = targetParts[0] || ''; html += `
→ ${targetFunc} (${dep.weight || 1}x)
${targetFile}
`; }); html += '
'; } // Find callers (functions that call this one) const callers = []; for (const [callerKey, deps] of Object.entries(funcDeps)) { for (const dep of deps) { if (dep.target === funcKey) { const callerParts = callerKey.split(':'); callers.push({ name: callerParts[1] || callerKey, file: callerParts[0] || '', weight: dep.weight || 1 }); } } } if (callers.length > 0) { html += `

Called By (${callers.length})

`; callers.forEach(caller => { html += `
← ${caller.name} (${caller.weight}x)
${caller.file}
`; }); html += '
'; } document.getElementById('details-content').innerHTML = html; // Bind breadcrumb click: Module → go back to module details const bcModule = document.getElementById('breadcrumb-func-module'); if (bcModule) { bcModule.addEventListener('click', function() { const modId = rawModule.startsWith('mod:') ? rawModule : 'mod:' + rawModule; const moduleNode = MODULES_GRAPH.nodes.find(n => n.id === modId || n.name === displayModule); if (moduleNode) { // Only pop if we pushed (sidebar drill-down path) if (parentFileData) { viewStack.pop(); // pop file entry pushed by showFunctionDetails viewStack.pop(); // pop module entry pushed by showFileDetails } showModuleDetails(moduleNode); } }); } // Bind breadcrumb click: File → go back to file details const bcFile = document.getElementById('breadcrumb-func-file'); if (bcFile && parentFileData) { bcFile.addEventListener('click', function() { // Pop the file entry pushed by showFunctionDetails viewStack.pop(); // Use skipPush to avoid re-pushing module onto viewStack showFileDetails(parentFileData, displayModule, { skipPush: true }); }); } // Bind focus graph button for this function const focusFuncDetailBtn = document.querySelector('.focus-func-detail-btn'); if (focusFuncDetailBtn) { focusFuncDetailBtn.addEventListener('click', function() { focusFunctionGraph(this.getAttribute('data-func-key')); }); } } // Focus on file's 1-hop dependency graph function focusFileGraph(centerFilePath) { // 1. Collect 1-hop neighbors const relatedNodes = new Set([centerFilePath]); const filteredLinks = []; FILES_GRAPH.links.forEach(link => { if (link.source === centerFilePath) { relatedNodes.add(link.target); filteredLinks.push({...link, direction: 'downstream'}); } if (link.target === centerFilePath) { relatedNodes.add(link.source); filteredLinks.push({...link, direction: 'upstream'}); } }); // 2. Filter nodes (max 20) let filteredNodes = FILES_GRAPH.nodes.filter(n => relatedNodes.has(n.id)); if (filteredNodes.length > 20) { const centerNode = filteredNodes.find(n => n.id === centerFilePath); const others = filteredNodes.filter(n => n.id !== centerFilePath) .sort((a, b) => (b.value || 0) - (a.value || 0)) .slice(0, 19); filteredNodes = [centerNode, ...others]; } // 3. Re-filter links after node truncation const keptNodeIds = new Set(filteredNodes.map(n => n.id)); const validLinks = filteredLinks.filter( link => keptNodeIds.has(link.source) && keptNodeIds.has(link.target) ); // 4. Handle empty neighbors if (filteredNodes.length <= 1) { const fileName = centerFilePath.split('/').pop(); mainChart.setOption({ title: { text: `Focus: ${fileName}`, subtext: 'No connected files found', left: 20, top: 10, textStyle: { fontSize: 16, color: '#333' } }, series: [{ type: 'graph', layout: 'force', data: filteredNodes.map(n => ({...n, symbolSize: 50, itemStyle: { color: '#ef4444', borderColor: '#fee2e2', borderWidth: 3 }})), links: [], roam: true, label: { show: true, fontSize: 11 } }] }, true); mainChart.off('click'); updateBreadcrumb(['Module Graph', `File: ${fileName}`]); showBackButton(); return; } // 5. Highlight center node const styledNodes = filteredNodes.map(n => ({ ...n, symbolSize: n.id === centerFilePath ? 50 : 20, itemStyle: n.id === centerFilePath ? { color: '#ef4444', borderColor: '#fee2e2', borderWidth: 3 } : n.itemStyle })); // 6. Style edges by direction const styledLinks = validLinks.map(link => ({ ...link, lineStyle: { width: link.direction === 'upstream' ? 2 : 1.5, type: link.direction === 'upstream' ? 'dashed' : 'solid', color: link.direction === 'upstream' ? '#ef4444' : '#3b82f6', opacity: 0.6 } })); // 5. Render graph const fileName = centerFilePath.split('/').pop(); const option = { title: { text: `Focus: ${fileName}`, subtext: `${filteredNodes.length - 1} connected files`, left: 20, top: 10, textStyle: { fontSize: 16, color: '#333' } }, tooltip: { backgroundColor: 'rgba(255, 255, 255, 0.9)', borderColor: '#E2E8F0', textStyle: { color: '#1E293B' }, padding: 12, formatter: function(params) { if (params.dataType === 'node') {