<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CodeGraph Explorer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.4/cytoscape.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dagre@0.8.5/dist/dagre.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #1c2128;
--bg-hover: #1f2937;
--border: #30363d;
--border-light: #3d444d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #656d76;
--accent: #58a6ff;
--accent-hover: #79c0ff;
--green: #3fb950;
--purple: #d2a8ff;
--orange: #ffa657;
--red: #ff7b72;
--yellow: #d29922;
--pink: #f778ba;
--cyan: #76e3ea;
--font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
--sidebar-width: 320px;
--panel-width: 460px;
--header-height: 52px;
--radius: 8px;
--radius-sm: 6px;
}
html, body {
height: 100%;
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
#header {
height: var(--header-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 16px;
gap: 16px;
flex-shrink: 0;
z-index: 10;
}
#header .logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
}
#header .logo span.icon { font-size: 20px; }
#search-container {
flex: 1;
max-width: 560px;
position: relative;
}
#search-input {
width: 100%;
height: 34px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 13px;
padding: 0 12px 0 34px;
outline: none;
transition: border-color 0.15s;
font-family: var(--font-sans);
}
#search-input:focus { border-color: var(--accent); }
#search-container .search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
font-size: 14px;
pointer-events: none;
}
#search-results-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
margin-top: 4px;
max-height: 400px;
overflow-y: auto;
z-index: 100;
display: none;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
#search-results-dropdown.visible { display: block; }
.search-result-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--border);
transition: background 0.1s;
}
.search-result-item:last-child { border-bottom: none; }
.search-result-item:hover { background: var(--bg-hover); }
.search-result-item .kind-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
text-transform: uppercase;
white-space: nowrap;
flex-shrink: 0;
}
.search-result-item .name {
font-weight: 500;
font-size: 13px;
color: var(--text-primary);
}
.search-result-item .file-path {
font-size: 11px;
color: var(--text-muted);
margin-left: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
#header-stats {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
}
#header-stats .stat { display: flex; align-items: center; gap: 4px; }
#header-stats .stat-value { color: var(--text-secondary); font-weight: 500; }
#main {
display: flex;
flex: 1;
overflow: hidden;
}
#sidebar {
width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
}
.sidebar-section {
border-bottom: 1px solid var(--border);
}
.sidebar-header {
padding: 10px 14px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
}
.sidebar-header:hover { color: var(--text-secondary); }
.sidebar-content {
overflow-y: auto;
max-height: 300px;
}
#file-tree {
padding: 4px 0;
}
.file-item {
padding: 5px 14px;
font-size: 12px;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: background 0.1s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-item:hover { background: var(--bg-hover); color: var(--text-primary); }
.file-item.active { background: var(--bg-hover); color: var(--accent); }
.file-item .file-icon { font-size: 12px; flex-shrink: 0; }
#legend {
padding: 10px 14px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
padding: 3px 0;
font-size: 12px;
color: var(--text-secondary);
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
#graph-toolbar {
padding: 8px 14px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.toolbar-btn {
padding: 4px 10px;
font-size: 11px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s;
font-family: var(--font-sans);
}
.toolbar-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
border-color: var(--border-light);
}
.toolbar-btn.active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
#graph-container {
flex: 1;
position: relative;
background: var(--bg-primary);
overflow: hidden;
}
#cy {
width: 100%;
height: 100%;
}
#graph-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: var(--text-muted);
pointer-events: none;
}
#graph-overlay .overlay-icon { font-size: 48px; margin-bottom: 12px; }
#graph-overlay .overlay-title { font-size: 18px; font-weight: 500; margin-bottom: 6px; }
#graph-overlay .overlay-subtitle { font-size: 13px; }
.example-btn {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 20px;
color: var(--text-secondary);
padding: 6px 16px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
font-family: var(--font-sans);
pointer-events: auto;
}
.example-btn:hover {
background: var(--bg-hover);
color: var(--accent);
border-color: var(--accent);
}
#breadcrumbs {
position: absolute;
top: 10px;
left: 10px;
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
z-index: 5;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 6px 10px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
#breadcrumbs.visible { opacity: 1; pointer-events: auto; }
.breadcrumb-item {
color: var(--accent);
cursor: pointer;
}
.breadcrumb-item:hover { text-decoration: underline; }
.breadcrumb-sep { color: var(--text-muted); }
#graph-controls {
position: absolute;
bottom: 14px;
right: 14px;
display: flex;
gap: 4px;
z-index: 5;
}
.graph-ctrl-btn {
width: 32px;
height: 32px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.15s;
font-family: var(--font-sans);
}
.graph-ctrl-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
#context-menu {
position: fixed;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 4px 0;
z-index: 200;
display: none;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
min-width: 180px;
}
#context-menu.visible { display: block; }
.ctx-item {
padding: 7px 14px;
font-size: 13px;
color: var(--text-primary);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.1s;
}
.ctx-item:hover { background: var(--bg-hover); }
.ctx-item .ctx-icon { font-size: 14px; width: 18px; text-align: center; }
.ctx-sep { height: 1px; background: var(--border); margin: 4px 0; }
#detail-panel {
width: 0;
background: var(--bg-secondary);
border-left: 1px solid var(--border);
flex-shrink: 0;
overflow: hidden;
transition: width 0.2s ease;
display: flex;
flex-direction: column;
}
#detail-panel.open { width: var(--panel-width); }
#detail-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
flex-shrink: 0;
}
#detail-header .node-title {
font-size: 15px;
font-weight: 600;
word-break: break-all;
}
#detail-header .close-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 18px;
padding: 0 4px;
line-height: 1;
flex-shrink: 0;
}
#detail-header .close-btn:hover { color: var(--text-primary); }
#detail-body {
flex: 1;
overflow-y: auto;
padding: 0;
}
.detail-section {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.detail-section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
margin-bottom: 8px;
}
.detail-meta {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 12px;
font-size: 12px;
}
.detail-meta .label { color: var(--text-muted); }
.detail-meta .value { color: var(--text-secondary); word-break: break-all; }
.detail-meta .value.accent { color: var(--accent); }
.code-block {
background: var(--bg-primary);
border-radius: var(--radius-sm);
overflow-x: auto;
font-size: 12px;
line-height: 1.5;
}
.code-block pre {
margin: 0;
padding: 12px;
}
.code-block code {
font-family: var(--font-mono);
}
.relation-list { list-style: none; }
.relation-item {
padding: 5px 0;
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: color 0.1s;
color: var(--text-secondary);
}
.relation-item:hover { color: var(--accent); }
.relation-item .rel-badge {
font-size: 9px;
padding: 1px 5px;
border-radius: 3px;
font-weight: 600;
text-transform: uppercase;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
.cy-tooltip {
position: fixed;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 6px 10px;
font-size: 12px;
color: var(--text-primary);
pointer-events: none;
z-index: 50;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
max-width: 300px;
display: none;
}
.cy-tooltip .tip-kind {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 2px;
}
.cy-tooltip .tip-name { font-weight: 500; }
.cy-tooltip .tip-file { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
#toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(80px);
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 20px;
font-size: 13px;
color: var(--text-primary);
z-index: 300;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
transition: transform 0.3s ease;
}
#toast.visible { transform: translateX(-50%) translateY(0); }
#dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 500;
display: none;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
#dialog-overlay.visible { display: flex; }
#dialog {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 32px;
max-width: 480px;
width: 90%;
box-shadow: 0 16px 48px rgba(0,0,0,0.5);
}
#dialog .dialog-icon { font-size: 36px; margin-bottom: 16px; }
#dialog .dialog-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
#dialog .dialog-body {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 20px;
}
#dialog .dialog-body strong { color: var(--text-primary); }
#dialog-progress {
display: none;
margin-bottom: 20px;
}
#dialog-progress .progress-bar-track {
width: 100%;
height: 8px;
background: var(--bg-primary);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
#dialog-progress .progress-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 4px;
width: 0%;
transition: width 0.2s ease;
}
#dialog-progress .progress-text {
font-size: 12px;
color: var(--text-muted);
}
#dialog-progress .progress-percent {
float: right;
color: var(--text-secondary);
font-weight: 500;
}
#dialog .dialog-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
#dialog .btn {
padding: 8px 20px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--border);
transition: all 0.15s;
font-family: var(--font-sans);
}
#dialog .btn-primary {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
#dialog .btn-primary:hover { background: var(--accent-hover); }
#dialog .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
#dialog .btn-secondary {
background: var(--bg-primary);
color: var(--text-secondary);
}
#dialog .btn-secondary:hover { color: var(--text-primary); }
</style>
</head>
<body>
<div id="app">
<div id="header">
<div class="logo">
<span class="icon">🔮</span>
<span>CodeGraph</span>
</div>
<div id="search-container">
<span class="search-icon">🔍</span>
<input id="search-input" type="text" placeholder="Search symbols... (Ctrl+K)" autocomplete="off" spellcheck="false" />
<div id="search-results-dropdown"></div>
</div>
<div id="header-stats">
<div class="stat"><span>Nodes:</span> <span class="stat-value" id="stat-nodes">-</span></div>
<div class="stat"><span>Edges:</span> <span class="stat-value" id="stat-edges">-</span></div>
<div class="stat"><span>Files:</span> <span class="stat-value" id="stat-files">-</span></div>
</div>
</div>
<div id="main">
<div id="sidebar">
<div class="sidebar-section">
<div class="sidebar-header">
<span>Graph Actions</span>
</div>
<div id="graph-toolbar">
<button class="toolbar-btn" onclick="loadOverview()" title="Show top-level symbols">Overview</button>
<button class="toolbar-btn" onclick="clearGraph()" title="Clear the graph">Clear</button>
<button class="toolbar-btn" onclick="runLayout()" title="Re-run layout">Layout</button>
<button class="toolbar-btn" onclick="fitGraph()" title="Fit graph to view">Fit</button>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-header" onclick="toggleSection(this)">
<span>Legend</span>
<span>▶</span>
</div>
<div class="sidebar-content" id="legend" style="display:none;"></div>
</div>
<div class="sidebar-section" style="flex:1; overflow:hidden; display:flex; flex-direction:column;">
<div class="sidebar-header" onclick="toggleSection(this)">
<span>Files</span>
<span>▼</span>
</div>
<div class="sidebar-content" id="file-tree" style="flex:1; max-height:none;"></div>
</div>
</div>
<div id="graph-container">
<div id="cy"></div>
<div id="graph-overlay">
<div class="overlay-icon">🔮</div>
<div class="overlay-title">Search for a starting point</div>
<div class="overlay-subtitle">Type a symbol name, pick it, and trace its call chain</div>
</div>
<div id="breadcrumbs"></div>
<div id="graph-controls">
<button class="graph-ctrl-btn" onclick="cy.zoom(cy.zoom() * 1.3); cy.center()" title="Zoom in">+</button>
<button class="graph-ctrl-btn" onclick="cy.zoom(cy.zoom() / 1.3); cy.center()" title="Zoom out">−</button>
<button class="graph-ctrl-btn" onclick="fitGraph()" title="Fit to view">⤢</button>
</div>
</div>
<div id="detail-panel">
<div id="detail-header">
<div>
<div class="node-title" id="detail-title">-</div>
</div>
<button class="close-btn" onclick="closeDetailPanel()">×</button>
</div>
<div id="detail-body"></div>
</div>
</div>
</div>
<div id="dialog-overlay">
<div id="dialog">
<div class="dialog-icon">🧠</div>
<div class="dialog-title" id="dialog-title">Enable Semantic Search</div>
<div class="dialog-body" id="dialog-body">
CodeGraph Explorer uses <strong>semantic embeddings</strong> to understand your code by meaning, not just keywords.
This lets you ask questions like "how does authentication work?" and get accurate results.
<br><br>
This is a <strong>one-time setup</strong> that generates a local embedding model for this project. No data leaves your machine.
</div>
<div id="dialog-progress">
<div class="progress-bar-track">
<div class="progress-bar-fill" id="dialog-progress-fill"></div>
</div>
<div class="progress-text">
<span id="dialog-progress-text">Preparing...</span>
<span class="progress-percent" id="dialog-progress-percent">0%</span>
</div>
</div>
<div class="dialog-actions" id="dialog-actions">
<button class="btn btn-secondary" id="dialog-skip" onclick="closeDialog()">Skip for now</button>
<button class="btn btn-primary" id="dialog-enable" onclick="startEmbeddings()">Enable Semantic Search</button>
</div>
</div>
</div>
<div id="context-menu">
<div class="ctx-item" onclick="ctxAction('expand-callees')"><span class="ctx-icon">→</span> Expand Callees</div>
<div class="ctx-item" onclick="ctxAction('expand-callers')"><span class="ctx-icon">←</span> Expand Callers</div>
<div class="ctx-sep"></div>
<div class="ctx-item" onclick="ctxAction('callgraph')"><span class="ctx-icon">🌐</span> Full Call Graph</div>
<div class="ctx-item" onclick="ctxAction('impact')"><span class="ctx-icon">💥</span> Impact Analysis</div>
<div class="ctx-sep"></div>
<div class="ctx-item" onclick="ctxAction('children')"><span class="ctx-icon">📂</span> Show Children</div>
<div class="ctx-item" onclick="ctxAction('details')"><span class="ctx-icon">📋</span> View Details</div>
<div class="ctx-sep"></div>
<div class="ctx-item" onclick="ctxAction('remove')"><span class="ctx-icon">✖</span> Remove from Graph</div>
</div>
<div class="cy-tooltip" id="tooltip"></div>
<div id="toast"></div>
<script>
let cy;
let ctxNodeId = null;
let searchDebounce = null;
const expandedSets = { callers: new Set(), callees: new Set() };
const kindColors = {
'function': '#79c0ff',
'method': '#7ee787',
'class': '#d2a8ff',
'interface': '#ffa657',
'struct': '#ffa657',
'trait': '#ffa657',
'protocol': '#ffa657',
'component': '#f778ba',
'enum': '#d29922',
'enum_member': '#d29922',
'type_alias': '#d2a8ff',
'variable': '#ff7b72',
'constant': '#ff7b72',
'property': '#76e3ea',
'field': '#76e3ea',
'file': '#8b949e',
'module': '#8b949e',
'namespace': '#8b949e',
'import': '#f0883e',
'export': '#3fb950',
'route': '#f778ba',
'parameter': '#8b949e',
};
const kindShapes = {
'class': 'round-rectangle',
'interface': 'round-diamond',
'struct': 'round-rectangle',
'trait': 'round-diamond',
'protocol': 'round-diamond',
'enum': 'round-hexagon',
'component': 'round-pentagon',
'file': 'round-rectangle',
'module': 'round-rectangle',
'namespace': 'round-rectangle',
};
const edgeColors = {
'calls': '#58a6ff',
'imports': '#f0883e',
'extends': '#d2a8ff',
'implements': '#ffa657',
'references': '#8b949e',
'contains': '#3d444d',
'type_of': '#76e3ea',
'returns': '#76e3ea',
'instantiates': '#f778ba',
'overrides': '#d29922',
'decorates': '#f778ba',
'exports': '#3fb950',
};
const api = {
async get(path) {
const res = await fetch('/api/' + path);
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
},
embeddingsStatus: () => api.get('embeddings/status'),
status: () => api.get('status'),
search: (q, kind, limit) => api.get(`search?q=${encodeURIComponent(q)}${kind ? '&kind='+kind : ''}&limit=${limit||30}`),
explore: (q) => api.get(`explore?q=${encodeURIComponent(q)}`),
overview: (limit) => api.get(`overview?limit=${limit||60}`),
files: () => api.get('files'),
fileNodes: (p) => api.get(`file-nodes?path=${encodeURIComponent(p)}`),
node: (id) => api.get(`node/${encodeURIComponent(id)}`),
callers: (id, d) => api.get(`node/${encodeURIComponent(id)}/callers?depth=${d||1}`),
callees: (id, d) => api.get(`node/${encodeURIComponent(id)}/callees?depth=${d||1}`),
children: (id) => api.get(`node/${encodeURIComponent(id)}/children`),
impact: (id, d) => api.get(`node/${encodeURIComponent(id)}/impact?depth=${d||2}`),
callgraph: (id, d) => api.get(`node/${encodeURIComponent(id)}/callgraph?depth=${d||2}`),
context: (id) => api.get(`node/${encodeURIComponent(id)}/context`),
};
function initCytoscape() {
cy = cytoscape({
container: document.getElementById('cy'),
style: [
{
selector: 'node',
style: {
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'font-size': '12px',
'font-weight': 'bold',
'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
'color': '#ffffff',
'text-outline-color': '#000000',
'text-outline-width': 2,
'text-outline-opacity': 0.6,
'background-color': 'data(color)',
'background-opacity': 0.85,
'border-width': 2,
'border-color': 'data(color)',
'border-opacity': 0.7,
'width': 'label',
'height': 'label',
'padding': '12px',
'shape': 'data(shape)',
'text-wrap': 'wrap',
'text-max-width': '180px',
'transition-property': 'background-opacity, border-color, border-opacity, opacity, text-opacity',
'transition-duration': '0.2s',
}
},
{
selector: 'node:selected',
style: {
'border-width': 3,
'border-color': '#ffffff',
'border-opacity': 1,
'background-opacity': 1,
'z-index': 10,
}
},
{
selector: 'node.hover',
style: {
'border-width': 3,
'border-color': '#ffffff',
'border-opacity': 0.9,
'background-opacity': 1,
}
},
{
selector: 'node.faded',
style: {
'background-opacity': 0.3,
'border-opacity': 0.2,
'text-opacity': 0.7,
}
},
{
selector: 'node.highlighted',
style: {
'border-width': 3,
'border-color': '#f0e68c',
'border-opacity': 1,
'z-index': 10,
}
},
{
selector: 'edge',
style: {
'width': 1.5,
'line-color': 'data(color)',
'target-arrow-color': 'data(color)',
'target-arrow-shape': 'triangle',
'arrow-scale': 0.8,
'curve-style': 'bezier',
'opacity': 0.6,
'label': 'data(label)',
'font-size': '9px',
'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
'color': '#656d76',
'text-rotation': 'autorotate',
'text-margin-y': -8,
'text-outline-color': '#0d1117',
'text-outline-width': 2,
'transition-property': 'opacity, line-color',
'transition-duration': '0.15s',
}
},
{
selector: 'edge:selected',
style: { 'opacity': 1, 'width': 2.5 }
},
{
selector: 'edge.faded',
style: { 'opacity': 0.15 }
},
{
selector: 'edge.highlighted',
style: { 'opacity': 1, 'width': 2.5 }
},
],
layout: { name: 'preset' },
minZoom: 0.1,
maxZoom: 4,
wheelSensitivity: 0.3,
});
cy.on('tap', 'node', (e) => {
const nodeId = e.target.data('nodeId');
if (nodeId) showNodeDetails(nodeId);
highlightNeighborhood(e.target);
});
cy.on('cxttap', 'node', (e) => {
e.originalEvent.preventDefault();
ctxNodeId = e.target.data('nodeId');
showContextMenu(e.originalEvent.clientX, e.originalEvent.clientY);
});
cy.on('tap', (e) => {
if (e.target === cy) {
clearHighlights();
hideContextMenu();
}
});
cy.on('mouseover', 'node', (e) => {
e.target.addClass('hover');
showTooltip(e);
});
cy.on('mouseout', 'node', (e) => {
e.target.removeClass('hover');
hideTooltip();
});
cy.on('dblclick', 'node', (e) => {
const nodeId = e.target.data('nodeId');
if (nodeId) expandCallees(nodeId);
});
document.addEventListener('click', (e) => {
if (!e.target.closest('#context-menu')) hideContextMenu();
});
document.addEventListener('contextmenu', (e) => {
if (e.target.closest('#cy')) e.preventDefault();
});
}
const kindLabels = {
'function': 'fn', 'method': 'method', 'class': 'class', 'interface': 'iface',
'component': 'comp', 'route': 'route', 'enum': 'enum', 'type_alias': 'type',
'struct': 'struct', 'trait': 'trait', 'variable': 'var', 'constant': 'const',
'property': 'prop', 'field': 'field', 'file': 'file', 'module': 'mod',
};
function addNodeToGraph(node) {
if (cy.getElementById(node.id).length > 0) return;
const color = kindColors[node.kind] || '#8b949e';
const shape = kindShapes[node.kind] || 'round-rectangle';
const kindLabel = kindLabels[node.kind] || node.kind;
cy.add({
group: 'nodes',
data: {
id: node.id,
nodeId: node.id,
label: `${node.name}\n${kindLabel}`,
color: color,
shape: shape,
kind: node.kind,
filePath: node.filePath,
signature: node.signature || '',
},
});
}
function addEdgeToGraph(edge) {
const edgeId = `${edge.source}-${edge.kind}-${edge.target}`;
if (cy.getElementById(edgeId).length > 0) return;
if (cy.getElementById(edge.source).length === 0 || cy.getElementById(edge.target).length === 0) return;
const color = edgeColors[edge.kind] || '#8b949e';
cy.add({
group: 'edges',
data: {
id: edgeId,
source: edge.source,
target: edge.target,
kind: edge.kind,
label: edge.kind,
color: color,
},
});
}
function addSubgraph(nodes, edges) {
const batchElements = [];
for (const node of nodes) {
if (cy.getElementById(node.id).length > 0) continue;
const color = kindColors[node.kind] || '#8b949e';
const shape = kindShapes[node.kind] || 'round-rectangle';
batchElements.push({
group: 'nodes',
data: {
id: node.id,
nodeId: node.id,
label: node.name,
color: color,
shape: shape,
kind: node.kind,
filePath: node.filePath,
signature: node.signature || '',
},
});
}
for (const edge of edges) {
const edgeId = `${edge.source}-${edge.kind}-${edge.target}`;
if (cy.getElementById(edgeId).length > 0) continue;
const srcExists = cy.getElementById(edge.source).length > 0 || batchElements.some(e => e.data.id === edge.source);
const tgtExists = cy.getElementById(edge.target).length > 0 || batchElements.some(e => e.data.id === edge.target);
if (!srcExists || !tgtExists) continue;
const color = edgeColors[edge.kind] || '#8b949e';
batchElements.push({
group: 'edges',
data: {
id: edgeId,
source: edge.source,
target: edge.target,
kind: edge.kind,
label: edge.kind,
color: color,
},
});
}
if (batchElements.length > 0) {
cy.add(batchElements);
}
}
function clearGraph() {
cy.elements().remove();
expandedSets.callers.clear();
expandedSets.callees.clear();
hideOverlay(false);
closeDetailPanel();
}
function runLayout() {
if (cy.nodes().length === 0) return;
const layout = cy.layout({
name: 'dagre',
rankDir: 'LR',
nodeSep: 50,
rankSep: 80,
edgeSep: 20,
animate: true,
animationDuration: 300,
fit: true,
padding: 40,
});
layout.run();
}
function fitGraph() {
if (cy.nodes().length > 0) {
cy.animate({ fit: { eles: cy.elements(), padding: 40 } }, { duration: 300 });
}
}
function highlightNeighborhood(node) {
clearHighlights();
const neighborhood = node.closedNeighborhood();
cy.elements().not(neighborhood).addClass('faded');
neighborhood.edges().addClass('highlighted');
}
function clearHighlights() {
cy.elements().removeClass('faded highlighted');
}
function hideOverlay(hide = true) {
const overlay = document.getElementById('graph-overlay');
overlay.style.display = hide ? 'none' : 'block';
}
async function loadOverview() {
showToast('Loading overview...');
try {
const data = await api.overview(60);
if (data.nodes.length === 0) {
showToast('No symbols found. Is the project indexed?');
return;
}
clearGraph();
hideOverlay();
for (const node of data.nodes) addNodeToGraph(node);
runLayout();
showToast(`Loaded ${data.nodes.length} symbols`);
} catch (err) {
showToast('Error: ' + err.message);
}
}
async function expandCallers(nodeId) {
if (expandedSets.callers.has(nodeId)) return;
expandedSets.callers.add(nodeId);
try {
const data = await api.callers(nodeId, 1);
if (data.items.length === 0) {
showToast('No callers found');
return;
}
for (const item of data.items) {
addNodeToGraph(item.node);
addEdgeToGraph(item.edge);
}
runLayout();
showToast(`Found ${data.items.length} callers`);
} catch (err) {
showToast('Error: ' + err.message);
}
}
async function expandCallees(nodeId) {
if (expandedSets.callees.has(nodeId)) return;
expandedSets.callees.add(nodeId);
try {
const data = await api.callees(nodeId, 1);
if (data.items.length === 0) {
showToast('No callees found');
return;
}
for (const item of data.items) {
addNodeToGraph(item.node);
addEdgeToGraph(item.edge);
}
runLayout();
showToast(`Found ${data.items.length} callees`);
} catch (err) {
showToast('Error: ' + err.message);
}
}
async function loadCallGraph(nodeId) {
showToast('Loading call graph...');
try {
const data = await api.callgraph(nodeId, 2);
addSubgraph(data.nodes, data.edges);
runLayout();
showToast(`Loaded call graph: ${data.nodes.length} nodes`);
} catch (err) {
showToast('Error: ' + err.message);
}
}
async function loadImpact(nodeId) {
showToast('Analyzing impact...');
try {
const data = await api.impact(nodeId, 2);
addSubgraph(data.nodes, data.edges);
runLayout();
const rootEle = cy.getElementById(nodeId);
if (rootEle.length > 0) {
rootEle.addClass('highlighted');
}
showToast(`Impact: ${data.nodes.length} nodes potentially affected`);
} catch (err) {
showToast('Error: ' + err.message);
}
}
async function loadChildren(nodeId) {
try {
const data = await api.children(nodeId);
if (data.children.length === 0) {
showToast('No children found');
return;
}
for (const child of data.children) {
addNodeToGraph(child);
addEdgeToGraph({ source: nodeId, target: child.id, kind: 'contains' });
}
runLayout();
showToast(`Found ${data.children.length} children`);
} catch (err) {
showToast('Error: ' + err.message);
}
}
async function loadFileNodes(filePath) {
showToast('Loading file symbols...');
try {
const data = await api.fileNodes(filePath);
if (data.nodes.length === 0) {
showToast('No symbols in this file');
return;
}
clearGraph();
hideOverlay();
for (const node of data.nodes) addNodeToGraph(node);
runLayout();
showToast(`Loaded ${data.nodes.length} symbols from file`);
} catch (err) {
showToast('Error: ' + err.message);
}
}
async function exploreQuery(question) {
hideSearchDropdown();
clearGraph();
hideOverlay();
document.getElementById('search-input').value = question;
showToast('Finding entry point...');
try {
const data = await api.explore(question);
if (data.nodes.length === 0) {
showToast('No relevant code found. Try searching for a specific symbol.');
hideOverlay(false);
return;
}
addSubgraph(data.nodes, data.edges);
runLayout();
if (data.entryPoint) {
const entryEle = cy.getElementById(data.entryPoint);
if (entryEle.length > 0) {
entryEle.select();
entryEle.addClass('highlighted');
setTimeout(() => {
cy.animate({ center: { eles: entryEle } }, { duration: 400 });
showNodeDetails(data.entryPoint);
}, 350);
}
}
const source = data.usedClaude ? ' (via Claude)' : '';
showToast(`Traced ${data.nodes.length} symbols from entry point${source}`);
} catch (err) {
showToast('Error: ' + err.message);
}
}
function onSearchInput(e) {
const query = e.target.value.trim();
clearTimeout(searchDebounce);
if (!query) {
hideSearchDropdown();
return;
}
searchDebounce = setTimeout(async () => {
try {
const data = await api.search(query, null, 20);
showSearchResults(data.results);
} catch (err) {
console.error('Search error:', err);
}
}, 200);
}
function onSearchKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
const query = e.target.value.trim();
if (!query) return;
const dropdown = document.getElementById('search-results-dropdown');
const firstItem = dropdown.querySelector('.search-result-item');
if (dropdown.classList.contains('visible') && firstItem) {
firstItem.click();
} else {
(async () => {
try {
const data = await api.search(query, null, 10);
if (data.results.length > 0) {
selectSearchResult(data.results[0].node.id);
} else {
showToast('No symbols found. Try a different search.');
}
} catch (err) {
showToast('Search error: ' + err.message);
}
})();
}
}
}
function showSearchResults(results) {
const dropdown = document.getElementById('search-results-dropdown');
if (results.length === 0) {
dropdown.innerHTML = '<div style="padding:12px;color:var(--text-muted);font-size:13px;">No results found</div>';
dropdown.classList.add('visible');
return;
}
dropdown.innerHTML = results.map(r => `
<div class="search-result-item" onclick="selectSearchResult('${escapeAttr(r.node.id)}')">
<span class="kind-badge" style="background:${kindColors[r.node.kind] || '#8b949e'}22;color:${kindColors[r.node.kind] || '#8b949e'}">${r.node.kind}</span>
<span class="name">${escapeHtml(r.node.name)}</span>
<span class="file-path">${escapeHtml(r.node.filePath)}</span>
</div>
`).join('');
dropdown.classList.add('visible');
}
function hideSearchDropdown() {
document.getElementById('search-results-dropdown').classList.remove('visible');
}
async function selectSearchResult(nodeId) {
hideSearchDropdown();
document.getElementById('search-input').value = '';
hideOverlay();
clearGraph();
hideOverlay();
showToast('Tracing call chain...');
try {
const data = await api.callgraph(nodeId, 3);
if (data.nodes.length === 0) {
const nodeData = await api.node(nodeId);
if (nodeData.node) addNodeToGraph(nodeData.node);
} else {
addSubgraph(data.nodes, data.edges);
}
runLayout();
const ele = cy.getElementById(nodeId);
if (ele.length > 0) {
ele.select();
ele.addClass('highlighted');
setTimeout(() => {
cy.animate({ center: { eles: ele } }, { duration: 300 });
}, 350);
}
showNodeDetails(nodeId);
showToast(`Traced ${data.nodes.length} symbols from entry point`);
} catch (err) {
showToast('Error: ' + err.message);
}
}
async function showNodeDetails(nodeId) {
const panel = document.getElementById('detail-panel');
const body = document.getElementById('detail-body');
const title = document.getElementById('detail-title');
panel.classList.add('open');
body.innerHTML = '<div style="padding:20px;text-align:center;"><div class="spinner"></div></div>';
try {
const [nodeData, contextData] = await Promise.all([
api.node(nodeId),
api.context(nodeId),
]);
const node = nodeData.node;
const code = nodeData.code;
const ancestors = nodeData.ancestors || [];
const ctx = contextData.context;
title.textContent = node.name;
let html = '';
html += `<div class="detail-section" style="padding:8px 16px;">
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<button class="toolbar-btn" onclick="expandCallees('${escapeAttr(node.id)}')" style="font-size:12px;">Expand Callees →</button>
<button class="toolbar-btn" onclick="expandCallers('${escapeAttr(node.id)}')" style="font-size:12px;">← Expand Callers</button>
<button class="toolbar-btn" onclick="loadCallGraph('${escapeAttr(node.id)}')" style="font-size:12px;">Full Call Graph</button>
<button class="toolbar-btn" onclick="loadImpact('${escapeAttr(node.id)}')" style="font-size:12px;">Impact Analysis</button>
</div>
</div>`;
html += `<div class="detail-section">
<div class="detail-section-title">Info</div>
<div class="detail-meta">
<span class="label">Kind</span>
<span class="value"><span class="kind-badge" style="background:${kindColors[node.kind] || '#8b949e'}22;color:${kindColors[node.kind] || '#8b949e'};font-size:10px;padding:1px 5px;border-radius:3px;">${node.kind}</span></span>
<span class="label">File</span>
<span class="value accent">${escapeHtml(node.filePath)}</span>
<span class="label">Lines</span>
<span class="value">${node.startLine} - ${node.endLine}</span>
${node.signature ? `<span class="label">Signature</span><span class="value" style="font-family:var(--font-mono);font-size:11px;">${escapeHtml(node.signature)}</span>` : ''}
${node.visibility ? `<span class="label">Visibility</span><span class="value">${node.visibility}</span>` : ''}
${node.isExported ? `<span class="label">Exported</span><span class="value">Yes</span>` : ''}
${node.isAsync ? `<span class="label">Async</span><span class="value">Yes</span>` : ''}
${node.decorators && node.decorators.length ? `<span class="label">Decorators</span><span class="value">${escapeHtml(node.decorators.join(', '))}</span>` : ''}
</div>
</div>`;
if (ancestors.length > 0) {
html += `<div class="detail-section">
<div class="detail-section-title">Hierarchy</div>
<div style="font-size:12px;color:var(--text-secondary);">
${ancestors.map(a => `<span class="relation-item" onclick="selectSearchResult('${escapeAttr(a.id)}')" style="display:inline;cursor:pointer;color:var(--accent);">${escapeHtml(a.name)}</span>`).join(' <span style="color:var(--text-muted);">›</span> ')} <span style="color:var(--text-muted);">›</span> <strong>${escapeHtml(node.name)}</strong>
</div>
</div>`;
}
if (code) {
const lang = langForHighlight(node.language);
html += `<div class="detail-section">
<div class="detail-section-title">Source Code</div>
<div class="code-block"><pre><code class="language-${lang}">${escapeHtml(code)}</code></pre></div>
</div>`;
}
if (ctx.incomingRefs && ctx.incomingRefs.length > 0) {
html += `<div class="detail-section">
<div class="detail-section-title">Called By (${ctx.incomingRefs.length})</div>
<ul class="relation-list">
${ctx.incomingRefs.slice(0, 20).map(r => `
<li class="relation-item" onclick="selectSearchResult('${escapeAttr(r.node.id)}')">
<span class="rel-badge" style="background:${kindColors[r.node.kind] || '#8b949e'}22;color:${kindColors[r.node.kind] || '#8b949e'}">${r.node.kind}</span>
${escapeHtml(r.node.name)}
<span style="margin-left:auto;color:var(--text-muted);font-size:11px;">${r.edge.kind}</span>
</li>
`).join('')}
</ul>
</div>`;
}
if (ctx.outgoingRefs && ctx.outgoingRefs.length > 0) {
html += `<div class="detail-section">
<div class="detail-section-title">Calls (${ctx.outgoingRefs.length})</div>
<ul class="relation-list">
${ctx.outgoingRefs.slice(0, 20).map(r => `
<li class="relation-item" onclick="selectSearchResult('${escapeAttr(r.node.id)}')">
<span class="rel-badge" style="background:${kindColors[r.node.kind] || '#8b949e'}22;color:${kindColors[r.node.kind] || '#8b949e'}">${r.node.kind}</span>
${escapeHtml(r.node.name)}
<span style="margin-left:auto;color:var(--text-muted);font-size:11px;">${r.edge.kind}</span>
</li>
`).join('')}
</ul>
</div>`;
}
if (ctx.children && ctx.children.length > 0) {
html += `<div class="detail-section">
<div class="detail-section-title">Contains (${ctx.children.length})</div>
<ul class="relation-list">
${ctx.children.slice(0, 30).map(c => `
<li class="relation-item" onclick="selectSearchResult('${escapeAttr(c.id)}')">
<span class="rel-badge" style="background:${kindColors[c.kind] || '#8b949e'}22;color:${kindColors[c.kind] || '#8b949e'}">${c.kind}</span>
${escapeHtml(c.name)}
</li>
`).join('')}
</ul>
</div>`;
}
if (node.docstring) {
html += `<div class="detail-section">
<div class="detail-section-title">Documentation</div>
<div style="font-size:12px;color:var(--text-secondary);white-space:pre-wrap;font-family:var(--font-mono);line-height:1.5;">${escapeHtml(node.docstring)}</div>
</div>`;
}
body.innerHTML = html;
body.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
} catch (err) {
body.innerHTML = `<div style="padding:20px;color:var(--red);">Error loading details: ${escapeHtml(err.message)}</div>`;
}
}
function closeDetailPanel() {
document.getElementById('detail-panel').classList.remove('open');
}
function showContextMenu(x, y) {
const menu = document.getElementById('context-menu');
menu.style.left = x + 'px';
menu.style.top = y + 'px';
menu.classList.add('visible');
}
function hideContextMenu() {
document.getElementById('context-menu').classList.remove('visible');
}
function ctxAction(action) {
hideContextMenu();
if (!ctxNodeId) return;
switch (action) {
case 'expand-callees': expandCallees(ctxNodeId); break;
case 'expand-callers': expandCallers(ctxNodeId); break;
case 'callgraph': loadCallGraph(ctxNodeId); break;
case 'impact': loadImpact(ctxNodeId); break;
case 'children': loadChildren(ctxNodeId); break;
case 'details': showNodeDetails(ctxNodeId); break;
case 'remove':
cy.getElementById(ctxNodeId).remove();
expandedSets.callers.delete(ctxNodeId);
expandedSets.callees.delete(ctxNodeId);
break;
}
}
function showTooltip(e) {
const node = e.target;
const tip = document.getElementById('tooltip');
const pos = e.originalEvent;
tip.innerHTML = `
<div class="tip-kind">${node.data('kind')}</div>
<div class="tip-name">${escapeHtml(node.data('label'))}</div>
${node.data('signature') ? `<div class="tip-file" style="font-family:var(--font-mono);">${escapeHtml(node.data('signature'))}</div>` : ''}
<div class="tip-file">${escapeHtml(node.data('filePath') || '')}</div>
`;
tip.style.left = (pos.clientX + 12) + 'px';
tip.style.top = (pos.clientY + 12) + 'px';
tip.style.display = 'block';
}
function hideTooltip() {
document.getElementById('tooltip').style.display = 'none';
}
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('visible');
clearTimeout(toast._timeout);
toast._timeout = setTimeout(() => toast.classList.remove('visible'), 2500);
}
function toggleSection(header) {
const content = header.nextElementSibling;
const arrow = header.querySelector('span:last-child');
if (content.style.display === 'none') {
content.style.display = '';
arrow.innerHTML = '▼';
} else {
content.style.display = 'none';
arrow.innerHTML = '▶';
}
}
async function loadFileTree() {
try {
const data = await api.files();
const tree = document.getElementById('file-tree');
if (data.files.length === 0) {
tree.innerHTML = '<div style="padding:12px;color:var(--text-muted);font-size:12px;">No files indexed</div>';
return;
}
const dirs = {};
for (const f of data.files) {
const dir = f.filePath.split('/').slice(0, -1).join('/') || '.';
if (!dirs[dir]) dirs[dir] = [];
dirs[dir].push(f);
}
let html = '';
const sortedDirs = Object.keys(dirs).sort();
for (const dir of sortedDirs) {
html += `<div class="file-item" style="color:var(--text-muted);font-weight:500;padding-top:8px;" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? '' : 'none'">
<span class="file-icon">📁</span> ${escapeHtml(dir)}/
</div><div>`;
for (const f of dirs[dir].sort((a, b) => a.filePath.localeCompare(b.filePath))) {
const fileName = f.filePath.split('/').pop();
html += `<div class="file-item" style="padding-left:28px;" onclick="loadFileNodes('${escapeAttr(f.filePath)}')">
<span class="file-icon">📄</span> ${escapeHtml(fileName)}
</div>`;
}
html += '</div>';
}
tree.innerHTML = html;
} catch (err) {
console.error('Failed to load file tree:', err);
}
}
function buildLegend() {
const legendEl = document.getElementById('legend');
const kinds = ['function', 'method', 'class', 'interface', 'component', 'enum', 'variable', 'constant', 'property', 'type_alias', 'import', 'export'];
legendEl.innerHTML = kinds.map(k => `
<div class="legend-item">
<div class="legend-dot" style="background:${kindColors[k]};"></div>
<span>${k.replace('_', ' ')}</span>
</div>
`).join('');
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function escapeAttr(str) {
if (!str) return '';
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
function langForHighlight(lang) {
const map = {
'typescript': 'typescript', 'javascript': 'javascript', 'tsx': 'typescript',
'jsx': 'javascript', 'python': 'python', 'go': 'go', 'rust': 'rust',
'java': 'java', 'c': 'c', 'cpp': 'cpp', 'csharp': 'csharp',
'php': 'php', 'ruby': 'ruby', 'swift': 'swift', 'kotlin': 'kotlin',
'dart': 'dart', 'svelte': 'xml', 'liquid': 'xml', 'pascal': 'delphi',
};
return map[lang] || 'plaintext';
}
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('search-input').focus();
}
if (e.key === 'Escape') {
hideSearchDropdown();
hideContextMenu();
closeDetailPanel();
clearHighlights();
document.getElementById('search-input').blur();
}
if ((e.key === 'Delete' || e.key === 'Backspace') && document.activeElement.tagName !== 'INPUT') {
const selected = cy.$(':selected');
if (selected.length > 0) {
selected.remove();
}
}
});
function showDialog() {
document.getElementById('dialog-overlay').classList.add('visible');
}
function closeDialog() {
document.getElementById('dialog-overlay').classList.remove('visible');
}
async function checkEmbeddings() {
try {
const data = await api.embeddingsStatus();
if (!data.isReady) {
showDialog();
}
} catch (err) {
console.error('Failed to check embeddings status:', err);
}
}
function startEmbeddings() {
const btnEnable = document.getElementById('dialog-enable');
const btnSkip = document.getElementById('dialog-skip');
const progress = document.getElementById('dialog-progress');
const progressFill = document.getElementById('dialog-progress-fill');
const progressText = document.getElementById('dialog-progress-text');
const progressPercent = document.getElementById('dialog-progress-percent');
const title = document.getElementById('dialog-title');
const body = document.getElementById('dialog-body');
btnEnable.disabled = true;
btnEnable.textContent = 'Setting up...';
btnSkip.style.display = 'none';
progress.style.display = 'block';
body.innerHTML = 'Setting up semantic search for your project. This only needs to happen once.';
const evtSource = new EventSource('/api/embeddings/generate');
evtSource.addEventListener('status', (e) => {
const data = JSON.parse(e.data);
progressText.textContent = data.message;
title.textContent = data.phase === 'model' ? 'Downloading Model...' :
data.phase === 'embedding' ? 'Generating Embeddings...' :
'Setting Up...';
});
evtSource.addEventListener('progress', (e) => {
const data = JSON.parse(e.data);
progressFill.style.width = data.percent + '%';
progressPercent.textContent = data.percent + '%';
progressText.textContent = data.nodeName
? `Embedding: ${data.nodeName} (${data.current}/${data.total})`
: `Processing ${data.current} of ${data.total}...`;
});
evtSource.addEventListener('complete', (e) => {
const data = JSON.parse(e.data);
evtSource.close();
title.textContent = 'Ready!';
body.innerHTML = `<strong>${data.message}</strong><br><br>Semantic search is now active. Your explore queries will understand code meaning, not just keywords.`;
progressFill.style.width = '100%';
progressPercent.textContent = '100%';
progressText.textContent = 'Complete';
document.getElementById('dialog-actions').innerHTML =
'<button class="btn btn-primary" onclick="closeDialog()">Start Exploring</button>';
});
evtSource.addEventListener('error', (e) => {
let msg = 'An error occurred during setup.';
try {
const data = JSON.parse(e.data);
msg = data.message || msg;
} catch {}
evtSource.close();
title.textContent = 'Setup Error';
body.innerHTML = `<span style="color:var(--red);">${escapeHtml(msg)}</span><br><br>You can still use the explorer with keyword-based search.`;
document.getElementById('dialog-actions').innerHTML =
'<button class="btn btn-secondary" onclick="closeDialog()">Close</button>';
});
evtSource.onerror = () => {
};
}
async function init() {
initCytoscape();
buildLegend();
try {
const data = await api.status();
document.getElementById('stat-nodes').textContent = data.stats.nodeCount.toLocaleString();
document.getElementById('stat-edges').textContent = data.stats.edgeCount.toLocaleString();
document.getElementById('stat-files').textContent = data.stats.fileCount.toLocaleString();
document.title = `CodeGraph - ${data.projectName}`;
} catch (err) {
console.error('Failed to load status:', err);
}
loadFileTree();
document.getElementById('search-input').addEventListener('input', onSearchInput);
document.getElementById('search-input').addEventListener('keydown', onSearchKeydown);
document.getElementById('search-input').addEventListener('focus', () => {
if (document.getElementById('search-input').value.trim()) {
document.getElementById('search-results-dropdown').classList.add('visible');
}
});
document.addEventListener('click', (e) => {
if (!e.target.closest('#search-container')) hideSearchDropdown();
});
}
init();
</script>
</body>
</html>