@rangerchaz/aimem 0.1.0

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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -0
  3. package/dist/cli/commands/git.d.ts +6 -0
  4. package/dist/cli/commands/git.d.ts.map +1 -0
  5. package/dist/cli/commands/git.js +298 -0
  6. package/dist/cli/commands/git.js.map +1 -0
  7. package/dist/cli/commands/hook-session-end.d.ts +7 -0
  8. package/dist/cli/commands/hook-session-end.d.ts.map +1 -0
  9. package/dist/cli/commands/hook-session-end.js +109 -0
  10. package/dist/cli/commands/hook-session-end.js.map +1 -0
  11. package/dist/cli/commands/hook-session-start.d.ts +7 -0
  12. package/dist/cli/commands/hook-session-start.d.ts.map +1 -0
  13. package/dist/cli/commands/hook-session-start.js +116 -0
  14. package/dist/cli/commands/hook-session-start.js.map +1 -0
  15. package/dist/cli/commands/import.d.ts +14 -0
  16. package/dist/cli/commands/import.d.ts.map +1 -0
  17. package/dist/cli/commands/import.js +527 -0
  18. package/dist/cli/commands/import.js.map +1 -0
  19. package/dist/cli/commands/init.d.ts +2 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -0
  21. package/dist/cli/commands/init.js +32 -0
  22. package/dist/cli/commands/init.js.map +1 -0
  23. package/dist/cli/commands/mcp-serve.d.ts +2 -0
  24. package/dist/cli/commands/mcp-serve.d.ts.map +1 -0
  25. package/dist/cli/commands/mcp-serve.js +5 -0
  26. package/dist/cli/commands/mcp-serve.js.map +1 -0
  27. package/dist/cli/commands/query.d.ts +8 -0
  28. package/dist/cli/commands/query.d.ts.map +1 -0
  29. package/dist/cli/commands/query.js +83 -0
  30. package/dist/cli/commands/query.js.map +1 -0
  31. package/dist/cli/commands/setup.d.ts +10 -0
  32. package/dist/cli/commands/setup.d.ts.map +1 -0
  33. package/dist/cli/commands/setup.js +504 -0
  34. package/dist/cli/commands/setup.js.map +1 -0
  35. package/dist/cli/commands/start.d.ts +8 -0
  36. package/dist/cli/commands/start.d.ts.map +1 -0
  37. package/dist/cli/commands/start.js +90 -0
  38. package/dist/cli/commands/start.js.map +1 -0
  39. package/dist/cli/commands/status.d.ts +2 -0
  40. package/dist/cli/commands/status.d.ts.map +1 -0
  41. package/dist/cli/commands/status.js +85 -0
  42. package/dist/cli/commands/status.js.map +1 -0
  43. package/dist/cli/commands/stop.d.ts +7 -0
  44. package/dist/cli/commands/stop.d.ts.map +1 -0
  45. package/dist/cli/commands/stop.js +46 -0
  46. package/dist/cli/commands/stop.js.map +1 -0
  47. package/dist/cli/commands/visualize.d.ts +8 -0
  48. package/dist/cli/commands/visualize.d.ts.map +1 -0
  49. package/dist/cli/commands/visualize.js +96 -0
  50. package/dist/cli/commands/visualize.js.map +1 -0
  51. package/dist/cli/index.d.ts +3 -0
  52. package/dist/cli/index.d.ts.map +1 -0
  53. package/dist/cli/index.js +114 -0
  54. package/dist/cli/index.js.map +1 -0
  55. package/dist/db/index.d.ts +55 -0
  56. package/dist/db/index.d.ts.map +1 -0
  57. package/dist/db/index.js +464 -0
  58. package/dist/db/index.js.map +1 -0
  59. package/dist/db/schema.d.ts +4 -0
  60. package/dist/db/schema.d.ts.map +1 -0
  61. package/dist/db/schema.js +200 -0
  62. package/dist/db/schema.js.map +1 -0
  63. package/dist/extractor/index.d.ts +27 -0
  64. package/dist/extractor/index.d.ts.map +1 -0
  65. package/dist/extractor/index.js +227 -0
  66. package/dist/extractor/index.js.map +1 -0
  67. package/dist/git/extractor.d.ts +30 -0
  68. package/dist/git/extractor.d.ts.map +1 -0
  69. package/dist/git/extractor.js +126 -0
  70. package/dist/git/extractor.js.map +1 -0
  71. package/dist/git/hooks.d.ts +36 -0
  72. package/dist/git/hooks.d.ts.map +1 -0
  73. package/dist/git/hooks.js +142 -0
  74. package/dist/git/hooks.js.map +1 -0
  75. package/dist/git/index.d.ts +69 -0
  76. package/dist/git/index.d.ts.map +1 -0
  77. package/dist/git/index.js +250 -0
  78. package/dist/git/index.js.map +1 -0
  79. package/dist/indexer/index.d.ts +20 -0
  80. package/dist/indexer/index.d.ts.map +1 -0
  81. package/dist/indexer/index.js +173 -0
  82. package/dist/indexer/index.js.map +1 -0
  83. package/dist/indexer/parsers/base.d.ts +19 -0
  84. package/dist/indexer/parsers/base.d.ts.map +1 -0
  85. package/dist/indexer/parsers/base.js +46 -0
  86. package/dist/indexer/parsers/base.js.map +1 -0
  87. package/dist/indexer/parsers/cpp.d.ts +3 -0
  88. package/dist/indexer/parsers/cpp.d.ts.map +1 -0
  89. package/dist/indexer/parsers/cpp.js +180 -0
  90. package/dist/indexer/parsers/cpp.js.map +1 -0
  91. package/dist/indexer/parsers/go.d.ts +3 -0
  92. package/dist/indexer/parsers/go.d.ts.map +1 -0
  93. package/dist/indexer/parsers/go.js +98 -0
  94. package/dist/indexer/parsers/go.js.map +1 -0
  95. package/dist/indexer/parsers/java.d.ts +3 -0
  96. package/dist/indexer/parsers/java.d.ts.map +1 -0
  97. package/dist/indexer/parsers/java.js +204 -0
  98. package/dist/indexer/parsers/java.js.map +1 -0
  99. package/dist/indexer/parsers/javascript.d.ts +3 -0
  100. package/dist/indexer/parsers/javascript.d.ts.map +1 -0
  101. package/dist/indexer/parsers/javascript.js +157 -0
  102. package/dist/indexer/parsers/javascript.js.map +1 -0
  103. package/dist/indexer/parsers/kotlin.d.ts +3 -0
  104. package/dist/indexer/parsers/kotlin.d.ts.map +1 -0
  105. package/dist/indexer/parsers/kotlin.js +182 -0
  106. package/dist/indexer/parsers/kotlin.js.map +1 -0
  107. package/dist/indexer/parsers/php.d.ts +3 -0
  108. package/dist/indexer/parsers/php.d.ts.map +1 -0
  109. package/dist/indexer/parsers/php.js +190 -0
  110. package/dist/indexer/parsers/php.js.map +1 -0
  111. package/dist/indexer/parsers/python.d.ts +3 -0
  112. package/dist/indexer/parsers/python.d.ts.map +1 -0
  113. package/dist/indexer/parsers/python.js +101 -0
  114. package/dist/indexer/parsers/python.js.map +1 -0
  115. package/dist/indexer/parsers/ruby.d.ts +3 -0
  116. package/dist/indexer/parsers/ruby.d.ts.map +1 -0
  117. package/dist/indexer/parsers/ruby.js +92 -0
  118. package/dist/indexer/parsers/ruby.js.map +1 -0
  119. package/dist/indexer/parsers/rust.d.ts +3 -0
  120. package/dist/indexer/parsers/rust.d.ts.map +1 -0
  121. package/dist/indexer/parsers/rust.js +190 -0
  122. package/dist/indexer/parsers/rust.js.map +1 -0
  123. package/dist/indexer/watcher-daemon.d.ts +2 -0
  124. package/dist/indexer/watcher-daemon.d.ts.map +1 -0
  125. package/dist/indexer/watcher-daemon.js +27 -0
  126. package/dist/indexer/watcher-daemon.js.map +1 -0
  127. package/dist/indexer/watcher.d.ts +7 -0
  128. package/dist/indexer/watcher.d.ts.map +1 -0
  129. package/dist/indexer/watcher.js +77 -0
  130. package/dist/indexer/watcher.js.map +1 -0
  131. package/dist/mcp/server.d.ts +2 -0
  132. package/dist/mcp/server.d.ts.map +1 -0
  133. package/dist/mcp/server.js +241 -0
  134. package/dist/mcp/server.js.map +1 -0
  135. package/dist/proxy/interceptor-mockttp.d.ts +27 -0
  136. package/dist/proxy/interceptor-mockttp.d.ts.map +1 -0
  137. package/dist/proxy/interceptor-mockttp.js +274 -0
  138. package/dist/proxy/interceptor-mockttp.js.map +1 -0
  139. package/dist/proxy/proxy-daemon.d.ts +5 -0
  140. package/dist/proxy/proxy-daemon.d.ts.map +1 -0
  141. package/dist/proxy/proxy-daemon.js +26 -0
  142. package/dist/proxy/proxy-daemon.js.map +1 -0
  143. package/dist/query/index.d.ts +32 -0
  144. package/dist/query/index.d.ts.map +1 -0
  145. package/dist/query/index.js +135 -0
  146. package/dist/query/index.js.map +1 -0
  147. package/dist/types/index.d.ts +89 -0
  148. package/dist/types/index.d.ts.map +1 -0
  149. package/dist/types/index.js +3 -0
  150. package/dist/types/index.js.map +1 -0
  151. package/dist/visualize/index.d.ts +144 -0
  152. package/dist/visualize/index.d.ts.map +1 -0
  153. package/dist/visualize/index.js +707 -0
  154. package/dist/visualize/index.js.map +1 -0
  155. package/dist/visualize/server.d.ts +7 -0
  156. package/dist/visualize/server.d.ts.map +1 -0
  157. package/dist/visualize/server.js +77 -0
  158. package/dist/visualize/server.js.map +1 -0
  159. package/dist/visualize/template.d.ts +3 -0
  160. package/dist/visualize/template.d.ts.map +1 -0
  161. package/dist/visualize/template.js +3465 -0
  162. package/dist/visualize/template.js.map +1 -0
  163. package/package.json +56 -0
@@ -0,0 +1,3465 @@
1
+ export function generateDashboardHTML(data) {
2
+ // Escape JSON for safe embedding in HTML script tag
3
+ // We use a script tag with type="application/json" to avoid parsing issues
4
+ const jsonData = JSON.stringify(data)
5
+ .replace(/</g, '\\u003c') // Escape < to prevent </script> issues
6
+ .replace(/>/g, '\\u003e') // Escape > for safety
7
+ .replace(/&/g, '\\u0026'); // Escape & for safety
8
+ return `<!DOCTYPE html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="UTF-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
+ <title>aimem Dashboard - ${escapeHtml(data.project.name)}</title>
14
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
15
+ <style>
16
+ * {
17
+ margin: 0;
18
+ padding: 0;
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ body {
23
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
24
+ background: #1a1a2e;
25
+ color: #eee;
26
+ height: 100vh;
27
+ overflow: hidden;
28
+ }
29
+
30
+ /* Header */
31
+ .header {
32
+ background: #16213e;
33
+ padding: 12px 20px;
34
+ display: flex;
35
+ justify-content: space-between;
36
+ align-items: center;
37
+ border-bottom: 1px solid #0f3460;
38
+ }
39
+
40
+ .header h1 {
41
+ font-size: 18px;
42
+ font-weight: 500;
43
+ color: #e94560;
44
+ }
45
+
46
+ .header h1 span {
47
+ color: #eee;
48
+ font-weight: 400;
49
+ }
50
+
51
+ .search-box {
52
+ display: flex;
53
+ gap: 8px;
54
+ }
55
+
56
+ .search-box input {
57
+ background: #0f3460;
58
+ border: 1px solid #1a1a2e;
59
+ color: #eee;
60
+ padding: 6px 12px;
61
+ border-radius: 4px;
62
+ width: 200px;
63
+ }
64
+
65
+ .search-box input::placeholder {
66
+ color: #666;
67
+ }
68
+
69
+ /* View mode toggle */
70
+ .view-toggle {
71
+ display: flex;
72
+ background: #0f3460;
73
+ border-radius: 4px;
74
+ overflow: hidden;
75
+ }
76
+
77
+ .view-toggle-btn {
78
+ background: transparent;
79
+ border: none;
80
+ color: #888;
81
+ padding: 6px 12px;
82
+ cursor: pointer;
83
+ font-size: 13px;
84
+ }
85
+
86
+ .view-toggle-btn:hover {
87
+ color: #ccc;
88
+ }
89
+
90
+ .view-toggle-btn.active {
91
+ background: #e94560;
92
+ color: white;
93
+ }
94
+
95
+ .back-btn {
96
+ background: #0f3460;
97
+ border: 1px solid #1a1a2e;
98
+ color: #aaa;
99
+ padding: 6px 12px;
100
+ border-radius: 4px;
101
+ cursor: pointer;
102
+ font-size: 13px;
103
+ display: none;
104
+ }
105
+
106
+ .back-btn:hover {
107
+ background: #1a1a2e;
108
+ color: #fff;
109
+ }
110
+
111
+ .back-btn.visible {
112
+ display: inline-block;
113
+ }
114
+
115
+ .visualize-search-btn {
116
+ background: #e94560;
117
+ border: 1px solid #e94560;
118
+ color: white;
119
+ padding: 6px 12px;
120
+ border-radius: 4px;
121
+ cursor: pointer;
122
+ font-size: 13px;
123
+ }
124
+
125
+ .visualize-search-btn:hover {
126
+ background: #ff6b8a;
127
+ }
128
+
129
+ .fullscreen-btn {
130
+ background: #0f3460;
131
+ border: 1px solid #1a1a2e;
132
+ color: #aaa;
133
+ padding: 6px 12px;
134
+ border-radius: 4px;
135
+ cursor: pointer;
136
+ font-size: 13px;
137
+ }
138
+
139
+ .fullscreen-btn:hover {
140
+ background: #1a1a2e;
141
+ color: #fff;
142
+ }
143
+
144
+ /* Fullscreen mode */
145
+ body.fullscreen .sidebar,
146
+ body.fullscreen .details-panel,
147
+ body.fullscreen .header,
148
+ body.fullscreen .tabs {
149
+ display: none;
150
+ }
151
+
152
+ body.fullscreen .main {
153
+ height: 100vh;
154
+ }
155
+
156
+ body.fullscreen .graph-container {
157
+ position: fixed;
158
+ top: 0;
159
+ left: 0;
160
+ right: 0;
161
+ bottom: 0;
162
+ z-index: 1000;
163
+ }
164
+
165
+ body.fullscreen .view-description {
166
+ position: absolute;
167
+ top: 0;
168
+ left: 0;
169
+ right: 0;
170
+ z-index: 1001;
171
+ }
172
+
173
+ body.fullscreen .stats-bar {
174
+ position: absolute;
175
+ bottom: 0;
176
+ left: 0;
177
+ right: 0;
178
+ z-index: 1001;
179
+ }
180
+
181
+ body.fullscreen .exit-fullscreen {
182
+ position: fixed;
183
+ top: 16px;
184
+ right: 16px;
185
+ z-index: 1002;
186
+ background: rgba(22, 33, 62, 0.9);
187
+ color: #ccc;
188
+ border: 1px solid #0f3460;
189
+ padding: 10px 20px;
190
+ border-radius: 6px;
191
+ cursor: pointer;
192
+ font-size: 13px;
193
+ backdrop-filter: blur(4px);
194
+ transition: all 0.2s ease;
195
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
196
+ }
197
+
198
+ body.fullscreen .exit-fullscreen:hover {
199
+ background: rgba(233, 69, 96, 0.9);
200
+ color: white;
201
+ border-color: #e94560;
202
+ }
203
+
204
+ /* Fullscreen details panel - slides in from right */
205
+ body.fullscreen .details-panel {
206
+ display: none;
207
+ position: fixed;
208
+ top: 60px;
209
+ right: 16px;
210
+ bottom: 16px;
211
+ width: 380px;
212
+ z-index: 1001;
213
+ border-radius: 8px;
214
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
215
+ background: rgba(22, 33, 62, 0.95);
216
+ backdrop-filter: blur(8px);
217
+ border: 1px solid #0f3460;
218
+ }
219
+
220
+ body.fullscreen .details-panel.visible {
221
+ display: flex;
222
+ }
223
+
224
+ body.fullscreen .details-panel .details-header {
225
+ position: relative;
226
+ }
227
+
228
+ body.fullscreen .details-panel .close-details {
229
+ display: block;
230
+ }
231
+
232
+ .close-details {
233
+ display: none;
234
+ background: none;
235
+ border: none;
236
+ color: #888;
237
+ font-size: 20px;
238
+ cursor: pointer;
239
+ padding: 4px 8px;
240
+ }
241
+
242
+ .close-details:hover {
243
+ color: #e94560;
244
+ }
245
+
246
+ /* Visualize button in details panel */
247
+ .visualize-btn {
248
+ background: #e94560;
249
+ color: white;
250
+ border: none;
251
+ padding: 10px 16px;
252
+ border-radius: 6px;
253
+ cursor: pointer;
254
+ font-size: 13px;
255
+ width: 100%;
256
+ margin-top: 12px;
257
+ display: flex;
258
+ align-items: center;
259
+ justify-content: center;
260
+ gap: 8px;
261
+ transition: background 0.2s;
262
+ }
263
+
264
+ .visualize-btn:hover {
265
+ background: #d63d56;
266
+ }
267
+
268
+ .visualize-btn:disabled {
269
+ background: #475569;
270
+ cursor: not-allowed;
271
+ }
272
+
273
+ .visualize-btn-group {
274
+ display: flex;
275
+ gap: 8px;
276
+ margin-top: 12px;
277
+ }
278
+
279
+ .visualize-btn-group .visualize-btn {
280
+ flex: 1;
281
+ margin-top: 0;
282
+ padding: 8px 12px;
283
+ font-size: 12px;
284
+ }
285
+
286
+ .visualize-btn-group .visualize-btn.secondary {
287
+ background: #0f3460;
288
+ color: #ccc;
289
+ }
290
+
291
+ .visualize-btn-group .visualize-btn.secondary:hover {
292
+ background: #1a4a7a;
293
+ color: #fff;
294
+ }
295
+
296
+ /* Smooth transitions for fullscreen */
297
+ .main, .graph-container, #cy {
298
+ transition: all 0.2s ease;
299
+ }
300
+
301
+ /* Tabs */
302
+ .tabs {
303
+ background: #16213e;
304
+ display: flex;
305
+ gap: 0;
306
+ border-bottom: 1px solid #0f3460;
307
+ }
308
+
309
+ .tab {
310
+ padding: 10px 20px;
311
+ cursor: pointer;
312
+ color: #888;
313
+ border-bottom: 2px solid transparent;
314
+ transition: all 0.2s;
315
+ }
316
+
317
+ .tab:hover {
318
+ color: #ccc;
319
+ background: rgba(233, 69, 96, 0.1);
320
+ }
321
+
322
+ .tab.active {
323
+ color: #e94560;
324
+ border-bottom-color: #e94560;
325
+ }
326
+
327
+ /* Main layout */
328
+ .main {
329
+ display: flex;
330
+ height: calc(100vh - 90px);
331
+ min-height: 0;
332
+ overflow: hidden;
333
+ }
334
+
335
+ /* Sidebar */
336
+ .sidebar {
337
+ width: 220px;
338
+ background: #16213e;
339
+ padding: 16px;
340
+ border-right: 1px solid #0f3460;
341
+ overflow-y: auto;
342
+ }
343
+
344
+ .sidebar h3 {
345
+ font-size: 12px;
346
+ text-transform: uppercase;
347
+ color: #666;
348
+ margin-bottom: 12px;
349
+ }
350
+
351
+ .filter-group {
352
+ margin-bottom: 20px;
353
+ }
354
+
355
+ .filter-item {
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 8px;
359
+ padding: 6px 0;
360
+ cursor: pointer;
361
+ }
362
+
363
+ .filter-item input {
364
+ accent-color: #e94560;
365
+ }
366
+
367
+ .filter-item label {
368
+ font-size: 13px;
369
+ cursor: pointer;
370
+ }
371
+
372
+ .filter-item .count {
373
+ margin-left: auto;
374
+ font-size: 11px;
375
+ color: #666;
376
+ background: #0f3460;
377
+ padding: 2px 6px;
378
+ border-radius: 10px;
379
+ }
380
+
381
+ .layout-select {
382
+ width: 100%;
383
+ background: #0f3460;
384
+ border: 1px solid #1a1a2e;
385
+ color: #eee;
386
+ padding: 8px;
387
+ border-radius: 4px;
388
+ margin-top: 8px;
389
+ }
390
+
391
+ /* Graph container */
392
+ .graph-container {
393
+ flex: 1;
394
+ display: flex;
395
+ flex-direction: column;
396
+ min-height: 0; /* Important for flex overflow */
397
+ min-width: 0;
398
+ position: relative;
399
+ }
400
+
401
+ #cy {
402
+ flex: 1;
403
+ background: #1a1a2e;
404
+ min-height: 0;
405
+ }
406
+
407
+ /* Details panel */
408
+ .details-panel {
409
+ width: 350px;
410
+ background: #16213e;
411
+ border-left: 1px solid #0f3460;
412
+ display: flex;
413
+ flex-direction: column;
414
+ overflow: hidden;
415
+ }
416
+
417
+ .details-header {
418
+ padding: 12px 16px;
419
+ border-bottom: 1px solid #0f3460;
420
+ display: flex;
421
+ justify-content: space-between;
422
+ align-items: center;
423
+ }
424
+
425
+ .details-header h3 {
426
+ font-size: 14px;
427
+ font-weight: 500;
428
+ }
429
+
430
+ .details-content {
431
+ flex: 1;
432
+ overflow-y: auto;
433
+ padding: 16px;
434
+ }
435
+
436
+ .detail-row {
437
+ margin-bottom: 12px;
438
+ }
439
+
440
+ .detail-label {
441
+ font-size: 11px;
442
+ text-transform: uppercase;
443
+ color: #666;
444
+ margin-bottom: 4px;
445
+ }
446
+
447
+ .detail-value {
448
+ font-size: 13px;
449
+ color: #eee;
450
+ }
451
+
452
+ .detail-value a {
453
+ color: #e94560;
454
+ text-decoration: none;
455
+ }
456
+
457
+ .detail-value a:hover {
458
+ text-decoration: underline;
459
+ }
460
+
461
+ /* Code block */
462
+ .code-block {
463
+ background: #0f3460;
464
+ border-radius: 4px;
465
+ padding: 12px;
466
+ font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
467
+ font-size: 12px;
468
+ line-height: 1.5;
469
+ overflow-x: auto;
470
+ white-space: pre;
471
+ max-height: 300px;
472
+ overflow-y: auto;
473
+ }
474
+
475
+ /* Tags */
476
+ .tag {
477
+ display: inline-block;
478
+ padding: 2px 8px;
479
+ border-radius: 3px;
480
+ font-size: 11px;
481
+ font-weight: 500;
482
+ }
483
+
484
+ .tag.function { background: #2563eb; color: white; }
485
+ .tag.class { background: #7c3aed; color: white; }
486
+ .tag.method { background: #0891b2; color: white; }
487
+ .tag.interface { background: #059669; color: white; }
488
+ .tag.type { background: #d97706; color: white; }
489
+ .tag.variable { background: #dc2626; color: white; }
490
+ .tag.module { background: #4f46e5; color: white; }
491
+ .tag.file { background: #475569; color: white; }
492
+ .tag.decision { background: #16a34a; color: white; }
493
+ .tag.pattern { background: #0d9488; color: white; }
494
+ .tag.rejection { background: #dc2626; color: white; }
495
+
496
+ /* List view */
497
+ #list-view {
498
+ display: none;
499
+ flex: 1;
500
+ overflow-y: auto;
501
+ overflow-x: hidden;
502
+ padding: 16px;
503
+ background: #1a1a2e;
504
+ min-height: 0; /* Important for flex overflow */
505
+ }
506
+
507
+ #list-view.active {
508
+ display: flex;
509
+ flex-direction: column;
510
+ }
511
+
512
+ #list-view .list-content {
513
+ flex: 1;
514
+ overflow-y: auto;
515
+ }
516
+
517
+ #cy.hidden {
518
+ display: none !important;
519
+ }
520
+
521
+ .list-section {
522
+ margin-bottom: 24px;
523
+ }
524
+
525
+ .list-section h4 {
526
+ font-size: 12px;
527
+ text-transform: uppercase;
528
+ color: #666;
529
+ margin-bottom: 12px;
530
+ padding-bottom: 8px;
531
+ border-bottom: 1px solid #0f3460;
532
+ position: sticky;
533
+ top: 0;
534
+ background: #1a1a2e;
535
+ z-index: 10;
536
+ padding-top: 8px;
537
+ }
538
+
539
+ .list-item {
540
+ display: flex;
541
+ align-items: flex-start;
542
+ gap: 12px;
543
+ padding: 10px 12px;
544
+ background: #16213e;
545
+ border-radius: 4px;
546
+ margin-bottom: 8px;
547
+ cursor: pointer;
548
+ transition: background 0.2s;
549
+ max-width: 100%;
550
+ overflow: hidden;
551
+ }
552
+
553
+ .list-item:hover {
554
+ background: #1e2a4a;
555
+ }
556
+
557
+ .list-item .tag {
558
+ flex-shrink: 0;
559
+ }
560
+
561
+ .list-item-content {
562
+ flex: 1;
563
+ min-width: 0;
564
+ overflow: hidden;
565
+ }
566
+
567
+ .list-item-name {
568
+ font-weight: 500;
569
+ color: #eee;
570
+ margin-bottom: 4px;
571
+ white-space: nowrap;
572
+ overflow: hidden;
573
+ text-overflow: ellipsis;
574
+ }
575
+
576
+ .list-item-location {
577
+ font-size: 12px;
578
+ color: #666;
579
+ white-space: nowrap;
580
+ overflow: hidden;
581
+ text-overflow: ellipsis;
582
+ }
583
+
584
+ .list-item-signature {
585
+ font-size: 11px;
586
+ color: #888;
587
+ font-family: 'Fira Code', monospace;
588
+ margin-top: 4px;
589
+ white-space: nowrap;
590
+ overflow: hidden;
591
+ text-overflow: ellipsis;
592
+ }
593
+
594
+ /* Stats bar */
595
+ .stats-bar {
596
+ padding: 8px 16px;
597
+ background: #0f3460;
598
+ display: flex;
599
+ gap: 20px;
600
+ font-size: 12px;
601
+ color: #888;
602
+ }
603
+
604
+ .stat {
605
+ display: flex;
606
+ gap: 4px;
607
+ }
608
+
609
+ .stat-value {
610
+ color: #e94560;
611
+ font-weight: 500;
612
+ }
613
+
614
+ /* Legend */
615
+ .legend {
616
+ position: absolute;
617
+ bottom: 60px;
618
+ left: 16px;
619
+ background: rgba(22, 33, 62, 0.95);
620
+ border: 1px solid #0f3460;
621
+ border-radius: 8px;
622
+ padding: 12px 16px;
623
+ font-size: 11px;
624
+ z-index: 100;
625
+ backdrop-filter: blur(4px);
626
+ transition: opacity 0.2s;
627
+ }
628
+
629
+ /* Hide legend in list view */
630
+ .legend.hidden {
631
+ display: none;
632
+ }
633
+
634
+ .legend-title {
635
+ font-weight: 600;
636
+ color: #888;
637
+ margin-bottom: 8px;
638
+ text-transform: uppercase;
639
+ font-size: 10px;
640
+ }
641
+
642
+ .legend-item {
643
+ display: flex;
644
+ align-items: center;
645
+ gap: 8px;
646
+ margin: 4px 0;
647
+ color: #ccc;
648
+ }
649
+
650
+ .legend-dot {
651
+ width: 12px;
652
+ height: 12px;
653
+ border-radius: 50%;
654
+ }
655
+
656
+ .legend-shape {
657
+ width: 16px;
658
+ height: 10px;
659
+ border-radius: 3px;
660
+ }
661
+
662
+ /* Breadcrumb */
663
+ .breadcrumb {
664
+ display: flex;
665
+ align-items: center;
666
+ gap: 8px;
667
+ padding: 8px 16px;
668
+ background: #0f3460;
669
+ font-size: 12px;
670
+ color: #888;
671
+ border-bottom: 1px solid #1a1a2e;
672
+ }
673
+
674
+ .breadcrumb-item {
675
+ color: #888;
676
+ cursor: pointer;
677
+ transition: color 0.2s;
678
+ }
679
+
680
+ .breadcrumb-item:hover {
681
+ color: #e94560;
682
+ }
683
+
684
+ .breadcrumb-item.current {
685
+ color: #eee;
686
+ cursor: default;
687
+ }
688
+
689
+ .breadcrumb-sep {
690
+ color: #444;
691
+ }
692
+
693
+ /* Tooltip */
694
+ #tooltip {
695
+ position: fixed;
696
+ background: rgba(22, 33, 62, 0.98);
697
+ border: 1px solid #0f3460;
698
+ border-radius: 6px;
699
+ padding: 10px 14px;
700
+ font-size: 12px;
701
+ color: #eee;
702
+ pointer-events: none;
703
+ z-index: 2000;
704
+ max-width: 350px;
705
+ box-shadow: 0 4px 20px rgba(0,0,0,0.4);
706
+ display: none;
707
+ }
708
+
709
+ #tooltip.visible {
710
+ display: block;
711
+ }
712
+
713
+ #tooltip .tip-type {
714
+ font-size: 10px;
715
+ text-transform: uppercase;
716
+ color: #888;
717
+ margin-bottom: 4px;
718
+ }
719
+
720
+ #tooltip .tip-name {
721
+ font-weight: 600;
722
+ font-size: 14px;
723
+ margin-bottom: 6px;
724
+ }
725
+
726
+ #tooltip .tip-location {
727
+ font-size: 11px;
728
+ color: #666;
729
+ margin-bottom: 6px;
730
+ }
731
+
732
+ #tooltip .tip-hint {
733
+ font-size: 10px;
734
+ color: #e94560;
735
+ margin-top: 8px;
736
+ padding-top: 6px;
737
+ border-top: 1px solid #0f3460;
738
+ }
739
+
740
+ /* Welcome overlay */
741
+ .welcome-overlay {
742
+ position: absolute;
743
+ top: 50%;
744
+ left: 50%;
745
+ transform: translate(-50%, -50%);
746
+ background: rgba(22, 33, 62, 0.98);
747
+ border: 1px solid #0f3460;
748
+ border-radius: 12px;
749
+ padding: 32px 40px;
750
+ text-align: center;
751
+ z-index: 500;
752
+ max-width: 400px;
753
+ }
754
+
755
+ .welcome-overlay h2 {
756
+ color: #e94560;
757
+ margin-bottom: 16px;
758
+ font-size: 20px;
759
+ }
760
+
761
+ .welcome-overlay p {
762
+ color: #aaa;
763
+ margin-bottom: 12px;
764
+ line-height: 1.5;
765
+ }
766
+
767
+ .welcome-overlay .hint {
768
+ display: flex;
769
+ align-items: center;
770
+ gap: 8px;
771
+ padding: 8px 12px;
772
+ background: #0f3460;
773
+ border-radius: 6px;
774
+ margin: 8px 0;
775
+ text-align: left;
776
+ font-size: 13px;
777
+ }
778
+
779
+ .welcome-overlay .hint-icon {
780
+ font-size: 18px;
781
+ }
782
+
783
+ .welcome-overlay button {
784
+ background: #e94560;
785
+ color: white;
786
+ border: none;
787
+ padding: 12px 32px;
788
+ border-radius: 6px;
789
+ cursor: pointer;
790
+ font-size: 14px;
791
+ margin-top: 16px;
792
+ }
793
+
794
+ .welcome-overlay button:hover {
795
+ background: #d63d56;
796
+ }
797
+
798
+ /* View description */
799
+ .view-description {
800
+ padding: 10px 16px;
801
+ background: #0f3460;
802
+ border-bottom: 1px solid #1a1a2e;
803
+ font-size: 13px;
804
+ color: #aaa;
805
+ line-height: 1.4;
806
+ }
807
+
808
+ .view-description strong {
809
+ color: #e94560;
810
+ }
811
+
812
+ /* Empty state */
813
+ .empty-state {
814
+ display: flex;
815
+ flex-direction: column;
816
+ align-items: center;
817
+ justify-content: center;
818
+ height: 100%;
819
+ color: #666;
820
+ }
821
+
822
+ .empty-state h3 {
823
+ margin-bottom: 8px;
824
+ }
825
+
826
+ /* Tooltip */
827
+ .cy-tooltip {
828
+ position: absolute;
829
+ background: #16213e;
830
+ border: 1px solid #0f3460;
831
+ padding: 8px 12px;
832
+ border-radius: 4px;
833
+ font-size: 12px;
834
+ pointer-events: none;
835
+ z-index: 1000;
836
+ max-width: 300px;
837
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
838
+ }
839
+
840
+ /* ============================================
841
+ New View Styles
842
+ ============================================ */
843
+
844
+ /* Custom view containers */
845
+ .custom-view {
846
+ display: none;
847
+ flex: 1;
848
+ overflow-y: auto;
849
+ padding: 20px;
850
+ background: #1a1a2e;
851
+ }
852
+
853
+ .custom-view.active {
854
+ display: block;
855
+ }
856
+
857
+ /* Code Smells View */
858
+ .smells-summary {
859
+ display: flex;
860
+ gap: 16px;
861
+ margin-bottom: 24px;
862
+ }
863
+
864
+ .smell-stat {
865
+ background: #16213e;
866
+ border-radius: 8px;
867
+ padding: 16px 24px;
868
+ text-align: center;
869
+ min-width: 120px;
870
+ }
871
+
872
+ .smell-stat .count {
873
+ font-size: 32px;
874
+ font-weight: bold;
875
+ }
876
+
877
+ .smell-stat .label {
878
+ font-size: 12px;
879
+ color: #888;
880
+ text-transform: uppercase;
881
+ margin-top: 4px;
882
+ }
883
+
884
+ .smell-stat.high .count { color: #ef4444; }
885
+ .smell-stat.medium .count { color: #f59e0b; }
886
+ .smell-stat.low .count { color: #22c55e; }
887
+
888
+ .smell-filters {
889
+ display: flex;
890
+ gap: 8px;
891
+ margin-bottom: 16px;
892
+ }
893
+
894
+ .smell-filter-btn {
895
+ background: #0f3460;
896
+ border: 1px solid #1a1a2e;
897
+ color: #888;
898
+ padding: 6px 14px;
899
+ border-radius: 4px;
900
+ cursor: pointer;
901
+ font-size: 13px;
902
+ }
903
+
904
+ .smell-filter-btn:hover { color: #ccc; }
905
+ .smell-filter-btn.active { background: #e94560; color: white; border-color: #e94560; }
906
+
907
+ .smell-list {
908
+ display: flex;
909
+ flex-direction: column;
910
+ gap: 8px;
911
+ }
912
+
913
+ .smell-item {
914
+ display: flex;
915
+ align-items: center;
916
+ gap: 12px;
917
+ padding: 12px 16px;
918
+ background: #16213e;
919
+ border-radius: 6px;
920
+ cursor: pointer;
921
+ border-left: 4px solid transparent;
922
+ }
923
+
924
+ .smell-item:hover { background: #1e2a4a; }
925
+ .smell-item.high { border-left-color: #ef4444; }
926
+ .smell-item.medium { border-left-color: #f59e0b; }
927
+ .smell-item.low { border-left-color: #22c55e; }
928
+
929
+ .smell-badge {
930
+ padding: 4px 8px;
931
+ border-radius: 4px;
932
+ font-size: 11px;
933
+ font-weight: 600;
934
+ text-transform: uppercase;
935
+ min-width: 100px;
936
+ text-align: center;
937
+ }
938
+
939
+ .smell-badge.large-file { background: #7c3aed; color: white; }
940
+ .smell-badge.long-function { background: #2563eb; color: white; }
941
+ .smell-badge.too-many-callers { background: #0891b2; color: white; }
942
+ .smell-badge.too-many-callees { background: #059669; color: white; }
943
+ .smell-badge.orphan { background: #6b7280; color: white; }
944
+
945
+ .smell-info { flex: 1; }
946
+ .smell-name { font-weight: 500; margin-bottom: 2px; }
947
+ .smell-desc { font-size: 12px; color: #888; }
948
+ .smell-metric { font-size: 14px; color: #e94560; font-weight: 500; }
949
+
950
+ /* Hotspots View */
951
+ .hotspots-grid {
952
+ display: grid;
953
+ grid-template-columns: repeat(2, 1fr);
954
+ gap: 20px;
955
+ }
956
+
957
+ .hotspot-section {
958
+ background: #16213e;
959
+ border-radius: 8px;
960
+ padding: 16px;
961
+ }
962
+
963
+ .hotspot-section h3 {
964
+ font-size: 14px;
965
+ color: #e94560;
966
+ margin-bottom: 12px;
967
+ padding-bottom: 8px;
968
+ border-bottom: 1px solid #0f3460;
969
+ }
970
+
971
+ .hotspot-list {
972
+ display: flex;
973
+ flex-direction: column;
974
+ gap: 8px;
975
+ }
976
+
977
+ .hotspot-item {
978
+ display: flex;
979
+ align-items: center;
980
+ gap: 12px;
981
+ padding: 8px 12px;
982
+ background: #0f3460;
983
+ border-radius: 4px;
984
+ cursor: pointer;
985
+ }
986
+
987
+ .hotspot-item:hover { background: #1a4a7a; }
988
+
989
+ .hotspot-rank {
990
+ width: 24px;
991
+ height: 24px;
992
+ background: #e94560;
993
+ color: white;
994
+ border-radius: 50%;
995
+ display: flex;
996
+ align-items: center;
997
+ justify-content: center;
998
+ font-size: 12px;
999
+ font-weight: bold;
1000
+ }
1001
+
1002
+ .hotspot-info { flex: 1; min-width: 0; }
1003
+ .hotspot-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1004
+ .hotspot-file { font-size: 11px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1005
+ .hotspot-value { font-size: 14px; color: #e94560; font-weight: 500; }
1006
+
1007
+ /* Gallery View */
1008
+ .gallery-filters {
1009
+ display: flex;
1010
+ gap: 8px;
1011
+ margin-bottom: 20px;
1012
+ }
1013
+
1014
+ .gallery-grid {
1015
+ display: grid;
1016
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1017
+ gap: 16px;
1018
+ }
1019
+
1020
+ .gallery-card {
1021
+ background: #16213e;
1022
+ border-radius: 8px;
1023
+ padding: 16px;
1024
+ cursor: pointer;
1025
+ transition: transform 0.2s, box-shadow 0.2s;
1026
+ }
1027
+
1028
+ .gallery-card:hover {
1029
+ transform: translateY(-2px);
1030
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
1031
+ }
1032
+
1033
+ .gallery-card-header {
1034
+ display: flex;
1035
+ align-items: center;
1036
+ gap: 8px;
1037
+ margin-bottom: 12px;
1038
+ }
1039
+
1040
+ .gallery-type-badge {
1041
+ padding: 4px 8px;
1042
+ border-radius: 4px;
1043
+ font-size: 11px;
1044
+ font-weight: 600;
1045
+ text-transform: uppercase;
1046
+ }
1047
+
1048
+ .gallery-type-badge.decision { background: #16a34a; color: white; }
1049
+ .gallery-type-badge.pattern { background: #0d9488; color: white; }
1050
+ .gallery-type-badge.rejection { background: #dc2626; color: white; }
1051
+
1052
+ .gallery-timestamp {
1053
+ font-size: 11px;
1054
+ color: #666;
1055
+ margin-left: auto;
1056
+ }
1057
+
1058
+ .gallery-content {
1059
+ font-size: 13px;
1060
+ line-height: 1.5;
1061
+ color: #ccc;
1062
+ margin-bottom: 12px;
1063
+ max-height: 80px;
1064
+ overflow: hidden;
1065
+ text-overflow: ellipsis;
1066
+ }
1067
+
1068
+ .gallery-card.expanded .gallery-content {
1069
+ max-height: none;
1070
+ }
1071
+
1072
+ .gallery-affected {
1073
+ display: flex;
1074
+ flex-wrap: wrap;
1075
+ gap: 6px;
1076
+ }
1077
+
1078
+ .gallery-code-chip {
1079
+ padding: 4px 8px;
1080
+ background: #0f3460;
1081
+ border-radius: 4px;
1082
+ font-size: 11px;
1083
+ color: #aaa;
1084
+ }
1085
+
1086
+ .gallery-code-chip:hover { background: #1a4a7a; color: #fff; }
1087
+
1088
+ /* Timeline View */
1089
+ .timeline-container {
1090
+ display: flex;
1091
+ flex-direction: column;
1092
+ height: 100%;
1093
+ }
1094
+
1095
+ .timeline-header {
1096
+ display: flex;
1097
+ justify-content: space-between;
1098
+ align-items: center;
1099
+ margin-bottom: 16px;
1100
+ }
1101
+
1102
+ .timeline-scroll {
1103
+ flex: 1;
1104
+ overflow-x: auto;
1105
+ overflow-y: hidden;
1106
+ padding-bottom: 16px;
1107
+ }
1108
+
1109
+ .timeline-track {
1110
+ display: flex;
1111
+ align-items: flex-end;
1112
+ min-width: max-content;
1113
+ padding: 0 20px;
1114
+ height: 300px;
1115
+ border-bottom: 2px solid #0f3460;
1116
+ position: relative;
1117
+ }
1118
+
1119
+ .timeline-entry {
1120
+ display: flex;
1121
+ flex-direction: column;
1122
+ align-items: center;
1123
+ width: 60px;
1124
+ cursor: pointer;
1125
+ }
1126
+
1127
+ .timeline-bar {
1128
+ width: 40px;
1129
+ background: #e94560;
1130
+ border-radius: 4px 4px 0 0;
1131
+ min-height: 20px;
1132
+ transition: background 0.2s;
1133
+ }
1134
+
1135
+ .timeline-entry:hover .timeline-bar { background: #ff6b8a; }
1136
+
1137
+ .timeline-dot {
1138
+ width: 12px;
1139
+ height: 12px;
1140
+ background: #e94560;
1141
+ border-radius: 50%;
1142
+ margin-top: 8px;
1143
+ border: 2px solid #1a1a2e;
1144
+ }
1145
+
1146
+ .timeline-date {
1147
+ font-size: 10px;
1148
+ color: #666;
1149
+ margin-top: 8px;
1150
+ writing-mode: vertical-rl;
1151
+ text-orientation: mixed;
1152
+ transform: rotate(180deg);
1153
+ max-height: 60px;
1154
+ overflow: hidden;
1155
+ }
1156
+
1157
+ .timeline-details {
1158
+ margin-top: 20px;
1159
+ background: #16213e;
1160
+ border-radius: 8px;
1161
+ padding: 16px;
1162
+ }
1163
+
1164
+ /* Treemap View */
1165
+ #treemap-container {
1166
+ width: 100%;
1167
+ height: calc(100% - 50px);
1168
+ min-height: 400px;
1169
+ }
1170
+
1171
+ .treemap-breadcrumb {
1172
+ display: flex;
1173
+ align-items: center;
1174
+ gap: 8px;
1175
+ margin-bottom: 12px;
1176
+ font-size: 13px;
1177
+ }
1178
+
1179
+ .treemap-breadcrumb span {
1180
+ color: #888;
1181
+ cursor: pointer;
1182
+ }
1183
+
1184
+ .treemap-breadcrumb span:hover { color: #e94560; }
1185
+ .treemap-breadcrumb span.current { color: #eee; cursor: default; }
1186
+
1187
+ .treemap-node {
1188
+ stroke: #1a1a2e;
1189
+ stroke-width: 1px;
1190
+ cursor: pointer;
1191
+ transition: opacity 0.2s;
1192
+ }
1193
+
1194
+ .treemap-node:hover { opacity: 0.8; }
1195
+
1196
+ .treemap-label {
1197
+ font-size: 11px;
1198
+ fill: white;
1199
+ pointer-events: none;
1200
+ text-anchor: middle;
1201
+ }
1202
+
1203
+ .treemap-tooltip {
1204
+ position: absolute;
1205
+ background: rgba(22, 33, 62, 0.95);
1206
+ border: 1px solid #0f3460;
1207
+ border-radius: 6px;
1208
+ padding: 8px 12px;
1209
+ font-size: 12px;
1210
+ pointer-events: none;
1211
+ z-index: 1000;
1212
+ }
1213
+ </style>
1214
+ </head>
1215
+ <body>
1216
+ <div class="header">
1217
+ <h1>aimem <span>Dashboard: ${escapeHtml(data.project.name)}</span></h1>
1218
+ <div class="search-box">
1219
+ <input type="text" id="search" placeholder="Search nodes...">
1220
+ <button class="visualize-search-btn" id="visualize-search-btn" title="Visualize search results as graph" style="display: none;">Visualize</button>
1221
+ <button class="back-btn" id="back-btn" title="Go back">← Back</button>
1222
+ <div class="view-toggle">
1223
+ <button class="view-toggle-btn active" data-mode="visual">Visual</button>
1224
+ <button class="view-toggle-btn" data-mode="list">List</button>
1225
+ </div>
1226
+ <button class="fullscreen-btn" id="fullscreen-btn" title="Toggle fullscreen">Fullscreen</button>
1227
+ </div>
1228
+ </div>
1229
+
1230
+ <div class="tabs">
1231
+ <div class="tab active" data-view="overview">Overview</div>
1232
+ <div class="tab" data-view="callGraph">Call Graph</div>
1233
+ <div class="tab" data-view="dependencies">Dependencies</div>
1234
+ <div class="tab" data-view="classes">Classes</div>
1235
+ <div class="tab" data-view="decisions">Decisions</div>
1236
+ <div class="tab" data-view="smells">Code Smells</div>
1237
+ <div class="tab" data-view="hotspots">Hotspots</div>
1238
+ <div class="tab" data-view="gallery">Gallery</div>
1239
+ <div class="tab" data-view="timeline">Timeline</div>
1240
+ <div class="tab" data-view="treemap">Treemap</div>
1241
+ </div>
1242
+
1243
+ <button class="exit-fullscreen" id="exit-fullscreen" style="display: none;">Exit Fullscreen (ESC)</button>
1244
+
1245
+ <div class="main">
1246
+ <div class="sidebar">
1247
+ <div class="filter-group">
1248
+ <h3>Filter by Type</h3>
1249
+ <div class="filter-item">
1250
+ <input type="checkbox" id="filter-function" checked>
1251
+ <label for="filter-function">Functions</label>
1252
+ <span class="count">${data.stats.byType['function'] || 0}</span>
1253
+ </div>
1254
+ <div class="filter-item">
1255
+ <input type="checkbox" id="filter-class" checked>
1256
+ <label for="filter-class">Classes</label>
1257
+ <span class="count">${data.stats.byType['class'] || 0}</span>
1258
+ </div>
1259
+ <div class="filter-item">
1260
+ <input type="checkbox" id="filter-method" checked>
1261
+ <label for="filter-method">Methods</label>
1262
+ <span class="count">${data.stats.byType['method'] || 0}</span>
1263
+ </div>
1264
+ <div class="filter-item">
1265
+ <input type="checkbox" id="filter-interface" checked>
1266
+ <label for="filter-interface">Interfaces</label>
1267
+ <span class="count">${data.stats.byType['interface'] || 0}</span>
1268
+ </div>
1269
+ <div class="filter-item">
1270
+ <input type="checkbox" id="filter-type" checked>
1271
+ <label for="filter-type">Types</label>
1272
+ <span class="count">${data.stats.byType['type'] || 0}</span>
1273
+ </div>
1274
+ <div class="filter-item">
1275
+ <input type="checkbox" id="filter-file" checked>
1276
+ <label for="filter-file">Files</label>
1277
+ <span class="count">${data.stats.totalFiles}</span>
1278
+ </div>
1279
+ </div>
1280
+
1281
+ <div class="filter-group">
1282
+ <h3>Layout</h3>
1283
+ <select class="layout-select" id="layout-select">
1284
+ <option value="grid">Grid</option>
1285
+ <option value="cose">Force Directed</option>
1286
+ <option value="breadthfirst">Hierarchical</option>
1287
+ <option value="circle">Circular</option>
1288
+ <option value="concentric">Concentric</option>
1289
+ </select>
1290
+ </div>
1291
+
1292
+ <div class="filter-group" id="flow-mode-group" style="display: none;">
1293
+ <h3>Flow Mode</h3>
1294
+ <div class="filter-item">
1295
+ <input type="radio" name="flow-mode" id="flow-connections" value="connections" checked>
1296
+ <label for="flow-connections">Connections</label>
1297
+ </div>
1298
+ <div class="filter-item">
1299
+ <input type="radio" name="flow-mode" id="flow-downstream" value="downstream">
1300
+ <label for="flow-downstream">Downstream (calls)</label>
1301
+ </div>
1302
+ <div class="filter-item">
1303
+ <input type="radio" name="flow-mode" id="flow-upstream" value="upstream">
1304
+ <label for="flow-upstream">Upstream (callers)</label>
1305
+ </div>
1306
+ <p style="font-size: 11px; color: #666; margin-top: 8px;">Click a function to trace its flow</p>
1307
+ </div>
1308
+ </div>
1309
+
1310
+ <div class="graph-container">
1311
+ <div class="breadcrumb" id="breadcrumb">
1312
+ <span class="breadcrumb-item current">Project</span>
1313
+ </div>
1314
+ <div class="view-description" id="view-description">
1315
+ <strong>Overview</strong> - All files in your codebase. Each node shows the file name and number of structures (functions, classes, etc.) it contains. Click a file to see details.
1316
+ </div>
1317
+ <div id="cy"></div>
1318
+ <div id="list-view"></div>
1319
+
1320
+ <!-- Custom Views (non-graph) -->
1321
+ <div id="smells-view" class="custom-view"></div>
1322
+ <div id="hotspots-view" class="custom-view"></div>
1323
+ <div id="gallery-view" class="custom-view"></div>
1324
+ <div id="timeline-view" class="custom-view"></div>
1325
+ <div id="treemap-view" class="custom-view"></div>
1326
+
1327
+ <!-- Legend -->
1328
+ <div class="legend" id="legend">
1329
+ <div class="legend-title">Legend</div>
1330
+ <div class="legend-item"><span class="legend-shape" style="background: #475569;"></span> File</div>
1331
+ <div class="legend-item"><span class="legend-dot" style="background: #2563eb;"></span> Function</div>
1332
+ <div class="legend-item"><span class="legend-dot" style="background: #7c3aed;"></span> Class</div>
1333
+ <div class="legend-item"><span class="legend-dot" style="background: #0891b2;"></span> Method</div>
1334
+ <div class="legend-item"><span class="legend-dot" style="background: #059669;"></span> Interface</div>
1335
+ </div>
1336
+
1337
+ <!-- Tooltip -->
1338
+ <div id="tooltip">
1339
+ <div class="tip-type"></div>
1340
+ <div class="tip-name"></div>
1341
+ <div class="tip-location"></div>
1342
+ <div class="tip-hint">Double-click to explore</div>
1343
+ </div>
1344
+
1345
+ <!-- Welcome overlay (shown on first visit) -->
1346
+ <div class="welcome-overlay" id="welcome" style="display: none;">
1347
+ <h2>Explore Your Code</h2>
1348
+ <p>Navigate your codebase like a map:</p>
1349
+ <div class="hint"><strong>Click</strong> - See details and source code</div>
1350
+ <div class="hint"><strong>Double-click</strong> - Dive deeper into that item</div>
1351
+ <div class="hint"><strong>Hover</strong> - See what is connected</div>
1352
+ <div class="hint"><strong>Search</strong> - Find anything by name</div>
1353
+ <button onclick="dismissWelcome()">Start Exploring</button>
1354
+ </div>
1355
+
1356
+ <div class="stats-bar">
1357
+ <div class="stat"><span class="stat-value">${data.stats.totalStructures}</span> structures</div>
1358
+ <div class="stat"><span class="stat-value">${data.stats.totalFiles}</span> files</div>
1359
+ <div class="stat"><span class="stat-value">${data.stats.totalLinks}</span> links</div>
1360
+ <div class="stat"><span class="stat-value">${data.stats.totalDecisions}</span> decisions</div>
1361
+ <div class="stat"><span class="stat-value">${data.stats.totalConversations}</span> conversations</div>
1362
+ </div>
1363
+ </div>
1364
+
1365
+ <div class="details-panel" id="details-panel">
1366
+ <div class="details-header">
1367
+ <h3>Details</h3>
1368
+ <button class="close-details" id="close-details" title="Close">&times;</button>
1369
+ </div>
1370
+ <div class="details-content" id="details-content">
1371
+ <div class="empty-state">
1372
+ <h3>No selection</h3>
1373
+ <p>Click a node to view details</p>
1374
+ </div>
1375
+ </div>
1376
+ </div>
1377
+ </div>
1378
+
1379
+ <script id="viz-data" type="application/json">${jsonData}</script>
1380
+ <script src="https://d3js.org/d3.v7.min.js"></script>
1381
+ <script>
1382
+ // Visualization data - parse from JSON script tag to avoid escaping issues
1383
+ const vizData = JSON.parse(document.getElementById('viz-data').textContent);
1384
+
1385
+ // Initialize Cytoscape
1386
+ let cy = null;
1387
+ let currentView = 'overview';
1388
+ let currentViewMode = 'visual'; // 'visual' or 'list'
1389
+
1390
+ // View descriptions - clear, action-oriented
1391
+ const viewDescriptions = {
1392
+ overview: '<strong>Files</strong> - Your codebase at a glance. Hover to see connections. <em>Double-click any file</em> to explore inside.',
1393
+ callGraph: '<strong>Call Graph</strong> - See how functions connect. Bigger nodes = more connections. <em>Double-click</em> to follow the flow.',
1394
+ dependencies: '<strong>Dependencies</strong> - File relationships. <em>Double-click</em> to see inside each file.',
1395
+ classes: '<strong>Classes</strong> - Your types and structures. <em>Double-click a class</em> to see its methods.',
1396
+ decisions: '<strong>Decisions</strong> - Why your code is the way it is. Connects decisions to the code they affect.',
1397
+ smells: '<strong>Code Smells</strong> - Potential issues detected in your codebase. Filter by type or severity.',
1398
+ hotspots: '<strong>Hotspots</strong> - The most complex and connected parts of your code. Focus refactoring efforts here.',
1399
+ gallery: '<strong>Gallery</strong> - Browse all decisions, patterns, and rejections from AI conversations.',
1400
+ timeline: '<strong>Timeline</strong> - See conversation activity over time. Click entries to see details.',
1401
+ treemap: '<strong>Treemap</strong> - Hierarchical view of your codebase by size. Click to zoom into directories.',
1402
+ };
1403
+
1404
+ // Store full graph data for focus mode filtering
1405
+ let fullCallGraphData = null;
1406
+
1407
+ // Navigation history for back button
1408
+ let drillHistory = [];
1409
+
1410
+ // Flow mode: 'connections' (default) or 'downstream' or 'upstream'
1411
+ let flowMode = 'connections';
1412
+
1413
+ // Node colors
1414
+ const nodeColors = {
1415
+ function: '#2563eb',
1416
+ class: '#7c3aed',
1417
+ method: '#0891b2',
1418
+ interface: '#059669',
1419
+ type: '#d97706',
1420
+ variable: '#dc2626',
1421
+ module: '#4f46e5',
1422
+ file: '#475569',
1423
+ decision: '#16a34a',
1424
+ pattern: '#0d9488',
1425
+ rejection: '#dc2626',
1426
+ };
1427
+
1428
+ // Cytoscape styles - node size based on weight (connections)
1429
+ const cyStyle = [
1430
+ {
1431
+ selector: 'node',
1432
+ style: {
1433
+ 'label': 'data(label)',
1434
+ 'text-valign': 'bottom',
1435
+ 'text-halign': 'center',
1436
+ 'font-size': '10px',
1437
+ 'color': '#ccc',
1438
+ 'text-margin-y': 4,
1439
+ 'background-color': '#475569',
1440
+ 'width': 'mapData(weight, 0, 10, 25, 60)', // Size based on connections
1441
+ 'height': 'mapData(weight, 0, 10, 25, 60)',
1442
+ }
1443
+ },
1444
+ {
1445
+ selector: 'node[type="function"]',
1446
+ style: { 'background-color': nodeColors.function }
1447
+ },
1448
+ {
1449
+ selector: 'node[type="class"]',
1450
+ style: { 'background-color': nodeColors.class, 'width': 40, 'height': 40 }
1451
+ },
1452
+ {
1453
+ selector: 'node[type="method"]',
1454
+ style: { 'background-color': nodeColors.method }
1455
+ },
1456
+ {
1457
+ selector: 'node[type="interface"]',
1458
+ style: { 'background-color': nodeColors.interface }
1459
+ },
1460
+ {
1461
+ selector: 'node[type="type"]',
1462
+ style: { 'background-color': nodeColors.type }
1463
+ },
1464
+ {
1465
+ selector: 'node[type="file"]',
1466
+ style: {
1467
+ 'background-color': nodeColors.file,
1468
+ 'width': 50,
1469
+ 'height': 30,
1470
+ 'shape': 'round-rectangle',
1471
+ 'font-size': '11px',
1472
+ 'text-wrap': 'ellipsis',
1473
+ 'text-max-width': '80px',
1474
+ }
1475
+ },
1476
+ {
1477
+ selector: 'node[type="decision"]',
1478
+ style: { 'background-color': nodeColors.decision, 'shape': 'diamond' }
1479
+ },
1480
+ {
1481
+ selector: 'node[type="pattern"]',
1482
+ style: { 'background-color': nodeColors.pattern, 'shape': 'diamond' }
1483
+ },
1484
+ {
1485
+ selector: 'node[type="rejection"]',
1486
+ style: { 'background-color': nodeColors.rejection, 'shape': 'diamond' }
1487
+ },
1488
+ {
1489
+ selector: 'edge',
1490
+ style: {
1491
+ 'width': 1.5,
1492
+ 'line-color': '#334155',
1493
+ 'target-arrow-color': '#334155',
1494
+ 'target-arrow-shape': 'triangle',
1495
+ 'curve-style': 'bezier',
1496
+ 'arrow-scale': 0.8,
1497
+ }
1498
+ },
1499
+ {
1500
+ selector: 'edge[type="calls"]',
1501
+ style: { 'line-color': '#2563eb', 'target-arrow-color': '#2563eb' }
1502
+ },
1503
+ {
1504
+ selector: 'edge[type="contains"]',
1505
+ style: { 'line-style': 'dashed', 'line-color': '#475569', 'target-arrow-shape': 'none' }
1506
+ },
1507
+ {
1508
+ selector: 'edge[type="decision"]',
1509
+ style: { 'line-color': '#16a34a', 'target-arrow-color': '#16a34a' }
1510
+ },
1511
+ {
1512
+ selector: ':selected',
1513
+ style: {
1514
+ 'border-width': 3,
1515
+ 'border-color': '#e94560',
1516
+ }
1517
+ },
1518
+ {
1519
+ selector: 'node.hover',
1520
+ style: {
1521
+ 'border-width': 2,
1522
+ 'border-color': '#e94560',
1523
+ }
1524
+ },
1525
+ {
1526
+ selector: 'node.faded',
1527
+ style: {
1528
+ 'opacity': 0.2,
1529
+ }
1530
+ },
1531
+ {
1532
+ selector: 'edge.highlighted',
1533
+ style: {
1534
+ 'width': 3,
1535
+ 'line-color': '#e94560',
1536
+ 'target-arrow-color': '#e94560',
1537
+ 'z-index': 999,
1538
+ }
1539
+ },
1540
+ {
1541
+ selector: 'edge.faded',
1542
+ style: {
1543
+ 'opacity': 0.1,
1544
+ }
1545
+ }
1546
+ ];
1547
+
1548
+ function destroyCy() {
1549
+ if (cy) {
1550
+ cy.removeAllListeners();
1551
+ cy.destroy();
1552
+ cy = null;
1553
+ }
1554
+ }
1555
+
1556
+ function initCytoscape(graphData) {
1557
+ destroyCy();
1558
+
1559
+ const container = document.getElementById('cy');
1560
+ if (!container) return;
1561
+
1562
+ // Clear any previous content
1563
+ container.innerHTML = '';
1564
+
1565
+ // Ensure all nodes have a weight for sizing
1566
+ const nodesWithWeight = graphData.nodes.map(n => ({
1567
+ ...n,
1568
+ data: {
1569
+ ...n.data,
1570
+ weight: n.data.weight || 1
1571
+ }
1572
+ }));
1573
+
1574
+ cy = cytoscape({
1575
+ container: container,
1576
+ elements: [...nodesWithWeight, ...graphData.edges],
1577
+ style: cyStyle,
1578
+ layout: { name: 'cose', animate: false, randomize: true },
1579
+ minZoom: 0.1,
1580
+ maxZoom: 3,
1581
+ });
1582
+
1583
+ // Click handler
1584
+ cy.on('tap', 'node', function(evt) {
1585
+ const node = evt.target;
1586
+ const nodeData = node.data();
1587
+ showDetails(nodeData);
1588
+
1589
+ // In call graph, handle flow mode
1590
+ if (currentView === 'callGraph' && fullCallGraphData && nodeData.id) {
1591
+ setTimeout(() => {
1592
+ if (node.selected()) {
1593
+ if (flowMode === 'downstream') {
1594
+ // Save history before tracing
1595
+ drillHistory.push({
1596
+ view: currentView,
1597
+ label: 'Call Graph',
1598
+ description: document.getElementById('view-description').innerHTML
1599
+ });
1600
+ updateBackButton();
1601
+ updateBreadcrumb([{ label: 'Call Graph' }, { label: nodeData.label + ' (downstream)' }]);
1602
+ traceFlow(nodeData.id, 'downstream');
1603
+ } else if (flowMode === 'upstream') {
1604
+ // Save history before tracing
1605
+ drillHistory.push({
1606
+ view: currentView,
1607
+ label: 'Call Graph',
1608
+ description: document.getElementById('view-description').innerHTML
1609
+ });
1610
+ updateBackButton();
1611
+ updateBreadcrumb([{ label: 'Call Graph' }, { label: nodeData.label + ' (upstream)' }]);
1612
+ traceFlow(nodeData.id, 'upstream');
1613
+ } else {
1614
+ // Default: focus on connections
1615
+ focusOnNode(nodeData.id);
1616
+ }
1617
+ }
1618
+ }, 300);
1619
+ }
1620
+ });
1621
+
1622
+ // Double-click to drill down
1623
+ cy.on('dbltap', 'node', function(evt) {
1624
+ const nodeData = evt.target.data();
1625
+ drillDown(nodeData);
1626
+ });
1627
+
1628
+ // Clear selection on background click
1629
+ cy.on('tap', function(evt) {
1630
+ if (evt.target === cy) {
1631
+ clearDetails();
1632
+ }
1633
+ });
1634
+
1635
+ // Hover effects - show tooltip and highlight connections
1636
+ cy.on('mouseover', 'node', function(evt) {
1637
+ const node = evt.target;
1638
+ const nodeData = node.data();
1639
+
1640
+ // Show tooltip
1641
+ showTooltip(evt.originalEvent, nodeData);
1642
+
1643
+ // Highlight this node and its connections
1644
+ node.addClass('hover');
1645
+
1646
+ // Get connected edges and nodes
1647
+ const connectedEdges = node.connectedEdges();
1648
+ const connectedNodes = connectedEdges.connectedNodes();
1649
+
1650
+ // Fade everything else
1651
+ cy.elements().addClass('faded');
1652
+ node.removeClass('faded');
1653
+ connectedNodes.removeClass('faded');
1654
+ connectedEdges.removeClass('faded').addClass('highlighted');
1655
+ });
1656
+
1657
+ cy.on('mouseout', 'node', function(evt) {
1658
+ hideTooltip();
1659
+
1660
+ // Remove all highlight classes
1661
+ cy.elements().removeClass('faded hover highlighted');
1662
+ });
1663
+
1664
+ // Update tooltip position on mouse move
1665
+ cy.on('mousemove', 'node', function(evt) {
1666
+ moveTooltip(evt.originalEvent);
1667
+ });
1668
+
1669
+ runLayout();
1670
+ }
1671
+
1672
+ function runLayout() {
1673
+ if (!cy) return;
1674
+ const layoutName = document.getElementById('layout-select').value;
1675
+ const nodeCount = cy.nodes(':visible').length;
1676
+
1677
+ const layoutOptions = {
1678
+ name: layoutName,
1679
+ animate: nodeCount < 100,
1680
+ animationDuration: 300,
1681
+ nodeDimensionsIncludeLabels: true,
1682
+ fit: true,
1683
+ padding: 50,
1684
+ };
1685
+
1686
+ // Layout-specific options for better spacing
1687
+ if (layoutName === 'cose') {
1688
+ Object.assign(layoutOptions, {
1689
+ nodeRepulsion: 8000,
1690
+ idealEdgeLength: 100,
1691
+ edgeElasticity: 100,
1692
+ nestingFactor: 1.2,
1693
+ gravity: 0.25,
1694
+ numIter: 1000,
1695
+ randomize: true,
1696
+ });
1697
+ } else if (layoutName === 'breadthfirst') {
1698
+ Object.assign(layoutOptions, {
1699
+ spacingFactor: 1.5,
1700
+ directed: true,
1701
+ });
1702
+ } else if (layoutName === 'circle') {
1703
+ Object.assign(layoutOptions, {
1704
+ spacingFactor: 1.2,
1705
+ });
1706
+ } else if (layoutName === 'grid') {
1707
+ Object.assign(layoutOptions, {
1708
+ spacingFactor: 1.5,
1709
+ condense: false,
1710
+ });
1711
+ } else if (layoutName === 'concentric') {
1712
+ Object.assign(layoutOptions, {
1713
+ spacingFactor: 2,
1714
+ minNodeSpacing: 50,
1715
+ });
1716
+ }
1717
+
1718
+ cy.layout(layoutOptions).run();
1719
+ }
1720
+
1721
+ // Store current selected node data for visualization buttons
1722
+ let currentSelectedData = null;
1723
+
1724
+ function showDetails(data) {
1725
+ currentSelectedData = data;
1726
+ const content = document.getElementById('details-content');
1727
+ let html = '';
1728
+
1729
+ // Type tag
1730
+ html += '<div class="detail-row">';
1731
+ html += '<span class="tag ' + data.type + '">' + data.type + '</span>';
1732
+ html += '</div>';
1733
+
1734
+ // Name
1735
+ html += '<div class="detail-row">';
1736
+ html += '<div class="detail-label">Name</div>';
1737
+ html += '<div class="detail-value">' + escapeHtml(data.label) + '</div>';
1738
+ html += '</div>';
1739
+
1740
+ // File location
1741
+ if (data.file) {
1742
+ html += '<div class="detail-row">';
1743
+ html += '<div class="detail-label">Location</div>';
1744
+ html += '<div class="detail-value">' + escapeHtml(data.file);
1745
+ if (data.line) {
1746
+ html += ':' + data.line;
1747
+ }
1748
+ html += '</div>';
1749
+ html += '</div>';
1750
+ }
1751
+
1752
+ // Signature
1753
+ if (data.signature) {
1754
+ html += '<div class="detail-row">';
1755
+ html += '<div class="detail-label">Signature</div>';
1756
+ html += '<div class="detail-value"><code>' + escapeHtml(data.signature) + '</code></div>';
1757
+ html += '</div>';
1758
+ }
1759
+
1760
+ // Visualize buttons for functions and methods
1761
+ if (data.type === 'function' || data.type === 'method') {
1762
+ html += '<div class="detail-row">';
1763
+ html += '<div class="detail-label">Visualize</div>';
1764
+ html += '<button class="visualize-btn" onclick="visualizeNode(\\'connections\\')">Show Connections</button>';
1765
+ html += '<div class="visualize-btn-group">';
1766
+ html += '<button class="visualize-btn secondary" onclick="visualizeNode(\\'downstream\\')">Trace Calls &#8594;</button>';
1767
+ html += '<button class="visualize-btn secondary" onclick="visualizeNode(\\'upstream\\')">&#8592; Trace Callers</button>';
1768
+ html += '</div>';
1769
+ html += '</div>';
1770
+ }
1771
+ // For files: show contents button
1772
+ else if (data.type === 'file') {
1773
+ html += '<div class="detail-row">';
1774
+ html += '<button class="visualize-btn" onclick="visualizeFile()">Show File Contents</button>';
1775
+ html += '</div>';
1776
+ }
1777
+ // For classes/interfaces: show methods button
1778
+ else if (data.type === 'class' || data.type === 'interface') {
1779
+ html += '<div class="detail-row">';
1780
+ html += '<button class="visualize-btn" onclick="visualizeClass()">Show Methods</button>';
1781
+ html += '</div>';
1782
+ }
1783
+
1784
+ // Source code
1785
+ if (data.content) {
1786
+ html += '<div class="detail-row">';
1787
+ html += '<div class="detail-label">Source Code</div>';
1788
+ html += '<div class="code-block">' + escapeHtml(data.content) + '</div>';
1789
+ html += '</div>';
1790
+ }
1791
+
1792
+ content.innerHTML = html;
1793
+
1794
+ // Show panel in fullscreen mode
1795
+ if (document.body.classList.contains('fullscreen')) {
1796
+ document.getElementById('details-panel').classList.add('visible');
1797
+ }
1798
+ }
1799
+
1800
+ // Visualize a node in the call graph
1801
+ function visualizeNode(mode) {
1802
+ if (!currentSelectedData) return;
1803
+
1804
+ // Switch to visual mode if in list mode
1805
+ if (currentViewMode === 'list') {
1806
+ setViewMode('visual');
1807
+ }
1808
+
1809
+ // Make sure we have call graph data
1810
+ fullCallGraphData = vizData.graphs.callGraph;
1811
+
1812
+ // Save current state to history
1813
+ drillHistory.push({
1814
+ view: currentView,
1815
+ label: getBreadcrumbLabel(currentView),
1816
+ description: document.getElementById('view-description').innerHTML
1817
+ });
1818
+ updateBackButton();
1819
+
1820
+ // Set flow mode and update UI
1821
+ flowMode = mode;
1822
+ document.querySelectorAll('input[name="flow-mode"]').forEach(radio => {
1823
+ radio.checked = radio.value === mode;
1824
+ });
1825
+
1826
+ // Switch to call graph tab
1827
+ currentView = 'callGraph';
1828
+ document.querySelectorAll('.tab').forEach(tab => {
1829
+ tab.classList.toggle('active', tab.dataset.view === 'callGraph');
1830
+ });
1831
+ document.getElementById('flow-mode-group').style.display = 'block';
1832
+
1833
+ // Perform the visualization
1834
+ if (mode === 'downstream') {
1835
+ updateBreadcrumb([{ label: 'Call Graph' }, { label: currentSelectedData.label + ' (downstream)' }]);
1836
+ traceFlow(currentSelectedData.id, 'downstream');
1837
+ } else if (mode === 'upstream') {
1838
+ updateBreadcrumb([{ label: 'Call Graph' }, { label: currentSelectedData.label + ' (upstream)' }]);
1839
+ traceFlow(currentSelectedData.id, 'upstream');
1840
+ } else {
1841
+ updateBreadcrumb([{ label: 'Call Graph' }, { label: currentSelectedData.label }]);
1842
+ focusOnNode(currentSelectedData.id);
1843
+ }
1844
+ }
1845
+
1846
+ // Visualize file contents
1847
+ function visualizeFile() {
1848
+ if (!currentSelectedData || !currentSelectedData.file) return;
1849
+
1850
+ // Switch to visual mode if in list mode
1851
+ if (currentViewMode === 'list') {
1852
+ setViewMode('visual');
1853
+ }
1854
+
1855
+ drillDown(currentSelectedData);
1856
+ }
1857
+
1858
+ // Visualize class methods
1859
+ function visualizeClass() {
1860
+ if (!currentSelectedData) return;
1861
+
1862
+ // Switch to visual mode if in list mode
1863
+ if (currentViewMode === 'list') {
1864
+ setViewMode('visual');
1865
+ }
1866
+
1867
+ drillDown(currentSelectedData);
1868
+ }
1869
+
1870
+ function clearDetails() {
1871
+ document.getElementById('details-content').innerHTML = \`
1872
+ <div class="empty-state">
1873
+ <h3>No selection</h3>
1874
+ <p>Click a node to view details</p>
1875
+ </div>
1876
+ \`;
1877
+ // Hide panel in fullscreen mode
1878
+ document.getElementById('details-panel').classList.remove('visible');
1879
+ }
1880
+
1881
+ function escapeHtml(text) {
1882
+ if (!text) return '';
1883
+ const div = document.createElement('div');
1884
+ div.textContent = text;
1885
+ return div.innerHTML;
1886
+ }
1887
+
1888
+ // Tooltip functions
1889
+ function showTooltip(event, nodeData) {
1890
+ const tooltip = document.getElementById('tooltip');
1891
+ tooltip.querySelector('.tip-type').textContent = nodeData.type || '';
1892
+ tooltip.querySelector('.tip-name').textContent = nodeData.label || '';
1893
+
1894
+ let location = '';
1895
+ if (nodeData.file) {
1896
+ location = nodeData.file.split('/').pop();
1897
+ if (nodeData.line) location += ':' + nodeData.line;
1898
+ }
1899
+ tooltip.querySelector('.tip-location').textContent = location;
1900
+
1901
+ // Update hint based on type
1902
+ const hint = tooltip.querySelector('.tip-hint');
1903
+ if (nodeData.type === 'file') {
1904
+ hint.textContent = 'Double-click to see contents';
1905
+ } else if (nodeData.type === 'class' || nodeData.type === 'interface') {
1906
+ hint.textContent = 'Double-click to see methods';
1907
+ } else if (nodeData.type === 'function' || nodeData.type === 'method') {
1908
+ hint.textContent = 'Double-click to see call graph';
1909
+ } else {
1910
+ hint.textContent = 'Double-click to explore';
1911
+ }
1912
+
1913
+ tooltip.classList.add('visible');
1914
+ moveTooltip(event);
1915
+ }
1916
+
1917
+ function moveTooltip(event) {
1918
+ const tooltip = document.getElementById('tooltip');
1919
+ const x = event.clientX + 15;
1920
+ const y = event.clientY + 15;
1921
+
1922
+ // Keep tooltip on screen
1923
+ const rect = tooltip.getBoundingClientRect();
1924
+ const maxX = window.innerWidth - rect.width - 10;
1925
+ const maxY = window.innerHeight - rect.height - 10;
1926
+
1927
+ tooltip.style.left = Math.min(x, maxX) + 'px';
1928
+ tooltip.style.top = Math.min(y, maxY) + 'px';
1929
+ }
1930
+
1931
+ function hideTooltip() {
1932
+ document.getElementById('tooltip').classList.remove('visible');
1933
+ }
1934
+
1935
+ // Breadcrumb functions
1936
+ function updateBreadcrumb(items) {
1937
+ const bc = document.getElementById('breadcrumb');
1938
+ let html = '';
1939
+
1940
+ items.forEach((item, i) => {
1941
+ if (i > 0) html += '<span class="breadcrumb-sep">›</span>';
1942
+
1943
+ const isLast = i === items.length - 1;
1944
+ if (isLast) {
1945
+ html += '<span class="breadcrumb-item current">' + escapeHtml(item.label) + '</span>';
1946
+ } else {
1947
+ html += '<span class="breadcrumb-item" onclick="goBackTo(' + i + ')">' + escapeHtml(item.label) + '</span>';
1948
+ }
1949
+ });
1950
+
1951
+ bc.innerHTML = html;
1952
+ }
1953
+
1954
+ function goBackTo(index) {
1955
+ // Pop history until we reach the target index
1956
+ while (drillHistory.length > index) {
1957
+ drillHistory.pop();
1958
+ }
1959
+ updateBackButton();
1960
+
1961
+ if (drillHistory.length === 0) {
1962
+ switchView(currentView);
1963
+ } else {
1964
+ const target = drillHistory[drillHistory.length - 1];
1965
+ drillHistory.pop();
1966
+ switchView(target.view);
1967
+ }
1968
+ }
1969
+
1970
+ // Welcome overlay
1971
+ function dismissWelcome() {
1972
+ document.getElementById('welcome').style.display = 'none';
1973
+ localStorage.setItem('aimem-welcome-dismissed', 'true');
1974
+ }
1975
+
1976
+ function maybeShowWelcome() {
1977
+ if (!localStorage.getItem('aimem-welcome-dismissed')) {
1978
+ document.getElementById('welcome').style.display = 'block';
1979
+ }
1980
+ }
1981
+
1982
+ function switchView(view) {
1983
+ currentView = view;
1984
+
1985
+ // Update tabs
1986
+ document.querySelectorAll('.tab').forEach(tab => {
1987
+ tab.classList.toggle('active', tab.dataset.view === view);
1988
+ });
1989
+
1990
+ // Update description
1991
+ document.getElementById('view-description').innerHTML = viewDescriptions[view] || '';
1992
+
1993
+ // Show/hide flow mode controls for Call Graph
1994
+ const flowModeGroup = document.getElementById('flow-mode-group');
1995
+ if (view === 'callGraph') {
1996
+ flowModeGroup.style.display = 'block';
1997
+ } else {
1998
+ flowModeGroup.style.display = 'none';
1999
+ }
2000
+
2001
+ // Hide all custom views
2002
+ const customViews = ['smells', 'hotspots', 'gallery', 'timeline', 'treemap'];
2003
+ customViews.forEach(v => {
2004
+ const el = document.getElementById(v + '-view');
2005
+ if (el) el.classList.remove('active');
2006
+ });
2007
+
2008
+ // Show/hide cy and list-view based on view type
2009
+ const isCustomView = customViews.includes(view);
2010
+ document.getElementById('cy').style.display = isCustomView ? 'none' : 'block';
2011
+ document.getElementById('list-view').style.display = isCustomView ? 'none' : (currentViewMode === 'list' ? 'block' : 'none');
2012
+ document.getElementById('legend').style.display = isCustomView ? 'none' : 'block';
2013
+
2014
+ // Handle custom views
2015
+ if (isCustomView) {
2016
+ destroyCy();
2017
+ const customViewEl = document.getElementById(view + '-view');
2018
+ if (customViewEl) customViewEl.classList.add('active');
2019
+
2020
+ if (view === 'smells') renderSmellsView();
2021
+ else if (view === 'hotspots') renderHotspotsView();
2022
+ else if (view === 'gallery') renderGalleryView();
2023
+ else if (view === 'timeline') renderTimelineView();
2024
+ else if (view === 'treemap') renderTreemapView();
2025
+
2026
+ clearDetails();
2027
+ return;
2028
+ }
2029
+
2030
+ // Special handling for call graph - focus mode
2031
+ if (view === 'callGraph') {
2032
+ fullCallGraphData = vizData.graphs.callGraph;
2033
+ destroyCy();
2034
+
2035
+ // Find entry points: functions that call others but aren't called themselves
2036
+ const entryPoints = findEntryPoints();
2037
+
2038
+ if (entryPoints.length > 0) {
2039
+ // Show entry points view
2040
+ showEntryPoints(entryPoints);
2041
+ } else {
2042
+ const stats = fullCallGraphData ? fullCallGraphData.nodes.length + ' functions, ' + fullCallGraphData.edges.length + ' call relationships' : 'No data';
2043
+ document.getElementById('cy').innerHTML =
2044
+ '<div class="empty-state">' +
2045
+ '<h3>Search for a function</h3>' +
2046
+ '<p>Type a function name in the search box above to explore its call relationships</p>' +
2047
+ '<p style="margin-top: 16px; font-size: 12px; color: #666;">' + stats + '</p>' +
2048
+ '</div>';
2049
+ }
2050
+ // Re-render list view if in list mode
2051
+ if (currentViewMode === 'list') {
2052
+ renderListView();
2053
+ }
2054
+ clearDetails();
2055
+ return;
2056
+ }
2057
+
2058
+ fullCallGraphData = null;
2059
+
2060
+ // Load graph data
2061
+ const graphData = vizData.graphs[view];
2062
+ if (graphData && (graphData.nodes.length > 0 || graphData.edges.length > 0)) {
2063
+ initCytoscape(graphData);
2064
+ } else {
2065
+ destroyCy();
2066
+ document.getElementById('cy').innerHTML = \`
2067
+ <div class="empty-state">
2068
+ <h3>No data</h3>
2069
+ <p>No nodes to display for this view</p>
2070
+ </div>
2071
+ \`;
2072
+ }
2073
+
2074
+ // Re-render list view if in list mode
2075
+ if (currentViewMode === 'list') {
2076
+ renderListView();
2077
+ }
2078
+
2079
+ clearDetails();
2080
+ }
2081
+
2082
+ // Find entry points: functions that call others but aren't called
2083
+ function findEntryPoints() {
2084
+ if (!fullCallGraphData || !fullCallGraphData.edges.length) return [];
2085
+
2086
+ const callers = new Set(); // nodes that call something
2087
+ const callees = new Set(); // nodes that are called
2088
+
2089
+ for (const edge of fullCallGraphData.edges) {
2090
+ callers.add(edge.data.source);
2091
+ callees.add(edge.data.target);
2092
+ }
2093
+
2094
+ // Entry points: call others but aren't called
2095
+ const entryPointIds = [];
2096
+ for (const callerId of callers) {
2097
+ if (!callees.has(callerId)) {
2098
+ entryPointIds.push(callerId);
2099
+ }
2100
+ }
2101
+
2102
+ // Get the actual nodes, sorted by number of outgoing calls
2103
+ const outgoingCounts = {};
2104
+ for (const edge of fullCallGraphData.edges) {
2105
+ outgoingCounts[edge.data.source] = (outgoingCounts[edge.data.source] || 0) + 1;
2106
+ }
2107
+
2108
+ return fullCallGraphData.nodes
2109
+ .filter(n => entryPointIds.includes(n.data.id))
2110
+ .sort((a, b) => (outgoingCounts[b.data.id] || 0) - (outgoingCounts[a.data.id] || 0));
2111
+ }
2112
+
2113
+ // Show entry points with their immediate callees
2114
+ function showEntryPoints(entryPoints) {
2115
+ // Take top entry points (most outgoing calls)
2116
+ const topEntries = entryPoints.slice(0, 10);
2117
+ const entryIds = new Set(topEntries.map(n => n.data.id));
2118
+
2119
+ // Get edges from entry points
2120
+ const relevantEdges = fullCallGraphData.edges.filter(e => entryIds.has(e.data.source));
2121
+
2122
+ // Get called nodes
2123
+ const calledIds = new Set();
2124
+ for (const edge of relevantEdges) {
2125
+ calledIds.add(edge.data.target);
2126
+ }
2127
+
2128
+ // Build node set
2129
+ const nodeIds = new Set([...entryIds, ...calledIds]);
2130
+ const nodes = fullCallGraphData.nodes.filter(n => nodeIds.has(n.data.id));
2131
+
2132
+ if (nodes.length > 0) {
2133
+ initCytoscape({ nodes, edges: relevantEdges });
2134
+
2135
+ // Update description
2136
+ document.getElementById('view-description').innerHTML =
2137
+ '<strong>Entry Points</strong> - Functions that call others but are not called. These are typically command handlers or main functions. Click any node to explore further.';
2138
+ }
2139
+ }
2140
+
2141
+ // Focus mode: show only a node and its direct connections
2142
+ function focusOnNode(nodeId) {
2143
+ if (!fullCallGraphData) return;
2144
+
2145
+ const nodeIdStr = nodeId.startsWith('structure:') ? nodeId : 'structure:' + nodeId;
2146
+
2147
+ // Find the center node
2148
+ const centerNode = fullCallGraphData.nodes.find(n => n.data.id === nodeIdStr);
2149
+ if (!centerNode) return;
2150
+
2151
+ // Find all edges connected to this node
2152
+ const connectedEdges = fullCallGraphData.edges.filter(e =>
2153
+ e.data.source === nodeIdStr || e.data.target === nodeIdStr
2154
+ );
2155
+
2156
+ // Find all connected node IDs
2157
+ const connectedNodeIds = new Set([nodeIdStr]);
2158
+ for (const edge of connectedEdges) {
2159
+ connectedNodeIds.add(edge.data.source);
2160
+ connectedNodeIds.add(edge.data.target);
2161
+ }
2162
+
2163
+ // Build focused graph
2164
+ const focusedNodes = fullCallGraphData.nodes.filter(n => connectedNodeIds.has(n.data.id));
2165
+ const focusedGraph = {
2166
+ nodes: focusedNodes,
2167
+ edges: connectedEdges,
2168
+ };
2169
+
2170
+ if (focusedNodes.length > 0) {
2171
+ initCytoscape(focusedGraph);
2172
+
2173
+ // Highlight the center node
2174
+ setTimeout(() => {
2175
+ if (cy) {
2176
+ const centerEle = cy.getElementById(nodeIdStr);
2177
+ if (centerEle) {
2178
+ centerEle.select();
2179
+ showDetails(centerEle.data());
2180
+ }
2181
+ }
2182
+ }, 100);
2183
+ }
2184
+ }
2185
+
2186
+ // Trace code flow from a starting node
2187
+ function traceFlow(nodeId, direction) {
2188
+ if (!fullCallGraphData) return;
2189
+
2190
+ const nodeIdStr = nodeId.startsWith('structure:') ? nodeId : 'structure:' + nodeId;
2191
+ const visited = new Set();
2192
+ const nodesToInclude = new Set([nodeIdStr]);
2193
+ const edgesToInclude = [];
2194
+
2195
+ // BFS to trace flow
2196
+ const queue = [nodeIdStr];
2197
+ const maxDepth = 5; // Limit depth to avoid overwhelming
2198
+ const depthMap = { [nodeIdStr]: 0 };
2199
+
2200
+ while (queue.length > 0) {
2201
+ const current = queue.shift();
2202
+ if (visited.has(current)) continue;
2203
+ visited.add(current);
2204
+
2205
+ const currentDepth = depthMap[current] || 0;
2206
+ if (currentDepth >= maxDepth) continue;
2207
+
2208
+ for (const edge of fullCallGraphData.edges) {
2209
+ let nextNode = null;
2210
+
2211
+ if (direction === 'downstream' && edge.data.source === current) {
2212
+ // Following calls: source -> target
2213
+ nextNode = edge.data.target;
2214
+ } else if (direction === 'upstream' && edge.data.target === current) {
2215
+ // Following callers: target <- source
2216
+ nextNode = edge.data.source;
2217
+ }
2218
+
2219
+ if (nextNode && !visited.has(nextNode)) {
2220
+ nodesToInclude.add(nextNode);
2221
+ edgesToInclude.push(edge);
2222
+ queue.push(nextNode);
2223
+ depthMap[nextNode] = currentDepth + 1;
2224
+ }
2225
+ }
2226
+ }
2227
+
2228
+ // Build flow graph
2229
+ const flowNodes = fullCallGraphData.nodes.filter(n => nodesToInclude.has(n.data.id));
2230
+ const flowGraph = {
2231
+ nodes: flowNodes,
2232
+ edges: edgesToInclude,
2233
+ };
2234
+
2235
+ if (flowNodes.length > 0) {
2236
+ initCytoscape(flowGraph);
2237
+
2238
+ // Use hierarchical layout for flow
2239
+ setTimeout(() => {
2240
+ if (cy) {
2241
+ cy.layout({
2242
+ name: 'breadthfirst',
2243
+ directed: true,
2244
+ roots: direction === 'downstream' ? [nodeIdStr] : undefined,
2245
+ spacingFactor: 1.5,
2246
+ animate: true,
2247
+ animationDuration: 300,
2248
+ }).run();
2249
+ }
2250
+ }, 100);
2251
+
2252
+ // Highlight the starting node
2253
+ setTimeout(() => {
2254
+ if (cy) {
2255
+ const startNode = cy.getElementById(nodeIdStr);
2256
+ if (startNode) {
2257
+ startNode.select();
2258
+ showDetails(startNode.data());
2259
+ }
2260
+ }
2261
+ }, 200);
2262
+
2263
+ const dirLabel = direction === 'downstream' ? 'calls from' : 'callers of';
2264
+ document.getElementById('view-description').innerHTML =
2265
+ '<strong>Flow: ' + dirLabel + '</strong> - Tracing ' + (flowNodes.length - 1) + ' ' + (direction === 'downstream' ? 'called functions' : 'calling functions') + '. Click nodes to see details.';
2266
+ }
2267
+ }
2268
+
2269
+ // Drill down into a node to see related structures
2270
+ function drillDown(nodeData) {
2271
+ if (!nodeData) return;
2272
+
2273
+ let nodes = [];
2274
+ let edges = [];
2275
+ let title = '';
2276
+
2277
+ // For files: show all structures in that file
2278
+ if (nodeData.type === 'file' && nodeData.file) {
2279
+ const filePath = nodeData.file;
2280
+ title = 'Structures in ' + filePath.split('/').pop();
2281
+
2282
+ // Find all structures in this file from callGraph data
2283
+ const allNodes = vizData.graphs.callGraph.nodes;
2284
+ nodes = allNodes.filter(n => n.data.file === filePath);
2285
+
2286
+ // Find edges between these nodes
2287
+ const nodeIds = new Set(nodes.map(n => n.data.id));
2288
+ edges = vizData.graphs.callGraph.edges.filter(e =>
2289
+ nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
2290
+ );
2291
+ }
2292
+ // For classes/interfaces: show methods in same file near the class
2293
+ else if ((nodeData.type === 'class' || nodeData.type === 'interface') && nodeData.file) {
2294
+ const filePath = nodeData.file;
2295
+ const classLine = nodeData.line || 0;
2296
+ const classLineEnd = nodeData.lineEnd || classLine + 1000;
2297
+ title = 'Contents of ' + nodeData.label;
2298
+
2299
+ // Find methods in the same file within the class line range
2300
+ const allNodes = vizData.graphs.callGraph.nodes;
2301
+ nodes = allNodes.filter(n => {
2302
+ if (n.data.file !== filePath) return false;
2303
+ if (n.data.id === nodeData.id) return true; // Include the class itself
2304
+ if (n.data.type === 'method') {
2305
+ const line = n.data.line || 0;
2306
+ return line >= classLine && line <= classLineEnd;
2307
+ }
2308
+ return false;
2309
+ });
2310
+
2311
+ // Find edges between these nodes
2312
+ const nodeIds = new Set(nodes.map(n => n.data.id));
2313
+ edges = vizData.graphs.callGraph.edges.filter(e =>
2314
+ nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
2315
+ );
2316
+ }
2317
+ // For functions/methods: show what they call (focus mode)
2318
+ else if (nodeData.type === 'function' || nodeData.type === 'method') {
2319
+ // Save current state before drilling
2320
+ drillHistory.push({
2321
+ view: currentView,
2322
+ label: getBreadcrumbLabel(currentView),
2323
+ description: document.getElementById('view-description').innerHTML
2324
+ });
2325
+ updateBackButton();
2326
+
2327
+ // Update breadcrumb
2328
+ const breadcrumbItems = drillHistory.map(h => ({ label: h.label }));
2329
+ breadcrumbItems.push({ label: nodeData.label });
2330
+ updateBreadcrumb(breadcrumbItems);
2331
+
2332
+ fullCallGraphData = vizData.graphs.callGraph;
2333
+ focusOnNode(nodeData.id);
2334
+
2335
+ document.getElementById('view-description').innerHTML =
2336
+ '<strong>' + nodeData.label + '</strong> - Shows calls to/from this function. Double-click to explore further.';
2337
+ return;
2338
+ }
2339
+ // Default: show structures in same file
2340
+ else if (nodeData.file) {
2341
+ const filePath = nodeData.file;
2342
+ title = 'Structures in ' + filePath.split('/').pop();
2343
+
2344
+ const allNodes = vizData.graphs.callGraph.nodes;
2345
+ nodes = allNodes.filter(n => n.data.file === filePath);
2346
+
2347
+ const nodeIds = new Set(nodes.map(n => n.data.id));
2348
+ edges = vizData.graphs.callGraph.edges.filter(e =>
2349
+ nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
2350
+ );
2351
+ }
2352
+
2353
+ if (nodes.length > 0) {
2354
+ // Save current state before drilling
2355
+ drillHistory.push({
2356
+ view: currentView,
2357
+ label: getBreadcrumbLabel(currentView),
2358
+ description: document.getElementById('view-description').innerHTML
2359
+ });
2360
+ updateBackButton();
2361
+
2362
+ // Update breadcrumb
2363
+ const breadcrumbItems = drillHistory.map(h => ({ label: h.label }));
2364
+ breadcrumbItems.push({ label: title.replace('Structures in ', '').replace('Contents of ', '') });
2365
+ updateBreadcrumb(breadcrumbItems);
2366
+
2367
+ initCytoscape({ nodes, edges });
2368
+ document.getElementById('view-description').innerHTML =
2369
+ '<strong>' + title + '</strong> - Double-click to drill deeper.';
2370
+ }
2371
+ }
2372
+
2373
+ function getBreadcrumbLabel(view) {
2374
+ const labels = {
2375
+ overview: 'Files',
2376
+ callGraph: 'Call Graph',
2377
+ dependencies: 'Dependencies',
2378
+ classes: 'Classes',
2379
+ decisions: 'Decisions',
2380
+ };
2381
+ return labels[view] || view;
2382
+ }
2383
+
2384
+ // Go back to previous view
2385
+ function goBack() {
2386
+ if (drillHistory.length === 0) return;
2387
+
2388
+ const prev = drillHistory.pop();
2389
+ updateBackButton();
2390
+
2391
+ // Restore the previous view
2392
+ switchView(prev.view);
2393
+ }
2394
+
2395
+ // Update back button visibility
2396
+ function updateBackButton() {
2397
+ const btn = document.getElementById('back-btn');
2398
+ if (drillHistory.length > 0) {
2399
+ btn.classList.add('visible');
2400
+ } else {
2401
+ btn.classList.remove('visible');
2402
+ }
2403
+ }
2404
+
2405
+ function applyFilters() {
2406
+ if (!cy || cy.destroyed()) return;
2407
+
2408
+ const filters = {
2409
+ function: document.getElementById('filter-function').checked,
2410
+ class: document.getElementById('filter-class').checked,
2411
+ method: document.getElementById('filter-method').checked,
2412
+ interface: document.getElementById('filter-interface').checked,
2413
+ type: document.getElementById('filter-type').checked,
2414
+ file: document.getElementById('filter-file').checked,
2415
+ };
2416
+
2417
+ cy.nodes().forEach(node => {
2418
+ const type = node.data('type');
2419
+ const visible = filters[type] !== false;
2420
+ node.style('display', visible ? 'element' : 'none');
2421
+ });
2422
+
2423
+ // Refit to show visible nodes
2424
+ setTimeout(() => {
2425
+ cy.fit(cy.nodes(':visible'), 50);
2426
+ }, 50);
2427
+ }
2428
+
2429
+ // View mode toggle (visual vs list)
2430
+ function setViewMode(mode) {
2431
+ currentViewMode = mode;
2432
+
2433
+ // Update toggle buttons
2434
+ document.querySelectorAll('.view-toggle-btn').forEach(btn => {
2435
+ btn.classList.toggle('active', btn.dataset.mode === mode);
2436
+ });
2437
+
2438
+ // Toggle visibility
2439
+ const cyEl = document.getElementById('cy');
2440
+ const listEl = document.getElementById('list-view');
2441
+ const legendEl = document.getElementById('legend');
2442
+
2443
+ if (mode === 'visual') {
2444
+ cyEl.classList.remove('hidden');
2445
+ listEl.classList.remove('active');
2446
+ legendEl.classList.remove('hidden');
2447
+ if (cy) {
2448
+ setTimeout(() => {
2449
+ cy.resize();
2450
+ cy.fit(50);
2451
+ }, 50);
2452
+ }
2453
+ } else {
2454
+ cyEl.classList.add('hidden');
2455
+ listEl.classList.add('active');
2456
+ legendEl.classList.add('hidden');
2457
+ renderListView();
2458
+ }
2459
+ }
2460
+
2461
+ // Render list view for current data
2462
+ function renderListView() {
2463
+ const listEl = document.getElementById('list-view');
2464
+ const graphData = currentView === 'callGraph' ? fullCallGraphData : vizData.graphs[currentView];
2465
+
2466
+ if (!graphData || graphData.nodes.length === 0) {
2467
+ listEl.innerHTML = '<div class="empty-state"><h3>No data</h3><p>No items to display for this view</p></div>';
2468
+ return;
2469
+ }
2470
+
2471
+ // Group nodes by type
2472
+ const byType = {};
2473
+ for (const node of graphData.nodes) {
2474
+ const type = node.data.type || 'other';
2475
+ if (!byType[type]) byType[type] = [];
2476
+ byType[type].push(node.data);
2477
+ }
2478
+
2479
+ // Sort types
2480
+ const typeOrder = ['file', 'class', 'interface', 'function', 'method', 'type', 'variable', 'module', 'decision', 'pattern', 'rejection'];
2481
+ const sortedTypes = Object.keys(byType).sort((a, b) => {
2482
+ const aIdx = typeOrder.indexOf(a);
2483
+ const bIdx = typeOrder.indexOf(b);
2484
+ return (aIdx === -1 ? 999 : aIdx) - (bIdx === -1 ? 999 : bIdx);
2485
+ });
2486
+
2487
+ let html = '';
2488
+ for (const type of sortedTypes) {
2489
+ const items = byType[type];
2490
+ html += '<div class="list-section">';
2491
+ html += '<h4>' + type + 's (' + items.length + ')</h4>';
2492
+
2493
+ // Sort items by name
2494
+ items.sort((a, b) => (a.label || '').localeCompare(b.label || ''));
2495
+
2496
+ for (const item of items) {
2497
+ html += '<div class="list-item" data-id="' + escapeHtml(item.id) + '">';
2498
+ html += '<span class="tag ' + type + '">' + type + '</span>';
2499
+ html += '<div class="list-item-content">';
2500
+ html += '<div class="list-item-name">' + escapeHtml(item.label) + '</div>';
2501
+ if (item.file) {
2502
+ html += '<div class="list-item-location">' + escapeHtml(item.file);
2503
+ if (item.line) html += ':' + item.line;
2504
+ html += '</div>';
2505
+ }
2506
+ if (item.signature) {
2507
+ html += '<div class="list-item-signature">' + escapeHtml(item.signature) + '</div>';
2508
+ }
2509
+ html += '</div></div>';
2510
+ }
2511
+ html += '</div>';
2512
+ }
2513
+
2514
+ listEl.innerHTML = html;
2515
+
2516
+ // Add click handlers
2517
+ listEl.querySelectorAll('.list-item').forEach(item => {
2518
+ item.addEventListener('click', () => {
2519
+ const id = item.dataset.id;
2520
+ const nodeData = graphData.nodes.find(n => n.data.id === id);
2521
+ if (nodeData) {
2522
+ showDetails(nodeData.data);
2523
+ }
2524
+ });
2525
+
2526
+ // Double-click to drill down
2527
+ item.addEventListener('dblclick', () => {
2528
+ const id = item.dataset.id;
2529
+ const nodeData = graphData.nodes.find(n => n.data.id === id);
2530
+ if (nodeData) {
2531
+ drillDown(nodeData.data);
2532
+ }
2533
+ });
2534
+ });
2535
+ }
2536
+
2537
+ // Track last search matches for visualization
2538
+ let lastSearchMatches = [];
2539
+ let lastSearchQuery = '';
2540
+
2541
+ function searchNodes(query) {
2542
+ const searchInput = document.getElementById('search');
2543
+ const visualizeBtn = document.getElementById('visualize-search-btn');
2544
+
2545
+ if (!query) {
2546
+ // Reset visual view
2547
+ if (cy) {
2548
+ cy.nodes().style('opacity', 1);
2549
+ cy.nodes().removeClass('search-match');
2550
+ }
2551
+ // Reset list view filter
2552
+ if (currentViewMode === 'list') {
2553
+ document.querySelectorAll('.list-item').forEach(item => {
2554
+ item.style.display = '';
2555
+ });
2556
+ document.querySelectorAll('.list-section').forEach(section => {
2557
+ section.style.display = '';
2558
+ });
2559
+ }
2560
+ // Hide visualize button
2561
+ lastSearchMatches = [];
2562
+ lastSearchQuery = '';
2563
+ if (visualizeBtn) visualizeBtn.style.display = 'none';
2564
+ return;
2565
+ }
2566
+
2567
+ query = query.toLowerCase();
2568
+
2569
+ // Get the data source for current view
2570
+ // For custom views (smells, hotspots, etc.), use call graph data
2571
+ const customViews = ['smells', 'hotspots', 'gallery', 'timeline', 'treemap'];
2572
+ const isCustomView = customViews.includes(currentView);
2573
+ let graphData;
2574
+
2575
+ if (currentView === 'callGraph') {
2576
+ graphData = fullCallGraphData;
2577
+ } else if (isCustomView) {
2578
+ // Use call graph for searching from custom views
2579
+ graphData = vizData.graphs.callGraph;
2580
+ } else {
2581
+ graphData = vizData.graphs[currentView];
2582
+ }
2583
+
2584
+ if (!graphData) return;
2585
+
2586
+ // Search across name, file, and signature
2587
+ const matches = graphData.nodes.filter(n => {
2588
+ const label = (n.data.label || '').toLowerCase();
2589
+ const file = (n.data.file || '').toLowerCase();
2590
+ const signature = (n.data.signature || '').toLowerCase();
2591
+ return label.includes(query) || file.includes(query) || signature.includes(query);
2592
+ });
2593
+
2594
+ // Store matches for visualization
2595
+ lastSearchMatches = matches;
2596
+ lastSearchQuery = query;
2597
+
2598
+ // Show/hide visualize button based on matches
2599
+ if (visualizeBtn) {
2600
+ visualizeBtn.style.display = matches.length >= 2 && matches.length <= 100 ? 'inline-block' : 'none';
2601
+ }
2602
+
2603
+ // Handle list view
2604
+ if (currentViewMode === 'list') {
2605
+ document.querySelectorAll('.list-item').forEach(item => {
2606
+ const name = item.querySelector('.list-item-name');
2607
+ const location = item.querySelector('.list-item-location');
2608
+ const signature = item.querySelector('.list-item-signature');
2609
+ const text = [
2610
+ name ? name.textContent : '',
2611
+ location ? location.textContent : '',
2612
+ signature ? signature.textContent : ''
2613
+ ].join(' ').toLowerCase();
2614
+ item.style.display = text.includes(query) ? '' : 'none';
2615
+ });
2616
+
2617
+ // Hide empty sections
2618
+ document.querySelectorAll('.list-section').forEach(section => {
2619
+ const visibleItems = section.querySelectorAll('.list-item[style=""], .list-item:not([style])');
2620
+ const hasVisible = Array.from(section.querySelectorAll('.list-item')).some(
2621
+ item => item.style.display !== 'none'
2622
+ );
2623
+ section.style.display = hasVisible ? '' : 'none';
2624
+ });
2625
+ return;
2626
+ }
2627
+
2628
+ // Handle visual view
2629
+ if (matches.length === 0) {
2630
+ // No matches - show message
2631
+ document.getElementById('details-content').innerHTML = \`
2632
+ <div class="empty-state">
2633
+ <h3>No matches</h3>
2634
+ <p>No results for "\${escapeHtml(query)}"</p>
2635
+ </div>
2636
+ \`;
2637
+ if (cy) cy.nodes().style('opacity', 0.2);
2638
+ return;
2639
+ }
2640
+
2641
+ // For custom views, show results in details panel with option to switch to call graph
2642
+ if (isCustomView) {
2643
+ const listHtml = matches.slice(0, 30).map(m =>
2644
+ '<div style="padding: 6px 0; cursor: pointer; color: #e94560; border-bottom: 1px solid #0f3460;" onclick="switchView(\\'callGraph\\'); setTimeout(() => focusOnNode(\\'' + m.data.id + '\\'), 100);">' +
2645
+ escapeHtml(m.data.label) +
2646
+ '<span style="color: #666; font-size: 11px; margin-left: 8px;">' + (m.data.file ? m.data.file.split("/").pop() : '') + '</span></div>'
2647
+ ).join('');
2648
+
2649
+ document.getElementById('details-content').innerHTML = \`
2650
+ <div class="detail-row">
2651
+ <div class="detail-label">\${matches.length} matches found</div>
2652
+ <div style="font-size: 11px; color: #888; margin-bottom: 8px;">Click to view in Call Graph</div>
2653
+ <div style="margin-top: 8px;">\${listHtml}</div>
2654
+ \${matches.length > 30 ? '<div style="color: #666; margin-top: 8px;">...and ' + (matches.length - 30) + ' more</div>' : ''}
2655
+ </div>
2656
+ \`;
2657
+ return;
2658
+ }
2659
+
2660
+ // For call graph, rebuild with matching nodes
2661
+ if (currentView === 'callGraph' && fullCallGraphData) {
2662
+ if (matches.length === 1) {
2663
+ focusOnNode(matches[0].data.id);
2664
+ } else if (matches.length <= 30) {
2665
+ // Show matching nodes with their connections
2666
+ const matchIds = new Set(matches.map(m => m.data.id));
2667
+ const relevantEdges = fullCallGraphData.edges.filter(e =>
2668
+ matchIds.has(e.data.source) || matchIds.has(e.data.target)
2669
+ );
2670
+
2671
+ // Add connected nodes
2672
+ for (const edge of relevantEdges) {
2673
+ matchIds.add(edge.data.source);
2674
+ matchIds.add(edge.data.target);
2675
+ }
2676
+
2677
+ const focusedNodes = fullCallGraphData.nodes.filter(n => matchIds.has(n.data.id));
2678
+ initCytoscape({ nodes: focusedNodes, edges: relevantEdges });
2679
+
2680
+ document.getElementById('view-description').innerHTML =
2681
+ '<strong>Search results</strong> - Found ' + matches.length + ' matches for "' + escapeHtml(query) + '"';
2682
+ } else {
2683
+ // Too many - show clickable list
2684
+ const listHtml = matches.slice(0, 30).map(m =>
2685
+ '<div style="padding: 6px 0; cursor: pointer; color: #e94560; border-bottom: 1px solid #0f3460;" onclick="focusOnNode(\\'' + m.data.id + '\\')">' +
2686
+ escapeHtml(m.data.label) +
2687
+ '<span style="color: #666; font-size: 11px; margin-left: 8px;">' + (m.data.file ? m.data.file.split("/").pop() : '') + '</span></div>'
2688
+ ).join('');
2689
+ document.getElementById('details-content').innerHTML = \`
2690
+ <div class="detail-row">
2691
+ <div class="detail-label">\${matches.length} matches found</div>
2692
+ <div style="margin-top: 8px;">\${listHtml}</div>
2693
+ \${matches.length > 30 ? '<div style="color: #666; margin-top: 8px;">...and ' + (matches.length - 30) + ' more</div>' : ''}
2694
+ </div>
2695
+ \`;
2696
+ }
2697
+ return;
2698
+ }
2699
+
2700
+ // For other views, highlight matches
2701
+ if (cy) {
2702
+ const matchIds = new Set(matches.map(m => m.data.id));
2703
+ cy.nodes().forEach(node => {
2704
+ const isMatch = matchIds.has(node.data('id'));
2705
+ node.style('opacity', isMatch ? 1 : 0.15);
2706
+ });
2707
+
2708
+ // Fit to show matches
2709
+ const matchingNodes = cy.nodes().filter(n => matchIds.has(n.data('id')));
2710
+ if (matchingNodes.length > 0 && matchingNodes.length < 20) {
2711
+ cy.fit(matchingNodes, 80);
2712
+ }
2713
+ }
2714
+ }
2715
+
2716
+ // ============================================
2717
+ // Custom View Render Functions
2718
+ // ============================================
2719
+
2720
+ let currentSmellFilter = 'all';
2721
+ let currentGalleryFilter = 'all';
2722
+
2723
+ function renderSmellsView() {
2724
+ const container = document.getElementById('smells-view');
2725
+ const smells = vizData.smells;
2726
+
2727
+ if (!smells || smells.smells.length === 0) {
2728
+ container.innerHTML = '<div class="empty-state"><h3>No Code Smells Detected</h3><p>Your codebase looks clean!</p></div>';
2729
+ return;
2730
+ }
2731
+
2732
+ const filtered = currentSmellFilter === 'all'
2733
+ ? smells.smells
2734
+ : smells.smells.filter(s => s.type === currentSmellFilter || s.severity === currentSmellFilter);
2735
+
2736
+ container.innerHTML = \`
2737
+ <div class="smells-summary">
2738
+ <div class="smell-stat high">
2739
+ <div class="count">\${smells.summary.high}</div>
2740
+ <div class="label">High</div>
2741
+ </div>
2742
+ <div class="smell-stat medium">
2743
+ <div class="count">\${smells.summary.medium}</div>
2744
+ <div class="label">Medium</div>
2745
+ </div>
2746
+ <div class="smell-stat low">
2747
+ <div class="count">\${smells.summary.low}</div>
2748
+ <div class="label">Low</div>
2749
+ </div>
2750
+ <div class="smell-stat">
2751
+ <div class="count">\${smells.smells.length}</div>
2752
+ <div class="label">Total</div>
2753
+ </div>
2754
+ </div>
2755
+
2756
+ <div class="smell-filters">
2757
+ <button class="smell-filter-btn \${currentSmellFilter === 'all' ? 'active' : ''}" data-filter="all">All</button>
2758
+ <button class="smell-filter-btn \${currentSmellFilter === 'high' ? 'active' : ''}" data-filter="high">High</button>
2759
+ <button class="smell-filter-btn \${currentSmellFilter === 'medium' ? 'active' : ''}" data-filter="medium">Medium</button>
2760
+ <button class="smell-filter-btn \${currentSmellFilter === 'low' ? 'active' : ''}" data-filter="low">Low</button>
2761
+ <span style="margin-left: 16px; color: #444;">|</span>
2762
+ <button class="smell-filter-btn \${currentSmellFilter === 'large-file' ? 'active' : ''}" data-filter="large-file">Large Files</button>
2763
+ <button class="smell-filter-btn \${currentSmellFilter === 'long-function' ? 'active' : ''}" data-filter="long-function">Long Functions</button>
2764
+ <button class="smell-filter-btn \${currentSmellFilter === 'orphan' ? 'active' : ''}" data-filter="orphan">Orphan Code</button>
2765
+ </div>
2766
+
2767
+ <div class="smell-list">
2768
+ \${filtered.map(smell => \`
2769
+ <div class="smell-item \${smell.severity}" data-structure-id="\${smell.structureId || ''}" data-file="\${smell.filePath}">
2770
+ <span class="smell-badge \${smell.type}">\${smell.type.replace(/-/g, ' ')}</span>
2771
+ <div class="smell-info">
2772
+ <div class="smell-name">\${escapeHtml(smell.name)}</div>
2773
+ <div class="smell-desc">\${escapeHtml(smell.description)}</div>
2774
+ </div>
2775
+ <div class="smell-metric">\${smell.metric}</div>
2776
+ </div>
2777
+ \`).join('')}
2778
+ </div>
2779
+ \`;
2780
+
2781
+ // Add filter listeners
2782
+ container.querySelectorAll('.smell-filter-btn').forEach(btn => {
2783
+ btn.addEventListener('click', () => {
2784
+ currentSmellFilter = btn.dataset.filter;
2785
+ renderSmellsView();
2786
+ });
2787
+ });
2788
+
2789
+ // Add click listeners for smell items
2790
+ container.querySelectorAll('.smell-item').forEach(item => {
2791
+ item.addEventListener('click', () => {
2792
+ const structureId = item.dataset.structureId;
2793
+ const filePath = item.dataset.file;
2794
+ // Show details for the smell
2795
+ showSmellDetails(structureId, filePath);
2796
+ });
2797
+ });
2798
+ }
2799
+
2800
+ function showSmellDetails(structureId, filePath) {
2801
+ // Find structure in vizData
2802
+ let content = '<div class="detail-row"><div class="detail-label">File</div><div class="detail-value">' + escapeHtml(filePath) + '</div></div>';
2803
+
2804
+ if (structureId) {
2805
+ // Find the structure in call graph or class graph
2806
+ const allNodes = [...(vizData.graphs.callGraph?.nodes || []), ...(vizData.graphs.classes?.nodes || [])];
2807
+ const node = allNodes.find(n => n.data.id === 'structure:' + structureId);
2808
+ if (node) {
2809
+ content += '<div class="detail-row"><div class="detail-label">Name</div><div class="detail-value">' + escapeHtml(node.data.label) + '</div></div>';
2810
+ if (node.data.signature) {
2811
+ content += '<div class="detail-row"><div class="detail-label">Signature</div><div class="detail-value" style="font-family: monospace;">' + escapeHtml(node.data.signature) + '</div></div>';
2812
+ }
2813
+ if (node.data.line) {
2814
+ content += '<div class="detail-row"><div class="detail-label">Line</div><div class="detail-value">' + node.data.line + '</div></div>';
2815
+ }
2816
+ }
2817
+ }
2818
+
2819
+ document.getElementById('details-content').innerHTML = content;
2820
+ document.getElementById('details-panel').classList.add('visible');
2821
+ }
2822
+
2823
+ function renderHotspotsView() {
2824
+ const container = document.getElementById('hotspots-view');
2825
+ const hotspots = vizData.hotspots;
2826
+
2827
+ if (!hotspots) {
2828
+ container.innerHTML = '<div class="empty-state"><h3>No Hotspot Data</h3><p>Index your codebase first.</p></div>';
2829
+ return;
2830
+ }
2831
+
2832
+ function renderHotspotList(items, valueKey, valueLabel) {
2833
+ if (!items || items.length === 0) return '<div style="color: #666; padding: 12px;">No data</div>';
2834
+ return items.map((item, i) => \`
2835
+ <div class="hotspot-item" data-id="\${item.id}" data-file="\${item.file}">
2836
+ <div class="hotspot-rank">\${i + 1}</div>
2837
+ <div class="hotspot-info">
2838
+ <div class="hotspot-name">\${escapeHtml(item.name)}</div>
2839
+ <div class="hotspot-file">\${escapeHtml(item.file?.split('/').pop() || item.file || '')}</div>
2840
+ </div>
2841
+ <div class="hotspot-value">\${item[valueKey] || 0} \${valueLabel}</div>
2842
+ </div>
2843
+ \`).join('');
2844
+ }
2845
+
2846
+ container.innerHTML = \`
2847
+ <div class="hotspots-grid">
2848
+ <div class="hotspot-section">
2849
+ <h3>Largest Functions</h3>
2850
+ <div class="hotspot-list">
2851
+ \${renderHotspotList(hotspots.largestFunctions, 'lines', 'lines')}
2852
+ </div>
2853
+ </div>
2854
+ <div class="hotspot-section">
2855
+ <h3>Most Connected</h3>
2856
+ <div class="hotspot-list">
2857
+ \${renderHotspotList(hotspots.mostConnected, 'total', 'connections')}
2858
+ </div>
2859
+ </div>
2860
+ <div class="hotspot-section">
2861
+ <h3>Densest Files</h3>
2862
+ <div class="hotspot-list">
2863
+ \${renderHotspotList(hotspots.densestFiles, 'structureCount', 'structures')}
2864
+ </div>
2865
+ </div>
2866
+ <div class="hotspot-section">
2867
+ <h3>Hub Functions (Most Called)</h3>
2868
+ <div class="hotspot-list">
2869
+ \${renderHotspotList(hotspots.hubFunctions, 'inbound', 'callers')}
2870
+ </div>
2871
+ </div>
2872
+ </div>
2873
+ \`;
2874
+
2875
+ // Add click listeners
2876
+ container.querySelectorAll('.hotspot-item').forEach(item => {
2877
+ item.addEventListener('click', () => {
2878
+ const id = item.dataset.id;
2879
+ const file = item.dataset.file;
2880
+ if (id && id !== '0') {
2881
+ // Switch to call graph and focus
2882
+ switchView('callGraph');
2883
+ setTimeout(() => focusOnNode('structure:' + id), 100);
2884
+ } else if (file) {
2885
+ // Show file details
2886
+ showSmellDetails(null, file);
2887
+ }
2888
+ });
2889
+ });
2890
+ }
2891
+
2892
+ function renderGalleryView() {
2893
+ const container = document.getElementById('gallery-view');
2894
+ const gallery = vizData.gallery;
2895
+
2896
+ if (!gallery || gallery.items.length === 0) {
2897
+ container.innerHTML = '<div class="empty-state"><h3>No Gallery Items</h3><p>Decisions, patterns, and rejections from AI conversations will appear here.</p></div>';
2898
+ return;
2899
+ }
2900
+
2901
+ const filtered = currentGalleryFilter === 'all'
2902
+ ? gallery.items
2903
+ : gallery.items.filter(item => item.type === currentGalleryFilter);
2904
+
2905
+ container.innerHTML = \`
2906
+ <div class="gallery-filters">
2907
+ <button class="smell-filter-btn \${currentGalleryFilter === 'all' ? 'active' : ''}" data-filter="all">All (\${gallery.items.length})</button>
2908
+ <button class="smell-filter-btn \${currentGalleryFilter === 'decision' ? 'active' : ''}" data-filter="decision">Decisions (\${gallery.byType.decision})</button>
2909
+ <button class="smell-filter-btn \${currentGalleryFilter === 'pattern' ? 'active' : ''}" data-filter="pattern">Patterns (\${gallery.byType.pattern})</button>
2910
+ <button class="smell-filter-btn \${currentGalleryFilter === 'rejection' ? 'active' : ''}" data-filter="rejection">Rejections (\${gallery.byType.rejection})</button>
2911
+ </div>
2912
+
2913
+ <div class="gallery-grid">
2914
+ \${filtered.map(item => \`
2915
+ <div class="gallery-card" data-id="\${item.id}">
2916
+ <div class="gallery-card-header">
2917
+ <span class="gallery-type-badge \${item.type}">\${item.type}</span>
2918
+ <span class="gallery-timestamp">\${item.timestamp ? new Date(item.timestamp).toLocaleDateString() : ''}</span>
2919
+ </div>
2920
+ <div class="gallery-content">\${escapeHtml(item.content)}</div>
2921
+ \${item.affectedCode.length > 0 ? \`
2922
+ <div class="gallery-affected">
2923
+ \${item.affectedCode.slice(0, 5).map(code => \`
2924
+ <span class="gallery-code-chip" data-id="\${code.id}">\${escapeHtml(code.name)}</span>
2925
+ \`).join('')}
2926
+ \${item.affectedCode.length > 5 ? '<span class="gallery-code-chip">+' + (item.affectedCode.length - 5) + ' more</span>' : ''}
2927
+ </div>
2928
+ \` : ''}
2929
+ </div>
2930
+ \`).join('')}
2931
+ </div>
2932
+ \`;
2933
+
2934
+ // Add filter listeners
2935
+ container.querySelectorAll('.smell-filter-btn').forEach(btn => {
2936
+ btn.addEventListener('click', () => {
2937
+ currentGalleryFilter = btn.dataset.filter;
2938
+ renderGalleryView();
2939
+ });
2940
+ });
2941
+
2942
+ // Add expand card listeners
2943
+ container.querySelectorAll('.gallery-card').forEach(card => {
2944
+ card.addEventListener('click', () => {
2945
+ card.classList.toggle('expanded');
2946
+ });
2947
+ });
2948
+
2949
+ // Add code chip listeners
2950
+ container.querySelectorAll('.gallery-code-chip').forEach(chip => {
2951
+ chip.addEventListener('click', (e) => {
2952
+ e.stopPropagation();
2953
+ const id = chip.dataset.id;
2954
+ if (id) {
2955
+ switchView('callGraph');
2956
+ setTimeout(() => focusOnNode('structure:' + id), 100);
2957
+ }
2958
+ });
2959
+ });
2960
+ }
2961
+
2962
+ function renderTimelineView() {
2963
+ const container = document.getElementById('timeline-view');
2964
+ const timeline = vizData.timeline;
2965
+
2966
+ if (!timeline || timeline.entries.length === 0) {
2967
+ container.innerHTML = '<div class="empty-state"><h3>No Timeline Data</h3><p>AI conversation history will appear here.</p></div>';
2968
+ return;
2969
+ }
2970
+
2971
+ // Find max extraction count for scaling bars
2972
+ const maxExtractions = Math.max(...timeline.entries.map(e => e.extractionCount || 1), 1);
2973
+
2974
+ container.innerHTML = \`
2975
+ <div class="timeline-container">
2976
+ <div class="timeline-header">
2977
+ <div>
2978
+ <strong>\${timeline.entries.length}</strong> conversations
2979
+ \${timeline.dateRange ? \` from \${new Date(timeline.dateRange.start).toLocaleDateString()} to \${new Date(timeline.dateRange.end).toLocaleDateString()}\` : ''}
2980
+ </div>
2981
+ </div>
2982
+ <div class="timeline-scroll">
2983
+ <div class="timeline-track">
2984
+ \${timeline.entries.map((entry, i) => {
2985
+ const barHeight = Math.max(20, (entry.extractionCount / maxExtractions) * 200);
2986
+ const date = new Date(entry.timestamp);
2987
+ return \`
2988
+ <div class="timeline-entry" data-index="\${i}">
2989
+ <div class="timeline-bar" style="height: \${barHeight}px;" title="\${entry.extractionCount} extractions"></div>
2990
+ <div class="timeline-dot"></div>
2991
+ <div class="timeline-date">\${date.toLocaleDateString()}</div>
2992
+ </div>
2993
+ \`;
2994
+ }).join('')}
2995
+ </div>
2996
+ </div>
2997
+ <div class="timeline-details" id="timeline-details">
2998
+ <div style="color: #666;">Click on a timeline entry to see details</div>
2999
+ </div>
3000
+ </div>
3001
+ \`;
3002
+
3003
+ // Add click listeners
3004
+ container.querySelectorAll('.timeline-entry').forEach(entry => {
3005
+ entry.addEventListener('click', () => {
3006
+ const index = parseInt(entry.dataset.index);
3007
+ const item = timeline.entries[index];
3008
+ showTimelineDetails(item);
3009
+ });
3010
+ });
3011
+ }
3012
+
3013
+ function showTimelineDetails(entry) {
3014
+ const detailsEl = document.getElementById('timeline-details');
3015
+ if (!detailsEl) return;
3016
+
3017
+ const date = new Date(entry.timestamp);
3018
+ detailsEl.innerHTML = \`
3019
+ <div style="display: flex; gap: 24px; flex-wrap: wrap;">
3020
+ <div>
3021
+ <div style="color: #888; font-size: 12px;">Date</div>
3022
+ <div>\${date.toLocaleString()}</div>
3023
+ </div>
3024
+ <div>
3025
+ <div style="color: #888; font-size: 12px;">Model</div>
3026
+ <div>\${entry.model || 'Unknown'}</div>
3027
+ </div>
3028
+ <div>
3029
+ <div style="color: #888; font-size: 12px;">Tool</div>
3030
+ <div>\${entry.tool || 'Unknown'}</div>
3031
+ </div>
3032
+ <div>
3033
+ <div style="color: #888; font-size: 12px;">Extractions</div>
3034
+ <div>\${entry.extractionCount}</div>
3035
+ </div>
3036
+ </div>
3037
+ \${entry.summary ? '<div style="margin-top: 16px;"><div style="color: #888; font-size: 12px;">Summary</div><div>' + escapeHtml(entry.summary) + '</div></div>' : ''}
3038
+ \${entry.touchedStructures.length > 0 ? \`
3039
+ <div style="margin-top: 16px;">
3040
+ <div style="color: #888; font-size: 12px;">Touched Structures</div>
3041
+ <div style="display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;">
3042
+ \${entry.touchedStructures.map(s => \`<span class="gallery-code-chip" data-id="\${s.id}">\${escapeHtml(s.name)}</span>\`).join('')}
3043
+ </div>
3044
+ </div>
3045
+ \` : ''}
3046
+ \`;
3047
+
3048
+ // Add code chip listeners
3049
+ detailsEl.querySelectorAll('.gallery-code-chip').forEach(chip => {
3050
+ chip.addEventListener('click', () => {
3051
+ const id = chip.dataset.id;
3052
+ if (id) {
3053
+ switchView('callGraph');
3054
+ setTimeout(() => focusOnNode('structure:' + id), 100);
3055
+ }
3056
+ });
3057
+ });
3058
+ }
3059
+
3060
+ let treemapRoot = null;
3061
+ let treemapCurrent = null;
3062
+
3063
+ function renderTreemapView() {
3064
+ const container = document.getElementById('treemap-view');
3065
+ const treemap = vizData.treemap;
3066
+
3067
+ if (!treemap || treemap.value === 0) {
3068
+ container.innerHTML = '<div class="empty-state"><h3>No Treemap Data</h3><p>Index your codebase first.</p></div>';
3069
+ return;
3070
+ }
3071
+
3072
+ treemapRoot = treemap;
3073
+ treemapCurrent = treemap;
3074
+
3075
+ container.innerHTML = \`
3076
+ <div class="treemap-breadcrumb" id="treemap-breadcrumb">
3077
+ <span class="current">root</span>
3078
+ </div>
3079
+ <div id="treemap-container"></div>
3080
+ \`;
3081
+
3082
+ renderTreemap(treemap);
3083
+ }
3084
+
3085
+ function renderTreemap(data) {
3086
+ const containerEl = document.getElementById('treemap-container');
3087
+ if (!containerEl) return;
3088
+
3089
+ const width = containerEl.clientWidth || 800;
3090
+ const height = containerEl.clientHeight || 500;
3091
+
3092
+ // Clear previous
3093
+ containerEl.innerHTML = '';
3094
+
3095
+ // If no children, show empty state
3096
+ if (!data.children || data.children.length === 0) {
3097
+ containerEl.innerHTML = '<div class="empty-state"><h3>No children</h3><p>This node has no child items to display.</p></div>';
3098
+ return;
3099
+ }
3100
+
3101
+ // D3 treemap layout - use children of current node, not leaves
3102
+ const root = d3.hierarchy(data)
3103
+ .sum(d => d.value || 0)
3104
+ .sort((a, b) => b.value - a.value);
3105
+
3106
+ d3.treemap()
3107
+ .size([width, height])
3108
+ .padding(3)
3109
+ .paddingTop(20)
3110
+ .round(true)(root);
3111
+
3112
+ // Create SVG
3113
+ const svg = d3.select(containerEl)
3114
+ .append('svg')
3115
+ .attr('width', width)
3116
+ .attr('height', height);
3117
+
3118
+ // Color scale for structure types
3119
+ const typeColors = {
3120
+ 'function': '#2563eb',
3121
+ 'class': '#7c3aed',
3122
+ 'method': '#0891b2',
3123
+ 'interface': '#059669',
3124
+ 'type': '#d97706',
3125
+ 'variable': '#dc2626',
3126
+ 'file': '#475569',
3127
+ 'directory': '#334155',
3128
+ 'structure': '#2563eb',
3129
+ };
3130
+
3131
+ // Get the direct children of root (depth 1)
3132
+ const children = root.children || [];
3133
+
3134
+ const cell = svg.selectAll('g')
3135
+ .data(children)
3136
+ .join('g')
3137
+ .attr('transform', d => \`translate(\${d.x0},\${d.y0})\`);
3138
+
3139
+ // Add background rect for each cell
3140
+ cell.append('rect')
3141
+ .attr('class', 'treemap-node')
3142
+ .attr('width', d => Math.max(0, d.x1 - d.x0))
3143
+ .attr('height', d => Math.max(0, d.y1 - d.y0))
3144
+ .attr('fill', d => typeColors[d.data.structureType] || typeColors[d.data.type] || '#475569')
3145
+ .attr('rx', 3)
3146
+ .style('cursor', d => (d.data.children && d.data.children.length > 0) ? 'pointer' : 'default')
3147
+ .on('click', (event, d) => {
3148
+ event.stopPropagation();
3149
+ if (d.data.children && d.data.children.length > 0) {
3150
+ // Has children - zoom in
3151
+ zoomToTreemapNode(d.data);
3152
+ } else {
3153
+ // No children - show details
3154
+ showSmellDetails(null, d.data.path);
3155
+ }
3156
+ });
3157
+
3158
+ // Add header label at top of cell
3159
+ cell.append('text')
3160
+ .attr('class', 'treemap-label')
3161
+ .attr('x', 6)
3162
+ .attr('y', 14)
3163
+ .attr('text-anchor', 'start')
3164
+ .attr('fill', 'white')
3165
+ .attr('font-size', '11px')
3166
+ .attr('font-weight', '500')
3167
+ .text(d => {
3168
+ const w = d.x1 - d.x0;
3169
+ if (w < 30) return '';
3170
+ const name = d.data.name;
3171
+ const maxChars = Math.floor((w - 12) / 6);
3172
+ return name.length > maxChars ? name.slice(0, maxChars - 1) + '…' : name;
3173
+ });
3174
+
3175
+ // Add value label below name
3176
+ cell.append('text')
3177
+ .attr('class', 'treemap-label')
3178
+ .attr('x', d => (d.x1 - d.x0) / 2)
3179
+ .attr('y', d => (d.y1 - d.y0) / 2 + 5)
3180
+ .attr('text-anchor', 'middle')
3181
+ .attr('fill', 'rgba(255,255,255,0.7)')
3182
+ .attr('font-size', '10px')
3183
+ .text(d => {
3184
+ const w = d.x1 - d.x0;
3185
+ const h = d.y1 - d.y0;
3186
+ if (w < 50 || h < 40) return '';
3187
+ return d.data.value + ' lines';
3188
+ });
3189
+
3190
+ // Add click indicator for zoomable nodes
3191
+ cell.append('text')
3192
+ .attr('x', d => d.x1 - d.x0 - 8)
3193
+ .attr('y', 14)
3194
+ .attr('text-anchor', 'end')
3195
+ .attr('fill', 'rgba(255,255,255,0.5)')
3196
+ .attr('font-size', '10px')
3197
+ .text(d => (d.data.children && d.data.children.length > 0) ? '→' : '');
3198
+
3199
+ // Add tooltips
3200
+ cell.append('title')
3201
+ .text(d => {
3202
+ const hasChildren = d.data.children && d.data.children.length > 0;
3203
+ return d.data.name + '\\n' +
3204
+ d.data.value + ' lines\\n' +
3205
+ (d.data.structureType || d.data.type) +
3206
+ (hasChildren ? '\\nClick to zoom in' : '');
3207
+ });
3208
+ }
3209
+
3210
+ function zoomToTreemapNode(node) {
3211
+ if (!node.children || node.children.length === 0) return;
3212
+
3213
+ treemapCurrent = node;
3214
+ updateTreemapBreadcrumb();
3215
+ renderTreemap(node);
3216
+ }
3217
+
3218
+ function updateTreemapBreadcrumb() {
3219
+ const breadcrumbEl = document.getElementById('treemap-breadcrumb');
3220
+ if (!breadcrumbEl) return;
3221
+
3222
+ // Build path from root to current
3223
+ const path = [];
3224
+ let node = treemapCurrent;
3225
+
3226
+ // Walk up using path comparison
3227
+ const findPath = (root, target, currentPath) => {
3228
+ if (root.path === target.path) {
3229
+ return [...currentPath, root];
3230
+ }
3231
+ if (root.children) {
3232
+ for (const child of root.children) {
3233
+ const result = findPath(child, target, [...currentPath, root]);
3234
+ if (result) return result;
3235
+ }
3236
+ }
3237
+ return null;
3238
+ };
3239
+
3240
+ const pathNodes = findPath(treemapRoot, treemapCurrent, []) || [treemapRoot];
3241
+
3242
+ breadcrumbEl.innerHTML = pathNodes.map((n, i) => {
3243
+ const isLast = i === pathNodes.length - 1;
3244
+ return \`<span class="\${isLast ? 'current' : ''}" data-path="\${n.path}">\${n.name}</span>\${isLast ? '' : ' / '}\`;
3245
+ }).join('');
3246
+
3247
+ // Add click listeners
3248
+ breadcrumbEl.querySelectorAll('span:not(.current)').forEach(span => {
3249
+ span.addEventListener('click', () => {
3250
+ const targetPath = span.dataset.path;
3251
+ const findNode = (root, path) => {
3252
+ if (root.path === path) return root;
3253
+ if (root.children) {
3254
+ for (const child of root.children) {
3255
+ const found = findNode(child, path);
3256
+ if (found) return found;
3257
+ }
3258
+ }
3259
+ return null;
3260
+ };
3261
+ const targetNode = findNode(treemapRoot, targetPath);
3262
+ if (targetNode) {
3263
+ zoomToTreemapNode(targetNode);
3264
+ }
3265
+ });
3266
+ });
3267
+ }
3268
+
3269
+ // Event listeners
3270
+ document.querySelectorAll('.tab').forEach(tab => {
3271
+ tab.addEventListener('click', () => {
3272
+ // Clear history when manually switching tabs
3273
+ drillHistory = [];
3274
+ updateBackButton();
3275
+ switchView(tab.dataset.view);
3276
+ // Reset breadcrumb to just the view name
3277
+ updateBreadcrumb([{ label: getBreadcrumbLabel(tab.dataset.view) }]);
3278
+ });
3279
+ });
3280
+
3281
+ document.getElementById('back-btn').addEventListener('click', goBack);
3282
+
3283
+ document.getElementById('layout-select').addEventListener('change', runLayout);
3284
+
3285
+ document.querySelectorAll('[id^="filter-"]').forEach(checkbox => {
3286
+ checkbox.addEventListener('change', applyFilters);
3287
+ });
3288
+
3289
+ document.getElementById('search').addEventListener('input', (e) => {
3290
+ searchNodes(e.target.value);
3291
+ });
3292
+
3293
+ // Visualize search results button
3294
+ document.getElementById('visualize-search-btn').addEventListener('click', visualizeSearchResults);
3295
+
3296
+ function visualizeSearchResults() {
3297
+ if (lastSearchMatches.length < 2) return;
3298
+
3299
+ // Get IDs of all matches
3300
+ const matchIds = new Set(lastSearchMatches.map(m => m.data.id));
3301
+
3302
+ // Find all edges between matches (using call graph data)
3303
+ const callGraphData = vizData.graphs.callGraph;
3304
+ const relevantEdges = [];
3305
+ const neighborIds = new Set();
3306
+
3307
+ if (callGraphData) {
3308
+ for (const edge of callGraphData.edges) {
3309
+ const sourceMatch = matchIds.has(edge.data.source);
3310
+ const targetMatch = matchIds.has(edge.data.target);
3311
+
3312
+ if (sourceMatch && targetMatch) {
3313
+ // Edge between two matches
3314
+ relevantEdges.push(edge);
3315
+ } else if (sourceMatch || targetMatch) {
3316
+ // Edge to/from a neighbor - add the neighbor
3317
+ if (sourceMatch) neighborIds.add(edge.data.target);
3318
+ if (targetMatch) neighborIds.add(edge.data.source);
3319
+ relevantEdges.push(edge);
3320
+ }
3321
+ }
3322
+ }
3323
+
3324
+ // Build nodes: matches + neighbors (neighbors will be faded)
3325
+ const allNodes = [];
3326
+ const allNodeIds = new Set();
3327
+
3328
+ // Add match nodes
3329
+ for (const match of lastSearchMatches) {
3330
+ allNodes.push({
3331
+ ...match,
3332
+ data: { ...match.data, searchMatch: true }
3333
+ });
3334
+ allNodeIds.add(match.data.id);
3335
+ }
3336
+
3337
+ // Add neighbor nodes (from call graph)
3338
+ if (callGraphData) {
3339
+ for (const node of callGraphData.nodes) {
3340
+ if (neighborIds.has(node.data.id) && !allNodeIds.has(node.data.id)) {
3341
+ allNodes.push({
3342
+ ...node,
3343
+ data: { ...node.data, searchMatch: false }
3344
+ });
3345
+ allNodeIds.add(node.data.id);
3346
+ }
3347
+ }
3348
+ }
3349
+
3350
+ // Filter edges to only include those with both endpoints in our node set
3351
+ const finalEdges = relevantEdges.filter(e =>
3352
+ allNodeIds.has(e.data.source) && allNodeIds.has(e.data.target)
3353
+ );
3354
+
3355
+ // Clear drill history and switch to this custom view
3356
+ drillHistory = [];
3357
+ updateBackButton();
3358
+
3359
+ // Initialize cytoscape with custom styles for matches vs neighbors
3360
+ destroyCy();
3361
+
3362
+ // Show cy container
3363
+ document.getElementById('cy').style.display = 'block';
3364
+ document.getElementById('list-view').style.display = 'none';
3365
+ document.getElementById('legend').style.display = 'block';
3366
+
3367
+ // Hide custom views
3368
+ ['smells', 'hotspots', 'gallery', 'timeline', 'treemap'].forEach(v => {
3369
+ const el = document.getElementById(v + '-view');
3370
+ if (el) el.classList.remove('active');
3371
+ });
3372
+
3373
+ initCytoscape({ nodes: allNodes, edges: finalEdges });
3374
+
3375
+ // Apply styles: highlight matches, fade neighbors
3376
+ if (cy) {
3377
+ cy.nodes().forEach(node => {
3378
+ if (node.data('searchMatch') === false) {
3379
+ node.style('opacity', 0.4);
3380
+ }
3381
+ });
3382
+ }
3383
+
3384
+ // Update description
3385
+ document.getElementById('view-description').innerHTML =
3386
+ '<strong>Search Results Graph</strong> - ' + lastSearchMatches.length + ' matches for "' +
3387
+ escapeHtml(lastSearchQuery) + '" with their connections. Faded nodes are neighbors.';
3388
+
3389
+ // Update breadcrumb
3390
+ updateBreadcrumb([{ label: 'Search: ' + lastSearchQuery }]);
3391
+
3392
+ // Clear tab active state
3393
+ document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
3394
+ }
3395
+
3396
+ // View mode toggle (visual/list)
3397
+ document.querySelectorAll('.view-toggle-btn').forEach(btn => {
3398
+ btn.addEventListener('click', () => setViewMode(btn.dataset.mode));
3399
+ });
3400
+
3401
+ // Flow mode radio buttons
3402
+ document.querySelectorAll('input[name="flow-mode"]').forEach(radio => {
3403
+ radio.addEventListener('change', (e) => {
3404
+ flowMode = e.target.value;
3405
+ // Update hint text based on mode
3406
+ const hint = document.querySelector('#flow-mode-group p');
3407
+ if (hint) {
3408
+ if (flowMode === 'connections') {
3409
+ hint.textContent = 'Click a function to see its connections';
3410
+ } else if (flowMode === 'downstream') {
3411
+ hint.textContent = 'Click a function to trace what it calls';
3412
+ } else if (flowMode === 'upstream') {
3413
+ hint.textContent = 'Click a function to trace what calls it';
3414
+ }
3415
+ }
3416
+ });
3417
+ });
3418
+
3419
+ // Fullscreen toggle
3420
+ function toggleFullscreen() {
3421
+ const isFullscreen = document.body.classList.toggle('fullscreen');
3422
+ document.getElementById('exit-fullscreen').style.display = isFullscreen ? 'block' : 'none';
3423
+
3424
+ // When exiting fullscreen, reinitialize the view to fix layout
3425
+ if (!isFullscreen) {
3426
+ setTimeout(() => {
3427
+ switchView(currentView);
3428
+ }, 300);
3429
+ } else if (cy && currentViewMode === 'visual') {
3430
+ // Entering fullscreen - just resize
3431
+ setTimeout(() => {
3432
+ cy.resize();
3433
+ cy.fit(50);
3434
+ }, 100);
3435
+ }
3436
+ }
3437
+
3438
+ document.getElementById('fullscreen-btn').addEventListener('click', toggleFullscreen);
3439
+ document.getElementById('exit-fullscreen').addEventListener('click', toggleFullscreen);
3440
+ document.getElementById('close-details').addEventListener('click', clearDetails);
3441
+
3442
+ // ESC key to exit fullscreen
3443
+ document.addEventListener('keydown', (e) => {
3444
+ if (e.key === 'Escape' && document.body.classList.contains('fullscreen')) {
3445
+ toggleFullscreen();
3446
+ }
3447
+ });
3448
+
3449
+ // Initialize
3450
+ switchView('overview');
3451
+ updateBreadcrumb([{ label: 'Files' }]);
3452
+ maybeShowWelcome();
3453
+ </script>
3454
+ </body>
3455
+ </html>`;
3456
+ }
3457
+ function escapeHtml(text) {
3458
+ return text
3459
+ .replace(/&/g, '&amp;')
3460
+ .replace(/</g, '&lt;')
3461
+ .replace(/>/g, '&gt;')
3462
+ .replace(/"/g, '&quot;')
3463
+ .replace(/'/g, '&#039;');
3464
+ }
3465
+ //# sourceMappingURL=template.js.map