<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Codemap - DeepWiki</title>
<style>
:root {
--bg-color: #0d1117;
--text-color: #c9d1d9;
--link-color: #58a6ff;
--border-color: #30363d;
--sidebar-bg: #161b22;
--code-bg: #1f2428;
--heading-color: #f0f6fc;
--muted-color: #8b949e;
--entry-color: #2d6a4f;
--crossfile-color: #1d3557;
--leaf-color: #6c757d;
--parent-bg: #161b22;
}
[data-theme="light"] {
--bg-color: #ffffff;
--text-color: #24292f;
--link-color: #0969da;
--border-color: #d0d7de;
--sidebar-bg: #f6f8fa;
--code-bg: #f6f8fa;
--heading-color: #1f2328;
--muted-color: #656d76;
--entry-color: #2d6a4f;
--crossfile-color: #1d3557;
--leaf-color: #6c757d;
--parent-bg: #f0f0f0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--border-color);
padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header h1 { font-size: 1.2em; color: var(--heading-color); }
.header a { color: var(--link-color); text-decoration: none; font-size: 0.9em; }
.header a:hover { text-decoration: underline; }
.header-controls {
display: flex;
align-items: center;
gap: 12px;
}
.theme-toggle, .export-btn {
background: transparent;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
color: var(--text-color);
transition: background 0.2s;
}
.theme-toggle:hover, .export-btn:hover { background: var(--border-color); }
.export-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Query bar */
.query-bar {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--border-color);
padding: 12px 20px;
}
.query-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.query-input {
flex: 1;
min-width: 200px;
padding: 8px 12px;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-color);
font-size: 14px;
outline: none;
}
.query-input:focus { border-color: var(--link-color); }
.query-select, .query-number {
padding: 8px 10px;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-color);
font-size: 13px;
}
.query-number { width: 70px; }
.query-label { font-size: 12px; color: var(--muted-color); }
.generate-btn {
padding: 8px 18px;
background: var(--link-color);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.generate-btn:hover:not(:disabled) { opacity: 0.9; }
.generate-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Topic chips */
.topics-row {
display: flex;
gap: 8px;
margin-top: 8px;
flex-wrap: wrap;
align-items: center;
}
.topics-label { font-size: 12px; color: var(--muted-color); }
.topic-chip {
padding: 4px 10px;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 12px;
font-size: 12px;
color: var(--text-color);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.topic-chip:hover {
border-color: var(--link-color);
background: var(--sidebar-bg);
}
/* Main content */
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
.graph-panel {
flex: 3;
position: relative;
border-right: 1px solid var(--border-color);
overflow: hidden;
}
#cy {
width: 100%;
height: 100%;
}
.graph-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--muted-color);
font-size: 1.1em;
text-align: center;
padding: 40px;
}
.graph-placeholder p { max-width: 400px; }
/* Progress overlay */
.progress-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--sidebar-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px 30px;
text-align: center;
z-index: 10;
display: none;
}
.progress-overlay.active { display: block; }
.progress-spinner {
width: 24px;
height: 24px;
border: 3px solid var(--border-color);
border-top-color: var(--link-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Node tooltip */
.node-tooltip {
position: absolute;
background: var(--sidebar-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px 14px;
font-size: 12px;
max-width: 320px;
max-height: 280px;
overflow-y: auto;
z-index: 20;
pointer-events: auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: none;
line-height: 1.5;
}
.node-tooltip.visible { display: block; }
.tooltip-name {
font-weight: 600;
color: var(--heading-color);
margin-bottom: 4px;
word-break: break-all;
}
.tooltip-meta {
color: var(--muted-color);
font-size: 11px;
margin-bottom: 4px;
}
.tooltip-docstring {
color: var(--text-color);
font-size: 11px;
border-top: 1px solid var(--border-color);
padding-top: 6px;
margin-top: 4px;
white-space: pre-wrap;
max-height: 120px;
overflow-y: auto;
}
/* Zoom controls */
.zoom-controls {
position: absolute;
bottom: 16px;
right: 16px;
display: flex;
flex-direction: column;
gap: 4px;
z-index: 15;
}
.zoom-btn {
width: 32px;
height: 32px;
background: var(--sidebar-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-color);
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
line-height: 1;
}
.zoom-btn:hover { background: var(--border-color); }
.zoom-btn.fit-btn { font-size: 12px; }
/* Keyboard shortcuts hint */
.shortcuts-hint {
position: absolute;
bottom: 16px;
left: 16px;
font-size: 10px;
color: var(--muted-color);
z-index: 15;
opacity: 0.7;
}
.shortcuts-hint kbd {
background: var(--sidebar-bg);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 1px 4px;
font-family: inherit;
font-size: 10px;
}
/* Narrative panel */
.narrative-panel {
flex: 2;
overflow-y: auto;
padding: 20px;
}
.narrative-panel h3 {
color: var(--heading-color);
margin-bottom: 12px;
font-size: 1em;
}
.narrative-content {
font-size: 0.9em;
line-height: 1.7;
}
.narrative-content code {
background: var(--code-bg);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
}
.narrative-content pre {
background: var(--code-bg);
padding: 12px;
border-radius: 8px;
overflow-x: auto;
margin: 8px 0;
}
.narrative-content a { color: var(--link-color); }
/* Stats */
.stats-bar {
display: flex;
gap: 16px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
flex-wrap: wrap;
}
.stat { font-size: 0.85em; }
.stat-value { font-weight: 600; color: var(--heading-color); }
.stat-label { color: var(--muted-color); }
/* Legend */
.legend {
margin-top: 20px;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.legend h4 {
font-size: 0.85em;
color: var(--muted-color);
margin-bottom: 8px;
cursor: pointer;
user-select: none;
}
.legend h4:hover { color: var(--text-color); }
.legend.collapsed .legend-items { display: none; }
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8em;
margin-bottom: 4px;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.legend-line {
width: 20px;
height: 2px;
flex-shrink: 0;
}
/* Play controls (animated trace) */
.play-controls {
position: absolute;
top: 16px;
left: 16px;
display: none;
gap: 4px;
z-index: 15;
background: var(--sidebar-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 4px;
}
.play-controls.visible { display: flex; }
.play-btn {
width: 28px;
height: 28px;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-color);
font-size: 13px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.play-btn:hover { background: var(--border-color); }
.play-btn.active { background: var(--link-color); color: white; border-color: var(--link-color); }
.play-step-label {
font-size: 11px;
color: var(--muted-color);
padding: 0 6px;
align-self: center;
white-space: nowrap;
}
/* Animated trace: pulsing node */
@keyframes trace-pulse {
0%, 100% { border-width: 3px; }
50% { border-width: 5px; }
}
/* Diff overlay toggle */
.diff-toggle {
display: flex;
align-items: center;
gap: 6px;
}
.diff-toggle label {
font-size: 12px;
color: var(--muted-color);
cursor: pointer;
}
.diff-toggle input[type="checkbox"] { cursor: pointer; }
/* Diff overlay: changed node style */
/* (Cytoscape styles added dynamically) */
/* Minimap */
.cy-minimap {
position: absolute;
bottom: 56px;
right: 16px;
width: 150px;
height: 120px;
background: var(--sidebar-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
z-index: 14;
display: none;
}
.cy-minimap.visible { display: block; }
/* Recent codemaps */
.recent-codemaps {
margin-top: 8px;
display: none;
}
.recent-codemaps.visible { display: block; }
.recent-label { font-size: 12px; color: var(--muted-color); }
.recent-item {
display: inline-block;
padding: 3px 8px;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 10px;
font-size: 11px;
color: var(--text-color);
cursor: pointer;
margin: 2px;
text-decoration: none;
}
.recent-item:hover { border-color: var(--link-color); }
/* Mermaid fallback */
.mermaid-fallback {
padding: 20px;
overflow: auto;
display: none;
}
/* Right-click context menu */
.ctx-menu {
display: none;
position: absolute;
z-index: 100;
background: var(--sidebar-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
min-width: 180px;
overflow: hidden;
}
.ctx-menu.visible { display: block; }
.ctx-menu-item {
display: block;
padding: 8px 14px;
font-size: 0.85em;
color: var(--text-color);
text-decoration: none;
cursor: pointer;
border: none;
background: none;
width: 100%;
text-align: left;
}
.ctx-menu-item:hover {
background: var(--border-color);
color: var(--link-color);
}
.ctx-menu-sep {
height: 1px;
background: var(--border-color);
margin: 2px 0;
}
/* Inline code preview in narrative panel */
.code-preview-section {
margin-top: 16px;
border-top: 1px solid var(--border-color);
padding-top: 12px;
}
.code-preview-section h4 {
color: var(--heading-color);
font-size: 0.9em;
margin-bottom: 8px;
}
.code-preview-section pre {
background: var(--code-bg);
padding: 12px;
border-radius: 6px;
overflow-x: auto;
font-size: 0.82em;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
}
.code-preview-section pre code {
background: none;
padding: 0;
}
.code-preview-meta {
font-size: 0.78em;
color: var(--muted-color);
margin-bottom: 6px;
}
/* Active narrative step highlight during animated trace */
.narrative-content li.trace-step-active {
background: color-mix(in srgb, var(--link-color) 12%, transparent);
border-left: 3px solid var(--link-color);
padding-left: 8px;
margin-left: -11px;
border-radius: 4px;
transition: background 0.3s;
}
/* Welcome state */
.narrative-welcome {
color: var(--muted-color);
text-align: center;
padding: 40px 20px;
}
.narrative-welcome h3 { color: var(--heading-color); margin-bottom: 8px; }
@media (max-width: 900px) {
.main-content { flex-direction: column; }
.graph-panel { border-right: none; border-bottom: 1px solid var(--border-color); min-height: 300px; }
.narrative-panel { flex: 1; }
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css" crossorigin="anonymous" id="hljs-theme">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3/dist/cytoscape.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/dagre@0.8/dist/dagre.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2/cytoscape-dagre.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@14/marked.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-navigator@2/cytoscape-navigator.min.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cytoscape-navigator@2/cytoscape.js-navigator.css" crossorigin="anonymous">
</head>
<body>
<header class="header">
<div class="header-left">
<h1>DeepWiki Codemap</h1>
<a href="/">Back to Wiki</a>
<a href="/chat">Chat</a>
</div>
<div class="header-controls">
<div class="diff-toggle">
<input type="checkbox" id="diff-toggle-cb" title="Show git changes overlay">
<label for="diff-toggle-cb">Show Changes</label>
</div>
<a href="/codemap/compare" class="export-btn" title="Compare two codemaps side by side" style="text-decoration:none;">Compare</a>
<button id="export-png-btn" class="export-btn" title="Export graph as PNG" disabled>Export PNG</button>
<button id="theme-toggle" class="theme-toggle" title="Toggle theme">🌙</button>
</div>
</header>
<div class="query-bar">
<div class="query-row">
<input type="text" id="query-input" class="query-input"
placeholder="How does indexing work? / Trace the request handling pipeline...">
<label class="query-label">Focus</label>
<select id="focus-select" class="query-select">
<option value="execution_flow">Execution Flow</option>
<option value="data_flow">Data Flow</option>
<option value="dependency_chain">Dependency Chain</option>
</select>
<label class="query-label">Depth</label>
<input type="number" id="depth-input" class="query-number" value="5" min="1" max="10">
<label class="query-label">Nodes</label>
<input type="number" id="nodes-input" class="query-number" value="30" min="5" max="60">
<button id="generate-btn" class="generate-btn">Generate</button>
</div>
<div class="topics-row" id="topics-row">
<span class="topics-label">Suggested:</span>
</div>
<div class="recent-codemaps" id="recent-codemaps">
<span class="recent-label">Recent:</span>
</div>
</div>
<div class="main-content">
<div class="graph-panel" id="graph-panel">
<div class="graph-placeholder" id="graph-placeholder">
<p>Enter a query above or click a suggested topic to generate an interactive codemap.</p>
</div>
<div id="cy" style="display:none;"></div>
<div class="mermaid-fallback" id="mermaid-fallback"></div>
<div class="progress-overlay" id="progress-overlay">
<div class="progress-spinner"></div>
<div id="progress-message">Generating codemap...</div>
</div>
<div class="node-tooltip" id="node-tooltip"></div>
<div class="ctx-menu" id="ctx-menu">
<a class="ctx-menu-item" id="ctx-wiki">View in Wiki</a>
<a class="ctx-menu-item" id="ctx-chat">Ask about this</a>
<div class="ctx-menu-sep"></div>
<button class="ctx-menu-item" id="ctx-copy">Copy name</button>
</div>
<div class="play-controls" id="play-controls">
<button class="play-btn" id="play-btn" title="Walk through the execution trace step-by-step, highlighting each node in the graph and scrolling the narrative panel to the matching step">▶</button>
<button class="play-btn" id="pause-btn" title="Pause the animated trace at the current step">❚❚</button>
<button class="play-btn" id="step-btn" title="Advance one step at a time — click repeatedly to step through at your own pace">▷|</button>
<button class="play-btn" id="stop-trace-btn" title="Stop the trace and reset all highlighting">■</button>
<span class="play-step-label" id="play-step-label">Step 0/0</span>
</div>
<div class="cy-minimap" id="cy-minimap"></div>
<div class="zoom-controls" id="zoom-controls" style="display:none;">
<button class="zoom-btn" id="zoom-in-btn" title="Zoom in (+)">+</button>
<button class="zoom-btn" id="zoom-out-btn" title="Zoom out (-)">-</button>
<button class="zoom-btn fit-btn" id="fit-btn" title="Fit to viewport (F)">Fit</button>
</div>
<div class="shortcuts-hint" id="shortcuts-hint" style="display:none;">
<kbd>F</kbd> fit <kbd>+</kbd><kbd>-</kbd> zoom <kbd>Esc</kbd> reset <kbd>L</kbd> legend <kbd>P</kbd> play
</div>
</div>
<div class="narrative-panel" id="narrative-panel">
<div class="narrative-welcome">
<h3>Narrative Trace</h3>
<p>The step-by-step execution trace will appear here after generating a codemap.</p>
</div>
</div>
</div>
<script>
// --- State ---
var cy = null;
var isGenerating = false;
var currentFocus = 'execution_flow';
var currentEntryPoint = null;
var pathHighlightActive = false;
var currentResult = null;
// Animated trace state
var traceNodes = [];
var traceStep = -1;
var traceTimer = null;
var tracePlaying = false;
// Diff overlay state
var diffFiles = [];
var diffOverlayActive = false;
// --- DOM ---
var queryInput = document.getElementById('query-input');
var focusSelect = document.getElementById('focus-select');
var depthInput = document.getElementById('depth-input');
var nodesInput = document.getElementById('nodes-input');
var generateBtn = document.getElementById('generate-btn');
var topicsRow = document.getElementById('topics-row');
var graphPlaceholder = document.getElementById('graph-placeholder');
var cyContainer = document.getElementById('cy');
var mermaidFallback = document.getElementById('mermaid-fallback');
var progressOverlay = document.getElementById('progress-overlay');
var progressMessage = document.getElementById('progress-message');
var narrativePanel = document.getElementById('narrative-panel');
var themeToggle = document.getElementById('theme-toggle');
var exportPngBtn = document.getElementById('export-png-btn');
var nodeTooltip = document.getElementById('node-tooltip');
var zoomControls = document.getElementById('zoom-controls');
var shortcutsHint = document.getElementById('shortcuts-hint');
var playControls = document.getElementById('play-controls');
var playBtn = document.getElementById('play-btn');
var pauseBtn = document.getElementById('pause-btn');
var stepBtn = document.getElementById('step-btn');
var stopTraceBtn = document.getElementById('stop-trace-btn');
var playStepLabel = document.getElementById('play-step-label');
var diffToggleCb = document.getElementById('diff-toggle-cb');
var cyMinimap = document.getElementById('cy-minimap');
var recentCodemaps = document.getElementById('recent-codemaps');
// --- Cytoscape Styles ---
var cytoscapeStyles = [
{ selector: 'node', style: {
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'font-size': '11px',
'color': '#c9d1d9',
'text-wrap': 'ellipsis',
'text-max-width': '120px',
'width': 'label',
'height': 30,
'padding': '8px',
'shape': 'round-rectangle',
'background-color': '#21262d',
'border-width': 1,
'border-color': '#30363d',
'transition-property': 'opacity, border-color, border-width',
'transition-duration': '0.2s',
}},
{ selector: 'node.entry', style: {
'background-color': '#2d6a4f',
'border-color': '#40916c',
'color': '#ffffff',
'font-weight': 'bold',
}},
{ selector: 'node.crossfile', style: {
'background-color': '#1d3557',
'border-color': '#457b9d',
'color': '#ffffff',
}},
{ selector: 'node.leaf', style: {
'background-color': '#6c757d',
'border-color': '#8b949e',
'color': '#ffffff',
}},
{ selector: ':parent', style: {
'background-color': '#161b22',
'background-opacity': 0.6,
'border-color': '#30363d',
'border-width': 1,
'text-valign': 'top',
'text-halign': 'center',
'font-size': '10px',
'color': '#8b949e',
'padding': '12px',
'shape': 'round-rectangle',
}},
{ selector: 'edge', style: {
'width': 1.5,
'line-color': '#8b949e',
'target-arrow-color': '#8b949e',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'arrow-scale': 0.8,
'transition-property': 'opacity, line-color, width',
'transition-duration': '0.2s',
}},
{ selector: 'edge.cross-file-edge', style: {
'line-color': '#58a6ff',
'target-arrow-color': '#58a6ff',
'line-style': 'dashed',
'width': 2,
}},
{ selector: 'edge[label]', style: {
'label': 'data(label)',
'font-size': '9px',
'color': '#8b949e',
'text-rotation': 'autorotate',
'text-margin-y': -8,
'text-background-color': '#0d1117',
'text-background-opacity': 0.8,
'text-background-padding': '2px',
}},
{ selector: 'node:selected', style: {
'border-color': '#f0f6fc',
'border-width': 2,
'overlay-opacity': 0.1,
}},
// Path highlighting: dimmed elements
{ selector: 'node.dimmed', style: {
'opacity': 0.25,
}},
{ selector: 'edge.dimmed', style: {
'opacity': 0.15,
}},
// Path highlighting: highlighted elements
{ selector: 'node.highlighted', style: {
'border-color': '#f0f6fc',
'border-width': 3,
'z-index': 10,
}},
{ selector: 'edge.highlighted', style: {
'width': 3,
'line-color': '#f0883e',
'target-arrow-color': '#f0883e',
'z-index': 10,
}},
// Diff overlay: changed file nodes (with glow via overlay)
{ selector: 'node.diff-changed', style: {
'border-color': '#f85149',
'border-width': 3,
'z-index': 11,
'overlay-color': '#f85149',
'overlay-opacity': 0.15,
'overlay-padding': 8,
}},
{ selector: ':parent.diff-changed', style: {
'border-color': '#f85149',
'border-width': 2,
'background-opacity': 0.8,
'overlay-color': '#f85149',
'overlay-opacity': 0.1,
'overlay-padding': 6,
}},
// Animated trace: current step
{ selector: 'node.trace-active', style: {
'border-color': '#f0883e',
'border-width': 4,
'z-index': 20,
}},
{ selector: 'node.trace-visited', style: {
'border-color': '#3fb950',
'border-width': 2,
}},
// Active edge during animated trace (current step transition)
{ selector: 'edge.trace-bridge', style: {
'line-color': '#f0883e',
'target-arrow-color': '#f0883e',
'width': 4,
'line-style': 'solid',
'z-index': 20,
'overlay-color': '#f0883e',
'overlay-opacity': 0.2,
'overlay-padding': 4,
}},
// Previously traversed edge (visited path)
{ selector: 'edge.trace-visited-edge', style: {
'line-color': '#3fb950',
'target-arrow-color': '#3fb950',
'width': 2.5,
'line-style': 'solid',
'z-index': 5,
}},
];
// --- Build Cytoscape Elements ---
function buildCytoscapeElements(result) {
var elements = [];
var nodeIds = {};
var sourceNodes = {};
var targetNodes = {};
// Classify nodes by edge participation
for (var i = 0; i < result.edges.length; i++) {
var edge = result.edges[i];
sourceNodes[edge.source] = true;
targetNodes[edge.target] = true;
}
// Create compound parent nodes per file
var fileParents = {};
for (var i = 0; i < result.nodes.length; i++) {
var node = result.nodes[i];
var fileId = 'file:' + node.file_path;
if (!fileParents[fileId]) {
fileParents[fileId] = true;
var parts = node.file_path.split('/');
var shortPath = parts.slice(-2).join('/');
elements.push({
group: 'nodes',
data: { id: fileId, label: shortPath },
});
}
}
// Create child nodes
for (var i = 0; i < result.nodes.length; i++) {
var node = result.nodes[i];
var fileId = 'file:' + node.file_path;
var qn = node.qualified_name;
// Classify: entry / crossfile / leaf / internal
var nodeClass = 'internal';
var isSource = sourceNodes[qn];
var isTarget = targetNodes[qn];
if (qn === result.entry_point || (isSource && !isTarget)) {
nodeClass = 'entry';
} else if (!isSource && isTarget) {
nodeClass = 'leaf';
}
// Check cross-file participation
var hasCrossFile = false;
for (var j = 0; j < result.edges.length; j++) {
var e = result.edges[j];
if ((e.source === qn || e.target === qn) && e.source_file !== e.target_file) {
hasCrossFile = true;
break;
}
}
if (hasCrossFile && nodeClass !== 'entry') {
nodeClass = 'crossfile';
}
elements.push({
group: 'nodes',
data: {
id: qn,
label: node.name,
parent: fileId,
filePath: node.file_path,
startLine: node.start_line,
endLine: node.end_line,
chunkType: node.chunk_type,
qualifiedName: qn,
docstring: node.docstring || '',
contentPreview: node.content_preview || '',
},
classes: nodeClass,
});
nodeIds[qn] = true;
}
// Create edges
var showEdgeLabels = (currentFocus === 'data_flow');
for (var i = 0; i < result.edges.length; i++) {
var edge = result.edges[i];
if (nodeIds[edge.source] && nodeIds[edge.target]) {
var isCrossFile = edge.source_file !== edge.target_file;
var edgeData = {
source: edge.source,
target: edge.target,
edgeType: edge.edge_type,
};
// Show edge labels in data_flow mode
if (showEdgeLabels && edge.edge_type) {
edgeData.label = edge.edge_type;
}
elements.push({
group: 'edges',
data: edgeData,
classes: isCrossFile ? 'cross-file-edge' : 'same-file-edge',
});
}
}
return elements;
}
// --- Tooltip ---
function showTooltip(node, renderedPos) {
var name = node.data('qualifiedName') || node.data('label');
var chunkType = node.data('chunkType') || '';
var filePath = node.data('filePath') || '';
var startLine = node.data('startLine');
var endLine = node.data('endLine');
var docstring = node.data('docstring') || '';
var html = '<div class="tooltip-name">' + escapeHtml(name) + '</div>';
html += '<div class="tooltip-meta">';
html += escapeHtml(chunkType);
if (filePath) {
html += ' · ' + escapeHtml(filePath);
}
if (startLine && endLine) {
html += ':' + startLine + '-' + endLine;
}
html += '</div>';
if (docstring) {
var truncated = docstring.length > 200 ? docstring.substring(0, 200) + '...' : docstring;
html += '<div class="tooltip-docstring">' + escapeHtml(truncated) + '</div>';
}
// Content is built from escapeHtml()-sanitized values above
nodeTooltip.innerHTML = html;
nodeTooltip.classList.add('visible');
// Position relative to graph-panel, measuring actual tooltip size
var panelRect = document.getElementById('graph-panel').getBoundingClientRect();
var tipRect = nodeTooltip.getBoundingClientRect();
var x = renderedPos.x + 15;
var y = renderedPos.y - 10;
// Keep tooltip within panel bounds using actual dimensions
if (x + tipRect.width > panelRect.width) {
x = renderedPos.x - tipRect.width - 15;
}
if (x < 0) { x = 10; }
if (y + tipRect.height > panelRect.height) {
y = panelRect.height - tipRect.height - 10;
}
if (y < 0) { y = 10; }
nodeTooltip.style.left = x + 'px';
nodeTooltip.style.top = y + 'px';
}
function hideTooltip() {
nodeTooltip.classList.remove('visible');
}
// --- Context Menu ---
var ctxMenu = document.getElementById('ctx-menu');
var ctxWiki = document.getElementById('ctx-wiki');
var ctxChat = document.getElementById('ctx-chat');
var ctxCopy = document.getElementById('ctx-copy');
var ctxNode = null;
function showContextMenu(node, mouseEvent) {
ctxNode = node;
var filePath = node.data('filePath') || '';
var name = node.data('qualifiedName') || node.data('label') || '';
// Set up links
if (filePath) {
var wikiPath = '/wiki/files/' + filePath.replace(/\.[^/.]+$/, '.md');
ctxWiki.href = wikiPath;
ctxWiki.style.display = 'block';
} else {
ctxWiki.style.display = 'none';
}
ctxChat.href = '/chat?q=' + encodeURIComponent('Explain ' + name);
// Position menu at mouse location relative to graph-panel
var panelRect = document.getElementById('graph-panel').getBoundingClientRect();
var x = mouseEvent.clientX - panelRect.left;
var y = mouseEvent.clientY - panelRect.top;
// Keep within bounds
if (x + 200 > panelRect.width) x = panelRect.width - 210;
if (y + 120 > panelRect.height) y = panelRect.height - 130;
ctxMenu.style.left = x + 'px';
ctxMenu.style.top = y + 'px';
ctxMenu.classList.add('visible');
}
function hideContextMenu() {
ctxMenu.classList.remove('visible');
ctxNode = null;
}
ctxCopy.addEventListener('click', function() {
if (ctxNode) {
var name = ctxNode.data('qualifiedName') || ctxNode.data('label') || '';
navigator.clipboard.writeText(name);
}
hideContextMenu();
});
// Hide tooltip when mouse leaves it
nodeTooltip.addEventListener('mouseleave', hideTooltip);
// Close context menu on Escape or outside click
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') hideContextMenu();
});
document.addEventListener('click', function(e) {
if (!ctxMenu.contains(e.target)) hideContextMenu();
});
// --- Inline Code Preview ---
function showCodePreview(node) {
var preview = node.data('contentPreview') || '';
var name = node.data('qualifiedName') || node.data('label') || '';
var filePath = node.data('filePath') || '';
var startLine = node.data('startLine');
var endLine = node.data('endLine');
var language = node.data('language') || '';
if (!preview) return;
// Remove existing preview section
var existing = narrativePanel.querySelector('.code-preview-section');
if (existing) existing.remove();
var section = document.createElement('div');
section.className = 'code-preview-section';
var heading = document.createElement('h4');
heading.textContent = name;
section.appendChild(heading);
if (filePath) {
var meta = document.createElement('div');
meta.className = 'code-preview-meta';
meta.textContent = filePath + (startLine ? ':' + startLine + '-' + endLine : '');
section.appendChild(meta);
}
var pre = document.createElement('pre');
var code = document.createElement('code');
if (language) code.className = 'language-' + language;
code.textContent = preview;
pre.appendChild(code);
section.appendChild(pre);
narrativePanel.appendChild(section);
// Syntax highlight if hljs available
if (typeof hljs !== 'undefined') {
hljs.highlightElement(code);
}
}
// --- Path Highlighting (BFS) ---
function findPathFromEntry(targetId) {
if (!cy || !currentEntryPoint) return null;
// BFS from entry to target
var queue = [[currentEntryPoint]];
var visited = {};
visited[currentEntryPoint] = true;
while (queue.length > 0) {
var path = queue.shift();
var current = path[path.length - 1];
if (current === targetId) {
return path;
}
var outEdges = cy.getElementById(current).outgoers('edge');
outEdges.forEach(function(edge) {
var nextId = edge.target().id();
if (!visited[nextId] && !edge.target().isParent()) {
visited[nextId] = true;
queue.push(path.concat([nextId]));
}
});
}
return null;
}
function highlightPath(targetId) {
if (!cy) return;
var path = findPathFromEntry(targetId);
if (!path || path.length < 2) {
// No path found, just highlight the clicked node
clearHighlighting();
return;
}
pathHighlightActive = true;
// Dim everything
cy.elements().addClass('dimmed');
// Build set of path nodes and edges
for (var i = 0; i < path.length; i++) {
var nodeEle = cy.getElementById(path[i]);
nodeEle.removeClass('dimmed').addClass('highlighted');
// Also un-dim the parent compound node
var parent = nodeEle.parent();
if (parent.length > 0) {
parent.removeClass('dimmed');
}
}
// Highlight edges between consecutive path nodes
for (var i = 0; i < path.length - 1; i++) {
var edges = cy.getElementById(path[i]).edgesTo(cy.getElementById(path[i + 1]));
edges.removeClass('dimmed').addClass('highlighted');
}
}
function clearHighlighting() {
if (!cy) return;
pathHighlightActive = false;
cy.elements().removeClass('dimmed highlighted');
}
// --- Animated Execution Trace ---
function buildTraceSequence() {
// DFS traversal from entry point so animation follows graph edges
if (!currentResult || !currentResult.nodes || !cy || !currentEntryPoint) return [];
var visited = {};
var ordered = [];
function dfs(nodeId) {
if (visited[nodeId]) return;
var node = cy.getElementById(nodeId);
if (node.length === 0 || node.isParent()) return;
visited[nodeId] = true;
ordered.push(nodeId);
// Follow outgoing edges (caller → callee)
var outEdges = node.outgoers('edge');
outEdges.forEach(function(edge) {
dfs(edge.target().id());
});
}
dfs(currentEntryPoint);
// Add any remaining nodes not reachable from entry (disconnected)
var allNodes = currentResult.nodes;
for (var i = 0; i < allNodes.length; i++) {
var qn = allNodes[i].qualified_name;
if (!visited[qn]) {
var node = cy.getElementById(qn);
if (node.length > 0 && !node.isParent()) {
ordered.push(qn);
}
}
}
return ordered;
}
function startTrace() {
traceNodes = buildTraceSequence();
if (traceNodes.length === 0) return;
tracePlaying = true;
traceStep = -1;
clearHighlighting();
cy.elements().removeClass('trace-active trace-visited trace-bridge');
playStepLabel.textContent = 'Step 0/' + traceNodes.length;
playBtn.classList.add('active');
stepTrace();
scheduleNextStep();
}
function scheduleNextStep() {
if (!tracePlaying || !cy || traceStep >= traceNodes.length - 1) return;
// Check if next step crosses a file boundary
var currentNodeId = traceNodes[traceStep];
var nextNodeId = traceNodes[traceStep + 1];
var currentFile = cy.getElementById(currentNodeId).data('filePath') || '';
var nextFile = cy.getElementById(nextNodeId).data('filePath') || '';
var isCrossFile = currentFile !== nextFile && currentFile !== '' && nextFile !== '';
// Pause longer on cross-file transitions for visual bridge effect
var delay = isCrossFile ? 2400 : 1200;
traceTimer = setTimeout(function() {
stepTrace();
scheduleNextStep();
}, delay);
}
function stepTrace() {
if (!cy || traceNodes.length === 0) return;
// Downgrade active bridge edges to visited
cy.edges('.trace-bridge').removeClass('trace-bridge').addClass('trace-visited-edge');
// Un-highlight previous
var prevStep = traceStep;
if (traceStep >= 0 && traceStep < traceNodes.length) {
var prev = cy.getElementById(traceNodes[traceStep]);
prev.removeClass('trace-active').addClass('trace-visited');
}
traceStep++;
if (traceStep >= traceNodes.length) {
stopTrace();
return;
}
var nodeId = traceNodes[traceStep];
var node = cy.getElementById(nodeId);
node.addClass('trace-active');
// Highlight the edge connecting any visited node to the current node
if (traceStep > 0) {
var incomingEdges = node.incomers('edge');
incomingEdges.forEach(function(edge) {
var sourceNode = edge.source();
if (sourceNode.hasClass('trace-visited') || sourceNode.hasClass('trace-active')) {
edge.addClass('trace-bridge');
}
});
// If no incoming from visited, try outgoing (reverse edges)
if (incomingEdges.filter('.trace-bridge').length === 0) {
var outgoingEdges = node.outgoers('edge');
outgoingEdges.forEach(function(edge) {
var targetNode = edge.target();
if (targetNode.hasClass('trace-visited') || targetNode.hasClass('trace-active')) {
edge.addClass('trace-bridge');
}
});
}
}
// Pan to the node
cy.animate({ center: { eles: node }, duration: 300 });
playStepLabel.textContent = 'Step ' + (traceStep + 1) + '/' + traceNodes.length;
// Sync narrative panel: highlight and scroll to the matching step
syncNarrativeStep(traceStep);
}
function pauseTrace() {
if (traceTimer) clearTimeout(traceTimer);
traceTimer = null;
tracePlaying = false;
playBtn.classList.remove('active');
}
function stopTrace() {
pauseTrace();
traceStep = -1;
if (cy) cy.elements().removeClass('trace-active trace-visited trace-bridge trace-visited-edge');
playStepLabel.textContent = 'Step 0/' + traceNodes.length;
clearNarrativeHighlight();
}
function syncNarrativeStep(stepIndex) {
// Find list items in the narrative that correspond to execution steps
var items = narrativePanel.querySelectorAll('.narrative-content li');
if (items.length === 0) return;
// Clear previous highlight
items.forEach(function(li) { li.classList.remove('trace-step-active'); });
// Highlight the matching step (narrative steps map 1:1 to trace nodes)
if (stepIndex >= 0 && stepIndex < items.length) {
var activeItem = items[stepIndex];
activeItem.classList.add('trace-step-active');
activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
function clearNarrativeHighlight() {
var items = narrativePanel.querySelectorAll('.narrative-content li.trace-step-active');
items.forEach(function(li) { li.classList.remove('trace-step-active'); });
}
// --- Git Diff Overlay ---
async function loadDiffFiles() {
try {
var response = await fetch('/api/codemap/diff');
if (!response.ok) return [];
var data = await response.json();
return data.changed_files || [];
} catch (err) {
return [];
}
}
function applyDiffOverlay() {
if (!cy || diffFiles.length === 0) return;
// Mark nodes whose file_path matches any changed file
var changedSet = {};
for (var i = 0; i < diffFiles.length; i++) {
changedSet[diffFiles[i].file] = diffFiles[i].status;
}
cy.nodes().forEach(function(node) {
var filePath = node.data('filePath');
var nodeId = node.id();
// Check compound parent nodes (id = 'file:path')
if (nodeId.startsWith('file:')) {
var fp = nodeId.substring(5);
if (changedSet[fp]) {
node.addClass('diff-changed');
}
}
// Check child nodes
if (filePath && changedSet[filePath]) {
node.addClass('diff-changed');
}
});
diffOverlayActive = true;
}
function removeDiffOverlay() {
if (!cy) return;
cy.elements().removeClass('diff-changed');
diffOverlayActive = false;
}
// --- Minimap ---
function initMinimap() {
// Disabled: the cytoscape-navigator plugin renders its thumbnail
// outside the container bounds, overlapping the narrative panel.
cyMinimap.classList.remove('visible');
}
// --- Recent Codemaps (Cache) ---
async function loadRecentCodemaps() {
try {
var response = await fetch('/api/codemap/cache');
if (!response.ok) return;
var items = await response.json();
if (!Array.isArray(items) || items.length === 0) return;
recentCodemaps.classList.add('visible');
items.slice(0, 8).forEach(function(item) {
var chip = document.createElement('a');
chip.className = 'recent-item';
chip.textContent = (item.query || 'Untitled').substring(0, 30);
chip.title = item.query + ' (' + item.total_nodes + ' nodes)';
chip.href = '#';
chip.addEventListener('click', function(e) {
e.preventDefault();
// Load from cache
loadCachedCodemap(item.cache_key);
});
recentCodemaps.appendChild(chip);
});
} catch (err) {
// Ignore cache load errors
}
}
async function loadCachedCodemap(cacheKey) {
try {
var response = await fetch('/api/codemap/cache?key=' + encodeURIComponent(cacheKey));
if (!response.ok) return;
var data = await response.json();
// Render as if it were a live result
data.type = 'result';
data.from_cache = true;
currentResult = data;
renderGraph(data);
renderNarrative(data);
// Update input fields
if (data.query) queryInput.value = data.query;
if (data.focus) focusSelect.value = data.focus;
updateUrlState();
} catch (err) {
// Fall back to normal generation
}
}
// --- Render Graph ---
function renderGraph(result) {
var elements = buildCytoscapeElements(result);
if (elements.length === 0) {
graphPlaceholder.textContent = 'No nodes found for this query.';
graphPlaceholder.style.display = 'flex';
cyContainer.style.display = 'none';
zoomControls.style.display = 'none';
shortcutsHint.style.display = 'none';
exportPngBtn.disabled = true;
return;
}
graphPlaceholder.style.display = 'none';
cyContainer.style.display = 'block';
zoomControls.style.display = 'flex';
shortcutsHint.style.display = 'block';
exportPngBtn.disabled = false;
currentEntryPoint = result.entry_point;
currentFocus = result.focus || focusSelect.value;
currentResult = result;
if (cy) {
cy.destroy();
}
try {
cy = cytoscape({
container: cyContainer,
elements: elements,
style: cytoscapeStyles,
layout: {
name: 'dagre',
rankDir: 'TB',
nodeSep: 50,
rankSep: 80,
padding: 20,
},
minZoom: 0.2,
maxZoom: 3,
wheelSensitivity: 0.3,
});
// Click handler - path highlighting + navigate
cy.on('tap', 'node:childless', function(evt) {
var node = evt.target;
var nodeId = node.id();
// Path highlighting
highlightPath(nodeId);
});
// Double-click to navigate to wiki page (if it exists)
cy.on('dbltap', 'node:childless', function(evt) {
var node = evt.target;
var filePath = node.data('filePath');
if (filePath) {
var wikiPath = filePath.replace(/\.[^/.]+$/, '');
var url = '/wiki/files/' + wikiPath + '.md';
fetch(url, { method: 'HEAD' }).then(function(resp) {
if (resp.ok) {
window.open(url, '_blank');
}
});
}
});
// Click on background to clear highlighting
cy.on('tap', function(evt) {
if (evt.target === cy) {
clearHighlighting();
}
});
// Tooltip on hover
cy.on('mouseover', 'node:childless', function(evt) {
cyContainer.style.cursor = 'pointer';
showTooltip(evt.target, evt.target.renderedPosition());
});
cy.on('mouseout', 'node:childless', function() {
cyContainer.style.cursor = 'default';
// Delay hide so user can hover onto the tooltip
setTimeout(function() {
if (!nodeTooltip.matches(':hover')) hideTooltip();
}, 200);
});
// Right-click context menu
cy.on('cxttap', 'node:childless', function(evt) {
evt.originalEvent.preventDefault();
var node = evt.target;
showContextMenu(node, evt.originalEvent);
});
// Single-click: also show code preview in narrative panel
cy.on('tap', 'node:childless', function(evt) {
showCodePreview(evt.target);
});
// Dismiss context menu on background click
cy.on('tap', function(evt) {
if (evt.target === cy) hideContextMenu();
});
// Show play controls and init minimap
playControls.classList.add('visible');
stopTrace();
initMinimap();
// Apply diff overlay if toggle is checked
if (diffToggleCb.checked && diffFiles.length > 0) {
applyDiffOverlay();
}
} catch (err) {
// Fallback to mermaid
renderMermaidFallback(result.mermaid_diagram);
}
}
// --- Mermaid Fallback ---
function renderMermaidFallback(mermaidDiagram) {
cyContainer.style.display = 'none';
graphPlaceholder.style.display = 'none';
mermaidFallback.style.display = 'block';
zoomControls.style.display = 'none';
shortcutsHint.style.display = 'none';
mermaidFallback.textContent = '';
var pre = document.createElement('pre');
pre.className = 'mermaid';
pre.textContent = mermaidDiagram;
mermaidFallback.appendChild(pre);
// Try to render with mermaid if loaded
if (typeof mermaid !== 'undefined') {
mermaid.run({ nodes: mermaidFallback.querySelectorAll('.mermaid') });
}
}
// --- Render Narrative ---
function renderNarrative(result) {
// Clear panel safely
narrativePanel.textContent = '';
// Stats bar
var statsBar = document.createElement('div');
statsBar.className = 'stats-bar';
var stats = [
{ value: result.total_nodes, label: 'nodes' },
{ value: result.total_edges, label: 'edges' },
{ value: result.cross_file_edges, label: 'cross-file' },
{ value: result.files_involved ? result.files_involved.length : 0, label: 'files' },
];
stats.forEach(function(s) {
var stat = document.createElement('div');
stat.className = 'stat';
var valSpan = document.createElement('span');
valSpan.className = 'stat-value';
valSpan.textContent = s.value;
var labelSpan = document.createElement('span');
labelSpan.className = 'stat-label';
labelSpan.textContent = ' ' + s.label;
stat.appendChild(valSpan);
stat.appendChild(labelSpan);
statsBar.appendChild(stat);
});
narrativePanel.appendChild(statsBar);
// Heading
var heading = document.createElement('h3');
heading.textContent = 'Execution Trace';
narrativePanel.appendChild(heading);
// Narrative content - rendered markdown (same pattern as chat.html)
var narrativeDiv = document.createElement('div');
narrativeDiv.className = 'narrative-content';
narrativeDiv.innerHTML = renderMarkdown(result.narrative);
narrativePanel.appendChild(narrativeDiv);
// Legend
var legendDiv = document.createElement('div');
legendDiv.className = 'legend';
legendDiv.id = 'codemap-legend';
var legendTitle = document.createElement('h4');
legendTitle.textContent = 'Legend';
legendTitle.addEventListener('click', function() {
legendDiv.classList.toggle('collapsed');
});
legendDiv.appendChild(legendTitle);
var legendItemsContainer = document.createElement('div');
legendItemsContainer.className = 'legend-items';
var legendItems = [
{ color: '#2d6a4f', label: 'Entry point', type: 'dot' },
{ color: '#1d3557', label: 'Cross-file call', type: 'dot' },
{ color: '#6c757d', label: 'Leaf node', type: 'dot' },
{ color: '#21262d', label: 'Internal', type: 'dot', border: '#30363d' },
{ color: '#8b949e', label: 'Same-file edge', type: 'line' },
{ color: '#58a6ff', label: 'Cross-file edge', type: 'dashed' },
{ color: '#f0883e', label: 'Highlighted path', type: 'line' },
{ color: '#f85149', label: 'Changed (git diff)', type: 'dot', border: '#f85149' },
{ color: '#3fb950', label: 'Trace visited', type: 'dot', border: '#3fb950' },
];
legendItems.forEach(function(item) {
var row = document.createElement('div');
row.className = 'legend-item';
if (item.type === 'dot') {
var dot = document.createElement('div');
dot.className = 'legend-dot';
dot.style.background = item.color;
if (item.border) {
dot.style.border = '1px solid ' + item.border;
}
row.appendChild(dot);
} else {
var line = document.createElement('div');
line.className = 'legend-line';
if (item.type === 'dashed') {
line.style.borderTop = '2px dashed ' + item.color;
line.style.height = '0';
} else {
line.style.background = item.color;
}
row.appendChild(line);
}
var labelSpan = document.createElement('span');
labelSpan.textContent = item.label;
row.appendChild(labelSpan);
legendItemsContainer.appendChild(row);
});
legendDiv.appendChild(legendItemsContainer);
narrativePanel.appendChild(legendDiv);
}
// --- Export PNG ---
function exportPng() {
if (!cy) return;
var pngData = cy.png({ full: true, scale: 2, bg: getComputedStyle(document.documentElement).getPropertyValue('--bg-color').trim() });
var link = document.createElement('a');
var slug = queryInput.value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 40);
link.download = 'codemap-' + (slug || 'graph') + '.png';
link.href = pngData;
link.click();
}
// --- URL State ---
function readUrlState() {
var params = new URLSearchParams(window.location.search);
if (params.has('query')) {
queryInput.value = params.get('query');
}
if (params.has('focus')) {
var focus = params.get('focus');
if (['execution_flow', 'data_flow', 'dependency_chain'].indexOf(focus) !== -1) {
focusSelect.value = focus;
}
}
if (params.has('max_depth')) {
var d = parseInt(params.get('max_depth'), 10);
if (d >= 1 && d <= 10) depthInput.value = d;
}
if (params.has('max_nodes')) {
var n = parseInt(params.get('max_nodes'), 10);
if (n >= 5 && n <= 60) nodesInput.value = n;
}
return params.has('query') && params.get('query').trim().length > 0;
}
function updateUrlState() {
var query = queryInput.value.trim();
if (!query) return;
var params = new URLSearchParams();
params.set('query', query);
params.set('focus', focusSelect.value);
params.set('max_depth', depthInput.value);
params.set('max_nodes', nodesInput.value);
history.replaceState(null, '', '/codemap?' + params.toString());
}
// --- Generate Codemap ---
async function generateCodemap() {
var query = queryInput.value.trim();
if (!query || isGenerating) return;
isGenerating = true;
generateBtn.disabled = true;
progressOverlay.classList.add('active');
progressMessage.textContent = 'Generating codemap...';
graphPlaceholder.style.display = 'none';
mermaidFallback.style.display = 'none';
pathHighlightActive = false;
// Update URL state
updateUrlState();
try {
var response = await fetch('/api/codemap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: query,
focus: focusSelect.value,
max_depth: parseInt(depthInput.value, 10),
max_nodes: parseInt(nodesInput.value, 10),
}),
});
var reader = response.body.getReader();
var decoder = new TextDecoder();
var buffer = '';
while (true) {
var chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
var lines = buffer.split('\n');
buffer = lines.pop() || '';
for (var li = 0; li < lines.length; li++) {
var line = lines[li];
if (!line.startsWith('data: ')) continue;
try {
var data = JSON.parse(line.slice(6));
if (data.type === 'progress') {
progressMessage.textContent = data.message;
} else if (data.type === 'result') {
progressOverlay.classList.remove('active');
renderGraph(data);
renderNarrative(data);
} else if (data.type === 'error') {
throw new Error(data.message);
} else if (data.type === 'done') {
break;
}
} catch (parseErr) {
if (parseErr.message && parseErr.message.indexOf('JSON') === -1) {
throw parseErr;
}
}
}
}
} catch (err) {
progressOverlay.classList.remove('active');
graphPlaceholder.textContent = 'Error: ' + err.message;
graphPlaceholder.style.display = 'flex';
}
isGenerating = false;
generateBtn.disabled = false;
progressOverlay.classList.remove('active');
}
// --- Load Topics ---
async function loadTopics() {
try {
var response = await fetch('/api/codemap/topics');
if (!response.ok) {
topicsRow.style.display = 'none';
return;
}
var topics = await response.json();
if (Array.isArray(topics) && topics.length > 0) {
topics.slice(0, 6).forEach(function(topic) {
var chip = document.createElement('span');
chip.className = 'topic-chip';
chip.textContent = topic.topic || topic.suggested_query || 'Topic';
chip.title = topic.reason || '';
chip.addEventListener('click', function() {
queryInput.value = topic.suggested_query || topic.topic;
generateCodemap();
});
topicsRow.appendChild(chip);
});
} else {
topicsRow.style.display = 'none';
}
} catch (err) {
topicsRow.style.display = 'none';
}
}
// --- Utilities ---
function renderMarkdown(text) {
if (!text) return '';
try {
const html = marked.parse(text);
return typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html) : html;
} catch (e) {
return escapeHtml(text);
}
}
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// --- Theme ---
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('deepwiki-theme', theme);
themeToggle.textContent = theme === 'dark' ? '\uD83C\uDF19' : '\u2600';
}
// --- Event Listeners ---
generateBtn.addEventListener('click', generateCodemap);
queryInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
generateCodemap();
}
});
themeToggle.addEventListener('click', function() {
var current = document.documentElement.getAttribute('data-theme') || 'dark';
setTheme(current === 'dark' ? 'light' : 'dark');
});
// Export PNG
exportPngBtn.addEventListener('click', exportPng);
// Zoom controls
document.getElementById('zoom-in-btn').addEventListener('click', function() {
if (cy) cy.zoom({ level: cy.zoom() * 1.3, renderedPosition: { x: cyContainer.clientWidth / 2, y: cyContainer.clientHeight / 2 } });
});
document.getElementById('zoom-out-btn').addEventListener('click', function() {
if (cy) cy.zoom({ level: cy.zoom() / 1.3, renderedPosition: { x: cyContainer.clientWidth / 2, y: cyContainer.clientHeight / 2 } });
});
document.getElementById('fit-btn').addEventListener('click', function() {
if (cy) cy.fit(undefined, 20);
});
// Play controls
playBtn.addEventListener('click', function() {
if (tracePlaying) { pauseTrace(); } else { startTrace(); }
});
pauseBtn.addEventListener('click', pauseTrace);
stepBtn.addEventListener('click', function() {
if (traceTimer) pauseTrace();
if (traceNodes.length === 0) traceNodes = buildTraceSequence();
if (traceNodes.length > 0) stepTrace();
});
stopTraceBtn.addEventListener('click', stopTrace);
// Diff toggle
diffToggleCb.addEventListener('change', async function() {
if (this.checked) {
if (diffFiles.length === 0) {
diffFiles = await loadDiffFiles();
}
applyDiffOverlay();
} else {
removeDiffOverlay();
}
});
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Skip if typing in input fields
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
if (e.key === 'f' || e.key === 'F') {
e.preventDefault();
if (cy) cy.fit(undefined, 20);
} else if (e.key === '+' || e.key === '=') {
e.preventDefault();
if (cy) cy.zoom({ level: cy.zoom() * 1.3, renderedPosition: { x: cyContainer.clientWidth / 2, y: cyContainer.clientHeight / 2 } });
} else if (e.key === '-') {
e.preventDefault();
if (cy) cy.zoom({ level: cy.zoom() / 1.3, renderedPosition: { x: cyContainer.clientWidth / 2, y: cyContainer.clientHeight / 2 } });
} else if (e.key === 'Escape') {
clearHighlighting();
if (cy) cy.elements().unselect();
} else if (e.key === 'l' || e.key === 'L') {
var legend = document.getElementById('codemap-legend');
if (legend) legend.classList.toggle('collapsed');
} else if (e.key === 'p' || e.key === 'P') {
e.preventDefault();
if (tracePlaying) { pauseTrace(); } else { startTrace(); }
}
});
// --- Init ---
var savedTheme = localStorage.getItem('deepwiki-theme') || 'dark';
setTheme(savedTheme);
loadTopics();
loadRecentCodemaps();
// Read URL state and auto-generate if query present
var hasUrlQuery = readUrlState();
if (hasUrlQuery) {
generateCodemap();
} else {
queryInput.focus();
}
</script>
</body>
</html>