@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.
@@ -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">&#x1F52E;</span>
742
+ <span>CodeGraph</span>
743
+ </div>
744
+
745
+ <div id="search-container">
746
+ <span class="search-icon">&#x1F50D;</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>&#x25B6;</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>&#x25BC;</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">&#x1F52E;</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">&minus;</button>
806
+ <button class="graph-ctrl-btn" onclick="fitGraph()" title="Fit to view">&#x2922;</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()">&times;</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">&#x1F9E0;</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">&#x2192;</span> Expand Callees</div>
853
+ <div class="ctx-item" onclick="ctxAction('expand-callers')"><span class="ctx-icon">&#x2190;</span> Expand Callers</div>
854
+ <div class="ctx-sep"></div>
855
+ <div class="ctx-item" onclick="ctxAction('callgraph')"><span class="ctx-icon">&#x1F310;</span> Full Call Graph</div>
856
+ <div class="ctx-item" onclick="ctxAction('impact')"><span class="ctx-icon">&#x1F4A5;</span> Impact Analysis</div>
857
+ <div class="ctx-sep"></div>
858
+ <div class="ctx-item" onclick="ctxAction('children')"><span class="ctx-icon">&#x1F4C2;</span> Show Children</div>
859
+ <div class="ctx-item" onclick="ctxAction('details')"><span class="ctx-icon">&#x1F4CB;</span> View Details</div>
860
+ <div class="ctx-sep"></div>
861
+ <div class="ctx-item" onclick="ctxAction('remove')"><span class="ctx-icon">&#x2716;</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 &rarr;</button>
1572
+ <button class="toolbar-btn" onclick="expandCallers('${escapeAttr(node.id)}')" style="font-size:12px;">&larr; 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);">&#x203A;</span> ')} <span style="color:var(--text-muted);">&#x203A;</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 = '&#x25BC;';
1758
+ } else {
1759
+ content.style.display = 'none';
1760
+ arrow.innerHTML = '&#x25B6;';
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">&#x1F4C1;</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">&#x1F4C4;</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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>