@colbymchenry/codegraph 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/bin/codegraph.d.ts +1 -0
- package/dist/bin/codegraph.d.ts.map +1 -1
- package/dist/bin/codegraph.js +189 -0
- package/dist/bin/codegraph.js.map +1 -1
- package/dist/graph/queries.d.ts.map +1 -1
- package/dist/graph/queries.js +12 -1
- package/dist/graph/queries.js.map +1 -1
- package/dist/installer/config-writer.d.ts +3 -1
- package/dist/installer/config-writer.d.ts.map +1 -1
- package/dist/installer/config-writer.js +8 -4
- package/dist/installer/config-writer.js.map +1 -1
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +37 -17
- package/dist/installer/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +30 -4
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/search/query-utils.d.ts +5 -2
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +30 -7
- package/dist/search/query-utils.js.map +1 -1
- package/dist/sentry.d.ts +2 -0
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +7 -0
- package/dist/sentry.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/visualizer/public/index.html +1994 -0
- package/dist/visualizer/server.d.ts +46 -0
- package/dist/visualizer/server.d.ts.map +1 -0
- package/dist/visualizer/server.js +499 -0
- package/dist/visualizer/server.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1994 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>CodeGraph Explorer</title>
|
|
7
|
+
|
|
8
|
+
<!-- Cytoscape.js + Dagre layout -->
|
|
9
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.4/cytoscape.min.js"></script>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/dagre@0.8.5/dist/dagre.min.js"></script>
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
|
|
12
|
+
|
|
13
|
+
<!-- Highlight.js for code syntax highlighting -->
|
|
14
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
|
|
15
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
/* ====================================================================
|
|
19
|
+
CSS Reset & Base
|
|
20
|
+
==================================================================== */
|
|
21
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
22
|
+
|
|
23
|
+
:root {
|
|
24
|
+
--bg-primary: #0d1117;
|
|
25
|
+
--bg-secondary: #161b22;
|
|
26
|
+
--bg-tertiary: #1c2128;
|
|
27
|
+
--bg-hover: #1f2937;
|
|
28
|
+
--border: #30363d;
|
|
29
|
+
--border-light: #3d444d;
|
|
30
|
+
--text-primary: #e6edf3;
|
|
31
|
+
--text-secondary: #8b949e;
|
|
32
|
+
--text-muted: #656d76;
|
|
33
|
+
--accent: #58a6ff;
|
|
34
|
+
--accent-hover: #79c0ff;
|
|
35
|
+
--green: #3fb950;
|
|
36
|
+
--purple: #d2a8ff;
|
|
37
|
+
--orange: #ffa657;
|
|
38
|
+
--red: #ff7b72;
|
|
39
|
+
--yellow: #d29922;
|
|
40
|
+
--pink: #f778ba;
|
|
41
|
+
--cyan: #76e3ea;
|
|
42
|
+
--font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
|
|
43
|
+
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
44
|
+
--sidebar-width: 320px;
|
|
45
|
+
--panel-width: 460px;
|
|
46
|
+
--header-height: 52px;
|
|
47
|
+
--radius: 8px;
|
|
48
|
+
--radius-sm: 6px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
html, body {
|
|
52
|
+
height: 100%;
|
|
53
|
+
font-family: var(--font-sans);
|
|
54
|
+
background: var(--bg-primary);
|
|
55
|
+
color: var(--text-primary);
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ====================================================================
|
|
60
|
+
Layout
|
|
61
|
+
==================================================================== */
|
|
62
|
+
#app {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
height: 100vh;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Header */
|
|
69
|
+
#header {
|
|
70
|
+
height: var(--header-height);
|
|
71
|
+
background: var(--bg-secondary);
|
|
72
|
+
border-bottom: 1px solid var(--border);
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
padding: 0 16px;
|
|
76
|
+
gap: 16px;
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
z-index: 10;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#header .logo {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 8px;
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
color: var(--text-primary);
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#header .logo span.icon { font-size: 20px; }
|
|
92
|
+
|
|
93
|
+
#search-container {
|
|
94
|
+
flex: 1;
|
|
95
|
+
max-width: 560px;
|
|
96
|
+
position: relative;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#search-input {
|
|
100
|
+
width: 100%;
|
|
101
|
+
height: 34px;
|
|
102
|
+
background: var(--bg-primary);
|
|
103
|
+
border: 1px solid var(--border);
|
|
104
|
+
border-radius: var(--radius-sm);
|
|
105
|
+
color: var(--text-primary);
|
|
106
|
+
font-size: 13px;
|
|
107
|
+
padding: 0 12px 0 34px;
|
|
108
|
+
outline: none;
|
|
109
|
+
transition: border-color 0.15s;
|
|
110
|
+
font-family: var(--font-sans);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#search-input:focus { border-color: var(--accent); }
|
|
114
|
+
|
|
115
|
+
#search-container .search-icon {
|
|
116
|
+
position: absolute;
|
|
117
|
+
left: 10px;
|
|
118
|
+
top: 50%;
|
|
119
|
+
transform: translateY(-50%);
|
|
120
|
+
color: var(--text-muted);
|
|
121
|
+
font-size: 14px;
|
|
122
|
+
pointer-events: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#search-results-dropdown {
|
|
126
|
+
position: absolute;
|
|
127
|
+
top: 100%;
|
|
128
|
+
left: 0;
|
|
129
|
+
right: 0;
|
|
130
|
+
background: var(--bg-secondary);
|
|
131
|
+
border: 1px solid var(--border);
|
|
132
|
+
border-radius: var(--radius-sm);
|
|
133
|
+
margin-top: 4px;
|
|
134
|
+
max-height: 400px;
|
|
135
|
+
overflow-y: auto;
|
|
136
|
+
z-index: 100;
|
|
137
|
+
display: none;
|
|
138
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#search-results-dropdown.visible { display: block; }
|
|
142
|
+
|
|
143
|
+
.search-result-item {
|
|
144
|
+
padding: 8px 12px;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 8px;
|
|
149
|
+
border-bottom: 1px solid var(--border);
|
|
150
|
+
transition: background 0.1s;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.search-result-item:last-child { border-bottom: none; }
|
|
154
|
+
.search-result-item:hover { background: var(--bg-hover); }
|
|
155
|
+
|
|
156
|
+
.search-result-item .kind-badge {
|
|
157
|
+
font-size: 10px;
|
|
158
|
+
padding: 2px 6px;
|
|
159
|
+
border-radius: 4px;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
text-transform: uppercase;
|
|
162
|
+
white-space: nowrap;
|
|
163
|
+
flex-shrink: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.search-result-item .name {
|
|
167
|
+
font-weight: 500;
|
|
168
|
+
font-size: 13px;
|
|
169
|
+
color: var(--text-primary);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.search-result-item .file-path {
|
|
173
|
+
font-size: 11px;
|
|
174
|
+
color: var(--text-muted);
|
|
175
|
+
margin-left: auto;
|
|
176
|
+
white-space: nowrap;
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
text-overflow: ellipsis;
|
|
179
|
+
max-width: 200px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#header-stats {
|
|
183
|
+
display: flex;
|
|
184
|
+
gap: 12px;
|
|
185
|
+
font-size: 12px;
|
|
186
|
+
color: var(--text-muted);
|
|
187
|
+
white-space: nowrap;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#header-stats .stat { display: flex; align-items: center; gap: 4px; }
|
|
191
|
+
#header-stats .stat-value { color: var(--text-secondary); font-weight: 500; }
|
|
192
|
+
|
|
193
|
+
/* Main content */
|
|
194
|
+
#main {
|
|
195
|
+
display: flex;
|
|
196
|
+
flex: 1;
|
|
197
|
+
overflow: hidden;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Sidebar */
|
|
201
|
+
#sidebar {
|
|
202
|
+
width: var(--sidebar-width);
|
|
203
|
+
background: var(--bg-secondary);
|
|
204
|
+
border-right: 1px solid var(--border);
|
|
205
|
+
display: flex;
|
|
206
|
+
flex-direction: column;
|
|
207
|
+
flex-shrink: 0;
|
|
208
|
+
overflow: hidden;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.sidebar-section {
|
|
212
|
+
border-bottom: 1px solid var(--border);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.sidebar-header {
|
|
216
|
+
padding: 10px 14px;
|
|
217
|
+
font-size: 11px;
|
|
218
|
+
font-weight: 600;
|
|
219
|
+
text-transform: uppercase;
|
|
220
|
+
letter-spacing: 0.5px;
|
|
221
|
+
color: var(--text-muted);
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: space-between;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
user-select: none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.sidebar-header:hover { color: var(--text-secondary); }
|
|
230
|
+
|
|
231
|
+
.sidebar-content {
|
|
232
|
+
overflow-y: auto;
|
|
233
|
+
max-height: 300px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#file-tree {
|
|
237
|
+
padding: 4px 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.file-item {
|
|
241
|
+
padding: 5px 14px;
|
|
242
|
+
font-size: 12px;
|
|
243
|
+
color: var(--text-secondary);
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
gap: 6px;
|
|
248
|
+
transition: background 0.1s;
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
overflow: hidden;
|
|
251
|
+
text-overflow: ellipsis;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.file-item:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
255
|
+
.file-item.active { background: var(--bg-hover); color: var(--accent); }
|
|
256
|
+
|
|
257
|
+
.file-item .file-icon { font-size: 12px; flex-shrink: 0; }
|
|
258
|
+
|
|
259
|
+
/* Graph legend */
|
|
260
|
+
#legend {
|
|
261
|
+
padding: 10px 14px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.legend-item {
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: center;
|
|
267
|
+
gap: 8px;
|
|
268
|
+
padding: 3px 0;
|
|
269
|
+
font-size: 12px;
|
|
270
|
+
color: var(--text-secondary);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.legend-dot {
|
|
274
|
+
width: 10px;
|
|
275
|
+
height: 10px;
|
|
276
|
+
border-radius: 50%;
|
|
277
|
+
flex-shrink: 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* Graph toolbar */
|
|
281
|
+
#graph-toolbar {
|
|
282
|
+
padding: 8px 14px;
|
|
283
|
+
display: flex;
|
|
284
|
+
flex-wrap: wrap;
|
|
285
|
+
gap: 6px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.toolbar-btn {
|
|
289
|
+
padding: 4px 10px;
|
|
290
|
+
font-size: 11px;
|
|
291
|
+
background: var(--bg-primary);
|
|
292
|
+
border: 1px solid var(--border);
|
|
293
|
+
border-radius: 4px;
|
|
294
|
+
color: var(--text-secondary);
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
transition: all 0.15s;
|
|
297
|
+
font-family: var(--font-sans);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.toolbar-btn:hover {
|
|
301
|
+
background: var(--bg-hover);
|
|
302
|
+
color: var(--text-primary);
|
|
303
|
+
border-color: var(--border-light);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.toolbar-btn.active {
|
|
307
|
+
background: var(--accent);
|
|
308
|
+
color: #fff;
|
|
309
|
+
border-color: var(--accent);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* Graph canvas */
|
|
313
|
+
#graph-container {
|
|
314
|
+
flex: 1;
|
|
315
|
+
position: relative;
|
|
316
|
+
background: var(--bg-primary);
|
|
317
|
+
overflow: hidden;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#cy {
|
|
321
|
+
width: 100%;
|
|
322
|
+
height: 100%;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#graph-overlay {
|
|
326
|
+
position: absolute;
|
|
327
|
+
top: 50%;
|
|
328
|
+
left: 50%;
|
|
329
|
+
transform: translate(-50%, -50%);
|
|
330
|
+
text-align: center;
|
|
331
|
+
color: var(--text-muted);
|
|
332
|
+
pointer-events: none;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#graph-overlay .overlay-icon { font-size: 48px; margin-bottom: 12px; }
|
|
336
|
+
#graph-overlay .overlay-title { font-size: 18px; font-weight: 500; margin-bottom: 6px; }
|
|
337
|
+
#graph-overlay .overlay-subtitle { font-size: 13px; }
|
|
338
|
+
|
|
339
|
+
.example-btn {
|
|
340
|
+
background: var(--bg-secondary);
|
|
341
|
+
border: 1px solid var(--border);
|
|
342
|
+
border-radius: 20px;
|
|
343
|
+
color: var(--text-secondary);
|
|
344
|
+
padding: 6px 16px;
|
|
345
|
+
font-size: 12px;
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
transition: all 0.15s;
|
|
348
|
+
font-family: var(--font-sans);
|
|
349
|
+
pointer-events: auto;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.example-btn:hover {
|
|
353
|
+
background: var(--bg-hover);
|
|
354
|
+
color: var(--accent);
|
|
355
|
+
border-color: var(--accent);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* Breadcrumbs */
|
|
359
|
+
#breadcrumbs {
|
|
360
|
+
position: absolute;
|
|
361
|
+
top: 10px;
|
|
362
|
+
left: 10px;
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
gap: 4px;
|
|
366
|
+
font-size: 12px;
|
|
367
|
+
z-index: 5;
|
|
368
|
+
background: var(--bg-secondary);
|
|
369
|
+
border: 1px solid var(--border);
|
|
370
|
+
border-radius: var(--radius-sm);
|
|
371
|
+
padding: 6px 10px;
|
|
372
|
+
opacity: 0;
|
|
373
|
+
transition: opacity 0.2s;
|
|
374
|
+
pointer-events: none;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#breadcrumbs.visible { opacity: 1; pointer-events: auto; }
|
|
378
|
+
|
|
379
|
+
.breadcrumb-item {
|
|
380
|
+
color: var(--accent);
|
|
381
|
+
cursor: pointer;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.breadcrumb-item:hover { text-decoration: underline; }
|
|
385
|
+
.breadcrumb-sep { color: var(--text-muted); }
|
|
386
|
+
|
|
387
|
+
/* Graph controls */
|
|
388
|
+
#graph-controls {
|
|
389
|
+
position: absolute;
|
|
390
|
+
bottom: 14px;
|
|
391
|
+
right: 14px;
|
|
392
|
+
display: flex;
|
|
393
|
+
gap: 4px;
|
|
394
|
+
z-index: 5;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.graph-ctrl-btn {
|
|
398
|
+
width: 32px;
|
|
399
|
+
height: 32px;
|
|
400
|
+
background: var(--bg-secondary);
|
|
401
|
+
border: 1px solid var(--border);
|
|
402
|
+
border-radius: var(--radius-sm);
|
|
403
|
+
color: var(--text-secondary);
|
|
404
|
+
cursor: pointer;
|
|
405
|
+
display: flex;
|
|
406
|
+
align-items: center;
|
|
407
|
+
justify-content: center;
|
|
408
|
+
font-size: 16px;
|
|
409
|
+
transition: all 0.15s;
|
|
410
|
+
font-family: var(--font-sans);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.graph-ctrl-btn:hover {
|
|
414
|
+
background: var(--bg-hover);
|
|
415
|
+
color: var(--text-primary);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* Context menu */
|
|
419
|
+
#context-menu {
|
|
420
|
+
position: fixed;
|
|
421
|
+
background: var(--bg-secondary);
|
|
422
|
+
border: 1px solid var(--border);
|
|
423
|
+
border-radius: var(--radius-sm);
|
|
424
|
+
padding: 4px 0;
|
|
425
|
+
z-index: 200;
|
|
426
|
+
display: none;
|
|
427
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
|
428
|
+
min-width: 180px;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#context-menu.visible { display: block; }
|
|
432
|
+
|
|
433
|
+
.ctx-item {
|
|
434
|
+
padding: 7px 14px;
|
|
435
|
+
font-size: 13px;
|
|
436
|
+
color: var(--text-primary);
|
|
437
|
+
cursor: pointer;
|
|
438
|
+
display: flex;
|
|
439
|
+
align-items: center;
|
|
440
|
+
gap: 8px;
|
|
441
|
+
transition: background 0.1s;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.ctx-item:hover { background: var(--bg-hover); }
|
|
445
|
+
.ctx-item .ctx-icon { font-size: 14px; width: 18px; text-align: center; }
|
|
446
|
+
.ctx-sep { height: 1px; background: var(--border); margin: 4px 0; }
|
|
447
|
+
|
|
448
|
+
/* Detail panel */
|
|
449
|
+
#detail-panel {
|
|
450
|
+
width: 0;
|
|
451
|
+
background: var(--bg-secondary);
|
|
452
|
+
border-left: 1px solid var(--border);
|
|
453
|
+
flex-shrink: 0;
|
|
454
|
+
overflow: hidden;
|
|
455
|
+
transition: width 0.2s ease;
|
|
456
|
+
display: flex;
|
|
457
|
+
flex-direction: column;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
#detail-panel.open { width: var(--panel-width); }
|
|
461
|
+
|
|
462
|
+
#detail-header {
|
|
463
|
+
padding: 12px 16px;
|
|
464
|
+
border-bottom: 1px solid var(--border);
|
|
465
|
+
display: flex;
|
|
466
|
+
align-items: flex-start;
|
|
467
|
+
justify-content: space-between;
|
|
468
|
+
gap: 8px;
|
|
469
|
+
flex-shrink: 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
#detail-header .node-title {
|
|
473
|
+
font-size: 15px;
|
|
474
|
+
font-weight: 600;
|
|
475
|
+
word-break: break-all;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
#detail-header .close-btn {
|
|
479
|
+
background: none;
|
|
480
|
+
border: none;
|
|
481
|
+
color: var(--text-muted);
|
|
482
|
+
cursor: pointer;
|
|
483
|
+
font-size: 18px;
|
|
484
|
+
padding: 0 4px;
|
|
485
|
+
line-height: 1;
|
|
486
|
+
flex-shrink: 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#detail-header .close-btn:hover { color: var(--text-primary); }
|
|
490
|
+
|
|
491
|
+
#detail-body {
|
|
492
|
+
flex: 1;
|
|
493
|
+
overflow-y: auto;
|
|
494
|
+
padding: 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.detail-section {
|
|
498
|
+
padding: 12px 16px;
|
|
499
|
+
border-bottom: 1px solid var(--border);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.detail-section-title {
|
|
503
|
+
font-size: 11px;
|
|
504
|
+
font-weight: 600;
|
|
505
|
+
text-transform: uppercase;
|
|
506
|
+
letter-spacing: 0.5px;
|
|
507
|
+
color: var(--text-muted);
|
|
508
|
+
margin-bottom: 8px;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.detail-meta {
|
|
512
|
+
display: grid;
|
|
513
|
+
grid-template-columns: auto 1fr;
|
|
514
|
+
gap: 4px 12px;
|
|
515
|
+
font-size: 12px;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.detail-meta .label { color: var(--text-muted); }
|
|
519
|
+
.detail-meta .value { color: var(--text-secondary); word-break: break-all; }
|
|
520
|
+
.detail-meta .value.accent { color: var(--accent); }
|
|
521
|
+
|
|
522
|
+
/* Code block */
|
|
523
|
+
.code-block {
|
|
524
|
+
background: var(--bg-primary);
|
|
525
|
+
border-radius: var(--radius-sm);
|
|
526
|
+
overflow-x: auto;
|
|
527
|
+
font-size: 12px;
|
|
528
|
+
line-height: 1.5;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.code-block pre {
|
|
532
|
+
margin: 0;
|
|
533
|
+
padding: 12px;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.code-block code {
|
|
537
|
+
font-family: var(--font-mono);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/* Relations list */
|
|
541
|
+
.relation-list { list-style: none; }
|
|
542
|
+
|
|
543
|
+
.relation-item {
|
|
544
|
+
padding: 5px 0;
|
|
545
|
+
font-size: 12px;
|
|
546
|
+
display: flex;
|
|
547
|
+
align-items: center;
|
|
548
|
+
gap: 6px;
|
|
549
|
+
cursor: pointer;
|
|
550
|
+
transition: color 0.1s;
|
|
551
|
+
color: var(--text-secondary);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.relation-item:hover { color: var(--accent); }
|
|
555
|
+
|
|
556
|
+
.relation-item .rel-badge {
|
|
557
|
+
font-size: 9px;
|
|
558
|
+
padding: 1px 5px;
|
|
559
|
+
border-radius: 3px;
|
|
560
|
+
font-weight: 600;
|
|
561
|
+
text-transform: uppercase;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/* Loading spinner */
|
|
565
|
+
.spinner {
|
|
566
|
+
display: inline-block;
|
|
567
|
+
width: 16px;
|
|
568
|
+
height: 16px;
|
|
569
|
+
border: 2px solid var(--border);
|
|
570
|
+
border-top-color: var(--accent);
|
|
571
|
+
border-radius: 50%;
|
|
572
|
+
animation: spin 0.6s linear infinite;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
576
|
+
|
|
577
|
+
/* Scrollbar */
|
|
578
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
579
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
580
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
581
|
+
::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
|
|
582
|
+
|
|
583
|
+
/* Tooltip */
|
|
584
|
+
.cy-tooltip {
|
|
585
|
+
position: fixed;
|
|
586
|
+
background: var(--bg-secondary);
|
|
587
|
+
border: 1px solid var(--border);
|
|
588
|
+
border-radius: var(--radius-sm);
|
|
589
|
+
padding: 6px 10px;
|
|
590
|
+
font-size: 12px;
|
|
591
|
+
color: var(--text-primary);
|
|
592
|
+
pointer-events: none;
|
|
593
|
+
z-index: 50;
|
|
594
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
595
|
+
max-width: 300px;
|
|
596
|
+
display: none;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.cy-tooltip .tip-kind {
|
|
600
|
+
font-size: 10px;
|
|
601
|
+
color: var(--text-muted);
|
|
602
|
+
text-transform: uppercase;
|
|
603
|
+
margin-bottom: 2px;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.cy-tooltip .tip-name { font-weight: 500; }
|
|
607
|
+
.cy-tooltip .tip-file { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
|
608
|
+
|
|
609
|
+
/* Notification toast */
|
|
610
|
+
#toast {
|
|
611
|
+
position: fixed;
|
|
612
|
+
bottom: 20px;
|
|
613
|
+
left: 50%;
|
|
614
|
+
transform: translateX(-50%) translateY(80px);
|
|
615
|
+
background: var(--bg-secondary);
|
|
616
|
+
border: 1px solid var(--border);
|
|
617
|
+
border-radius: var(--radius-sm);
|
|
618
|
+
padding: 10px 20px;
|
|
619
|
+
font-size: 13px;
|
|
620
|
+
color: var(--text-primary);
|
|
621
|
+
z-index: 300;
|
|
622
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
623
|
+
transition: transform 0.3s ease;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
#toast.visible { transform: translateX(-50%) translateY(0); }
|
|
627
|
+
|
|
628
|
+
/* Embeddings setup dialog */
|
|
629
|
+
#dialog-overlay {
|
|
630
|
+
position: fixed;
|
|
631
|
+
inset: 0;
|
|
632
|
+
background: rgba(0,0,0,0.7);
|
|
633
|
+
z-index: 500;
|
|
634
|
+
display: none;
|
|
635
|
+
align-items: center;
|
|
636
|
+
justify-content: center;
|
|
637
|
+
backdrop-filter: blur(4px);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
#dialog-overlay.visible { display: flex; }
|
|
641
|
+
|
|
642
|
+
#dialog {
|
|
643
|
+
background: var(--bg-secondary);
|
|
644
|
+
border: 1px solid var(--border);
|
|
645
|
+
border-radius: 12px;
|
|
646
|
+
padding: 32px;
|
|
647
|
+
max-width: 480px;
|
|
648
|
+
width: 90%;
|
|
649
|
+
box-shadow: 0 16px 48px rgba(0,0,0,0.5);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
#dialog .dialog-icon { font-size: 36px; margin-bottom: 16px; }
|
|
653
|
+
|
|
654
|
+
#dialog .dialog-title {
|
|
655
|
+
font-size: 18px;
|
|
656
|
+
font-weight: 600;
|
|
657
|
+
margin-bottom: 8px;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
#dialog .dialog-body {
|
|
661
|
+
font-size: 13px;
|
|
662
|
+
color: var(--text-secondary);
|
|
663
|
+
line-height: 1.6;
|
|
664
|
+
margin-bottom: 20px;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
#dialog .dialog-body strong { color: var(--text-primary); }
|
|
668
|
+
|
|
669
|
+
#dialog-progress {
|
|
670
|
+
display: none;
|
|
671
|
+
margin-bottom: 20px;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
#dialog-progress .progress-bar-track {
|
|
675
|
+
width: 100%;
|
|
676
|
+
height: 8px;
|
|
677
|
+
background: var(--bg-primary);
|
|
678
|
+
border-radius: 4px;
|
|
679
|
+
overflow: hidden;
|
|
680
|
+
margin-bottom: 8px;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#dialog-progress .progress-bar-fill {
|
|
684
|
+
height: 100%;
|
|
685
|
+
background: var(--accent);
|
|
686
|
+
border-radius: 4px;
|
|
687
|
+
width: 0%;
|
|
688
|
+
transition: width 0.2s ease;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
#dialog-progress .progress-text {
|
|
692
|
+
font-size: 12px;
|
|
693
|
+
color: var(--text-muted);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
#dialog-progress .progress-percent {
|
|
697
|
+
float: right;
|
|
698
|
+
color: var(--text-secondary);
|
|
699
|
+
font-weight: 500;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
#dialog .dialog-actions {
|
|
703
|
+
display: flex;
|
|
704
|
+
gap: 10px;
|
|
705
|
+
justify-content: flex-end;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
#dialog .btn {
|
|
709
|
+
padding: 8px 20px;
|
|
710
|
+
border-radius: var(--radius-sm);
|
|
711
|
+
font-size: 13px;
|
|
712
|
+
font-weight: 500;
|
|
713
|
+
cursor: pointer;
|
|
714
|
+
border: 1px solid var(--border);
|
|
715
|
+
transition: all 0.15s;
|
|
716
|
+
font-family: var(--font-sans);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
#dialog .btn-primary {
|
|
720
|
+
background: var(--accent);
|
|
721
|
+
color: #fff;
|
|
722
|
+
border-color: var(--accent);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
#dialog .btn-primary:hover { background: var(--accent-hover); }
|
|
726
|
+
#dialog .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
727
|
+
|
|
728
|
+
#dialog .btn-secondary {
|
|
729
|
+
background: var(--bg-primary);
|
|
730
|
+
color: var(--text-secondary);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
#dialog .btn-secondary:hover { color: var(--text-primary); }
|
|
734
|
+
</style>
|
|
735
|
+
</head>
|
|
736
|
+
<body>
|
|
737
|
+
<div id="app">
|
|
738
|
+
<!-- Header -->
|
|
739
|
+
<div id="header">
|
|
740
|
+
<div class="logo">
|
|
741
|
+
<span class="icon">🔮</span>
|
|
742
|
+
<span>CodeGraph</span>
|
|
743
|
+
</div>
|
|
744
|
+
|
|
745
|
+
<div id="search-container">
|
|
746
|
+
<span class="search-icon">🔍</span>
|
|
747
|
+
<input id="search-input" type="text" placeholder="Search symbols... (Ctrl+K)" autocomplete="off" spellcheck="false" />
|
|
748
|
+
<div id="search-results-dropdown"></div>
|
|
749
|
+
</div>
|
|
750
|
+
|
|
751
|
+
<div id="header-stats">
|
|
752
|
+
<div class="stat"><span>Nodes:</span> <span class="stat-value" id="stat-nodes">-</span></div>
|
|
753
|
+
<div class="stat"><span>Edges:</span> <span class="stat-value" id="stat-edges">-</span></div>
|
|
754
|
+
<div class="stat"><span>Files:</span> <span class="stat-value" id="stat-files">-</span></div>
|
|
755
|
+
</div>
|
|
756
|
+
</div>
|
|
757
|
+
|
|
758
|
+
<!-- Main content -->
|
|
759
|
+
<div id="main">
|
|
760
|
+
<!-- Sidebar -->
|
|
761
|
+
<div id="sidebar">
|
|
762
|
+
<!-- Graph controls section -->
|
|
763
|
+
<div class="sidebar-section">
|
|
764
|
+
<div class="sidebar-header">
|
|
765
|
+
<span>Graph Actions</span>
|
|
766
|
+
</div>
|
|
767
|
+
<div id="graph-toolbar">
|
|
768
|
+
<button class="toolbar-btn" onclick="loadOverview()" title="Show top-level symbols">Overview</button>
|
|
769
|
+
<button class="toolbar-btn" onclick="clearGraph()" title="Clear the graph">Clear</button>
|
|
770
|
+
<button class="toolbar-btn" onclick="runLayout()" title="Re-run layout">Layout</button>
|
|
771
|
+
<button class="toolbar-btn" onclick="fitGraph()" title="Fit graph to view">Fit</button>
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
|
|
775
|
+
<!-- Legend -->
|
|
776
|
+
<div class="sidebar-section">
|
|
777
|
+
<div class="sidebar-header" onclick="toggleSection(this)">
|
|
778
|
+
<span>Legend</span>
|
|
779
|
+
<span>▶</span>
|
|
780
|
+
</div>
|
|
781
|
+
<div class="sidebar-content" id="legend" style="display:none;"></div>
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<!-- Files -->
|
|
785
|
+
<div class="sidebar-section" style="flex:1; overflow:hidden; display:flex; flex-direction:column;">
|
|
786
|
+
<div class="sidebar-header" onclick="toggleSection(this)">
|
|
787
|
+
<span>Files</span>
|
|
788
|
+
<span>▼</span>
|
|
789
|
+
</div>
|
|
790
|
+
<div class="sidebar-content" id="file-tree" style="flex:1; max-height:none;"></div>
|
|
791
|
+
</div>
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
<!-- Graph area -->
|
|
795
|
+
<div id="graph-container">
|
|
796
|
+
<div id="cy"></div>
|
|
797
|
+
<div id="graph-overlay">
|
|
798
|
+
<div class="overlay-icon">🔮</div>
|
|
799
|
+
<div class="overlay-title">Search for a starting point</div>
|
|
800
|
+
<div class="overlay-subtitle">Type a symbol name, pick it, and trace its call chain</div>
|
|
801
|
+
</div>
|
|
802
|
+
<div id="breadcrumbs"></div>
|
|
803
|
+
<div id="graph-controls">
|
|
804
|
+
<button class="graph-ctrl-btn" onclick="cy.zoom(cy.zoom() * 1.3); cy.center()" title="Zoom in">+</button>
|
|
805
|
+
<button class="graph-ctrl-btn" onclick="cy.zoom(cy.zoom() / 1.3); cy.center()" title="Zoom out">−</button>
|
|
806
|
+
<button class="graph-ctrl-btn" onclick="fitGraph()" title="Fit to view">⤢</button>
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
|
|
810
|
+
<!-- Detail panel -->
|
|
811
|
+
<div id="detail-panel">
|
|
812
|
+
<div id="detail-header">
|
|
813
|
+
<div>
|
|
814
|
+
<div class="node-title" id="detail-title">-</div>
|
|
815
|
+
</div>
|
|
816
|
+
<button class="close-btn" onclick="closeDetailPanel()">×</button>
|
|
817
|
+
</div>
|
|
818
|
+
<div id="detail-body"></div>
|
|
819
|
+
</div>
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
822
|
+
|
|
823
|
+
<!-- Embeddings setup dialog -->
|
|
824
|
+
<div id="dialog-overlay">
|
|
825
|
+
<div id="dialog">
|
|
826
|
+
<div class="dialog-icon">🧠</div>
|
|
827
|
+
<div class="dialog-title" id="dialog-title">Enable Semantic Search</div>
|
|
828
|
+
<div class="dialog-body" id="dialog-body">
|
|
829
|
+
CodeGraph Explorer uses <strong>semantic embeddings</strong> to understand your code by meaning, not just keywords.
|
|
830
|
+
This lets you ask questions like "how does authentication work?" and get accurate results.
|
|
831
|
+
<br><br>
|
|
832
|
+
This is a <strong>one-time setup</strong> that generates a local embedding model for this project. No data leaves your machine.
|
|
833
|
+
</div>
|
|
834
|
+
<div id="dialog-progress">
|
|
835
|
+
<div class="progress-bar-track">
|
|
836
|
+
<div class="progress-bar-fill" id="dialog-progress-fill"></div>
|
|
837
|
+
</div>
|
|
838
|
+
<div class="progress-text">
|
|
839
|
+
<span id="dialog-progress-text">Preparing...</span>
|
|
840
|
+
<span class="progress-percent" id="dialog-progress-percent">0%</span>
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
843
|
+
<div class="dialog-actions" id="dialog-actions">
|
|
844
|
+
<button class="btn btn-secondary" id="dialog-skip" onclick="closeDialog()">Skip for now</button>
|
|
845
|
+
<button class="btn btn-primary" id="dialog-enable" onclick="startEmbeddings()">Enable Semantic Search</button>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
|
|
850
|
+
<!-- Context menu -->
|
|
851
|
+
<div id="context-menu">
|
|
852
|
+
<div class="ctx-item" onclick="ctxAction('expand-callees')"><span class="ctx-icon">→</span> Expand Callees</div>
|
|
853
|
+
<div class="ctx-item" onclick="ctxAction('expand-callers')"><span class="ctx-icon">←</span> Expand Callers</div>
|
|
854
|
+
<div class="ctx-sep"></div>
|
|
855
|
+
<div class="ctx-item" onclick="ctxAction('callgraph')"><span class="ctx-icon">🌐</span> Full Call Graph</div>
|
|
856
|
+
<div class="ctx-item" onclick="ctxAction('impact')"><span class="ctx-icon">💥</span> Impact Analysis</div>
|
|
857
|
+
<div class="ctx-sep"></div>
|
|
858
|
+
<div class="ctx-item" onclick="ctxAction('children')"><span class="ctx-icon">📂</span> Show Children</div>
|
|
859
|
+
<div class="ctx-item" onclick="ctxAction('details')"><span class="ctx-icon">📋</span> View Details</div>
|
|
860
|
+
<div class="ctx-sep"></div>
|
|
861
|
+
<div class="ctx-item" onclick="ctxAction('remove')"><span class="ctx-icon">✖</span> Remove from Graph</div>
|
|
862
|
+
</div>
|
|
863
|
+
|
|
864
|
+
<!-- Tooltip -->
|
|
865
|
+
<div class="cy-tooltip" id="tooltip"></div>
|
|
866
|
+
|
|
867
|
+
<!-- Toast -->
|
|
868
|
+
<div id="toast"></div>
|
|
869
|
+
|
|
870
|
+
<script>
|
|
871
|
+
// ====================================================================
|
|
872
|
+
// State
|
|
873
|
+
// ====================================================================
|
|
874
|
+
let cy;
|
|
875
|
+
let ctxNodeId = null;
|
|
876
|
+
let searchDebounce = null;
|
|
877
|
+
const expandedSets = { callers: new Set(), callees: new Set() };
|
|
878
|
+
|
|
879
|
+
// Node kind → color mapping
|
|
880
|
+
const kindColors = {
|
|
881
|
+
'function': '#79c0ff',
|
|
882
|
+
'method': '#7ee787',
|
|
883
|
+
'class': '#d2a8ff',
|
|
884
|
+
'interface': '#ffa657',
|
|
885
|
+
'struct': '#ffa657',
|
|
886
|
+
'trait': '#ffa657',
|
|
887
|
+
'protocol': '#ffa657',
|
|
888
|
+
'component': '#f778ba',
|
|
889
|
+
'enum': '#d29922',
|
|
890
|
+
'enum_member': '#d29922',
|
|
891
|
+
'type_alias': '#d2a8ff',
|
|
892
|
+
'variable': '#ff7b72',
|
|
893
|
+
'constant': '#ff7b72',
|
|
894
|
+
'property': '#76e3ea',
|
|
895
|
+
'field': '#76e3ea',
|
|
896
|
+
'file': '#8b949e',
|
|
897
|
+
'module': '#8b949e',
|
|
898
|
+
'namespace': '#8b949e',
|
|
899
|
+
'import': '#f0883e',
|
|
900
|
+
'export': '#3fb950',
|
|
901
|
+
'route': '#f778ba',
|
|
902
|
+
'parameter': '#8b949e',
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
const kindShapes = {
|
|
906
|
+
'class': 'round-rectangle',
|
|
907
|
+
'interface': 'round-diamond',
|
|
908
|
+
'struct': 'round-rectangle',
|
|
909
|
+
'trait': 'round-diamond',
|
|
910
|
+
'protocol': 'round-diamond',
|
|
911
|
+
'enum': 'round-hexagon',
|
|
912
|
+
'component': 'round-pentagon',
|
|
913
|
+
'file': 'round-rectangle',
|
|
914
|
+
'module': 'round-rectangle',
|
|
915
|
+
'namespace': 'round-rectangle',
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
const edgeColors = {
|
|
919
|
+
'calls': '#58a6ff',
|
|
920
|
+
'imports': '#f0883e',
|
|
921
|
+
'extends': '#d2a8ff',
|
|
922
|
+
'implements': '#ffa657',
|
|
923
|
+
'references': '#8b949e',
|
|
924
|
+
'contains': '#3d444d',
|
|
925
|
+
'type_of': '#76e3ea',
|
|
926
|
+
'returns': '#76e3ea',
|
|
927
|
+
'instantiates': '#f778ba',
|
|
928
|
+
'overrides': '#d29922',
|
|
929
|
+
'decorates': '#f778ba',
|
|
930
|
+
'exports': '#3fb950',
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// ====================================================================
|
|
934
|
+
// API Client
|
|
935
|
+
// ====================================================================
|
|
936
|
+
const api = {
|
|
937
|
+
async get(path) {
|
|
938
|
+
const res = await fetch('/api/' + path);
|
|
939
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
940
|
+
return res.json();
|
|
941
|
+
},
|
|
942
|
+
embeddingsStatus: () => api.get('embeddings/status'),
|
|
943
|
+
status: () => api.get('status'),
|
|
944
|
+
search: (q, kind, limit) => api.get(`search?q=${encodeURIComponent(q)}${kind ? '&kind='+kind : ''}&limit=${limit||30}`),
|
|
945
|
+
explore: (q) => api.get(`explore?q=${encodeURIComponent(q)}`),
|
|
946
|
+
overview: (limit) => api.get(`overview?limit=${limit||60}`),
|
|
947
|
+
files: () => api.get('files'),
|
|
948
|
+
fileNodes: (p) => api.get(`file-nodes?path=${encodeURIComponent(p)}`),
|
|
949
|
+
node: (id) => api.get(`node/${encodeURIComponent(id)}`),
|
|
950
|
+
callers: (id, d) => api.get(`node/${encodeURIComponent(id)}/callers?depth=${d||1}`),
|
|
951
|
+
callees: (id, d) => api.get(`node/${encodeURIComponent(id)}/callees?depth=${d||1}`),
|
|
952
|
+
children: (id) => api.get(`node/${encodeURIComponent(id)}/children`),
|
|
953
|
+
impact: (id, d) => api.get(`node/${encodeURIComponent(id)}/impact?depth=${d||2}`),
|
|
954
|
+
callgraph: (id, d) => api.get(`node/${encodeURIComponent(id)}/callgraph?depth=${d||2}`),
|
|
955
|
+
context: (id) => api.get(`node/${encodeURIComponent(id)}/context`),
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// ====================================================================
|
|
959
|
+
// Cytoscape Initialization
|
|
960
|
+
// ====================================================================
|
|
961
|
+
function initCytoscape() {
|
|
962
|
+
cy = cytoscape({
|
|
963
|
+
container: document.getElementById('cy'),
|
|
964
|
+
style: [
|
|
965
|
+
// Nodes
|
|
966
|
+
{
|
|
967
|
+
selector: 'node',
|
|
968
|
+
style: {
|
|
969
|
+
'label': 'data(label)',
|
|
970
|
+
'text-valign': 'center',
|
|
971
|
+
'text-halign': 'center',
|
|
972
|
+
'font-size': '12px',
|
|
973
|
+
'font-weight': 'bold',
|
|
974
|
+
'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
975
|
+
'color': '#ffffff',
|
|
976
|
+
'text-outline-color': '#000000',
|
|
977
|
+
'text-outline-width': 2,
|
|
978
|
+
'text-outline-opacity': 0.6,
|
|
979
|
+
'background-color': 'data(color)',
|
|
980
|
+
'background-opacity': 0.85,
|
|
981
|
+
'border-width': 2,
|
|
982
|
+
'border-color': 'data(color)',
|
|
983
|
+
'border-opacity': 0.7,
|
|
984
|
+
'width': 'label',
|
|
985
|
+
'height': 'label',
|
|
986
|
+
'padding': '12px',
|
|
987
|
+
'shape': 'data(shape)',
|
|
988
|
+
'text-wrap': 'wrap',
|
|
989
|
+
'text-max-width': '180px',
|
|
990
|
+
'transition-property': 'background-opacity, border-color, border-opacity, opacity, text-opacity',
|
|
991
|
+
'transition-duration': '0.2s',
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
// Selected node
|
|
995
|
+
{
|
|
996
|
+
selector: 'node:selected',
|
|
997
|
+
style: {
|
|
998
|
+
'border-width': 3,
|
|
999
|
+
'border-color': '#ffffff',
|
|
1000
|
+
'border-opacity': 1,
|
|
1001
|
+
'background-opacity': 1,
|
|
1002
|
+
'z-index': 10,
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
// Hovered node
|
|
1006
|
+
{
|
|
1007
|
+
selector: 'node.hover',
|
|
1008
|
+
style: {
|
|
1009
|
+
'border-width': 3,
|
|
1010
|
+
'border-color': '#ffffff',
|
|
1011
|
+
'border-opacity': 0.9,
|
|
1012
|
+
'background-opacity': 1,
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
// Faded node — keep text readable
|
|
1016
|
+
{
|
|
1017
|
+
selector: 'node.faded',
|
|
1018
|
+
style: {
|
|
1019
|
+
'background-opacity': 0.3,
|
|
1020
|
+
'border-opacity': 0.2,
|
|
1021
|
+
'text-opacity': 0.7,
|
|
1022
|
+
}
|
|
1023
|
+
},
|
|
1024
|
+
// Highlighted node
|
|
1025
|
+
{
|
|
1026
|
+
selector: 'node.highlighted',
|
|
1027
|
+
style: {
|
|
1028
|
+
'border-width': 3,
|
|
1029
|
+
'border-color': '#f0e68c',
|
|
1030
|
+
'border-opacity': 1,
|
|
1031
|
+
'z-index': 10,
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
// Edges
|
|
1035
|
+
{
|
|
1036
|
+
selector: 'edge',
|
|
1037
|
+
style: {
|
|
1038
|
+
'width': 1.5,
|
|
1039
|
+
'line-color': 'data(color)',
|
|
1040
|
+
'target-arrow-color': 'data(color)',
|
|
1041
|
+
'target-arrow-shape': 'triangle',
|
|
1042
|
+
'arrow-scale': 0.8,
|
|
1043
|
+
'curve-style': 'bezier',
|
|
1044
|
+
'opacity': 0.6,
|
|
1045
|
+
'label': 'data(label)',
|
|
1046
|
+
'font-size': '9px',
|
|
1047
|
+
'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
1048
|
+
'color': '#656d76',
|
|
1049
|
+
'text-rotation': 'autorotate',
|
|
1050
|
+
'text-margin-y': -8,
|
|
1051
|
+
'text-outline-color': '#0d1117',
|
|
1052
|
+
'text-outline-width': 2,
|
|
1053
|
+
'transition-property': 'opacity, line-color',
|
|
1054
|
+
'transition-duration': '0.15s',
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
// Selected edge
|
|
1058
|
+
{
|
|
1059
|
+
selector: 'edge:selected',
|
|
1060
|
+
style: { 'opacity': 1, 'width': 2.5 }
|
|
1061
|
+
},
|
|
1062
|
+
// Faded edge
|
|
1063
|
+
{
|
|
1064
|
+
selector: 'edge.faded',
|
|
1065
|
+
style: { 'opacity': 0.15 }
|
|
1066
|
+
},
|
|
1067
|
+
// Highlighted edge
|
|
1068
|
+
{
|
|
1069
|
+
selector: 'edge.highlighted',
|
|
1070
|
+
style: { 'opacity': 1, 'width': 2.5 }
|
|
1071
|
+
},
|
|
1072
|
+
],
|
|
1073
|
+
layout: { name: 'preset' },
|
|
1074
|
+
minZoom: 0.1,
|
|
1075
|
+
maxZoom: 4,
|
|
1076
|
+
wheelSensitivity: 0.3,
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// Event handlers
|
|
1080
|
+
cy.on('tap', 'node', (e) => {
|
|
1081
|
+
const nodeId = e.target.data('nodeId');
|
|
1082
|
+
if (nodeId) showNodeDetails(nodeId);
|
|
1083
|
+
highlightNeighborhood(e.target);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
cy.on('cxttap', 'node', (e) => {
|
|
1087
|
+
e.originalEvent.preventDefault();
|
|
1088
|
+
ctxNodeId = e.target.data('nodeId');
|
|
1089
|
+
showContextMenu(e.originalEvent.clientX, e.originalEvent.clientY);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
cy.on('tap', (e) => {
|
|
1093
|
+
if (e.target === cy) {
|
|
1094
|
+
clearHighlights();
|
|
1095
|
+
hideContextMenu();
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
cy.on('mouseover', 'node', (e) => {
|
|
1100
|
+
e.target.addClass('hover');
|
|
1101
|
+
showTooltip(e);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
cy.on('mouseout', 'node', (e) => {
|
|
1105
|
+
e.target.removeClass('hover');
|
|
1106
|
+
hideTooltip();
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
cy.on('dblclick', 'node', (e) => {
|
|
1110
|
+
const nodeId = e.target.data('nodeId');
|
|
1111
|
+
if (nodeId) expandCallees(nodeId);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Click outside to close context menu
|
|
1115
|
+
document.addEventListener('click', (e) => {
|
|
1116
|
+
if (!e.target.closest('#context-menu')) hideContextMenu();
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
document.addEventListener('contextmenu', (e) => {
|
|
1120
|
+
if (e.target.closest('#cy')) e.preventDefault();
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// ====================================================================
|
|
1125
|
+
// Graph Operations
|
|
1126
|
+
// ====================================================================
|
|
1127
|
+
const kindLabels = {
|
|
1128
|
+
'function': 'fn', 'method': 'method', 'class': 'class', 'interface': 'iface',
|
|
1129
|
+
'component': 'comp', 'route': 'route', 'enum': 'enum', 'type_alias': 'type',
|
|
1130
|
+
'struct': 'struct', 'trait': 'trait', 'variable': 'var', 'constant': 'const',
|
|
1131
|
+
'property': 'prop', 'field': 'field', 'file': 'file', 'module': 'mod',
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
function addNodeToGraph(node) {
|
|
1135
|
+
if (cy.getElementById(node.id).length > 0) return;
|
|
1136
|
+
const color = kindColors[node.kind] || '#8b949e';
|
|
1137
|
+
const shape = kindShapes[node.kind] || 'round-rectangle';
|
|
1138
|
+
const kindLabel = kindLabels[node.kind] || node.kind;
|
|
1139
|
+
cy.add({
|
|
1140
|
+
group: 'nodes',
|
|
1141
|
+
data: {
|
|
1142
|
+
id: node.id,
|
|
1143
|
+
nodeId: node.id,
|
|
1144
|
+
label: `${node.name}\n${kindLabel}`,
|
|
1145
|
+
color: color,
|
|
1146
|
+
shape: shape,
|
|
1147
|
+
kind: node.kind,
|
|
1148
|
+
filePath: node.filePath,
|
|
1149
|
+
signature: node.signature || '',
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function addEdgeToGraph(edge) {
|
|
1155
|
+
const edgeId = `${edge.source}-${edge.kind}-${edge.target}`;
|
|
1156
|
+
if (cy.getElementById(edgeId).length > 0) return;
|
|
1157
|
+
// Don't add edge if source or target not in graph
|
|
1158
|
+
if (cy.getElementById(edge.source).length === 0 || cy.getElementById(edge.target).length === 0) return;
|
|
1159
|
+
const color = edgeColors[edge.kind] || '#8b949e';
|
|
1160
|
+
cy.add({
|
|
1161
|
+
group: 'edges',
|
|
1162
|
+
data: {
|
|
1163
|
+
id: edgeId,
|
|
1164
|
+
source: edge.source,
|
|
1165
|
+
target: edge.target,
|
|
1166
|
+
kind: edge.kind,
|
|
1167
|
+
label: edge.kind,
|
|
1168
|
+
color: color,
|
|
1169
|
+
},
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function addSubgraph(nodes, edges) {
|
|
1174
|
+
const batchElements = [];
|
|
1175
|
+
for (const node of nodes) {
|
|
1176
|
+
if (cy.getElementById(node.id).length > 0) continue;
|
|
1177
|
+
const color = kindColors[node.kind] || '#8b949e';
|
|
1178
|
+
const shape = kindShapes[node.kind] || 'round-rectangle';
|
|
1179
|
+
batchElements.push({
|
|
1180
|
+
group: 'nodes',
|
|
1181
|
+
data: {
|
|
1182
|
+
id: node.id,
|
|
1183
|
+
nodeId: node.id,
|
|
1184
|
+
label: node.name,
|
|
1185
|
+
color: color,
|
|
1186
|
+
shape: shape,
|
|
1187
|
+
kind: node.kind,
|
|
1188
|
+
filePath: node.filePath,
|
|
1189
|
+
signature: node.signature || '',
|
|
1190
|
+
},
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
for (const edge of edges) {
|
|
1194
|
+
const edgeId = `${edge.source}-${edge.kind}-${edge.target}`;
|
|
1195
|
+
if (cy.getElementById(edgeId).length > 0) continue;
|
|
1196
|
+
// Check source/target will exist
|
|
1197
|
+
const srcExists = cy.getElementById(edge.source).length > 0 || batchElements.some(e => e.data.id === edge.source);
|
|
1198
|
+
const tgtExists = cy.getElementById(edge.target).length > 0 || batchElements.some(e => e.data.id === edge.target);
|
|
1199
|
+
if (!srcExists || !tgtExists) continue;
|
|
1200
|
+
const color = edgeColors[edge.kind] || '#8b949e';
|
|
1201
|
+
batchElements.push({
|
|
1202
|
+
group: 'edges',
|
|
1203
|
+
data: {
|
|
1204
|
+
id: edgeId,
|
|
1205
|
+
source: edge.source,
|
|
1206
|
+
target: edge.target,
|
|
1207
|
+
kind: edge.kind,
|
|
1208
|
+
label: edge.kind,
|
|
1209
|
+
color: color,
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
if (batchElements.length > 0) {
|
|
1214
|
+
cy.add(batchElements);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function clearGraph() {
|
|
1219
|
+
cy.elements().remove();
|
|
1220
|
+
expandedSets.callers.clear();
|
|
1221
|
+
expandedSets.callees.clear();
|
|
1222
|
+
hideOverlay(false);
|
|
1223
|
+
closeDetailPanel();
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function runLayout() {
|
|
1227
|
+
if (cy.nodes().length === 0) return;
|
|
1228
|
+
const layout = cy.layout({
|
|
1229
|
+
name: 'dagre',
|
|
1230
|
+
rankDir: 'LR',
|
|
1231
|
+
nodeSep: 50,
|
|
1232
|
+
rankSep: 80,
|
|
1233
|
+
edgeSep: 20,
|
|
1234
|
+
animate: true,
|
|
1235
|
+
animationDuration: 300,
|
|
1236
|
+
fit: true,
|
|
1237
|
+
padding: 40,
|
|
1238
|
+
});
|
|
1239
|
+
layout.run();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function fitGraph() {
|
|
1243
|
+
if (cy.nodes().length > 0) {
|
|
1244
|
+
cy.animate({ fit: { eles: cy.elements(), padding: 40 } }, { duration: 300 });
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function highlightNeighborhood(node) {
|
|
1249
|
+
clearHighlights();
|
|
1250
|
+
const neighborhood = node.closedNeighborhood();
|
|
1251
|
+
cy.elements().not(neighborhood).addClass('faded');
|
|
1252
|
+
neighborhood.edges().addClass('highlighted');
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function clearHighlights() {
|
|
1256
|
+
cy.elements().removeClass('faded highlighted');
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function hideOverlay(hide = true) {
|
|
1260
|
+
const overlay = document.getElementById('graph-overlay');
|
|
1261
|
+
overlay.style.display = hide ? 'none' : 'block';
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// ====================================================================
|
|
1265
|
+
// Data Loading
|
|
1266
|
+
// ====================================================================
|
|
1267
|
+
async function loadOverview() {
|
|
1268
|
+
showToast('Loading overview...');
|
|
1269
|
+
try {
|
|
1270
|
+
const data = await api.overview(60);
|
|
1271
|
+
if (data.nodes.length === 0) {
|
|
1272
|
+
showToast('No symbols found. Is the project indexed?');
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
clearGraph();
|
|
1276
|
+
hideOverlay();
|
|
1277
|
+
for (const node of data.nodes) addNodeToGraph(node);
|
|
1278
|
+
runLayout();
|
|
1279
|
+
showToast(`Loaded ${data.nodes.length} symbols`);
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
showToast('Error: ' + err.message);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
async function expandCallers(nodeId) {
|
|
1286
|
+
if (expandedSets.callers.has(nodeId)) return;
|
|
1287
|
+
expandedSets.callers.add(nodeId);
|
|
1288
|
+
try {
|
|
1289
|
+
const data = await api.callers(nodeId, 1);
|
|
1290
|
+
if (data.items.length === 0) {
|
|
1291
|
+
showToast('No callers found');
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
for (const item of data.items) {
|
|
1295
|
+
addNodeToGraph(item.node);
|
|
1296
|
+
addEdgeToGraph(item.edge);
|
|
1297
|
+
}
|
|
1298
|
+
runLayout();
|
|
1299
|
+
showToast(`Found ${data.items.length} callers`);
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
showToast('Error: ' + err.message);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function expandCallees(nodeId) {
|
|
1306
|
+
if (expandedSets.callees.has(nodeId)) return;
|
|
1307
|
+
expandedSets.callees.add(nodeId);
|
|
1308
|
+
try {
|
|
1309
|
+
const data = await api.callees(nodeId, 1);
|
|
1310
|
+
if (data.items.length === 0) {
|
|
1311
|
+
showToast('No callees found');
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
for (const item of data.items) {
|
|
1315
|
+
addNodeToGraph(item.node);
|
|
1316
|
+
addEdgeToGraph(item.edge);
|
|
1317
|
+
}
|
|
1318
|
+
runLayout();
|
|
1319
|
+
showToast(`Found ${data.items.length} callees`);
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
showToast('Error: ' + err.message);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async function loadCallGraph(nodeId) {
|
|
1326
|
+
showToast('Loading call graph...');
|
|
1327
|
+
try {
|
|
1328
|
+
const data = await api.callgraph(nodeId, 2);
|
|
1329
|
+
addSubgraph(data.nodes, data.edges);
|
|
1330
|
+
runLayout();
|
|
1331
|
+
showToast(`Loaded call graph: ${data.nodes.length} nodes`);
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
showToast('Error: ' + err.message);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
async function loadImpact(nodeId) {
|
|
1338
|
+
showToast('Analyzing impact...');
|
|
1339
|
+
try {
|
|
1340
|
+
const data = await api.impact(nodeId, 2);
|
|
1341
|
+
addSubgraph(data.nodes, data.edges);
|
|
1342
|
+
runLayout();
|
|
1343
|
+
// Highlight the root
|
|
1344
|
+
const rootEle = cy.getElementById(nodeId);
|
|
1345
|
+
if (rootEle.length > 0) {
|
|
1346
|
+
rootEle.addClass('highlighted');
|
|
1347
|
+
}
|
|
1348
|
+
showToast(`Impact: ${data.nodes.length} nodes potentially affected`);
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
showToast('Error: ' + err.message);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function loadChildren(nodeId) {
|
|
1355
|
+
try {
|
|
1356
|
+
const data = await api.children(nodeId);
|
|
1357
|
+
if (data.children.length === 0) {
|
|
1358
|
+
showToast('No children found');
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
for (const child of data.children) {
|
|
1362
|
+
addNodeToGraph(child);
|
|
1363
|
+
// Add contains edge
|
|
1364
|
+
addEdgeToGraph({ source: nodeId, target: child.id, kind: 'contains' });
|
|
1365
|
+
}
|
|
1366
|
+
runLayout();
|
|
1367
|
+
showToast(`Found ${data.children.length} children`);
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
showToast('Error: ' + err.message);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async function loadFileNodes(filePath) {
|
|
1374
|
+
showToast('Loading file symbols...');
|
|
1375
|
+
try {
|
|
1376
|
+
const data = await api.fileNodes(filePath);
|
|
1377
|
+
if (data.nodes.length === 0) {
|
|
1378
|
+
showToast('No symbols in this file');
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
clearGraph();
|
|
1382
|
+
hideOverlay();
|
|
1383
|
+
for (const node of data.nodes) addNodeToGraph(node);
|
|
1384
|
+
runLayout();
|
|
1385
|
+
showToast(`Loaded ${data.nodes.length} symbols from file`);
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
showToast('Error: ' + err.message);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// ====================================================================
|
|
1392
|
+
// Explore — natural language question → graph
|
|
1393
|
+
// ====================================================================
|
|
1394
|
+
async function exploreQuery(question) {
|
|
1395
|
+
hideSearchDropdown();
|
|
1396
|
+
clearGraph();
|
|
1397
|
+
hideOverlay();
|
|
1398
|
+
document.getElementById('search-input').value = question;
|
|
1399
|
+
|
|
1400
|
+
showToast('Finding entry point...');
|
|
1401
|
+
|
|
1402
|
+
try {
|
|
1403
|
+
const data = await api.explore(question);
|
|
1404
|
+
if (data.nodes.length === 0) {
|
|
1405
|
+
showToast('No relevant code found. Try searching for a specific symbol.');
|
|
1406
|
+
hideOverlay(false);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
addSubgraph(data.nodes, data.edges);
|
|
1410
|
+
runLayout();
|
|
1411
|
+
|
|
1412
|
+
// Center on entry point
|
|
1413
|
+
if (data.entryPoint) {
|
|
1414
|
+
const entryEle = cy.getElementById(data.entryPoint);
|
|
1415
|
+
if (entryEle.length > 0) {
|
|
1416
|
+
entryEle.select();
|
|
1417
|
+
entryEle.addClass('highlighted');
|
|
1418
|
+
setTimeout(() => {
|
|
1419
|
+
cy.animate({ center: { eles: entryEle } }, { duration: 400 });
|
|
1420
|
+
showNodeDetails(data.entryPoint);
|
|
1421
|
+
}, 350);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const source = data.usedClaude ? ' (via Claude)' : '';
|
|
1426
|
+
showToast(`Traced ${data.nodes.length} symbols from entry point${source}`);
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
showToast('Error: ' + err.message);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// ====================================================================
|
|
1433
|
+
// Search
|
|
1434
|
+
// ====================================================================
|
|
1435
|
+
function onSearchInput(e) {
|
|
1436
|
+
const query = e.target.value.trim();
|
|
1437
|
+
clearTimeout(searchDebounce);
|
|
1438
|
+
if (!query) {
|
|
1439
|
+
hideSearchDropdown();
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
searchDebounce = setTimeout(async () => {
|
|
1443
|
+
try {
|
|
1444
|
+
const data = await api.search(query, null, 20);
|
|
1445
|
+
showSearchResults(data.results);
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
console.error('Search error:', err);
|
|
1448
|
+
}
|
|
1449
|
+
}, 200);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function onSearchKeydown(e) {
|
|
1453
|
+
if (e.key === 'Enter') {
|
|
1454
|
+
e.preventDefault();
|
|
1455
|
+
const query = e.target.value.trim();
|
|
1456
|
+
if (!query) return;
|
|
1457
|
+
|
|
1458
|
+
// If dropdown is visible and has results, select the first one
|
|
1459
|
+
const dropdown = document.getElementById('search-results-dropdown');
|
|
1460
|
+
const firstItem = dropdown.querySelector('.search-result-item');
|
|
1461
|
+
if (dropdown.classList.contains('visible') && firstItem) {
|
|
1462
|
+
firstItem.click();
|
|
1463
|
+
} else {
|
|
1464
|
+
// Trigger a search and auto-select first result
|
|
1465
|
+
(async () => {
|
|
1466
|
+
try {
|
|
1467
|
+
const data = await api.search(query, null, 10);
|
|
1468
|
+
if (data.results.length > 0) {
|
|
1469
|
+
selectSearchResult(data.results[0].node.id);
|
|
1470
|
+
} else {
|
|
1471
|
+
showToast('No symbols found. Try a different search.');
|
|
1472
|
+
}
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
showToast('Search error: ' + err.message);
|
|
1475
|
+
}
|
|
1476
|
+
})();
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function showSearchResults(results) {
|
|
1482
|
+
const dropdown = document.getElementById('search-results-dropdown');
|
|
1483
|
+
if (results.length === 0) {
|
|
1484
|
+
dropdown.innerHTML = '<div style="padding:12px;color:var(--text-muted);font-size:13px;">No results found</div>';
|
|
1485
|
+
dropdown.classList.add('visible');
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
dropdown.innerHTML = results.map(r => `
|
|
1489
|
+
<div class="search-result-item" onclick="selectSearchResult('${escapeAttr(r.node.id)}')">
|
|
1490
|
+
<span class="kind-badge" style="background:${kindColors[r.node.kind] || '#8b949e'}22;color:${kindColors[r.node.kind] || '#8b949e'}">${r.node.kind}</span>
|
|
1491
|
+
<span class="name">${escapeHtml(r.node.name)}</span>
|
|
1492
|
+
<span class="file-path">${escapeHtml(r.node.filePath)}</span>
|
|
1493
|
+
</div>
|
|
1494
|
+
`).join('');
|
|
1495
|
+
dropdown.classList.add('visible');
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function hideSearchDropdown() {
|
|
1499
|
+
document.getElementById('search-results-dropdown').classList.remove('visible');
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
async function selectSearchResult(nodeId) {
|
|
1503
|
+
hideSearchDropdown();
|
|
1504
|
+
document.getElementById('search-input').value = '';
|
|
1505
|
+
hideOverlay();
|
|
1506
|
+
clearGraph();
|
|
1507
|
+
hideOverlay();
|
|
1508
|
+
|
|
1509
|
+
showToast('Tracing call chain...');
|
|
1510
|
+
|
|
1511
|
+
try {
|
|
1512
|
+
// Load the call graph from this entry point (depth 3 forward)
|
|
1513
|
+
const data = await api.callgraph(nodeId, 3);
|
|
1514
|
+
if (data.nodes.length === 0) {
|
|
1515
|
+
// Fallback: just show the node
|
|
1516
|
+
const nodeData = await api.node(nodeId);
|
|
1517
|
+
if (nodeData.node) addNodeToGraph(nodeData.node);
|
|
1518
|
+
} else {
|
|
1519
|
+
addSubgraph(data.nodes, data.edges);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
runLayout();
|
|
1523
|
+
|
|
1524
|
+
// Select and center on the entry point
|
|
1525
|
+
const ele = cy.getElementById(nodeId);
|
|
1526
|
+
if (ele.length > 0) {
|
|
1527
|
+
ele.select();
|
|
1528
|
+
ele.addClass('highlighted');
|
|
1529
|
+
setTimeout(() => {
|
|
1530
|
+
cy.animate({ center: { eles: ele } }, { duration: 300 });
|
|
1531
|
+
}, 350);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
showNodeDetails(nodeId);
|
|
1535
|
+
showToast(`Traced ${data.nodes.length} symbols from entry point`);
|
|
1536
|
+
} catch (err) {
|
|
1537
|
+
showToast('Error: ' + err.message);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// ====================================================================
|
|
1542
|
+
// Detail Panel
|
|
1543
|
+
// ====================================================================
|
|
1544
|
+
async function showNodeDetails(nodeId) {
|
|
1545
|
+
const panel = document.getElementById('detail-panel');
|
|
1546
|
+
const body = document.getElementById('detail-body');
|
|
1547
|
+
const title = document.getElementById('detail-title');
|
|
1548
|
+
|
|
1549
|
+
panel.classList.add('open');
|
|
1550
|
+
|
|
1551
|
+
body.innerHTML = '<div style="padding:20px;text-align:center;"><div class="spinner"></div></div>';
|
|
1552
|
+
|
|
1553
|
+
try {
|
|
1554
|
+
const [nodeData, contextData] = await Promise.all([
|
|
1555
|
+
api.node(nodeId),
|
|
1556
|
+
api.context(nodeId),
|
|
1557
|
+
]);
|
|
1558
|
+
|
|
1559
|
+
const node = nodeData.node;
|
|
1560
|
+
const code = nodeData.code;
|
|
1561
|
+
const ancestors = nodeData.ancestors || [];
|
|
1562
|
+
const ctx = contextData.context;
|
|
1563
|
+
|
|
1564
|
+
title.textContent = node.name;
|
|
1565
|
+
|
|
1566
|
+
let html = '';
|
|
1567
|
+
|
|
1568
|
+
// Quick actions
|
|
1569
|
+
html += `<div class="detail-section" style="padding:8px 16px;">
|
|
1570
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
|
1571
|
+
<button class="toolbar-btn" onclick="expandCallees('${escapeAttr(node.id)}')" style="font-size:12px;">Expand Callees →</button>
|
|
1572
|
+
<button class="toolbar-btn" onclick="expandCallers('${escapeAttr(node.id)}')" style="font-size:12px;">← Expand Callers</button>
|
|
1573
|
+
<button class="toolbar-btn" onclick="loadCallGraph('${escapeAttr(node.id)}')" style="font-size:12px;">Full Call Graph</button>
|
|
1574
|
+
<button class="toolbar-btn" onclick="loadImpact('${escapeAttr(node.id)}')" style="font-size:12px;">Impact Analysis</button>
|
|
1575
|
+
</div>
|
|
1576
|
+
</div>`;
|
|
1577
|
+
|
|
1578
|
+
// Meta info
|
|
1579
|
+
html += `<div class="detail-section">
|
|
1580
|
+
<div class="detail-section-title">Info</div>
|
|
1581
|
+
<div class="detail-meta">
|
|
1582
|
+
<span class="label">Kind</span>
|
|
1583
|
+
<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>
|
|
1584
|
+
<span class="label">File</span>
|
|
1585
|
+
<span class="value accent">${escapeHtml(node.filePath)}</span>
|
|
1586
|
+
<span class="label">Lines</span>
|
|
1587
|
+
<span class="value">${node.startLine} - ${node.endLine}</span>
|
|
1588
|
+
${node.signature ? `<span class="label">Signature</span><span class="value" style="font-family:var(--font-mono);font-size:11px;">${escapeHtml(node.signature)}</span>` : ''}
|
|
1589
|
+
${node.visibility ? `<span class="label">Visibility</span><span class="value">${node.visibility}</span>` : ''}
|
|
1590
|
+
${node.isExported ? `<span class="label">Exported</span><span class="value">Yes</span>` : ''}
|
|
1591
|
+
${node.isAsync ? `<span class="label">Async</span><span class="value">Yes</span>` : ''}
|
|
1592
|
+
${node.decorators && node.decorators.length ? `<span class="label">Decorators</span><span class="value">${escapeHtml(node.decorators.join(', '))}</span>` : ''}
|
|
1593
|
+
</div>
|
|
1594
|
+
</div>`;
|
|
1595
|
+
|
|
1596
|
+
// Breadcrumb ancestors
|
|
1597
|
+
if (ancestors.length > 0) {
|
|
1598
|
+
html += `<div class="detail-section">
|
|
1599
|
+
<div class="detail-section-title">Hierarchy</div>
|
|
1600
|
+
<div style="font-size:12px;color:var(--text-secondary);">
|
|
1601
|
+
${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>
|
|
1602
|
+
</div>
|
|
1603
|
+
</div>`;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// Source code
|
|
1607
|
+
if (code) {
|
|
1608
|
+
const lang = langForHighlight(node.language);
|
|
1609
|
+
html += `<div class="detail-section">
|
|
1610
|
+
<div class="detail-section-title">Source Code</div>
|
|
1611
|
+
<div class="code-block"><pre><code class="language-${lang}">${escapeHtml(code)}</code></pre></div>
|
|
1612
|
+
</div>`;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// Callers
|
|
1616
|
+
if (ctx.incomingRefs && ctx.incomingRefs.length > 0) {
|
|
1617
|
+
html += `<div class="detail-section">
|
|
1618
|
+
<div class="detail-section-title">Called By (${ctx.incomingRefs.length})</div>
|
|
1619
|
+
<ul class="relation-list">
|
|
1620
|
+
${ctx.incomingRefs.slice(0, 20).map(r => `
|
|
1621
|
+
<li class="relation-item" onclick="selectSearchResult('${escapeAttr(r.node.id)}')">
|
|
1622
|
+
<span class="rel-badge" style="background:${kindColors[r.node.kind] || '#8b949e'}22;color:${kindColors[r.node.kind] || '#8b949e'}">${r.node.kind}</span>
|
|
1623
|
+
${escapeHtml(r.node.name)}
|
|
1624
|
+
<span style="margin-left:auto;color:var(--text-muted);font-size:11px;">${r.edge.kind}</span>
|
|
1625
|
+
</li>
|
|
1626
|
+
`).join('')}
|
|
1627
|
+
</ul>
|
|
1628
|
+
</div>`;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Callees
|
|
1632
|
+
if (ctx.outgoingRefs && ctx.outgoingRefs.length > 0) {
|
|
1633
|
+
html += `<div class="detail-section">
|
|
1634
|
+
<div class="detail-section-title">Calls (${ctx.outgoingRefs.length})</div>
|
|
1635
|
+
<ul class="relation-list">
|
|
1636
|
+
${ctx.outgoingRefs.slice(0, 20).map(r => `
|
|
1637
|
+
<li class="relation-item" onclick="selectSearchResult('${escapeAttr(r.node.id)}')">
|
|
1638
|
+
<span class="rel-badge" style="background:${kindColors[r.node.kind] || '#8b949e'}22;color:${kindColors[r.node.kind] || '#8b949e'}">${r.node.kind}</span>
|
|
1639
|
+
${escapeHtml(r.node.name)}
|
|
1640
|
+
<span style="margin-left:auto;color:var(--text-muted);font-size:11px;">${r.edge.kind}</span>
|
|
1641
|
+
</li>
|
|
1642
|
+
`).join('')}
|
|
1643
|
+
</ul>
|
|
1644
|
+
</div>`;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Children
|
|
1648
|
+
if (ctx.children && ctx.children.length > 0) {
|
|
1649
|
+
html += `<div class="detail-section">
|
|
1650
|
+
<div class="detail-section-title">Contains (${ctx.children.length})</div>
|
|
1651
|
+
<ul class="relation-list">
|
|
1652
|
+
${ctx.children.slice(0, 30).map(c => `
|
|
1653
|
+
<li class="relation-item" onclick="selectSearchResult('${escapeAttr(c.id)}')">
|
|
1654
|
+
<span class="rel-badge" style="background:${kindColors[c.kind] || '#8b949e'}22;color:${kindColors[c.kind] || '#8b949e'}">${c.kind}</span>
|
|
1655
|
+
${escapeHtml(c.name)}
|
|
1656
|
+
</li>
|
|
1657
|
+
`).join('')}
|
|
1658
|
+
</ul>
|
|
1659
|
+
</div>`;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Docstring
|
|
1663
|
+
if (node.docstring) {
|
|
1664
|
+
html += `<div class="detail-section">
|
|
1665
|
+
<div class="detail-section-title">Documentation</div>
|
|
1666
|
+
<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>
|
|
1667
|
+
</div>`;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
body.innerHTML = html;
|
|
1671
|
+
|
|
1672
|
+
// Apply syntax highlighting
|
|
1673
|
+
body.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
|
|
1674
|
+
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
body.innerHTML = `<div style="padding:20px;color:var(--red);">Error loading details: ${escapeHtml(err.message)}</div>`;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function closeDetailPanel() {
|
|
1681
|
+
document.getElementById('detail-panel').classList.remove('open');
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// ====================================================================
|
|
1685
|
+
// Context Menu
|
|
1686
|
+
// ====================================================================
|
|
1687
|
+
function showContextMenu(x, y) {
|
|
1688
|
+
const menu = document.getElementById('context-menu');
|
|
1689
|
+
menu.style.left = x + 'px';
|
|
1690
|
+
menu.style.top = y + 'px';
|
|
1691
|
+
menu.classList.add('visible');
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function hideContextMenu() {
|
|
1695
|
+
document.getElementById('context-menu').classList.remove('visible');
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function ctxAction(action) {
|
|
1699
|
+
hideContextMenu();
|
|
1700
|
+
if (!ctxNodeId) return;
|
|
1701
|
+
switch (action) {
|
|
1702
|
+
case 'expand-callees': expandCallees(ctxNodeId); break;
|
|
1703
|
+
case 'expand-callers': expandCallers(ctxNodeId); break;
|
|
1704
|
+
case 'callgraph': loadCallGraph(ctxNodeId); break;
|
|
1705
|
+
case 'impact': loadImpact(ctxNodeId); break;
|
|
1706
|
+
case 'children': loadChildren(ctxNodeId); break;
|
|
1707
|
+
case 'details': showNodeDetails(ctxNodeId); break;
|
|
1708
|
+
case 'remove':
|
|
1709
|
+
cy.getElementById(ctxNodeId).remove();
|
|
1710
|
+
expandedSets.callers.delete(ctxNodeId);
|
|
1711
|
+
expandedSets.callees.delete(ctxNodeId);
|
|
1712
|
+
break;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// ====================================================================
|
|
1717
|
+
// Tooltip
|
|
1718
|
+
// ====================================================================
|
|
1719
|
+
function showTooltip(e) {
|
|
1720
|
+
const node = e.target;
|
|
1721
|
+
const tip = document.getElementById('tooltip');
|
|
1722
|
+
const pos = e.originalEvent;
|
|
1723
|
+
tip.innerHTML = `
|
|
1724
|
+
<div class="tip-kind">${node.data('kind')}</div>
|
|
1725
|
+
<div class="tip-name">${escapeHtml(node.data('label'))}</div>
|
|
1726
|
+
${node.data('signature') ? `<div class="tip-file" style="font-family:var(--font-mono);">${escapeHtml(node.data('signature'))}</div>` : ''}
|
|
1727
|
+
<div class="tip-file">${escapeHtml(node.data('filePath') || '')}</div>
|
|
1728
|
+
`;
|
|
1729
|
+
tip.style.left = (pos.clientX + 12) + 'px';
|
|
1730
|
+
tip.style.top = (pos.clientY + 12) + 'px';
|
|
1731
|
+
tip.style.display = 'block';
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function hideTooltip() {
|
|
1735
|
+
document.getElementById('tooltip').style.display = 'none';
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// ====================================================================
|
|
1739
|
+
// Toast notifications
|
|
1740
|
+
// ====================================================================
|
|
1741
|
+
function showToast(msg) {
|
|
1742
|
+
const toast = document.getElementById('toast');
|
|
1743
|
+
toast.textContent = msg;
|
|
1744
|
+
toast.classList.add('visible');
|
|
1745
|
+
clearTimeout(toast._timeout);
|
|
1746
|
+
toast._timeout = setTimeout(() => toast.classList.remove('visible'), 2500);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// ====================================================================
|
|
1750
|
+
// Sidebar
|
|
1751
|
+
// ====================================================================
|
|
1752
|
+
function toggleSection(header) {
|
|
1753
|
+
const content = header.nextElementSibling;
|
|
1754
|
+
const arrow = header.querySelector('span:last-child');
|
|
1755
|
+
if (content.style.display === 'none') {
|
|
1756
|
+
content.style.display = '';
|
|
1757
|
+
arrow.innerHTML = '▼';
|
|
1758
|
+
} else {
|
|
1759
|
+
content.style.display = 'none';
|
|
1760
|
+
arrow.innerHTML = '▶';
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
async function loadFileTree() {
|
|
1765
|
+
try {
|
|
1766
|
+
const data = await api.files();
|
|
1767
|
+
const tree = document.getElementById('file-tree');
|
|
1768
|
+
if (data.files.length === 0) {
|
|
1769
|
+
tree.innerHTML = '<div style="padding:12px;color:var(--text-muted);font-size:12px;">No files indexed</div>';
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
// Group by directory
|
|
1773
|
+
const dirs = {};
|
|
1774
|
+
for (const f of data.files) {
|
|
1775
|
+
const dir = f.filePath.split('/').slice(0, -1).join('/') || '.';
|
|
1776
|
+
if (!dirs[dir]) dirs[dir] = [];
|
|
1777
|
+
dirs[dir].push(f);
|
|
1778
|
+
}
|
|
1779
|
+
let html = '';
|
|
1780
|
+
const sortedDirs = Object.keys(dirs).sort();
|
|
1781
|
+
for (const dir of sortedDirs) {
|
|
1782
|
+
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'">
|
|
1783
|
+
<span class="file-icon">📁</span> ${escapeHtml(dir)}/
|
|
1784
|
+
</div><div>`;
|
|
1785
|
+
for (const f of dirs[dir].sort((a, b) => a.filePath.localeCompare(b.filePath))) {
|
|
1786
|
+
const fileName = f.filePath.split('/').pop();
|
|
1787
|
+
html += `<div class="file-item" style="padding-left:28px;" onclick="loadFileNodes('${escapeAttr(f.filePath)}')">
|
|
1788
|
+
<span class="file-icon">📄</span> ${escapeHtml(fileName)}
|
|
1789
|
+
</div>`;
|
|
1790
|
+
}
|
|
1791
|
+
html += '</div>';
|
|
1792
|
+
}
|
|
1793
|
+
tree.innerHTML = html;
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
console.error('Failed to load file tree:', err);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function buildLegend() {
|
|
1800
|
+
const legendEl = document.getElementById('legend');
|
|
1801
|
+
const kinds = ['function', 'method', 'class', 'interface', 'component', 'enum', 'variable', 'constant', 'property', 'type_alias', 'import', 'export'];
|
|
1802
|
+
legendEl.innerHTML = kinds.map(k => `
|
|
1803
|
+
<div class="legend-item">
|
|
1804
|
+
<div class="legend-dot" style="background:${kindColors[k]};"></div>
|
|
1805
|
+
<span>${k.replace('_', ' ')}</span>
|
|
1806
|
+
</div>
|
|
1807
|
+
`).join('');
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// ====================================================================
|
|
1811
|
+
// Helpers
|
|
1812
|
+
// ====================================================================
|
|
1813
|
+
function escapeHtml(str) {
|
|
1814
|
+
if (!str) return '';
|
|
1815
|
+
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function escapeAttr(str) {
|
|
1819
|
+
if (!str) return '';
|
|
1820
|
+
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function langForHighlight(lang) {
|
|
1824
|
+
const map = {
|
|
1825
|
+
'typescript': 'typescript', 'javascript': 'javascript', 'tsx': 'typescript',
|
|
1826
|
+
'jsx': 'javascript', 'python': 'python', 'go': 'go', 'rust': 'rust',
|
|
1827
|
+
'java': 'java', 'c': 'c', 'cpp': 'cpp', 'csharp': 'csharp',
|
|
1828
|
+
'php': 'php', 'ruby': 'ruby', 'swift': 'swift', 'kotlin': 'kotlin',
|
|
1829
|
+
'dart': 'dart', 'svelte': 'xml', 'liquid': 'xml', 'pascal': 'delphi',
|
|
1830
|
+
};
|
|
1831
|
+
return map[lang] || 'plaintext';
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// ====================================================================
|
|
1835
|
+
// Keyboard Shortcuts
|
|
1836
|
+
// ====================================================================
|
|
1837
|
+
document.addEventListener('keydown', (e) => {
|
|
1838
|
+
// Ctrl+K or Cmd+K → focus search
|
|
1839
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
1840
|
+
e.preventDefault();
|
|
1841
|
+
document.getElementById('search-input').focus();
|
|
1842
|
+
}
|
|
1843
|
+
// Escape → close things
|
|
1844
|
+
if (e.key === 'Escape') {
|
|
1845
|
+
hideSearchDropdown();
|
|
1846
|
+
hideContextMenu();
|
|
1847
|
+
closeDetailPanel();
|
|
1848
|
+
clearHighlights();
|
|
1849
|
+
document.getElementById('search-input').blur();
|
|
1850
|
+
}
|
|
1851
|
+
// Delete/Backspace → remove selected nodes
|
|
1852
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && document.activeElement.tagName !== 'INPUT') {
|
|
1853
|
+
const selected = cy.$(':selected');
|
|
1854
|
+
if (selected.length > 0) {
|
|
1855
|
+
selected.remove();
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
// ====================================================================
|
|
1861
|
+
// Embeddings Setup Dialog
|
|
1862
|
+
// ====================================================================
|
|
1863
|
+
function showDialog() {
|
|
1864
|
+
document.getElementById('dialog-overlay').classList.add('visible');
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
function closeDialog() {
|
|
1868
|
+
document.getElementById('dialog-overlay').classList.remove('visible');
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
async function checkEmbeddings() {
|
|
1872
|
+
try {
|
|
1873
|
+
const data = await api.embeddingsStatus();
|
|
1874
|
+
if (!data.isReady) {
|
|
1875
|
+
showDialog();
|
|
1876
|
+
}
|
|
1877
|
+
} catch (err) {
|
|
1878
|
+
console.error('Failed to check embeddings status:', err);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function startEmbeddings() {
|
|
1883
|
+
const btnEnable = document.getElementById('dialog-enable');
|
|
1884
|
+
const btnSkip = document.getElementById('dialog-skip');
|
|
1885
|
+
const progress = document.getElementById('dialog-progress');
|
|
1886
|
+
const progressFill = document.getElementById('dialog-progress-fill');
|
|
1887
|
+
const progressText = document.getElementById('dialog-progress-text');
|
|
1888
|
+
const progressPercent = document.getElementById('dialog-progress-percent');
|
|
1889
|
+
const title = document.getElementById('dialog-title');
|
|
1890
|
+
const body = document.getElementById('dialog-body');
|
|
1891
|
+
|
|
1892
|
+
// Update UI to progress mode
|
|
1893
|
+
btnEnable.disabled = true;
|
|
1894
|
+
btnEnable.textContent = 'Setting up...';
|
|
1895
|
+
btnSkip.style.display = 'none';
|
|
1896
|
+
progress.style.display = 'block';
|
|
1897
|
+
body.innerHTML = 'Setting up semantic search for your project. This only needs to happen once.';
|
|
1898
|
+
|
|
1899
|
+
const evtSource = new EventSource('/api/embeddings/generate');
|
|
1900
|
+
|
|
1901
|
+
evtSource.addEventListener('status', (e) => {
|
|
1902
|
+
const data = JSON.parse(e.data);
|
|
1903
|
+
progressText.textContent = data.message;
|
|
1904
|
+
title.textContent = data.phase === 'model' ? 'Downloading Model...' :
|
|
1905
|
+
data.phase === 'embedding' ? 'Generating Embeddings...' :
|
|
1906
|
+
'Setting Up...';
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
evtSource.addEventListener('progress', (e) => {
|
|
1910
|
+
const data = JSON.parse(e.data);
|
|
1911
|
+
progressFill.style.width = data.percent + '%';
|
|
1912
|
+
progressPercent.textContent = data.percent + '%';
|
|
1913
|
+
progressText.textContent = data.nodeName
|
|
1914
|
+
? `Embedding: ${data.nodeName} (${data.current}/${data.total})`
|
|
1915
|
+
: `Processing ${data.current} of ${data.total}...`;
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
evtSource.addEventListener('complete', (e) => {
|
|
1919
|
+
const data = JSON.parse(e.data);
|
|
1920
|
+
evtSource.close();
|
|
1921
|
+
title.textContent = 'Ready!';
|
|
1922
|
+
body.innerHTML = `<strong>${data.message}</strong><br><br>Semantic search is now active. Your explore queries will understand code meaning, not just keywords.`;
|
|
1923
|
+
progressFill.style.width = '100%';
|
|
1924
|
+
progressPercent.textContent = '100%';
|
|
1925
|
+
progressText.textContent = 'Complete';
|
|
1926
|
+
|
|
1927
|
+
// Change actions to just a close button
|
|
1928
|
+
document.getElementById('dialog-actions').innerHTML =
|
|
1929
|
+
'<button class="btn btn-primary" onclick="closeDialog()">Start Exploring</button>';
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
evtSource.addEventListener('error', (e) => {
|
|
1933
|
+
let msg = 'An error occurred during setup.';
|
|
1934
|
+
try {
|
|
1935
|
+
const data = JSON.parse(e.data);
|
|
1936
|
+
msg = data.message || msg;
|
|
1937
|
+
} catch {}
|
|
1938
|
+
evtSource.close();
|
|
1939
|
+
title.textContent = 'Setup Error';
|
|
1940
|
+
body.innerHTML = `<span style="color:var(--red);">${escapeHtml(msg)}</span><br><br>You can still use the explorer with keyword-based search.`;
|
|
1941
|
+
document.getElementById('dialog-actions').innerHTML =
|
|
1942
|
+
'<button class="btn btn-secondary" onclick="closeDialog()">Close</button>';
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
// SSE connection error (different from app error event)
|
|
1946
|
+
evtSource.onerror = () => {
|
|
1947
|
+
// EventSource reconnects automatically; if it closes, we handle via the error event above
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// ====================================================================
|
|
1952
|
+
// Init
|
|
1953
|
+
// ====================================================================
|
|
1954
|
+
async function init() {
|
|
1955
|
+
initCytoscape();
|
|
1956
|
+
buildLegend();
|
|
1957
|
+
|
|
1958
|
+
// Load stats
|
|
1959
|
+
try {
|
|
1960
|
+
const data = await api.status();
|
|
1961
|
+
document.getElementById('stat-nodes').textContent = data.stats.nodeCount.toLocaleString();
|
|
1962
|
+
document.getElementById('stat-edges').textContent = data.stats.edgeCount.toLocaleString();
|
|
1963
|
+
document.getElementById('stat-files').textContent = data.stats.fileCount.toLocaleString();
|
|
1964
|
+
document.title = `CodeGraph - ${data.projectName}`;
|
|
1965
|
+
} catch (err) {
|
|
1966
|
+
console.error('Failed to load status:', err);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// Load file tree
|
|
1970
|
+
loadFileTree();
|
|
1971
|
+
|
|
1972
|
+
// Embeddings dialog available but not auto-shown
|
|
1973
|
+
// (local embedding model quality is insufficient for natural language queries)
|
|
1974
|
+
|
|
1975
|
+
// Search input
|
|
1976
|
+
document.getElementById('search-input').addEventListener('input', onSearchInput);
|
|
1977
|
+
document.getElementById('search-input').addEventListener('keydown', onSearchKeydown);
|
|
1978
|
+
document.getElementById('search-input').addEventListener('focus', () => {
|
|
1979
|
+
if (document.getElementById('search-input').value.trim()) {
|
|
1980
|
+
document.getElementById('search-results-dropdown').classList.add('visible');
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
// Click outside search dropdown to close
|
|
1985
|
+
document.addEventListener('click', (e) => {
|
|
1986
|
+
if (!e.target.closest('#search-container')) hideSearchDropdown();
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// Start
|
|
1991
|
+
init();
|
|
1992
|
+
</script>
|
|
1993
|
+
</body>
|
|
1994
|
+
</html>
|