codemap-ai 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.
- package/README.md +181 -0
- package/dist/chunk-5ONPBEWJ.js +350 -0
- package/dist/chunk-5ONPBEWJ.js.map +1 -0
- package/dist/chunk-FLUWKIEM.js +347 -0
- package/dist/chunk-FLUWKIEM.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1302 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +331 -0
- package/dist/index.js +1163 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +365 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/server-ACGCJ3GE.js +87 -0
- package/dist/server-ACGCJ3GE.js.map +1 -0
- package/dist/server-TBIVIIUJ.js +367 -0
- package/dist/server-TBIVIIUJ.js.map +1 -0
- package/package.json +70 -0
- package/web/index.html +639 -0
package/web/index.html
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>CodeMap - Codebase Visualization</title>
|
|
7
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
17
|
+
background: #0d1117;
|
|
18
|
+
color: #c9d1d9;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#app {
|
|
23
|
+
display: flex;
|
|
24
|
+
height: 100vh;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Sidebar */
|
|
28
|
+
.sidebar {
|
|
29
|
+
width: 320px;
|
|
30
|
+
background: #161b22;
|
|
31
|
+
border-right: 1px solid #30363d;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sidebar-header {
|
|
38
|
+
padding: 16px;
|
|
39
|
+
border-bottom: 1px solid #30363d;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.sidebar-header h1 {
|
|
43
|
+
font-size: 20px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
color: #58a6ff;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: 8px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.search-box {
|
|
52
|
+
padding: 12px 16px;
|
|
53
|
+
border-bottom: 1px solid #30363d;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.search-box input {
|
|
57
|
+
width: 100%;
|
|
58
|
+
padding: 8px 12px;
|
|
59
|
+
background: #0d1117;
|
|
60
|
+
border: 1px solid #30363d;
|
|
61
|
+
border-radius: 6px;
|
|
62
|
+
color: #c9d1d9;
|
|
63
|
+
font-size: 14px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.search-box input:focus {
|
|
67
|
+
outline: none;
|
|
68
|
+
border-color: #58a6ff;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.stats {
|
|
72
|
+
padding: 16px;
|
|
73
|
+
border-bottom: 1px solid #30363d;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.stat-grid {
|
|
77
|
+
display: grid;
|
|
78
|
+
grid-template-columns: repeat(2, 1fr);
|
|
79
|
+
gap: 12px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.stat-item {
|
|
83
|
+
background: #0d1117;
|
|
84
|
+
padding: 12px;
|
|
85
|
+
border-radius: 6px;
|
|
86
|
+
text-align: center;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.stat-value {
|
|
90
|
+
font-size: 24px;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
color: #58a6ff;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.stat-label {
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
color: #8b949e;
|
|
98
|
+
margin-top: 4px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.legend {
|
|
102
|
+
padding: 16px;
|
|
103
|
+
border-bottom: 1px solid #30363d;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.legend h3 {
|
|
107
|
+
font-size: 12px;
|
|
108
|
+
text-transform: uppercase;
|
|
109
|
+
color: #8b949e;
|
|
110
|
+
margin-bottom: 12px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.legend-items {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-wrap: wrap;
|
|
116
|
+
gap: 8px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.legend-item {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 6px;
|
|
123
|
+
font-size: 12px;
|
|
124
|
+
padding: 4px 8px;
|
|
125
|
+
background: #0d1117;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
transition: opacity 0.2s;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.legend-item.hidden {
|
|
132
|
+
opacity: 0.4;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.legend-dot {
|
|
136
|
+
width: 10px;
|
|
137
|
+
height: 10px;
|
|
138
|
+
border-radius: 50%;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.node-details {
|
|
142
|
+
flex: 1;
|
|
143
|
+
overflow-y: auto;
|
|
144
|
+
padding: 16px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.node-details h3 {
|
|
148
|
+
font-size: 14px;
|
|
149
|
+
color: #58a6ff;
|
|
150
|
+
margin-bottom: 8px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.node-details .path {
|
|
154
|
+
font-size: 12px;
|
|
155
|
+
color: #8b949e;
|
|
156
|
+
word-break: break-all;
|
|
157
|
+
margin-bottom: 16px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.connections {
|
|
161
|
+
margin-top: 16px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.connection-group h4 {
|
|
165
|
+
font-size: 12px;
|
|
166
|
+
color: #8b949e;
|
|
167
|
+
margin-bottom: 8px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.connection-item {
|
|
171
|
+
font-size: 12px;
|
|
172
|
+
padding: 6px 8px;
|
|
173
|
+
background: #0d1117;
|
|
174
|
+
border-radius: 4px;
|
|
175
|
+
margin-bottom: 4px;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
transition: background 0.2s;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.connection-item:hover {
|
|
181
|
+
background: #21262d;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* Graph container */
|
|
185
|
+
.graph-container {
|
|
186
|
+
flex: 1;
|
|
187
|
+
position: relative;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#graph {
|
|
191
|
+
width: 100%;
|
|
192
|
+
height: 100%;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.controls {
|
|
196
|
+
position: absolute;
|
|
197
|
+
bottom: 20px;
|
|
198
|
+
right: 20px;
|
|
199
|
+
display: flex;
|
|
200
|
+
gap: 8px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.control-btn {
|
|
204
|
+
width: 40px;
|
|
205
|
+
height: 40px;
|
|
206
|
+
background: #161b22;
|
|
207
|
+
border: 1px solid #30363d;
|
|
208
|
+
border-radius: 6px;
|
|
209
|
+
color: #c9d1d9;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
justify-content: center;
|
|
214
|
+
font-size: 18px;
|
|
215
|
+
transition: background 0.2s;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.control-btn:hover {
|
|
219
|
+
background: #21262d;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Node tooltip */
|
|
223
|
+
.tooltip {
|
|
224
|
+
position: absolute;
|
|
225
|
+
background: #161b22;
|
|
226
|
+
border: 1px solid #30363d;
|
|
227
|
+
border-radius: 6px;
|
|
228
|
+
padding: 12px;
|
|
229
|
+
pointer-events: none;
|
|
230
|
+
opacity: 0;
|
|
231
|
+
transition: opacity 0.2s;
|
|
232
|
+
max-width: 300px;
|
|
233
|
+
z-index: 100;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.tooltip.visible {
|
|
237
|
+
opacity: 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.tooltip-title {
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
color: #58a6ff;
|
|
243
|
+
margin-bottom: 4px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.tooltip-type {
|
|
247
|
+
font-size: 12px;
|
|
248
|
+
color: #8b949e;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* SVG styles */
|
|
252
|
+
.node {
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.node circle {
|
|
257
|
+
stroke-width: 2px;
|
|
258
|
+
transition: r 0.2s, stroke-width 0.2s;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.node:hover circle {
|
|
262
|
+
stroke-width: 3px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.node.selected circle {
|
|
266
|
+
stroke: #fff;
|
|
267
|
+
stroke-width: 3px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.node text {
|
|
271
|
+
font-size: 10px;
|
|
272
|
+
fill: #8b949e;
|
|
273
|
+
pointer-events: none;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.link {
|
|
277
|
+
stroke-opacity: 0.3;
|
|
278
|
+
transition: stroke-opacity 0.2s;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.link.highlighted {
|
|
282
|
+
stroke-opacity: 1;
|
|
283
|
+
stroke-width: 2px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.link.faded {
|
|
287
|
+
stroke-opacity: 0.05;
|
|
288
|
+
}
|
|
289
|
+
</style>
|
|
290
|
+
</head>
|
|
291
|
+
<body>
|
|
292
|
+
<div id="app">
|
|
293
|
+
<div class="sidebar">
|
|
294
|
+
<div class="sidebar-header">
|
|
295
|
+
<h1>
|
|
296
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
297
|
+
<circle cx="12" cy="12" r="3"/>
|
|
298
|
+
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
|
|
299
|
+
</svg>
|
|
300
|
+
CodeMap
|
|
301
|
+
</h1>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div class="search-box">
|
|
305
|
+
<input type="text" id="search" placeholder="Search functions, classes, files...">
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<div class="stats">
|
|
309
|
+
<div class="stat-grid">
|
|
310
|
+
<div class="stat-item">
|
|
311
|
+
<div class="stat-value" id="stat-files">-</div>
|
|
312
|
+
<div class="stat-label">Files</div>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="stat-item">
|
|
315
|
+
<div class="stat-value" id="stat-functions">-</div>
|
|
316
|
+
<div class="stat-label">Functions</div>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="stat-item">
|
|
319
|
+
<div class="stat-value" id="stat-classes">-</div>
|
|
320
|
+
<div class="stat-label">Classes</div>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="stat-item">
|
|
323
|
+
<div class="stat-value" id="stat-edges">-</div>
|
|
324
|
+
<div class="stat-label">Connections</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<div class="legend">
|
|
330
|
+
<h3>Node Types</h3>
|
|
331
|
+
<div class="legend-items" id="legend">
|
|
332
|
+
<!-- Populated by JS -->
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div class="node-details" id="node-details">
|
|
337
|
+
<p style="color: #8b949e; font-size: 12px;">Click a node to see details</p>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div class="graph-container">
|
|
342
|
+
<svg id="graph"></svg>
|
|
343
|
+
<div class="controls">
|
|
344
|
+
<button class="control-btn" id="zoom-in" title="Zoom In">+</button>
|
|
345
|
+
<button class="control-btn" id="zoom-out" title="Zoom Out">−</button>
|
|
346
|
+
<button class="control-btn" id="reset" title="Reset View">⟲</button>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="tooltip" id="tooltip"></div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<script>
|
|
353
|
+
// Color palette for node types
|
|
354
|
+
const colors = {
|
|
355
|
+
file: '#8b949e',
|
|
356
|
+
function: '#58a6ff',
|
|
357
|
+
class: '#a371f7',
|
|
358
|
+
method: '#7ee787',
|
|
359
|
+
variable: '#ffa657',
|
|
360
|
+
module: '#f778ba',
|
|
361
|
+
import: '#79c0ff'
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const edgeColors = {
|
|
365
|
+
imports: '#8b949e',
|
|
366
|
+
calls: '#58a6ff',
|
|
367
|
+
extends: '#a371f7',
|
|
368
|
+
implements: '#7ee787',
|
|
369
|
+
contains: '#30363d',
|
|
370
|
+
uses: '#ffa657',
|
|
371
|
+
exports: '#f778ba'
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
let graphData = { nodes: [], edges: [] };
|
|
375
|
+
let simulation = null;
|
|
376
|
+
let selectedNode = null;
|
|
377
|
+
let hiddenTypes = new Set();
|
|
378
|
+
|
|
379
|
+
// Initialize
|
|
380
|
+
async function init() {
|
|
381
|
+
await loadStats();
|
|
382
|
+
await loadGraph();
|
|
383
|
+
setupLegend();
|
|
384
|
+
setupSearch();
|
|
385
|
+
setupControls();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function loadStats() {
|
|
389
|
+
try {
|
|
390
|
+
const res = await fetch('/api/stats');
|
|
391
|
+
const stats = await res.json();
|
|
392
|
+
|
|
393
|
+
document.getElementById('stat-files').textContent = stats.totalFiles || 0;
|
|
394
|
+
document.getElementById('stat-functions').textContent =
|
|
395
|
+
(stats.nodesByType?.function || 0) + (stats.nodesByType?.method || 0);
|
|
396
|
+
document.getElementById('stat-classes').textContent = stats.nodesByType?.class || 0;
|
|
397
|
+
document.getElementById('stat-edges').textContent = stats.totalEdges || 0;
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.error('Failed to load stats:', e);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function loadGraph() {
|
|
404
|
+
try {
|
|
405
|
+
const res = await fetch('/api/graph');
|
|
406
|
+
graphData = await res.json();
|
|
407
|
+
renderGraph();
|
|
408
|
+
} catch (e) {
|
|
409
|
+
console.error('Failed to load graph:', e);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function renderGraph() {
|
|
414
|
+
const svg = d3.select('#graph');
|
|
415
|
+
const container = document.querySelector('.graph-container');
|
|
416
|
+
const width = container.clientWidth;
|
|
417
|
+
const height = container.clientHeight;
|
|
418
|
+
|
|
419
|
+
svg.selectAll('*').remove();
|
|
420
|
+
svg.attr('width', width).attr('height', height);
|
|
421
|
+
|
|
422
|
+
// Filter nodes by hidden types
|
|
423
|
+
const visibleNodes = graphData.nodes.filter(n => !hiddenTypes.has(n.type));
|
|
424
|
+
const visibleNodeIds = new Set(visibleNodes.map(n => n.id));
|
|
425
|
+
const visibleEdges = graphData.edges.filter(
|
|
426
|
+
e => visibleNodeIds.has(e.source) && visibleNodeIds.has(e.target)
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Create zoom behavior
|
|
430
|
+
const zoom = d3.zoom()
|
|
431
|
+
.scaleExtent([0.1, 4])
|
|
432
|
+
.on('zoom', (event) => {
|
|
433
|
+
g.attr('transform', event.transform);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
svg.call(zoom);
|
|
437
|
+
|
|
438
|
+
const g = svg.append('g');
|
|
439
|
+
|
|
440
|
+
// Create force simulation
|
|
441
|
+
simulation = d3.forceSimulation(visibleNodes)
|
|
442
|
+
.force('link', d3.forceLink(visibleEdges).id(d => d.id).distance(100))
|
|
443
|
+
.force('charge', d3.forceManyBody().strength(-300))
|
|
444
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
445
|
+
.force('collision', d3.forceCollide().radius(30));
|
|
446
|
+
|
|
447
|
+
// Draw edges
|
|
448
|
+
const links = g.append('g')
|
|
449
|
+
.selectAll('line')
|
|
450
|
+
.data(visibleEdges)
|
|
451
|
+
.enter()
|
|
452
|
+
.append('line')
|
|
453
|
+
.attr('class', 'link')
|
|
454
|
+
.attr('stroke', d => edgeColors[d.type] || '#30363d');
|
|
455
|
+
|
|
456
|
+
// Draw nodes
|
|
457
|
+
const nodes = g.append('g')
|
|
458
|
+
.selectAll('.node')
|
|
459
|
+
.data(visibleNodes)
|
|
460
|
+
.enter()
|
|
461
|
+
.append('g')
|
|
462
|
+
.attr('class', 'node')
|
|
463
|
+
.call(d3.drag()
|
|
464
|
+
.on('start', dragstarted)
|
|
465
|
+
.on('drag', dragged)
|
|
466
|
+
.on('end', dragended));
|
|
467
|
+
|
|
468
|
+
nodes.append('circle')
|
|
469
|
+
.attr('r', d => d.type === 'file' ? 8 : d.type === 'class' ? 10 : 6)
|
|
470
|
+
.attr('fill', d => colors[d.type] || '#8b949e')
|
|
471
|
+
.attr('stroke', d => d3.color(colors[d.type] || '#8b949e').darker(0.5));
|
|
472
|
+
|
|
473
|
+
nodes.append('text')
|
|
474
|
+
.attr('dx', 12)
|
|
475
|
+
.attr('dy', 4)
|
|
476
|
+
.text(d => d.label.length > 20 ? d.label.slice(0, 20) + '...' : d.label);
|
|
477
|
+
|
|
478
|
+
// Tooltip
|
|
479
|
+
const tooltip = document.getElementById('tooltip');
|
|
480
|
+
|
|
481
|
+
nodes
|
|
482
|
+
.on('mouseover', (event, d) => {
|
|
483
|
+
tooltip.innerHTML = `
|
|
484
|
+
<div class="tooltip-title">${d.label}</div>
|
|
485
|
+
<div class="tooltip-type">${d.type}</div>
|
|
486
|
+
`;
|
|
487
|
+
tooltip.style.left = (event.pageX + 10) + 'px';
|
|
488
|
+
tooltip.style.top = (event.pageY + 10) + 'px';
|
|
489
|
+
tooltip.classList.add('visible');
|
|
490
|
+
|
|
491
|
+
// Highlight connected edges
|
|
492
|
+
links
|
|
493
|
+
.classed('highlighted', l => l.source.id === d.id || l.target.id === d.id)
|
|
494
|
+
.classed('faded', l => l.source.id !== d.id && l.target.id !== d.id);
|
|
495
|
+
})
|
|
496
|
+
.on('mouseout', () => {
|
|
497
|
+
tooltip.classList.remove('visible');
|
|
498
|
+
links.classed('highlighted', false).classed('faded', false);
|
|
499
|
+
})
|
|
500
|
+
.on('click', (event, d) => {
|
|
501
|
+
selectNode(d);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Update positions on tick
|
|
505
|
+
simulation.on('tick', () => {
|
|
506
|
+
links
|
|
507
|
+
.attr('x1', d => d.source.x)
|
|
508
|
+
.attr('y1', d => d.source.y)
|
|
509
|
+
.attr('x2', d => d.target.x)
|
|
510
|
+
.attr('y2', d => d.target.y);
|
|
511
|
+
|
|
512
|
+
nodes.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Store zoom for controls
|
|
516
|
+
window.graphZoom = zoom;
|
|
517
|
+
window.graphSvg = svg;
|
|
518
|
+
|
|
519
|
+
function dragstarted(event) {
|
|
520
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
521
|
+
event.subject.fx = event.subject.x;
|
|
522
|
+
event.subject.fy = event.subject.y;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function dragged(event) {
|
|
526
|
+
event.subject.fx = event.x;
|
|
527
|
+
event.subject.fy = event.y;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function dragended(event) {
|
|
531
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
532
|
+
event.subject.fx = null;
|
|
533
|
+
event.subject.fy = null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function selectNode(node) {
|
|
538
|
+
selectedNode = node;
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const res = await fetch(`/api/node/${encodeURIComponent(node.id)}`);
|
|
542
|
+
const data = await res.json();
|
|
543
|
+
|
|
544
|
+
const details = document.getElementById('node-details');
|
|
545
|
+
details.innerHTML = `
|
|
546
|
+
<h3>${data.node.name}</h3>
|
|
547
|
+
<div class="path">${data.node.filePath}:${data.node.startLine}</div>
|
|
548
|
+
<div class="connections">
|
|
549
|
+
<div class="connection-group">
|
|
550
|
+
<h4>Outgoing (${data.edgesFrom.length})</h4>
|
|
551
|
+
${data.edgesFrom.slice(0, 10).map(e => `
|
|
552
|
+
<div class="connection-item">${e.type} → ${e.targetId.replace('ref:', '')}</div>
|
|
553
|
+
`).join('')}
|
|
554
|
+
</div>
|
|
555
|
+
<div class="connection-group" style="margin-top: 12px">
|
|
556
|
+
<h4>Incoming (${data.edgesTo.length})</h4>
|
|
557
|
+
${data.edgesTo.slice(0, 10).map(e => `
|
|
558
|
+
<div class="connection-item">${e.type} ← ${e.sourceId.split(':')[0]}</div>
|
|
559
|
+
`).join('')}
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
`;
|
|
563
|
+
} catch (e) {
|
|
564
|
+
console.error('Failed to load node details:', e);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function setupLegend() {
|
|
569
|
+
const legend = document.getElementById('legend');
|
|
570
|
+
legend.innerHTML = Object.entries(colors).map(([type, color]) => `
|
|
571
|
+
<div class="legend-item" data-type="${type}">
|
|
572
|
+
<div class="legend-dot" style="background: ${color}"></div>
|
|
573
|
+
${type}
|
|
574
|
+
</div>
|
|
575
|
+
`).join('');
|
|
576
|
+
|
|
577
|
+
legend.querySelectorAll('.legend-item').forEach(item => {
|
|
578
|
+
item.addEventListener('click', () => {
|
|
579
|
+
const type = item.dataset.type;
|
|
580
|
+
if (hiddenTypes.has(type)) {
|
|
581
|
+
hiddenTypes.delete(type);
|
|
582
|
+
item.classList.remove('hidden');
|
|
583
|
+
} else {
|
|
584
|
+
hiddenTypes.add(type);
|
|
585
|
+
item.classList.add('hidden');
|
|
586
|
+
}
|
|
587
|
+
renderGraph();
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function setupSearch() {
|
|
593
|
+
const search = document.getElementById('search');
|
|
594
|
+
let timeout;
|
|
595
|
+
|
|
596
|
+
search.addEventListener('input', (e) => {
|
|
597
|
+
clearTimeout(timeout);
|
|
598
|
+
timeout = setTimeout(async () => {
|
|
599
|
+
const query = e.target.value.trim();
|
|
600
|
+
if (!query) return;
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
|
604
|
+
const results = await res.json();
|
|
605
|
+
|
|
606
|
+
if (results.length > 0) {
|
|
607
|
+
// Find and select the first matching node
|
|
608
|
+
const node = graphData.nodes.find(n => n.id === results[0].id);
|
|
609
|
+
if (node) selectNode(node);
|
|
610
|
+
}
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.error('Search failed:', e);
|
|
613
|
+
}
|
|
614
|
+
}, 300);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function setupControls() {
|
|
619
|
+
document.getElementById('zoom-in').addEventListener('click', () => {
|
|
620
|
+
window.graphSvg.transition().call(window.graphZoom.scaleBy, 1.3);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
document.getElementById('zoom-out').addEventListener('click', () => {
|
|
624
|
+
window.graphSvg.transition().call(window.graphZoom.scaleBy, 0.7);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
document.getElementById('reset').addEventListener('click', () => {
|
|
628
|
+
window.graphSvg.transition().call(
|
|
629
|
+
window.graphZoom.transform,
|
|
630
|
+
d3.zoomIdentity
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Start
|
|
636
|
+
init();
|
|
637
|
+
</script>
|
|
638
|
+
</body>
|
|
639
|
+
</html>
|