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/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>