mcp-vector-search 0.12.6__py3-none-any.whl → 1.1.22__py3-none-any.whl
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.
- mcp_vector_search/__init__.py +3 -3
- mcp_vector_search/analysis/__init__.py +111 -0
- mcp_vector_search/analysis/baseline/__init__.py +68 -0
- mcp_vector_search/analysis/baseline/comparator.py +462 -0
- mcp_vector_search/analysis/baseline/manager.py +621 -0
- mcp_vector_search/analysis/collectors/__init__.py +74 -0
- mcp_vector_search/analysis/collectors/base.py +164 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/complexity.py +743 -0
- mcp_vector_search/analysis/collectors/coupling.py +1162 -0
- mcp_vector_search/analysis/collectors/halstead.py +514 -0
- mcp_vector_search/analysis/collectors/smells.py +325 -0
- mcp_vector_search/analysis/debt.py +516 -0
- mcp_vector_search/analysis/interpretation.py +685 -0
- mcp_vector_search/analysis/metrics.py +414 -0
- mcp_vector_search/analysis/reporters/__init__.py +7 -0
- mcp_vector_search/analysis/reporters/console.py +646 -0
- mcp_vector_search/analysis/reporters/markdown.py +480 -0
- mcp_vector_search/analysis/reporters/sarif.py +377 -0
- mcp_vector_search/analysis/storage/__init__.py +93 -0
- mcp_vector_search/analysis/storage/metrics_store.py +762 -0
- mcp_vector_search/analysis/storage/schema.py +245 -0
- mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
- mcp_vector_search/analysis/trends.py +308 -0
- mcp_vector_search/analysis/visualizer/__init__.py +90 -0
- mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
- mcp_vector_search/analysis/visualizer/exporter.py +484 -0
- mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
- mcp_vector_search/analysis/visualizer/schemas.py +525 -0
- mcp_vector_search/cli/commands/analyze.py +1062 -0
- mcp_vector_search/cli/commands/chat.py +1455 -0
- mcp_vector_search/cli/commands/index.py +621 -5
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/init.py +13 -0
- mcp_vector_search/cli/commands/install.py +597 -335
- mcp_vector_search/cli/commands/install_old.py +8 -4
- mcp_vector_search/cli/commands/mcp.py +78 -6
- mcp_vector_search/cli/commands/reset.py +68 -26
- mcp_vector_search/cli/commands/search.py +224 -8
- mcp_vector_search/cli/commands/setup.py +1184 -0
- mcp_vector_search/cli/commands/status.py +339 -5
- mcp_vector_search/cli/commands/uninstall.py +276 -357
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +292 -0
- mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
- mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +600 -0
- mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
- mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
- mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
- mcp_vector_search/cli/didyoumean.py +27 -2
- mcp_vector_search/cli/main.py +127 -160
- mcp_vector_search/cli/output.py +158 -13
- mcp_vector_search/config/__init__.py +4 -0
- mcp_vector_search/config/default_thresholds.yaml +52 -0
- mcp_vector_search/config/settings.py +12 -0
- mcp_vector_search/config/thresholds.py +273 -0
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/auto_indexer.py +3 -3
- mcp_vector_search/core/boilerplate.py +186 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/database.py +406 -94
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/exceptions.py +11 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/git_hooks.py +4 -4
- mcp_vector_search/core/indexer.py +632 -54
- mcp_vector_search/core/llm_client.py +756 -0
- mcp_vector_search/core/models.py +91 -1
- mcp_vector_search/core/project.py +17 -0
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/scheduler.py +11 -11
- mcp_vector_search/core/search.py +179 -29
- mcp_vector_search/mcp/server.py +819 -9
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/__init__.py +2 -0
- mcp_vector_search/utils/gitignore.py +0 -3
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +66 -4
- mcp_vector_search/utils/timing.py +10 -6
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
- mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
- mcp_vector_search/cli/commands/visualize.py +0 -1467
- mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,4542 @@
|
|
|
1
|
+
"""Simple D3.js tree visualization for code graph.
|
|
2
|
+
|
|
3
|
+
Clean, minimal implementation focusing on core functionality:
|
|
4
|
+
- Hierarchical tree layout (linear and circular)
|
|
5
|
+
- Expandable/collapsible directories and files
|
|
6
|
+
- File expansion shows code chunks as child nodes
|
|
7
|
+
- Chunk selection to view content in side panel
|
|
8
|
+
|
|
9
|
+
Design Decision: Complete rewrite from scratch
|
|
10
|
+
Rationale: Previous implementation was 4085 lines (5x over 800-line limit)
|
|
11
|
+
with excessive complexity. This minimal version provides core functionality
|
|
12
|
+
in <450 lines while maintaining clarity and maintainability.
|
|
13
|
+
|
|
14
|
+
Node Types and Colors:
|
|
15
|
+
- Orange (collapsed directory) / Blue (expanded directory)
|
|
16
|
+
- Gray (collapsed file) / White (expanded file)
|
|
17
|
+
- Purple (chunk nodes) - smaller circles with purple text
|
|
18
|
+
|
|
19
|
+
Trade-offs:
|
|
20
|
+
- Simplicity vs Features: Removed advanced features (force-directed, filters)
|
|
21
|
+
- Performance vs Clarity: Straightforward DOM updates over optimized rendering
|
|
22
|
+
- Flexibility vs Simplicity: Fixed layouts instead of customizable options
|
|
23
|
+
|
|
24
|
+
Extension Points: Add features incrementally based on user feedback rather
|
|
25
|
+
than preemptive feature bloat.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_all_scripts() -> str:
|
|
30
|
+
"""Generate all JavaScript for the visualization.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Complete JavaScript code as a single string
|
|
34
|
+
"""
|
|
35
|
+
return """
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// GLOBAL STATE
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
let allNodes = [];
|
|
41
|
+
let allLinks = [];
|
|
42
|
+
let currentLayout = 'linear'; // 'linear' or 'circular'
|
|
43
|
+
let treeData = null;
|
|
44
|
+
let isViewerOpen = false;
|
|
45
|
+
|
|
46
|
+
// Navigation history for back/forward
|
|
47
|
+
let navigationHistory = [];
|
|
48
|
+
let navigationIndex = -1;
|
|
49
|
+
|
|
50
|
+
// Call lines visibility
|
|
51
|
+
let showCallLines = true;
|
|
52
|
+
|
|
53
|
+
// File filter: 'all', 'code', 'docs'
|
|
54
|
+
let currentFileFilter = 'all';
|
|
55
|
+
|
|
56
|
+
// Chunk types for code nodes (function, class, method, text, imports, module)
|
|
57
|
+
const chunkTypes = ['function', 'class', 'method', 'text', 'imports', 'module'];
|
|
58
|
+
|
|
59
|
+
// Size scaling configuration
|
|
60
|
+
const sizeConfig = {
|
|
61
|
+
minRadius: 12, // Minimum node radius (50% larger for readability)
|
|
62
|
+
maxRadius: 24, // Maximum node radius
|
|
63
|
+
chunkMinRadius: 5, // Minimum for small chunks (more visible size contrast)
|
|
64
|
+
chunkMaxRadius: 28 // Maximum for large chunks (more visible size contrast)
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Dynamic dimensions that update when viewer opens/closes
|
|
68
|
+
function getViewportDimensions() {
|
|
69
|
+
const container = document.getElementById('main-container');
|
|
70
|
+
return {
|
|
71
|
+
width: container.clientWidth,
|
|
72
|
+
height: container.clientHeight
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const margin = {top: 40, right: 120, bottom: 20, left: 120};
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// DATA LOADING
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
let graphStatusCheckInterval = null;
|
|
83
|
+
|
|
84
|
+
async function checkGraphStatus() {
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch('/api/graph-status');
|
|
87
|
+
const status = await response.json();
|
|
88
|
+
return status;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Failed to check graph status:', error);
|
|
91
|
+
return { ready: false, size: 0 };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function showLoadingIndicator(message) {
|
|
96
|
+
const loadingDiv = document.getElementById('graph-loading-indicator') || createLoadingDiv();
|
|
97
|
+
loadingDiv.querySelector('.loading-message').textContent = message;
|
|
98
|
+
loadingDiv.style.display = 'flex';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hideLoadingIndicator() {
|
|
102
|
+
const loadingDiv = document.getElementById('graph-loading-indicator');
|
|
103
|
+
if (loadingDiv) {
|
|
104
|
+
loadingDiv.style.display = 'none';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createLoadingDiv() {
|
|
109
|
+
const div = document.createElement('div');
|
|
110
|
+
div.id = 'graph-loading-indicator';
|
|
111
|
+
div.style.cssText = `
|
|
112
|
+
position: fixed;
|
|
113
|
+
top: 50%;
|
|
114
|
+
left: 50%;
|
|
115
|
+
transform: translate(-50%, -50%);
|
|
116
|
+
background: rgba(255, 255, 255, 0.95);
|
|
117
|
+
padding: 30px 50px;
|
|
118
|
+
border-radius: 8px;
|
|
119
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 15px;
|
|
124
|
+
z-index: 10000;
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
const spinner = document.createElement('div');
|
|
128
|
+
spinner.style.cssText = `
|
|
129
|
+
border: 4px solid #f3f3f3;
|
|
130
|
+
border-top: 4px solid #3498db;
|
|
131
|
+
border-radius: 50%;
|
|
132
|
+
width: 40px;
|
|
133
|
+
height: 40px;
|
|
134
|
+
animation: spin 1s linear infinite;
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
const message = document.createElement('div');
|
|
138
|
+
message.className = 'loading-message';
|
|
139
|
+
message.style.cssText = 'color: #333; font-size: 16px; font-family: Arial, sans-serif;';
|
|
140
|
+
message.textContent = 'Loading graph data...';
|
|
141
|
+
|
|
142
|
+
div.appendChild(spinner);
|
|
143
|
+
div.appendChild(message);
|
|
144
|
+
document.body.appendChild(div);
|
|
145
|
+
|
|
146
|
+
// Add spinner animation
|
|
147
|
+
const style = document.createElement('style');
|
|
148
|
+
style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
|
|
149
|
+
document.head.appendChild(style);
|
|
150
|
+
|
|
151
|
+
return div;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function loadGraphData() {
|
|
155
|
+
try {
|
|
156
|
+
// Check if graph is ready
|
|
157
|
+
const status = await checkGraphStatus();
|
|
158
|
+
|
|
159
|
+
if (!status.ready) {
|
|
160
|
+
console.log('Graph data not ready yet, will poll every 5 seconds...');
|
|
161
|
+
showLoadingIndicator('Generating graph data... This may take a few minutes.');
|
|
162
|
+
|
|
163
|
+
// Start polling for graph readiness
|
|
164
|
+
graphStatusCheckInterval = setInterval(async () => {
|
|
165
|
+
const checkStatus = await checkGraphStatus();
|
|
166
|
+
if (checkStatus.ready) {
|
|
167
|
+
clearInterval(graphStatusCheckInterval);
|
|
168
|
+
console.log('Graph data is now ready, loading...');
|
|
169
|
+
showLoadingIndicator('Graph data ready! Loading visualization...');
|
|
170
|
+
await loadGraphDataActual();
|
|
171
|
+
hideLoadingIndicator();
|
|
172
|
+
}
|
|
173
|
+
}, 5000); // Poll every 5 seconds
|
|
174
|
+
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Graph is already ready, load it
|
|
179
|
+
await loadGraphDataActual();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Failed to load graph data:', error);
|
|
182
|
+
hideLoadingIndicator();
|
|
183
|
+
document.body.innerHTML =
|
|
184
|
+
'<div style="color: red; padding: 20px; font-family: Arial;">Error loading visualization data. Check console for details.</div>';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function loadGraphDataActual() {
|
|
189
|
+
try {
|
|
190
|
+
const response = await fetch('/api/graph');
|
|
191
|
+
const data = await response.json();
|
|
192
|
+
|
|
193
|
+
// Check if we got an error response
|
|
194
|
+
if (data.error) {
|
|
195
|
+
console.warn('Graph data not available yet:', data.error);
|
|
196
|
+
showLoadingIndicator('Waiting for graph data...');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
allNodes = data.nodes || [];
|
|
201
|
+
allLinks = data.links || [];
|
|
202
|
+
|
|
203
|
+
// Store trend data globally for visualization
|
|
204
|
+
window.graphTrendData = data.trends || null;
|
|
205
|
+
if (window.graphTrendData) {
|
|
206
|
+
console.log(`Loaded trend data: ${window.graphTrendData.entries_count} entries`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log(`Loaded ${allNodes.length} nodes and ${allLinks.length} links`);
|
|
210
|
+
|
|
211
|
+
// DEBUG: Log first few nodes to see actual structure
|
|
212
|
+
console.log('=== SAMPLE NODE STRUCTURE ===');
|
|
213
|
+
if (allNodes.length > 0) {
|
|
214
|
+
console.log('First node:', JSON.stringify(allNodes[0], null, 2));
|
|
215
|
+
if (allNodes.length > 1) {
|
|
216
|
+
console.log('Second node:', JSON.stringify(allNodes[1], null, 2));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Count node types
|
|
221
|
+
const typeCounts = {};
|
|
222
|
+
allNodes.forEach(node => {
|
|
223
|
+
const type = node.type || 'undefined';
|
|
224
|
+
typeCounts[type] = (typeCounts[type] || 0) + 1;
|
|
225
|
+
});
|
|
226
|
+
console.log('Node type counts:', typeCounts);
|
|
227
|
+
console.log('=== END SAMPLE NODE STRUCTURE ===');
|
|
228
|
+
|
|
229
|
+
buildTreeStructure();
|
|
230
|
+
renderVisualization();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error('Failed to load graph data:', error);
|
|
233
|
+
document.body.innerHTML =
|
|
234
|
+
'<div style="color: red; padding: 20px; font-family: Arial;">Error loading visualization data. Check console for details.</div>';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// TREE STRUCTURE BUILDING
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
242
|
+
function buildTreeStructure() {
|
|
243
|
+
// Include directories, files, AND chunks (function, class, method, text, imports, module)
|
|
244
|
+
const treeNodes = allNodes.filter(node => {
|
|
245
|
+
const type = node.type;
|
|
246
|
+
return type === 'directory' || type === 'file' || chunkTypes.includes(type);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
console.log(`Filtered to ${treeNodes.length} tree nodes (directories, files, and chunks)`);
|
|
250
|
+
|
|
251
|
+
// Count node types for debugging
|
|
252
|
+
const dirCount = treeNodes.filter(n => n.type === 'directory').length;
|
|
253
|
+
const fileCount = treeNodes.filter(n => n.type === 'file').length;
|
|
254
|
+
const chunkCount = treeNodes.filter(n => chunkTypes.includes(n.type)).length;
|
|
255
|
+
console.log(`Node breakdown: ${dirCount} directories, ${fileCount} files, ${chunkCount} chunks`);
|
|
256
|
+
|
|
257
|
+
// Create lookup maps
|
|
258
|
+
const nodeMap = new Map();
|
|
259
|
+
treeNodes.forEach(node => {
|
|
260
|
+
nodeMap.set(node.id, {
|
|
261
|
+
...node,
|
|
262
|
+
children: []
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Build parent-child relationships
|
|
267
|
+
const parentMap = new Map();
|
|
268
|
+
|
|
269
|
+
// DEBUG: Analyze link structure
|
|
270
|
+
console.log('=== LINK STRUCTURE DEBUG ===');
|
|
271
|
+
console.log(`Total links: ${allLinks.length}`);
|
|
272
|
+
|
|
273
|
+
// Get unique link types (handle undefined)
|
|
274
|
+
const linkTypes = [...new Set(allLinks.map(l => l.type || 'undefined'))];
|
|
275
|
+
console.log('Link types found:', linkTypes);
|
|
276
|
+
|
|
277
|
+
// Count links by type
|
|
278
|
+
const linkTypeCounts = {};
|
|
279
|
+
allLinks.forEach(link => {
|
|
280
|
+
const type = link.type || 'undefined';
|
|
281
|
+
linkTypeCounts[type] = (linkTypeCounts[type] || 0) + 1;
|
|
282
|
+
});
|
|
283
|
+
console.log('Link type counts:', linkTypeCounts);
|
|
284
|
+
|
|
285
|
+
// Sample first few links
|
|
286
|
+
console.log('Sample links (first 5):');
|
|
287
|
+
allLinks.slice(0, 5).forEach((link, i) => {
|
|
288
|
+
console.log(` Link ${i}:`, JSON.stringify(link, null, 2));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Check if links have properties we expect
|
|
292
|
+
if (allLinks.length > 0) {
|
|
293
|
+
const firstLink = allLinks[0];
|
|
294
|
+
console.log('Link properties:', Object.keys(firstLink));
|
|
295
|
+
}
|
|
296
|
+
console.log('=== END LINK STRUCTURE DEBUG ===');
|
|
297
|
+
|
|
298
|
+
// Build parent-child relationships from links
|
|
299
|
+
// Process all containment and hierarchy links to establish the tree structure
|
|
300
|
+
console.log('=== BUILDING TREE RELATIONSHIPS ===');
|
|
301
|
+
|
|
302
|
+
let relationshipsProcessed = {
|
|
303
|
+
dir_hierarchy: 0,
|
|
304
|
+
dir_containment: 0,
|
|
305
|
+
file_containment: 0,
|
|
306
|
+
chunk_hierarchy: 0 // chunk_hierarchy links = class -> method
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
let relationshipsMatched = {
|
|
310
|
+
dir_hierarchy: 0,
|
|
311
|
+
dir_containment: 0,
|
|
312
|
+
file_containment: 0,
|
|
313
|
+
chunk_hierarchy: 0
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Process all relationship links
|
|
317
|
+
allLinks.forEach(link => {
|
|
318
|
+
const linkType = link.type;
|
|
319
|
+
|
|
320
|
+
// Determine relationship category
|
|
321
|
+
let category = null;
|
|
322
|
+
if (linkType === 'dir_hierarchy') {
|
|
323
|
+
category = 'dir_hierarchy';
|
|
324
|
+
} else if (linkType === 'dir_containment') {
|
|
325
|
+
category = 'dir_containment';
|
|
326
|
+
} else if (linkType === 'file_containment') {
|
|
327
|
+
category = 'file_containment';
|
|
328
|
+
} else if (linkType === 'chunk_hierarchy') {
|
|
329
|
+
// chunk_hierarchy links are chunk-to-chunk (e.g., class -> method)
|
|
330
|
+
category = 'chunk_hierarchy';
|
|
331
|
+
} else {
|
|
332
|
+
// Skip semantic, caller, undefined, and other non-hierarchical links
|
|
333
|
+
// This includes links without a 'type' field (e.g., subproject links)
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
relationshipsProcessed[category]++;
|
|
338
|
+
|
|
339
|
+
// Get parent and child nodes from the map
|
|
340
|
+
const parentNode = nodeMap.get(link.source);
|
|
341
|
+
const childNode = nodeMap.get(link.target);
|
|
342
|
+
|
|
343
|
+
// Both nodes must exist in our tree node set
|
|
344
|
+
if (!parentNode || !childNode) {
|
|
345
|
+
if (relationshipsProcessed[category] <= 3) { // Log first few misses
|
|
346
|
+
console.log(`${category} link skipped - parent: ${link.source} (exists: ${!!parentNode}), child: ${link.target} (exists: ${!!childNode})`);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Establish parent-child relationship
|
|
352
|
+
// Add child to parent's children array
|
|
353
|
+
parentNode.children.push(childNode);
|
|
354
|
+
|
|
355
|
+
// Record the parent in parentMap (used to identify root nodes)
|
|
356
|
+
parentMap.set(link.target, link.source);
|
|
357
|
+
|
|
358
|
+
relationshipsMatched[category]++;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
console.log('Relationship processing summary:');
|
|
362
|
+
console.log(` dir_hierarchy: ${relationshipsMatched.dir_hierarchy}/${relationshipsProcessed.dir_hierarchy} matched`);
|
|
363
|
+
console.log(` dir_containment: ${relationshipsMatched.dir_containment}/${relationshipsProcessed.dir_containment} matched`);
|
|
364
|
+
console.log(` file_containment: ${relationshipsMatched.file_containment}/${relationshipsProcessed.file_containment} matched`);
|
|
365
|
+
console.log(` chunk_hierarchy (class→method): ${relationshipsMatched.chunk_hierarchy}/${relationshipsProcessed.chunk_hierarchy} matched`);
|
|
366
|
+
console.log(` Total parent-child links: ${parentMap.size}`);
|
|
367
|
+
console.log('=== END TREE RELATIONSHIPS ===');
|
|
368
|
+
|
|
369
|
+
// Find root nodes (nodes with no parents)
|
|
370
|
+
// IMPORTANT: Exclude chunk types from roots - they should only appear as children of files
|
|
371
|
+
// Orphaned chunks (without file_containment links) are excluded from the tree
|
|
372
|
+
const rootNodes = treeNodes
|
|
373
|
+
.filter(node => !parentMap.has(node.id))
|
|
374
|
+
.filter(node => !chunkTypes.includes(node.type)) // Exclude orphaned chunks
|
|
375
|
+
.map(node => nodeMap.get(node.id))
|
|
376
|
+
.filter(node => node !== undefined);
|
|
377
|
+
|
|
378
|
+
console.log('=== ROOT NODE ANALYSIS ===');
|
|
379
|
+
console.log(`Found ${rootNodes.length} root nodes (directories and files only)`);
|
|
380
|
+
|
|
381
|
+
// DEBUG: Count root node types
|
|
382
|
+
const rootTypeCounts = {};
|
|
383
|
+
rootNodes.forEach(node => {
|
|
384
|
+
const type = node.type || 'undefined';
|
|
385
|
+
rootTypeCounts[type] = (rootTypeCounts[type] || 0) + 1;
|
|
386
|
+
});
|
|
387
|
+
console.log('Root node type breakdown:', rootTypeCounts);
|
|
388
|
+
|
|
389
|
+
// If we have chunk nodes as roots, something went wrong
|
|
390
|
+
const chunkRoots = rootNodes.filter(n => chunkTypes.includes(n.type)).length;
|
|
391
|
+
if (chunkRoots > 0) {
|
|
392
|
+
console.warn(`WARNING: ${chunkRoots} chunk nodes are roots - they should be children of files!`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// If we have file nodes as roots (except for top-level files), might be missing dir_containment
|
|
396
|
+
const fileRoots = rootNodes.filter(n => n.type === 'file').length;
|
|
397
|
+
if (fileRoots > 0) {
|
|
398
|
+
console.log(`INFO: ${fileRoots} file nodes are roots (this is normal for files not in subdirectories)`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log('=== END ROOT NODE ANALYSIS ===');
|
|
402
|
+
|
|
403
|
+
// Create virtual root if multiple roots
|
|
404
|
+
if (rootNodes.length === 0) {
|
|
405
|
+
console.error('No root nodes found!');
|
|
406
|
+
treeData = {name: 'Empty', id: 'root', type: 'directory', children: []};
|
|
407
|
+
} else if (rootNodes.length === 1) {
|
|
408
|
+
treeData = rootNodes[0];
|
|
409
|
+
} else {
|
|
410
|
+
treeData = {
|
|
411
|
+
name: 'Project Root',
|
|
412
|
+
id: 'virtual-root',
|
|
413
|
+
type: 'directory',
|
|
414
|
+
children: rootNodes
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Collapse single-child chains to make the tree more compact
|
|
419
|
+
// - Directory with single directory child: src -> mcp_vector_search becomes "src/mcp_vector_search"
|
|
420
|
+
// - File with single chunk child: promote the chunk's children to the file level
|
|
421
|
+
function collapseSingleChildChains(node) {
|
|
422
|
+
if (!node || !node.children) return;
|
|
423
|
+
|
|
424
|
+
// First, recursively process all children
|
|
425
|
+
node.children.forEach(child => collapseSingleChildChains(child));
|
|
426
|
+
|
|
427
|
+
// Case 1: Directory with single directory child - combine names
|
|
428
|
+
if (node.type === 'directory' && node.children.length === 1) {
|
|
429
|
+
const onlyChild = node.children[0];
|
|
430
|
+
if (onlyChild.type === 'directory') {
|
|
431
|
+
// Merge: combine names with "/"
|
|
432
|
+
console.log(`Collapsing dir chain: ${node.name} + ${onlyChild.name}`);
|
|
433
|
+
node.name = `${node.name}/${onlyChild.name}`;
|
|
434
|
+
// Take the child's children as our own
|
|
435
|
+
node.children = onlyChild.children || [];
|
|
436
|
+
node._children = onlyChild._children || null;
|
|
437
|
+
// Preserve the deepest node's id for any link references
|
|
438
|
+
node.collapsed_ids = node.collapsed_ids || [node.id];
|
|
439
|
+
node.collapsed_ids.push(onlyChild.id);
|
|
440
|
+
|
|
441
|
+
// Recursively check again in case there's another single child
|
|
442
|
+
collapseSingleChildChains(node);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Case 2: File with single chunk child - promote chunk's children to file
|
|
447
|
+
// This handles files where there's just one L1 (e.g., imports or a single class)
|
|
448
|
+
if (node.type === 'file' && node.children && node.children.length === 1) {
|
|
449
|
+
const onlyChild = node.children[0];
|
|
450
|
+
if (chunkTypes.includes(onlyChild.type)) {
|
|
451
|
+
// If the chunk has children, promote them to the file level
|
|
452
|
+
const chunkChildren = onlyChild.children || onlyChild._children || [];
|
|
453
|
+
if (chunkChildren.length > 0) {
|
|
454
|
+
console.log(`Promoting ${chunkChildren.length} children from ${onlyChild.type} to file ${node.name}`);
|
|
455
|
+
// Replace the single chunk with its children
|
|
456
|
+
node.children = chunkChildren;
|
|
457
|
+
// Store info about the collapsed chunk (include ALL relevant properties)
|
|
458
|
+
node.collapsed_chunk = {
|
|
459
|
+
type: onlyChild.type,
|
|
460
|
+
name: onlyChild.name,
|
|
461
|
+
id: onlyChild.id,
|
|
462
|
+
content: onlyChild.content,
|
|
463
|
+
docstring: onlyChild.docstring,
|
|
464
|
+
start_line: onlyChild.start_line,
|
|
465
|
+
end_line: onlyChild.end_line,
|
|
466
|
+
file_path: onlyChild.file_path,
|
|
467
|
+
language: onlyChild.language,
|
|
468
|
+
complexity: onlyChild.complexity
|
|
469
|
+
};
|
|
470
|
+
} else {
|
|
471
|
+
// Collapse file+chunk into combined name (like directory chains)
|
|
472
|
+
console.log(`Collapsing file+chunk: ${node.name}/${onlyChild.name}`);
|
|
473
|
+
node.name = `${node.name}/${onlyChild.name}`;
|
|
474
|
+
node.children = null; // Remove chunk child - now a leaf node
|
|
475
|
+
node._children = null;
|
|
476
|
+
node.collapsed_ids = node.collapsed_ids || [node.id];
|
|
477
|
+
node.collapsed_ids.push(onlyChild.id);
|
|
478
|
+
|
|
479
|
+
// Store chunk data for display when clicked
|
|
480
|
+
node.collapsed_chunk = {
|
|
481
|
+
type: onlyChild.type,
|
|
482
|
+
name: onlyChild.name,
|
|
483
|
+
id: onlyChild.id,
|
|
484
|
+
content: onlyChild.content,
|
|
485
|
+
start_line: onlyChild.start_line,
|
|
486
|
+
end_line: onlyChild.end_line,
|
|
487
|
+
complexity: onlyChild.complexity
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Apply single-child chain collapsing to all root children
|
|
495
|
+
console.log('=== COLLAPSING SINGLE-CHILD CHAINS ===');
|
|
496
|
+
if (treeData.children) {
|
|
497
|
+
treeData.children.forEach(child => collapseSingleChildChains(child));
|
|
498
|
+
}
|
|
499
|
+
console.log('=== END COLLAPSING SINGLE-CHILD CHAINS ===');
|
|
500
|
+
|
|
501
|
+
// Collapse all directories and files by default
|
|
502
|
+
function collapseAll(node) {
|
|
503
|
+
if (node.children && node.children.length > 0) {
|
|
504
|
+
// First, recursively process all descendants
|
|
505
|
+
node.children.forEach(child => collapseAll(child));
|
|
506
|
+
|
|
507
|
+
// Then collapse this node (move children to _children)
|
|
508
|
+
node._children = node.children;
|
|
509
|
+
node.children = null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Collapse ALL nodes except the root itself
|
|
514
|
+
// This ensures only the root node is visible initially, all children are collapsed
|
|
515
|
+
if (treeData.children) {
|
|
516
|
+
treeData.children.forEach(child => collapseAll(child));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
console.log('Tree structure built with all directories and files collapsed');
|
|
520
|
+
|
|
521
|
+
// Calculate line counts for all nodes (for proportional node rendering)
|
|
522
|
+
allLineCounts = []; // Reset for fresh calculation
|
|
523
|
+
calculateNodeSizes(treeData);
|
|
524
|
+
calculatePercentiles(); // Calculate 20th/80th percentile thresholds
|
|
525
|
+
console.log('Node sizes calculated with percentile-based sizing');
|
|
526
|
+
|
|
527
|
+
// DEBUG: Check a few file nodes to see if they have chunks in _children
|
|
528
|
+
console.log('=== POST-COLLAPSE FILE CHECK ===');
|
|
529
|
+
let filesChecked = 0;
|
|
530
|
+
let filesWithChunks = 0;
|
|
531
|
+
|
|
532
|
+
function checkFilesRecursive(node) {
|
|
533
|
+
if (node.type === 'file') {
|
|
534
|
+
filesChecked++;
|
|
535
|
+
const chunkCount = (node._children || []).length;
|
|
536
|
+
if (chunkCount > 0) {
|
|
537
|
+
filesWithChunks++;
|
|
538
|
+
console.log(`File ${node.name} has ${chunkCount} chunks in _children`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Check both visible and hidden children
|
|
543
|
+
const childrenToCheck = node.children || node._children || [];
|
|
544
|
+
childrenToCheck.forEach(child => checkFilesRecursive(child));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
checkFilesRecursive(treeData);
|
|
548
|
+
console.log(`Checked ${filesChecked} files, ${filesWithChunks} have chunks`);
|
|
549
|
+
console.log('=== END POST-COLLAPSE FILE CHECK ===');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ============================================================================
|
|
553
|
+
// NODE SIZE CALCULATION
|
|
554
|
+
// ============================================================================
|
|
555
|
+
|
|
556
|
+
// Global variables for size scaling - now tracking line counts
|
|
557
|
+
let globalMinLines = Infinity;
|
|
558
|
+
let globalMaxLines = 0;
|
|
559
|
+
let allLineCounts = []; // Collect all line counts for percentile calculation
|
|
560
|
+
|
|
561
|
+
function calculateNodeSizes(node) {
|
|
562
|
+
if (!node) return 0;
|
|
563
|
+
|
|
564
|
+
// For chunks: use actual line count (primary metric)
|
|
565
|
+
// Falls back to content-based estimate only if line numbers unavailable
|
|
566
|
+
if (chunkTypes.includes(node.type)) {
|
|
567
|
+
// Primary: use actual line span from start_line/end_line
|
|
568
|
+
// This ensures visual correlation with displayed line ranges
|
|
569
|
+
const contentLength = node.content ? node.content.length : 0;
|
|
570
|
+
const lineCount = (node.start_line && node.end_line)
|
|
571
|
+
? node.end_line - node.start_line + 1
|
|
572
|
+
: Math.max(1, Math.floor(contentLength / 40)); // Fallback: estimate ~40 chars per line
|
|
573
|
+
|
|
574
|
+
// Use actual line count for sizing (NOT content length)
|
|
575
|
+
// This prevents inversion where sparse 101-line files appear smaller than dense 3-line files
|
|
576
|
+
node._lineCount = lineCount;
|
|
577
|
+
allLineCounts.push(lineCount);
|
|
578
|
+
|
|
579
|
+
if (lineCount > 0) {
|
|
580
|
+
globalMinLines = Math.min(globalMinLines, lineCount);
|
|
581
|
+
globalMaxLines = Math.max(globalMaxLines, lineCount);
|
|
582
|
+
}
|
|
583
|
+
return lineCount;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// For files and directories: sum of children line counts
|
|
587
|
+
const children = node.children || node._children || [];
|
|
588
|
+
let totalLines = 0;
|
|
589
|
+
|
|
590
|
+
children.forEach(child => {
|
|
591
|
+
totalLines += calculateNodeSizes(child);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Handle collapsed file+chunk: use the collapsed chunk's line count
|
|
595
|
+
if (node.type === 'file' && node.collapsed_chunk) {
|
|
596
|
+
const cc = node.collapsed_chunk;
|
|
597
|
+
if (cc.start_line && cc.end_line) {
|
|
598
|
+
totalLines = cc.end_line - cc.start_line + 1;
|
|
599
|
+
} else if (cc.content) {
|
|
600
|
+
totalLines = Math.max(1, Math.floor(cc.content.length / 40));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
node._lineCount = totalLines || 1; // Minimum 1 for empty dirs/files
|
|
605
|
+
|
|
606
|
+
// DON'T add files/directories to allLineCounts - they skew percentiles
|
|
607
|
+
// Only chunks should affect percentile calculation since only chunks use percentile sizing
|
|
608
|
+
// (Files and directories use separate minRadius/maxRadius, not chunkMinRadius/chunkMaxRadius)
|
|
609
|
+
|
|
610
|
+
if (node._lineCount > 0) {
|
|
611
|
+
globalMinLines = Math.min(globalMinLines, node._lineCount);
|
|
612
|
+
globalMaxLines = Math.max(globalMaxLines, node._lineCount);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return node._lineCount;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Calculate percentile thresholds after all nodes are processed
|
|
619
|
+
let percentile20 = 0;
|
|
620
|
+
let percentile80 = 0;
|
|
621
|
+
|
|
622
|
+
function calculatePercentiles() {
|
|
623
|
+
if (allLineCounts.length === 0) return;
|
|
624
|
+
|
|
625
|
+
const sorted = [...allLineCounts].sort((a, b) => a - b);
|
|
626
|
+
const p20Index = Math.floor(sorted.length * 0.20);
|
|
627
|
+
const p80Index = Math.floor(sorted.length * 0.80);
|
|
628
|
+
|
|
629
|
+
percentile20 = sorted[p20Index] || 1;
|
|
630
|
+
percentile80 = sorted[p80Index] || sorted[sorted.length - 1] || 1;
|
|
631
|
+
|
|
632
|
+
console.log(`Line count percentiles (chunks only): 20th=${percentile20}, 80th=${percentile80}, range=${percentile80 - percentile20}`);
|
|
633
|
+
console.log(`Total chunks: ${allLineCounts.length}, min=${globalMinLines}, max=${globalMaxLines}`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Count external calls for a node
|
|
637
|
+
function getExternalCallCounts(nodeData) {
|
|
638
|
+
if (!nodeData.id) return { inbound: 0, outbound: 0, inboundNodes: [], outboundNodes: [] };
|
|
639
|
+
|
|
640
|
+
const nodeFilePath = nodeData.file_path;
|
|
641
|
+
const inboundNodes = []; // Array of {id, name, file_path}
|
|
642
|
+
const outboundNodes = []; // Array of {id, name, file_path}
|
|
643
|
+
|
|
644
|
+
// Use a Set to deduplicate by source/target node
|
|
645
|
+
const inboundSeen = new Set();
|
|
646
|
+
const outboundSeen = new Set();
|
|
647
|
+
|
|
648
|
+
allLinks.forEach(link => {
|
|
649
|
+
if (link.type === 'caller') {
|
|
650
|
+
if (link.target === nodeData.id) {
|
|
651
|
+
// Something calls this node
|
|
652
|
+
const callerNode = allNodes.find(n => n.id === link.source);
|
|
653
|
+
if (callerNode && callerNode.file_path !== nodeFilePath && !inboundSeen.has(callerNode.id)) {
|
|
654
|
+
inboundSeen.add(callerNode.id);
|
|
655
|
+
inboundNodes.push({ id: callerNode.id, name: callerNode.name, file_path: callerNode.file_path });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (link.source === nodeData.id) {
|
|
659
|
+
// This node calls something
|
|
660
|
+
const calleeNode = allNodes.find(n => n.id === link.target);
|
|
661
|
+
if (calleeNode && calleeNode.file_path !== nodeFilePath && !outboundSeen.has(calleeNode.id)) {
|
|
662
|
+
outboundSeen.add(calleeNode.id);
|
|
663
|
+
outboundNodes.push({ id: calleeNode.id, name: calleeNode.name, file_path: calleeNode.file_path });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
inbound: inboundNodes.length,
|
|
671
|
+
outbound: outboundNodes.length,
|
|
672
|
+
inboundNodes,
|
|
673
|
+
outboundNodes
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Store external call data for line drawing
|
|
678
|
+
let externalCallData = [];
|
|
679
|
+
|
|
680
|
+
function collectExternalCallData() {
|
|
681
|
+
externalCallData = [];
|
|
682
|
+
|
|
683
|
+
allNodes.forEach(nodeData => {
|
|
684
|
+
if (!chunkTypes.includes(nodeData.type)) return;
|
|
685
|
+
|
|
686
|
+
const counts = getExternalCallCounts(nodeData);
|
|
687
|
+
if (counts.inbound > 0 || counts.outbound > 0) {
|
|
688
|
+
externalCallData.push({
|
|
689
|
+
nodeId: nodeData.id,
|
|
690
|
+
inboundNodes: counts.inboundNodes,
|
|
691
|
+
outboundNodes: counts.outboundNodes
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function drawExternalCallLines(svg, root) {
|
|
698
|
+
// Remove existing external call lines
|
|
699
|
+
svg.selectAll('.external-call-line').remove();
|
|
700
|
+
|
|
701
|
+
// Build a map of node positions from the tree (visible nodes only)
|
|
702
|
+
const nodePositions = new Map();
|
|
703
|
+
root.descendants().forEach(d => {
|
|
704
|
+
nodePositions.set(d.data.id, { x: d.x, y: d.y, node: d });
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Build a map from node ID to tree node (for parent traversal)
|
|
708
|
+
const treeNodeMap = new Map();
|
|
709
|
+
root.descendants().forEach(d => {
|
|
710
|
+
treeNodeMap.set(d.data.id, d);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Helper: Find position for a node, falling back to visible ancestors
|
|
714
|
+
// If the target node is not visible (collapsed), find its closest visible ancestor
|
|
715
|
+
function getPositionWithFallback(nodeId) {
|
|
716
|
+
// First check if node is directly visible
|
|
717
|
+
if (nodePositions.has(nodeId)) {
|
|
718
|
+
return nodePositions.get(nodeId);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Node not visible - try to find via tree traversal
|
|
722
|
+
// Strategy 1: Find by file_path matching in visible nodes
|
|
723
|
+
const targetNode = allNodes.find(n => n.id === nodeId);
|
|
724
|
+
if (!targetNode) {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Strategy 2: Look for the file that contains this chunk
|
|
729
|
+
if (targetNode.file_path) {
|
|
730
|
+
// Look for visible file nodes that match this path
|
|
731
|
+
for (const [id, pos] of nodePositions) {
|
|
732
|
+
const visibleNode = allNodes.find(n => n.id === id);
|
|
733
|
+
if (visibleNode) {
|
|
734
|
+
// Check if this is the file containing our chunk
|
|
735
|
+
if (visibleNode.type === 'file' &&
|
|
736
|
+
(visibleNode.path === targetNode.file_path ||
|
|
737
|
+
visibleNode.file_path === targetNode.file_path ||
|
|
738
|
+
visibleNode.name === targetNode.file_path.split('/').pop())) {
|
|
739
|
+
return pos;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Strategy 3: Look for directory containing the file
|
|
745
|
+
const pathParts = targetNode.file_path.split('/');
|
|
746
|
+
// Go from most specific (file's directory) to least specific (root)
|
|
747
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
748
|
+
const dirName = pathParts[i];
|
|
749
|
+
if (!dirName) continue;
|
|
750
|
+
|
|
751
|
+
// Find a visible directory with this name
|
|
752
|
+
for (const [id, pos] of nodePositions) {
|
|
753
|
+
const visibleNode = allNodes.find(n => n.id === id);
|
|
754
|
+
if (visibleNode && visibleNode.type === 'directory' && visibleNode.name === dirName) {
|
|
755
|
+
return pos;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Create a group for external call lines (behind nodes)
|
|
765
|
+
let lineGroup = svg.select('.external-lines-group');
|
|
766
|
+
if (lineGroup.empty()) {
|
|
767
|
+
lineGroup = svg.insert('g', ':first-child')
|
|
768
|
+
.attr('class', 'external-lines-group');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Respect the toggle state
|
|
772
|
+
lineGroup.style('display', showCallLines ? 'block' : 'none');
|
|
773
|
+
|
|
774
|
+
console.log(`[CallLines] Drawing lines for ${externalCallData.length} nodes with external calls`);
|
|
775
|
+
|
|
776
|
+
let linesDrawn = 0;
|
|
777
|
+
externalCallData.forEach(data => {
|
|
778
|
+
const sourcePos = getPositionWithFallback(data.nodeId);
|
|
779
|
+
if (!sourcePos) {
|
|
780
|
+
console.log(`[CallLines] No source position for ${data.nodeId}`);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Draw lines to inbound nodes (callers) - dashed blue (fainter)
|
|
785
|
+
data.inboundNodes.forEach(caller => {
|
|
786
|
+
const targetPos = getPositionWithFallback(caller.id);
|
|
787
|
+
if (targetPos) {
|
|
788
|
+
lineGroup.append('path')
|
|
789
|
+
.attr('class', 'external-call-line inbound-line')
|
|
790
|
+
.attr('d', `M${targetPos.y},${targetPos.x} C${(targetPos.y + sourcePos.y)/2},${targetPos.x} ${(targetPos.y + sourcePos.y)/2},${sourcePos.x} ${sourcePos.y},${sourcePos.x}`)
|
|
791
|
+
.attr('fill', 'none')
|
|
792
|
+
.attr('stroke', '#58a6ff')
|
|
793
|
+
.attr('stroke-width', 1)
|
|
794
|
+
.attr('stroke-dasharray', '4,2')
|
|
795
|
+
.attr('opacity', 0.35)
|
|
796
|
+
.attr('pointer-events', 'none');
|
|
797
|
+
linesDrawn++;
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Draw lines to outbound nodes (callees) - dashed orange (fainter)
|
|
802
|
+
data.outboundNodes.forEach(callee => {
|
|
803
|
+
const targetPos = getPositionWithFallback(callee.id);
|
|
804
|
+
if (targetPos) {
|
|
805
|
+
lineGroup.append('path')
|
|
806
|
+
.attr('class', 'external-call-line outbound-line')
|
|
807
|
+
.attr('d', `M${sourcePos.y},${sourcePos.x} C${(sourcePos.y + targetPos.y)/2},${sourcePos.x} ${(sourcePos.y + targetPos.y)/2},${targetPos.x} ${targetPos.y},${targetPos.x}`)
|
|
808
|
+
.attr('fill', 'none')
|
|
809
|
+
.attr('stroke', '#f0883e')
|
|
810
|
+
.attr('stroke-width', 1)
|
|
811
|
+
.attr('stroke-dasharray', '4,2')
|
|
812
|
+
.attr('opacity', 0.35)
|
|
813
|
+
.attr('pointer-events', 'none');
|
|
814
|
+
linesDrawn++;
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
console.log(`[CallLines] Drew ${linesDrawn} call lines`);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Draw external call lines for CIRCULAR layout
|
|
823
|
+
// Converts polar coordinates (angle, radius) to Cartesian (x, y)
|
|
824
|
+
function drawExternalCallLinesCircular(svg, root) {
|
|
825
|
+
// Remove existing external call lines
|
|
826
|
+
svg.selectAll('.external-call-line').remove();
|
|
827
|
+
|
|
828
|
+
// Helper: Convert polar to Cartesian coordinates
|
|
829
|
+
// In D3 radial layout: d.x = angle (radians), d.y = radius
|
|
830
|
+
function polarToCartesian(angle, radius) {
|
|
831
|
+
return {
|
|
832
|
+
x: radius * Math.cos(angle - Math.PI / 2),
|
|
833
|
+
y: radius * Math.sin(angle - Math.PI / 2)
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Build a map of node positions from the tree (visible nodes only)
|
|
838
|
+
const nodePositions = new Map();
|
|
839
|
+
root.descendants().forEach(d => {
|
|
840
|
+
const cartesian = polarToCartesian(d.x, d.y);
|
|
841
|
+
nodePositions.set(d.data.id, { x: cartesian.x, y: cartesian.y, angle: d.x, radius: d.y, node: d });
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// Helper: Find position for a node, falling back to visible ancestors
|
|
845
|
+
function getPositionWithFallback(nodeId) {
|
|
846
|
+
// First check if node is directly visible
|
|
847
|
+
if (nodePositions.has(nodeId)) {
|
|
848
|
+
return nodePositions.get(nodeId);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Node not visible - try to find via tree traversal
|
|
852
|
+
const targetNode = allNodes.find(n => n.id === nodeId);
|
|
853
|
+
if (!targetNode) {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Look for the file that contains this chunk
|
|
858
|
+
if (targetNode.file_path) {
|
|
859
|
+
// Look for visible file nodes that match this path
|
|
860
|
+
for (const [id, pos] of nodePositions) {
|
|
861
|
+
const visibleNode = allNodes.find(n => n.id === id);
|
|
862
|
+
if (visibleNode) {
|
|
863
|
+
if (visibleNode.type === 'file' &&
|
|
864
|
+
(visibleNode.path === targetNode.file_path ||
|
|
865
|
+
visibleNode.file_path === targetNode.file_path ||
|
|
866
|
+
visibleNode.name === targetNode.file_path.split('/').pop())) {
|
|
867
|
+
return pos;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Look for directory containing the file
|
|
873
|
+
const pathParts = targetNode.file_path.split('/');
|
|
874
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
875
|
+
const dirName = pathParts[i];
|
|
876
|
+
if (!dirName) continue;
|
|
877
|
+
|
|
878
|
+
for (const [id, pos] of nodePositions) {
|
|
879
|
+
const visibleNode = allNodes.find(n => n.id === id);
|
|
880
|
+
if (visibleNode && visibleNode.type === 'directory' && visibleNode.name === dirName) {
|
|
881
|
+
return pos;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Create a group for external call lines (behind nodes)
|
|
891
|
+
let lineGroup = svg.select('.external-lines-group');
|
|
892
|
+
if (lineGroup.empty()) {
|
|
893
|
+
lineGroup = svg.insert('g', ':first-child')
|
|
894
|
+
.attr('class', 'external-lines-group');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Respect the toggle state
|
|
898
|
+
lineGroup.style('display', showCallLines ? 'block' : 'none');
|
|
899
|
+
|
|
900
|
+
console.log(`[CallLines Circular] Drawing lines for ${externalCallData.length} nodes with external calls`);
|
|
901
|
+
|
|
902
|
+
let linesDrawn = 0;
|
|
903
|
+
externalCallData.forEach(data => {
|
|
904
|
+
const sourcePos = getPositionWithFallback(data.nodeId);
|
|
905
|
+
if (!sourcePos) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Draw lines to inbound nodes (callers) - dashed blue (fainter)
|
|
910
|
+
data.inboundNodes.forEach(caller => {
|
|
911
|
+
const targetPos = getPositionWithFallback(caller.id);
|
|
912
|
+
if (targetPos) {
|
|
913
|
+
// Use quadratic bezier curves that go through the center for circular layout
|
|
914
|
+
const midX = (sourcePos.x + targetPos.x) / 2 * 0.3;
|
|
915
|
+
const midY = (sourcePos.y + targetPos.y) / 2 * 0.3;
|
|
916
|
+
|
|
917
|
+
lineGroup.append('path')
|
|
918
|
+
.attr('class', 'external-call-line inbound-line')
|
|
919
|
+
.attr('d', `M${targetPos.x},${targetPos.y} Q${midX},${midY} ${sourcePos.x},${sourcePos.y}`)
|
|
920
|
+
.attr('fill', 'none')
|
|
921
|
+
.attr('stroke', '#58a6ff')
|
|
922
|
+
.attr('stroke-width', 1)
|
|
923
|
+
.attr('stroke-dasharray', '4,2')
|
|
924
|
+
.attr('opacity', 0.35)
|
|
925
|
+
.attr('pointer-events', 'none');
|
|
926
|
+
linesDrawn++;
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// Draw lines to outbound nodes (callees) - dashed orange (fainter)
|
|
931
|
+
data.outboundNodes.forEach(callee => {
|
|
932
|
+
const targetPos = getPositionWithFallback(callee.id);
|
|
933
|
+
if (targetPos) {
|
|
934
|
+
// Use quadratic bezier curves that go through the center for circular layout
|
|
935
|
+
const midX = (sourcePos.x + targetPos.x) / 2 * 0.3;
|
|
936
|
+
const midY = (sourcePos.y + targetPos.y) / 2 * 0.3;
|
|
937
|
+
|
|
938
|
+
lineGroup.append('path')
|
|
939
|
+
.attr('class', 'external-call-line outbound-line')
|
|
940
|
+
.attr('d', `M${sourcePos.x},${sourcePos.y} Q${midX},${midY} ${targetPos.x},${targetPos.y}`)
|
|
941
|
+
.attr('fill', 'none')
|
|
942
|
+
.attr('stroke', '#f0883e')
|
|
943
|
+
.attr('stroke-width', 1)
|
|
944
|
+
.attr('stroke-dasharray', '4,2')
|
|
945
|
+
.attr('opacity', 0.35)
|
|
946
|
+
.attr('pointer-events', 'none');
|
|
947
|
+
linesDrawn++;
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
console.log(`[CallLines Circular] Drew ${linesDrawn} call lines`);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Get color based on complexity (darker = more complex)
|
|
956
|
+
// Uses HSL color model for smooth gradients
|
|
957
|
+
function getComplexityColor(d, baseHue) {
|
|
958
|
+
const nodeData = d.data;
|
|
959
|
+
const complexity = nodeData.complexity;
|
|
960
|
+
|
|
961
|
+
// If no complexity data, return a default based on type
|
|
962
|
+
if (complexity === undefined || complexity === null) {
|
|
963
|
+
// Default colors for non-complex nodes
|
|
964
|
+
if (nodeData.type === 'directory') {
|
|
965
|
+
return nodeData._children ? '#f39c12' : '#3498db'; // Orange/Blue
|
|
966
|
+
} else if (nodeData.type === 'file') {
|
|
967
|
+
return nodeData._children ? '#95a5a6' : '#ecf0f1'; // Gray/White
|
|
968
|
+
} else if (chunkTypes.includes(nodeData.type)) {
|
|
969
|
+
return '#9b59b6'; // Default purple
|
|
970
|
+
}
|
|
971
|
+
return '#95a5a6';
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Complexity ranges: 0-5 (low), 5-10 (medium), 10-20 (high), 20+ (very high)
|
|
975
|
+
// Map to lightness: 70% (light) to 30% (dark)
|
|
976
|
+
const maxComplexity = 25; // Cap for scaling
|
|
977
|
+
const normalizedComplexity = Math.min(complexity, maxComplexity) / maxComplexity;
|
|
978
|
+
|
|
979
|
+
// Lightness goes from 65% (low complexity) to 35% (high complexity)
|
|
980
|
+
const lightness = 65 - (normalizedComplexity * 30);
|
|
981
|
+
|
|
982
|
+
// Saturation increases slightly with complexity (60% to 80%)
|
|
983
|
+
const saturation = 60 + (normalizedComplexity * 20);
|
|
984
|
+
|
|
985
|
+
return `hsl(${baseHue}, ${saturation}%, ${lightness}%)`;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Get node fill color with complexity shading
|
|
989
|
+
function getNodeFillColor(d) {
|
|
990
|
+
const nodeData = d.data;
|
|
991
|
+
|
|
992
|
+
if (nodeData.type === 'directory') {
|
|
993
|
+
// Orange (30°) if collapsed, Blue (210°) if expanded
|
|
994
|
+
const hue = nodeData._children ? 30 : 210;
|
|
995
|
+
// Directories aggregate complexity from children
|
|
996
|
+
const avgComplexity = calculateAverageComplexity(nodeData);
|
|
997
|
+
if (avgComplexity > 0) {
|
|
998
|
+
const lightness = 55 - (Math.min(avgComplexity, 15) / 15) * 20;
|
|
999
|
+
return `hsl(${hue}, 70%, ${lightness}%)`;
|
|
1000
|
+
}
|
|
1001
|
+
return nodeData._children ? '#f39c12' : '#3498db';
|
|
1002
|
+
} else if (nodeData.type === 'file') {
|
|
1003
|
+
// Gray files, but show complexity if available
|
|
1004
|
+
const avgComplexity = calculateAverageComplexity(nodeData);
|
|
1005
|
+
if (avgComplexity > 0) {
|
|
1006
|
+
// Gray hue (0° with 0 saturation) to slight red tint for complexity
|
|
1007
|
+
const saturation = Math.min(avgComplexity, 15) * 2; // 0-30%
|
|
1008
|
+
const lightness = 70 - (Math.min(avgComplexity, 15) / 15) * 25;
|
|
1009
|
+
return `hsl(0, ${saturation}%, ${lightness}%)`;
|
|
1010
|
+
}
|
|
1011
|
+
return nodeData._children ? '#95a5a6' : '#ecf0f1';
|
|
1012
|
+
} else if (chunkTypes.includes(nodeData.type)) {
|
|
1013
|
+
// Purple (280°) for chunks, darker with higher complexity
|
|
1014
|
+
return getComplexityColor(d, 280);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return '#95a5a6';
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Calculate average complexity for a node (recursively for dirs/files)
|
|
1021
|
+
function calculateAverageComplexity(node) {
|
|
1022
|
+
if (chunkTypes.includes(node.type)) {
|
|
1023
|
+
return node.complexity || 0;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const children = node.children || node._children || [];
|
|
1027
|
+
if (children.length === 0) return 0;
|
|
1028
|
+
|
|
1029
|
+
let totalComplexity = 0;
|
|
1030
|
+
let count = 0;
|
|
1031
|
+
|
|
1032
|
+
children.forEach(child => {
|
|
1033
|
+
if (chunkTypes.includes(child.type) && child.complexity) {
|
|
1034
|
+
totalComplexity += child.complexity;
|
|
1035
|
+
count++;
|
|
1036
|
+
} else {
|
|
1037
|
+
const childAvg = calculateAverageComplexity(child);
|
|
1038
|
+
if (childAvg > 0) {
|
|
1039
|
+
totalComplexity += childAvg;
|
|
1040
|
+
count++;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
return count > 0 ? totalComplexity / count : 0;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Get stroke color based on complexity - red outline for high complexity
|
|
1049
|
+
function getNodeStrokeColor(d) {
|
|
1050
|
+
const nodeData = d.data;
|
|
1051
|
+
|
|
1052
|
+
// Only chunks have direct complexity
|
|
1053
|
+
if (chunkTypes.includes(nodeData.type)) {
|
|
1054
|
+
const complexity = nodeData.complexity || 0;
|
|
1055
|
+
// Complexity thresholds:
|
|
1056
|
+
// 0-5: white (simple)
|
|
1057
|
+
// 5-10: orange (moderate)
|
|
1058
|
+
// 10+: red (complex)
|
|
1059
|
+
if (complexity >= 10) {
|
|
1060
|
+
return '#e74c3c'; // Red for high complexity
|
|
1061
|
+
} else if (complexity >= 5) {
|
|
1062
|
+
return '#f39c12'; // Orange for moderate
|
|
1063
|
+
}
|
|
1064
|
+
return '#fff'; // White for low complexity
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Files and directories: check average complexity of children
|
|
1068
|
+
if (nodeData.type === 'file' || nodeData.type === 'directory') {
|
|
1069
|
+
const avgComplexity = calculateAverageComplexity(nodeData);
|
|
1070
|
+
if (avgComplexity >= 10) {
|
|
1071
|
+
return '#e74c3c'; // Red
|
|
1072
|
+
} else if (avgComplexity >= 5) {
|
|
1073
|
+
return '#f39c12'; // Orange
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return '#fff'; // Default white
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Get stroke width based on complexity - thicker for high complexity
|
|
1081
|
+
function getNodeStrokeWidth(d) {
|
|
1082
|
+
const nodeData = d.data;
|
|
1083
|
+
const complexity = chunkTypes.includes(nodeData.type)
|
|
1084
|
+
? (nodeData.complexity || 0)
|
|
1085
|
+
: calculateAverageComplexity(nodeData);
|
|
1086
|
+
|
|
1087
|
+
if (complexity >= 10) return 3; // Thick red outline
|
|
1088
|
+
if (complexity >= 5) return 2.5; // Medium orange outline
|
|
1089
|
+
return 2; // Default
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function getNodeRadius(d) {
|
|
1093
|
+
const nodeData = d.data;
|
|
1094
|
+
|
|
1095
|
+
// Size configuration based on node type
|
|
1096
|
+
const dirMinRadius = 8; // Min for directories
|
|
1097
|
+
const dirMaxRadius = 40; // Max for directories
|
|
1098
|
+
const fileMinRadius = 6; // Min for files
|
|
1099
|
+
const fileMaxRadius = 30; // Max for files
|
|
1100
|
+
const chunkMinRadius = sizeConfig.chunkMinRadius; // From config
|
|
1101
|
+
const chunkMaxRadius = sizeConfig.chunkMaxRadius; // From config
|
|
1102
|
+
|
|
1103
|
+
// Directory nodes: size by file_count (logarithmic scale)
|
|
1104
|
+
if (nodeData.type === 'directory') {
|
|
1105
|
+
const fileCount = nodeData.file_count || 0;
|
|
1106
|
+
if (fileCount === 0) return dirMinRadius;
|
|
1107
|
+
|
|
1108
|
+
// Logarithmic scale: log2(fileCount + 1) for smooth scaling
|
|
1109
|
+
// +1 to avoid log(0), gives range [1, log2(max_files+1)]
|
|
1110
|
+
const logCount = Math.log2(fileCount + 1);
|
|
1111
|
+
const maxLogCount = Math.log2(100 + 1); // Assume max ~100 files per dir
|
|
1112
|
+
const normalized = Math.min(logCount / maxLogCount, 1.0);
|
|
1113
|
+
|
|
1114
|
+
return dirMinRadius + (normalized * (dirMaxRadius - dirMinRadius));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// File nodes: size by total lines of code (sum of all chunks)
|
|
1118
|
+
if (nodeData.type === 'file') {
|
|
1119
|
+
const lineCount = nodeData._lineCount || 1;
|
|
1120
|
+
|
|
1121
|
+
// Collapsed file+chunk: use chunk sizing (since it's really a chunk)
|
|
1122
|
+
if (nodeData.collapsed_chunk) {
|
|
1123
|
+
const minLines = 5;
|
|
1124
|
+
const maxLines = 150;
|
|
1125
|
+
|
|
1126
|
+
let normalized;
|
|
1127
|
+
if (lineCount <= minLines) {
|
|
1128
|
+
normalized = 0;
|
|
1129
|
+
} else if (lineCount >= maxLines) {
|
|
1130
|
+
normalized = 1;
|
|
1131
|
+
} else {
|
|
1132
|
+
const logMin = Math.log(minLines);
|
|
1133
|
+
const logMax = Math.log(maxLines);
|
|
1134
|
+
const logCount = Math.log(lineCount);
|
|
1135
|
+
normalized = (logCount - logMin) / (logMax - logMin);
|
|
1136
|
+
}
|
|
1137
|
+
return chunkMinRadius + (normalized * (chunkMaxRadius - chunkMinRadius));
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Regular files: linear scaling based on total lines
|
|
1141
|
+
const minFileLines = 5;
|
|
1142
|
+
const maxFileLines = 300;
|
|
1143
|
+
|
|
1144
|
+
let normalized;
|
|
1145
|
+
if (lineCount <= minFileLines) {
|
|
1146
|
+
normalized = 0;
|
|
1147
|
+
} else if (lineCount >= maxFileLines) {
|
|
1148
|
+
normalized = 1;
|
|
1149
|
+
} else {
|
|
1150
|
+
normalized = (lineCount - minFileLines) / (maxFileLines - minFileLines);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
return fileMinRadius + (normalized * (fileMaxRadius - fileMinRadius));
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Chunk nodes: size by lines of code (absolute thresholds, not percentiles)
|
|
1157
|
+
// This ensures 330-line functions are ALWAYS big, regardless of codebase distribution
|
|
1158
|
+
if (chunkTypes.includes(nodeData.type)) {
|
|
1159
|
+
const lineCount = nodeData._lineCount || 1;
|
|
1160
|
+
|
|
1161
|
+
// Absolute thresholds for intuitive sizing:
|
|
1162
|
+
// - 1-10 lines: small (imports, constants, simple functions)
|
|
1163
|
+
// - 10-50 lines: medium (typical functions)
|
|
1164
|
+
// - 50-150 lines: large (complex functions)
|
|
1165
|
+
// - 150+ lines: maximum (very large functions/classes)
|
|
1166
|
+
const minLines = 5;
|
|
1167
|
+
const maxLines = 150; // Anything over 150 lines gets max size
|
|
1168
|
+
|
|
1169
|
+
let normalized;
|
|
1170
|
+
if (lineCount <= minLines) {
|
|
1171
|
+
normalized = 0;
|
|
1172
|
+
} else if (lineCount >= maxLines) {
|
|
1173
|
+
normalized = 1;
|
|
1174
|
+
} else {
|
|
1175
|
+
// Logarithmic scaling for better visual distribution
|
|
1176
|
+
const logMin = Math.log(minLines);
|
|
1177
|
+
const logMax = Math.log(maxLines);
|
|
1178
|
+
const logCount = Math.log(lineCount);
|
|
1179
|
+
normalized = (logCount - logMin) / (logMax - logMin);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return chunkMinRadius + (normalized * (chunkMaxRadius - chunkMinRadius));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Default fallback for other node types
|
|
1186
|
+
return sizeConfig.minRadius;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// ============================================================================
|
|
1190
|
+
// VISUALIZATION RENDERING
|
|
1191
|
+
// ============================================================================
|
|
1192
|
+
|
|
1193
|
+
function renderVisualization() {
|
|
1194
|
+
console.log('=== RENDER VISUALIZATION ===');
|
|
1195
|
+
console.log(`Current layout: ${currentLayout}`);
|
|
1196
|
+
console.log(`Tree data exists: ${treeData !== null}`);
|
|
1197
|
+
if (treeData) {
|
|
1198
|
+
console.log(`Root node: ${treeData.name}, children: ${(treeData.children || []).length}, _children: ${(treeData._children || []).length}`);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Clear existing content
|
|
1202
|
+
const graphElement = d3.select('#graph');
|
|
1203
|
+
console.log(`Graph element found: ${!graphElement.empty()}`);
|
|
1204
|
+
graphElement.selectAll('*').remove();
|
|
1205
|
+
|
|
1206
|
+
if (currentLayout === 'linear') {
|
|
1207
|
+
console.log('Calling renderLinearTree()...');
|
|
1208
|
+
renderLinearTree();
|
|
1209
|
+
} else {
|
|
1210
|
+
console.log('Calling renderCircularTree()...');
|
|
1211
|
+
renderCircularTree();
|
|
1212
|
+
}
|
|
1213
|
+
console.log('=== END RENDER VISUALIZATION ===');
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// ============================================================================
|
|
1217
|
+
// LINEAR TREE LAYOUT
|
|
1218
|
+
// ============================================================================
|
|
1219
|
+
|
|
1220
|
+
function renderLinearTree() {
|
|
1221
|
+
console.log('=== RENDER LINEAR TREE ===');
|
|
1222
|
+
const { width, height } = getViewportDimensions();
|
|
1223
|
+
console.log(`Viewport dimensions: ${width}x${height}`);
|
|
1224
|
+
|
|
1225
|
+
const svg = d3.select('#graph')
|
|
1226
|
+
.attr('width', width)
|
|
1227
|
+
.attr('height', height);
|
|
1228
|
+
|
|
1229
|
+
const g = svg.append('g')
|
|
1230
|
+
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
1231
|
+
|
|
1232
|
+
// Create tree layout
|
|
1233
|
+
// For horizontal tree: size is [height, width] where height controls vertical spread
|
|
1234
|
+
const treeLayout = d3.tree()
|
|
1235
|
+
.size([height - margin.top - margin.bottom, width - margin.left - margin.right]);
|
|
1236
|
+
|
|
1237
|
+
console.log(`Tree layout size: ${height - margin.top - margin.bottom} x ${width - margin.left - margin.right}`);
|
|
1238
|
+
|
|
1239
|
+
// Create hierarchy from tree data
|
|
1240
|
+
// D3 hierarchy automatically respects children vs _children
|
|
1241
|
+
console.log('Creating D3 hierarchy...');
|
|
1242
|
+
|
|
1243
|
+
// DEBUG: Check if treeData children have content property BEFORE D3 processes them
|
|
1244
|
+
console.log('=== PRE-D3 HIERARCHY DEBUG ===');
|
|
1245
|
+
if (treeData.children && treeData.children.length > 0) {
|
|
1246
|
+
const firstChild = treeData.children[0];
|
|
1247
|
+
console.log('First root child:', firstChild.name, 'type:', firstChild.type);
|
|
1248
|
+
console.log('First child keys:', Object.keys(firstChild));
|
|
1249
|
+
console.log('First child has content:', 'content' in firstChild);
|
|
1250
|
+
|
|
1251
|
+
// Find a chunk node in the tree
|
|
1252
|
+
function findFirstChunk(node) {
|
|
1253
|
+
if (chunkTypes.includes(node.type)) {
|
|
1254
|
+
return node;
|
|
1255
|
+
}
|
|
1256
|
+
if (node.children) {
|
|
1257
|
+
for (const child of node.children) {
|
|
1258
|
+
const found = findFirstChunk(child);
|
|
1259
|
+
if (found) return found;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (node._children) {
|
|
1263
|
+
for (const child of node._children) {
|
|
1264
|
+
const found = findFirstChunk(child);
|
|
1265
|
+
if (found) return found;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const sampleChunk = findFirstChunk(treeData);
|
|
1272
|
+
if (sampleChunk) {
|
|
1273
|
+
console.log('Sample chunk node BEFORE D3:', sampleChunk.name, 'type:', sampleChunk.type);
|
|
1274
|
+
console.log('Sample chunk keys:', Object.keys(sampleChunk));
|
|
1275
|
+
console.log('Sample chunk has content:', 'content' in sampleChunk);
|
|
1276
|
+
console.log('Sample chunk content length:', sampleChunk.content ? sampleChunk.content.length : 0);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
console.log('=== END PRE-D3 HIERARCHY DEBUG ===');
|
|
1280
|
+
|
|
1281
|
+
const root = d3.hierarchy(treeData, d => d.children);
|
|
1282
|
+
console.log(`Hierarchy created: ${root.descendants().length} nodes`);
|
|
1283
|
+
|
|
1284
|
+
// DEBUG: Check if content is preserved AFTER D3 processes them
|
|
1285
|
+
console.log('=== POST-D3 HIERARCHY DEBUG ===');
|
|
1286
|
+
const debugDescendants = root.descendants();
|
|
1287
|
+
const chunkDescendants = debugDescendants.filter(d => chunkTypes.includes(d.data.type));
|
|
1288
|
+
console.log(`Found ${chunkDescendants.length} chunk nodes in D3 hierarchy`);
|
|
1289
|
+
if (chunkDescendants.length > 0) {
|
|
1290
|
+
const firstChunkD3 = chunkDescendants[0];
|
|
1291
|
+
console.log('First chunk in D3 hierarchy:', firstChunkD3.data.name, 'type:', firstChunkD3.data.type);
|
|
1292
|
+
console.log('First chunk d.data keys:', Object.keys(firstChunkD3.data));
|
|
1293
|
+
console.log('First chunk has content in d.data:', 'content' in firstChunkD3.data);
|
|
1294
|
+
console.log('First chunk content length:', firstChunkD3.data.content ? firstChunkD3.data.content.length : 0);
|
|
1295
|
+
}
|
|
1296
|
+
console.log('=== END POST-D3 HIERARCHY DEBUG ===');
|
|
1297
|
+
|
|
1298
|
+
// Apply tree layout
|
|
1299
|
+
console.log('Applying tree layout...');
|
|
1300
|
+
treeLayout(root);
|
|
1301
|
+
console.log('Tree layout applied');
|
|
1302
|
+
|
|
1303
|
+
// Add zoom behavior
|
|
1304
|
+
const zoom = d3.zoom()
|
|
1305
|
+
.scaleExtent([0.1, 3])
|
|
1306
|
+
.on('zoom', (event) => {
|
|
1307
|
+
g.attr('transform', `translate(${margin.left},${margin.top}) ${event.transform}`);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
svg.call(zoom);
|
|
1311
|
+
|
|
1312
|
+
// Draw links
|
|
1313
|
+
const links = root.links();
|
|
1314
|
+
console.log(`Drawing ${links.length} links`);
|
|
1315
|
+
g.selectAll('.link')
|
|
1316
|
+
.data(links)
|
|
1317
|
+
.enter()
|
|
1318
|
+
.append('path')
|
|
1319
|
+
.attr('class', 'link')
|
|
1320
|
+
.attr('d', d3.linkHorizontal()
|
|
1321
|
+
.x(d => d.y)
|
|
1322
|
+
.y(d => d.x))
|
|
1323
|
+
.attr('fill', 'none')
|
|
1324
|
+
.attr('stroke', '#ccc')
|
|
1325
|
+
.attr('stroke-width', 1.5);
|
|
1326
|
+
|
|
1327
|
+
// Draw nodes
|
|
1328
|
+
const descendants = root.descendants();
|
|
1329
|
+
console.log(`Drawing ${descendants.length} nodes`);
|
|
1330
|
+
const nodes = g.selectAll('.node')
|
|
1331
|
+
.data(descendants)
|
|
1332
|
+
.enter()
|
|
1333
|
+
.append('g')
|
|
1334
|
+
.attr('class', 'node')
|
|
1335
|
+
.attr('transform', d => `translate(${d.y},${d.x})`)
|
|
1336
|
+
.on('click', handleNodeClick)
|
|
1337
|
+
.style('cursor', 'pointer');
|
|
1338
|
+
|
|
1339
|
+
console.log(`Created ${nodes.size()} node elements`);
|
|
1340
|
+
|
|
1341
|
+
// Node circles - sized proportionally to content, colored by complexity
|
|
1342
|
+
nodes.append('circle')
|
|
1343
|
+
.attr('r', d => getNodeRadius(d)) // Dynamic size based on content
|
|
1344
|
+
.attr('fill', d => getNodeFillColor(d)) // Complexity-based coloring
|
|
1345
|
+
.attr('stroke', d => getNodeStrokeColor(d)) // Red/orange for high complexity
|
|
1346
|
+
.attr('stroke-width', d => getNodeStrokeWidth(d))
|
|
1347
|
+
.attr('class', d => {
|
|
1348
|
+
// Add complexity grade class if available
|
|
1349
|
+
const grade = d.data.complexity_grade || '';
|
|
1350
|
+
const hasSmells = (d.data.smell_count && d.data.smell_count > 0) || (d.data.smells && d.data.smells.length > 0);
|
|
1351
|
+
const classes = [];
|
|
1352
|
+
if (grade) classes.push(`grade-${grade}`);
|
|
1353
|
+
if (hasSmells) classes.push('has-smells');
|
|
1354
|
+
return classes.join(' ');
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
// Add external call arrow indicators (only for chunk nodes)
|
|
1358
|
+
nodes.each(function(d) {
|
|
1359
|
+
const node = d3.select(this);
|
|
1360
|
+
const nodeData = d.data;
|
|
1361
|
+
|
|
1362
|
+
// Only add indicators for code chunks (functions, classes, methods)
|
|
1363
|
+
if (!chunkTypes.includes(nodeData.type)) return;
|
|
1364
|
+
|
|
1365
|
+
const counts = getExternalCallCounts(nodeData);
|
|
1366
|
+
const radius = getNodeRadius(d);
|
|
1367
|
+
|
|
1368
|
+
// Inbound arrow: ← before the node (functions from other files call this)
|
|
1369
|
+
if (counts.inbound > 0) {
|
|
1370
|
+
node.append('text')
|
|
1371
|
+
.attr('class', 'call-indicator inbound')
|
|
1372
|
+
.attr('x', -(radius + 8))
|
|
1373
|
+
.attr('y', 5)
|
|
1374
|
+
.attr('text-anchor', 'end')
|
|
1375
|
+
.attr('fill', '#58a6ff')
|
|
1376
|
+
.attr('font-size', '14px')
|
|
1377
|
+
.attr('font-weight', 'bold')
|
|
1378
|
+
.attr('cursor', 'pointer')
|
|
1379
|
+
.text(counts.inbound > 1 ? `${counts.inbound}←` : '←')
|
|
1380
|
+
.append('title')
|
|
1381
|
+
.text(`Called by ${counts.inbound} external function(s):\n${counts.inboundNodes.map(n => n.name).join(', ')}`);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Outbound arrow: → after the label (this calls functions in other files)
|
|
1385
|
+
if (counts.outbound > 0) {
|
|
1386
|
+
// Get approximate label width
|
|
1387
|
+
const labelText = nodeData.name || '';
|
|
1388
|
+
const labelWidth = labelText.length * 7;
|
|
1389
|
+
|
|
1390
|
+
node.append('text')
|
|
1391
|
+
.attr('class', 'call-indicator outbound')
|
|
1392
|
+
.attr('x', radius + labelWidth + 16)
|
|
1393
|
+
.attr('y', 5)
|
|
1394
|
+
.attr('text-anchor', 'start')
|
|
1395
|
+
.attr('fill', '#f0883e')
|
|
1396
|
+
.attr('font-size', '14px')
|
|
1397
|
+
.attr('font-weight', 'bold')
|
|
1398
|
+
.attr('cursor', 'pointer')
|
|
1399
|
+
.text(counts.outbound > 1 ? `→${counts.outbound}` : '→')
|
|
1400
|
+
.append('title')
|
|
1401
|
+
.text(`Calls ${counts.outbound} external function(s):\n${counts.outboundNodes.map(n => n.name).join(', ')}`);
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
// Collect and draw external call lines
|
|
1406
|
+
collectExternalCallData();
|
|
1407
|
+
drawExternalCallLines(g, root);
|
|
1408
|
+
|
|
1409
|
+
// Node labels - positioned to the right of node, left-aligned
|
|
1410
|
+
// Use transform to position text, as x attribute can have rendering issues
|
|
1411
|
+
const labels = nodes.append('text')
|
|
1412
|
+
.attr('class', 'node-label')
|
|
1413
|
+
.attr('transform', d => `translate(${getNodeRadius(d) + 6}, 0)`)
|
|
1414
|
+
.attr('dominant-baseline', 'middle')
|
|
1415
|
+
.attr('text-anchor', 'start')
|
|
1416
|
+
.text(d => d.data.name)
|
|
1417
|
+
.style('font-size', d => chunkTypes.includes(d.data.type) ? '15px' : '18px')
|
|
1418
|
+
.style('font-family', 'Arial, sans-serif')
|
|
1419
|
+
.style('fill', d => chunkTypes.includes(d.data.type) ? '#bb86fc' : '#adbac7')
|
|
1420
|
+
.style('cursor', 'pointer')
|
|
1421
|
+
.style('pointer-events', 'all')
|
|
1422
|
+
.on('click', handleNodeClick);
|
|
1423
|
+
|
|
1424
|
+
console.log(`Created ${labels.size()} label elements`);
|
|
1425
|
+
console.log('=== END RENDER LINEAR TREE ===');
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// ============================================================================
|
|
1429
|
+
// CIRCULAR TREE LAYOUT
|
|
1430
|
+
// ============================================================================
|
|
1431
|
+
|
|
1432
|
+
function renderCircularTree() {
|
|
1433
|
+
const { width, height } = getViewportDimensions();
|
|
1434
|
+
const svg = d3.select('#graph')
|
|
1435
|
+
.attr('width', width)
|
|
1436
|
+
.attr('height', height);
|
|
1437
|
+
|
|
1438
|
+
const radius = Math.min(width, height) / 2 - 100;
|
|
1439
|
+
|
|
1440
|
+
const g = svg.append('g')
|
|
1441
|
+
.attr('transform', `translate(${width/2},${height/2})`);
|
|
1442
|
+
|
|
1443
|
+
// Create radial tree layout
|
|
1444
|
+
const treeLayout = d3.tree()
|
|
1445
|
+
.size([2 * Math.PI, radius])
|
|
1446
|
+
.separation((a, b) => (a.parent === b.parent ? 1 : 2) / a.depth);
|
|
1447
|
+
|
|
1448
|
+
// Create hierarchy
|
|
1449
|
+
// D3 hierarchy automatically respects children vs _children
|
|
1450
|
+
const root = d3.hierarchy(treeData, d => d.children);
|
|
1451
|
+
|
|
1452
|
+
// Apply layout
|
|
1453
|
+
treeLayout(root);
|
|
1454
|
+
|
|
1455
|
+
// Add zoom behavior
|
|
1456
|
+
const zoom = d3.zoom()
|
|
1457
|
+
.scaleExtent([0.1, 3])
|
|
1458
|
+
.on('zoom', (event) => {
|
|
1459
|
+
g.attr('transform', `translate(${width/2},${height/2}) ${event.transform}`);
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
svg.call(zoom);
|
|
1463
|
+
|
|
1464
|
+
// Draw links
|
|
1465
|
+
g.selectAll('.link')
|
|
1466
|
+
.data(root.links())
|
|
1467
|
+
.enter()
|
|
1468
|
+
.append('path')
|
|
1469
|
+
.attr('class', 'link')
|
|
1470
|
+
.attr('d', d3.linkRadial()
|
|
1471
|
+
.angle(d => d.x)
|
|
1472
|
+
.radius(d => d.y))
|
|
1473
|
+
.attr('fill', 'none')
|
|
1474
|
+
.attr('stroke', '#ccc')
|
|
1475
|
+
.attr('stroke-width', 1.5);
|
|
1476
|
+
|
|
1477
|
+
// Draw nodes
|
|
1478
|
+
const nodes = g.selectAll('.node')
|
|
1479
|
+
.data(root.descendants())
|
|
1480
|
+
.enter()
|
|
1481
|
+
.append('g')
|
|
1482
|
+
.attr('class', 'node')
|
|
1483
|
+
.attr('transform', d => `
|
|
1484
|
+
rotate(${d.x * 180 / Math.PI - 90})
|
|
1485
|
+
translate(${d.y},0)
|
|
1486
|
+
`)
|
|
1487
|
+
.on('click', handleNodeClick)
|
|
1488
|
+
.style('cursor', 'pointer');
|
|
1489
|
+
|
|
1490
|
+
// Node circles - sized proportionally to content, colored by complexity
|
|
1491
|
+
nodes.append('circle')
|
|
1492
|
+
.attr('r', d => getNodeRadius(d)) // Dynamic size based on content
|
|
1493
|
+
.attr('fill', d => getNodeFillColor(d)) // Complexity-based coloring
|
|
1494
|
+
.attr('stroke', d => getNodeStrokeColor(d)) // Red/orange for high complexity
|
|
1495
|
+
.attr('stroke-width', d => getNodeStrokeWidth(d))
|
|
1496
|
+
.attr('class', d => {
|
|
1497
|
+
// Add complexity grade class if available
|
|
1498
|
+
const grade = d.data.complexity_grade || '';
|
|
1499
|
+
const hasSmells = (d.data.smell_count && d.data.smell_count > 0) || (d.data.smells && d.data.smells.length > 0);
|
|
1500
|
+
const classes = [];
|
|
1501
|
+
if (grade) classes.push(`grade-${grade}`);
|
|
1502
|
+
if (hasSmells) classes.push('has-smells');
|
|
1503
|
+
return classes.join(' ');
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
// Add external call arrow indicators (only for chunk nodes)
|
|
1507
|
+
nodes.each(function(d) {
|
|
1508
|
+
const node = d3.select(this);
|
|
1509
|
+
const nodeData = d.data;
|
|
1510
|
+
|
|
1511
|
+
if (!chunkTypes.includes(nodeData.type)) return;
|
|
1512
|
+
|
|
1513
|
+
const counts = getExternalCallCounts(nodeData);
|
|
1514
|
+
const radius = getNodeRadius(d);
|
|
1515
|
+
|
|
1516
|
+
// Inbound indicator
|
|
1517
|
+
if (counts.inbound > 0) {
|
|
1518
|
+
node.append('text')
|
|
1519
|
+
.attr('x', 0)
|
|
1520
|
+
.attr('y', -(radius + 8))
|
|
1521
|
+
.attr('text-anchor', 'middle')
|
|
1522
|
+
.attr('fill', '#58a6ff')
|
|
1523
|
+
.attr('font-size', '10px')
|
|
1524
|
+
.attr('font-weight', 'bold')
|
|
1525
|
+
.text(counts.inbound > 1 ? `↓${counts.inbound}` : '↓')
|
|
1526
|
+
.append('title')
|
|
1527
|
+
.text(`Called by ${counts.inbound} external function(s)`);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Outbound indicator
|
|
1531
|
+
if (counts.outbound > 0) {
|
|
1532
|
+
node.append('text')
|
|
1533
|
+
.attr('x', 0)
|
|
1534
|
+
.attr('y', radius + 12)
|
|
1535
|
+
.attr('text-anchor', 'middle')
|
|
1536
|
+
.attr('fill', '#f0883e')
|
|
1537
|
+
.attr('font-size', '10px')
|
|
1538
|
+
.attr('font-weight', 'bold')
|
|
1539
|
+
.text(counts.outbound > 1 ? `↑${counts.outbound}` : '↑')
|
|
1540
|
+
.append('title')
|
|
1541
|
+
.text(`Calls ${counts.outbound} external function(s)`);
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
// Node labels - positioned to the right of node, left-aligned
|
|
1546
|
+
// Use transform to position text, as x attribute can have rendering issues
|
|
1547
|
+
nodes.append('text')
|
|
1548
|
+
.attr('class', 'node-label')
|
|
1549
|
+
.attr('transform', d => {
|
|
1550
|
+
const offset = getNodeRadius(d) + 6;
|
|
1551
|
+
const rotate = d.x >= Math.PI ? 'rotate(180)' : '';
|
|
1552
|
+
return `translate(${offset}, 0) ${rotate}`;
|
|
1553
|
+
})
|
|
1554
|
+
.attr('dominant-baseline', 'middle')
|
|
1555
|
+
.attr('text-anchor', d => d.x >= Math.PI ? 'end' : 'start')
|
|
1556
|
+
.text(d => d.data.name)
|
|
1557
|
+
.style('font-size', d => chunkTypes.includes(d.data.type) ? '15px' : '18px')
|
|
1558
|
+
.style('font-family', 'Arial, sans-serif')
|
|
1559
|
+
.style('fill', d => chunkTypes.includes(d.data.type) ? '#bb86fc' : '#adbac7')
|
|
1560
|
+
.style('cursor', 'pointer')
|
|
1561
|
+
.style('pointer-events', 'all')
|
|
1562
|
+
.on('click', handleNodeClick);
|
|
1563
|
+
|
|
1564
|
+
// Collect and draw external call lines (circular version)
|
|
1565
|
+
collectExternalCallData();
|
|
1566
|
+
drawExternalCallLinesCircular(g, root);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// ============================================================================
|
|
1570
|
+
// INTERACTION HANDLERS
|
|
1571
|
+
// ============================================================================
|
|
1572
|
+
|
|
1573
|
+
function handleNodeClick(event, d) {
|
|
1574
|
+
event.stopPropagation();
|
|
1575
|
+
|
|
1576
|
+
const nodeData = d.data;
|
|
1577
|
+
|
|
1578
|
+
console.log('=== NODE CLICK DEBUG ===');
|
|
1579
|
+
console.log(`Clicked node: ${nodeData.name} (type: ${nodeData.type}, id: ${nodeData.id})`);
|
|
1580
|
+
console.log(`Has children: ${nodeData.children ? nodeData.children.length : 0}`);
|
|
1581
|
+
console.log(`Has _children: ${nodeData._children ? nodeData._children.length : 0}`);
|
|
1582
|
+
|
|
1583
|
+
if (nodeData.type === 'directory') {
|
|
1584
|
+
// Toggle directory: swap children <-> _children
|
|
1585
|
+
if (nodeData.children) {
|
|
1586
|
+
// Currently expanded - collapse it
|
|
1587
|
+
console.log('Collapsing directory');
|
|
1588
|
+
nodeData._children = nodeData.children;
|
|
1589
|
+
nodeData.children = null;
|
|
1590
|
+
} else if (nodeData._children) {
|
|
1591
|
+
// Currently collapsed - expand it
|
|
1592
|
+
console.log('Expanding directory');
|
|
1593
|
+
nodeData.children = nodeData._children;
|
|
1594
|
+
nodeData._children = null;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// Re-render to show/hide children
|
|
1598
|
+
renderVisualization();
|
|
1599
|
+
|
|
1600
|
+
// Don't auto-open viewer panel for directories - just expand/collapse
|
|
1601
|
+
} else if (nodeData.type === 'file') {
|
|
1602
|
+
// Check if this file has a collapsed chunk (single chunk with no children)
|
|
1603
|
+
if (nodeData.collapsed_chunk) {
|
|
1604
|
+
console.log(`Collapsed file+chunk: ${nodeData.name}, showing content directly`);
|
|
1605
|
+
displayChunkContent({
|
|
1606
|
+
...nodeData.collapsed_chunk,
|
|
1607
|
+
name: nodeData.collapsed_chunk.name,
|
|
1608
|
+
type: nodeData.collapsed_chunk.type
|
|
1609
|
+
});
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Check if this is a single-chunk file - skip to content directly
|
|
1614
|
+
const childrenArray = nodeData._children || nodeData.children;
|
|
1615
|
+
|
|
1616
|
+
if (childrenArray && childrenArray.length === 1) {
|
|
1617
|
+
const onlyChild = childrenArray[0];
|
|
1618
|
+
|
|
1619
|
+
if (chunkTypes.includes(onlyChild.type)) {
|
|
1620
|
+
console.log(`Single-chunk file: ${nodeData.name}, showing content directly`);
|
|
1621
|
+
|
|
1622
|
+
// Expand the file visually (for tree consistency)
|
|
1623
|
+
if (nodeData._children) {
|
|
1624
|
+
nodeData.children = nodeData._children;
|
|
1625
|
+
nodeData._children = null;
|
|
1626
|
+
renderVisualization();
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Auto-display the chunk content
|
|
1630
|
+
displayChunkContent(onlyChild);
|
|
1631
|
+
return; // Skip normal file toggle behavior
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// Continue with existing toggle logic for multi-chunk files
|
|
1636
|
+
// Toggle file: swap children <-> _children
|
|
1637
|
+
if (nodeData.children) {
|
|
1638
|
+
// Currently expanded - collapse it
|
|
1639
|
+
console.log('Collapsing file');
|
|
1640
|
+
nodeData._children = nodeData.children;
|
|
1641
|
+
nodeData.children = null;
|
|
1642
|
+
} else if (nodeData._children) {
|
|
1643
|
+
// Currently collapsed - expand it
|
|
1644
|
+
console.log('Expanding file');
|
|
1645
|
+
nodeData.children = nodeData._children;
|
|
1646
|
+
nodeData._children = null;
|
|
1647
|
+
} else {
|
|
1648
|
+
console.log('WARNING: File has neither children nor _children!');
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// Re-render to show/hide children
|
|
1652
|
+
renderVisualization();
|
|
1653
|
+
|
|
1654
|
+
// Don't auto-open viewer panel for files - just expand/collapse
|
|
1655
|
+
} else if (chunkTypes.includes(nodeData.type)) {
|
|
1656
|
+
// Chunks can have children too (e.g., imports -> functions, class -> methods)
|
|
1657
|
+
// If chunk has children, toggle expand/collapse
|
|
1658
|
+
if (nodeData.children || nodeData._children) {
|
|
1659
|
+
if (nodeData.children) {
|
|
1660
|
+
// Currently expanded - collapse it
|
|
1661
|
+
console.log(`Collapsing ${nodeData.type} chunk`);
|
|
1662
|
+
nodeData._children = nodeData.children;
|
|
1663
|
+
nodeData.children = null;
|
|
1664
|
+
} else if (nodeData._children) {
|
|
1665
|
+
// Currently collapsed - expand it
|
|
1666
|
+
console.log(`Expanding ${nodeData.type} chunk to show ${nodeData._children.length} children`);
|
|
1667
|
+
nodeData.children = nodeData._children;
|
|
1668
|
+
nodeData._children = null;
|
|
1669
|
+
}
|
|
1670
|
+
// Re-render to show/hide children
|
|
1671
|
+
renderVisualization();
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// Also show chunk content in side panel
|
|
1675
|
+
console.log('Displaying chunk content');
|
|
1676
|
+
displayChunkContent(nodeData);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
console.log('=== END NODE CLICK DEBUG ===');
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function displayDirectoryInfo(dirData, addToHistory = true) {
|
|
1683
|
+
openViewerPanel();
|
|
1684
|
+
|
|
1685
|
+
// Add to navigation history
|
|
1686
|
+
if (addToHistory) {
|
|
1687
|
+
addToNavHistory({type: 'directory', data: dirData});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const title = document.getElementById('viewer-title');
|
|
1691
|
+
const content = document.getElementById('viewer-content');
|
|
1692
|
+
|
|
1693
|
+
title.textContent = `📁 ${dirData.name}`;
|
|
1694
|
+
|
|
1695
|
+
// Count children
|
|
1696
|
+
const children = dirData.children || dirData._children || [];
|
|
1697
|
+
const dirs = children.filter(c => c.type === 'directory').length;
|
|
1698
|
+
const files = children.filter(c => c.type === 'file').length;
|
|
1699
|
+
|
|
1700
|
+
let html = '';
|
|
1701
|
+
|
|
1702
|
+
// Navigation bar with breadcrumbs and back/forward
|
|
1703
|
+
html += renderNavigationBar(dirData);
|
|
1704
|
+
|
|
1705
|
+
html += '<div class="viewer-section">';
|
|
1706
|
+
html += '<div class="viewer-section-title">Directory Information</div>';
|
|
1707
|
+
html += '<div class="viewer-info-grid">';
|
|
1708
|
+
html += `<div class="viewer-info-row">`;
|
|
1709
|
+
html += `<span class="viewer-info-label">Name:</span>`;
|
|
1710
|
+
html += `<span class="viewer-info-value">${escapeHtml(dirData.name)}</span>`;
|
|
1711
|
+
html += `</div>`;
|
|
1712
|
+
html += `<div class="viewer-info-row">`;
|
|
1713
|
+
html += `<span class="viewer-info-label">Subdirectories:</span>`;
|
|
1714
|
+
html += `<span class="viewer-info-value">${dirs}</span>`;
|
|
1715
|
+
html += `</div>`;
|
|
1716
|
+
html += `<div class="viewer-info-row">`;
|
|
1717
|
+
html += `<span class="viewer-info-label">Files:</span>`;
|
|
1718
|
+
html += `<span class="viewer-info-value">${files}</span>`;
|
|
1719
|
+
html += `</div>`;
|
|
1720
|
+
html += `<div class="viewer-info-row">`;
|
|
1721
|
+
html += `<span class="viewer-info-label">Total Items:</span>`;
|
|
1722
|
+
html += `<span class="viewer-info-value">${children.length}</span>`;
|
|
1723
|
+
html += `</div>`;
|
|
1724
|
+
html += '</div>';
|
|
1725
|
+
html += '</div>';
|
|
1726
|
+
|
|
1727
|
+
if (children.length > 0) {
|
|
1728
|
+
html += '<div class="viewer-section">';
|
|
1729
|
+
html += '<div class="viewer-section-title">Contents</div>';
|
|
1730
|
+
html += '<div class="dir-list">';
|
|
1731
|
+
|
|
1732
|
+
// Sort: directories first, then files
|
|
1733
|
+
const sortedChildren = [...children].sort((a, b) => {
|
|
1734
|
+
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
|
1735
|
+
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
|
1736
|
+
return a.name.localeCompare(b.name);
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
sortedChildren.forEach(child => {
|
|
1740
|
+
const icon = child.type === 'directory' ? '📁' : '📄';
|
|
1741
|
+
const type = child.type === 'directory' ? 'dir' : 'file';
|
|
1742
|
+
const childData = JSON.stringify(child).replace(/"/g, '"');
|
|
1743
|
+
const clickHandler = child.type === 'directory'
|
|
1744
|
+
? `navigateToDirectory(${childData})`
|
|
1745
|
+
: `navigateToFile(${childData})`;
|
|
1746
|
+
html += `<div class="dir-list-item clickable" onclick="${clickHandler}">`;
|
|
1747
|
+
html += `<span class="dir-icon">${icon}</span>`;
|
|
1748
|
+
html += `<span class="dir-name">${escapeHtml(child.name)}</span>`;
|
|
1749
|
+
html += `<span class="dir-type">${type}</span>`;
|
|
1750
|
+
html += `<span class="dir-arrow">→</span>`;
|
|
1751
|
+
html += `</div>`;
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
html += '</div>';
|
|
1755
|
+
html += '</div>';
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
content.innerHTML = html;
|
|
1759
|
+
|
|
1760
|
+
// Hide section dropdown for directories (no code sections)
|
|
1761
|
+
populateSectionDropdown([]);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function displayFileInfo(fileData, addToHistory = true) {
|
|
1765
|
+
openViewerPanel();
|
|
1766
|
+
|
|
1767
|
+
// Add to navigation history
|
|
1768
|
+
if (addToHistory) {
|
|
1769
|
+
addToNavHistory({type: 'file', data: fileData});
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
const title = document.getElementById('viewer-title');
|
|
1773
|
+
const content = document.getElementById('viewer-content');
|
|
1774
|
+
|
|
1775
|
+
title.textContent = `📄 ${fileData.name}`;
|
|
1776
|
+
|
|
1777
|
+
// Get chunks
|
|
1778
|
+
const chunks = fileData.children || fileData._children || [];
|
|
1779
|
+
|
|
1780
|
+
let html = '';
|
|
1781
|
+
|
|
1782
|
+
// Navigation bar with breadcrumbs and back/forward
|
|
1783
|
+
html += renderNavigationBar(fileData);
|
|
1784
|
+
|
|
1785
|
+
html += '<div class="viewer-section">';
|
|
1786
|
+
html += '<div class="viewer-section-title">File Information</div>';
|
|
1787
|
+
html += '<div class="viewer-info-grid">';
|
|
1788
|
+
html += `<div class="viewer-info-row">`;
|
|
1789
|
+
html += `<span class="viewer-info-label">Name:</span>`;
|
|
1790
|
+
html += `<span class="viewer-info-value">${escapeHtml(fileData.name)}</span>`;
|
|
1791
|
+
html += `</div>`;
|
|
1792
|
+
html += `<div class="viewer-info-row">`;
|
|
1793
|
+
html += `<span class="viewer-info-label">Chunks:</span>`;
|
|
1794
|
+
html += `<span class="viewer-info-value">${chunks.length}</span>`;
|
|
1795
|
+
html += `</div>`;
|
|
1796
|
+
if (fileData.path) {
|
|
1797
|
+
html += `<div class="viewer-info-row">`;
|
|
1798
|
+
html += `<span class="viewer-info-label">Path:</span>`;
|
|
1799
|
+
html += `<span class="viewer-info-value" style="word-break: break-all;">${escapeHtml(fileData.path)}</span>`;
|
|
1800
|
+
html += `</div>`;
|
|
1801
|
+
}
|
|
1802
|
+
html += '</div>';
|
|
1803
|
+
html += '</div>';
|
|
1804
|
+
|
|
1805
|
+
if (chunks.length > 0) {
|
|
1806
|
+
html += '<div class="viewer-section">';
|
|
1807
|
+
html += '<div class="viewer-section-title">Code Chunks</div>';
|
|
1808
|
+
html += '<div class="chunk-list">';
|
|
1809
|
+
|
|
1810
|
+
chunks.forEach(chunk => {
|
|
1811
|
+
const icon = getChunkIcon(chunk.type);
|
|
1812
|
+
const chunkName = chunk.name || chunk.type || 'chunk';
|
|
1813
|
+
const lines = chunk.start_line && chunk.end_line
|
|
1814
|
+
? `Lines ${chunk.start_line}-${chunk.end_line}`
|
|
1815
|
+
: 'Unknown lines';
|
|
1816
|
+
|
|
1817
|
+
html += `<div class="chunk-list-item" onclick="displayChunkContent(${JSON.stringify(chunk).replace(/"/g, '"')})">`;
|
|
1818
|
+
html += `<span class="chunk-icon">${icon}</span>`;
|
|
1819
|
+
html += `<div class="chunk-info">`;
|
|
1820
|
+
html += `<div class="chunk-name">${escapeHtml(chunkName)}</div>`;
|
|
1821
|
+
html += `<div class="chunk-meta">${lines} • ${chunk.type || 'code'}</div>`;
|
|
1822
|
+
html += `</div>`;
|
|
1823
|
+
html += `</div>`;
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
html += '</div>';
|
|
1827
|
+
html += '</div>';
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
content.innerHTML = html;
|
|
1831
|
+
|
|
1832
|
+
// Hide section dropdown for files (no code sections in file view)
|
|
1833
|
+
populateSectionDropdown([]);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function displayChunkContent(chunkData, addToHistory = true) {
|
|
1837
|
+
openViewerPanel();
|
|
1838
|
+
|
|
1839
|
+
// Expand path to node and highlight it in tree
|
|
1840
|
+
if (chunkData.id) {
|
|
1841
|
+
expandAndHighlightNode(chunkData.id);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// Add to navigation history
|
|
1845
|
+
if (addToHistory) {
|
|
1846
|
+
addToNavHistory({type: 'chunk', data: chunkData});
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
const title = document.getElementById('viewer-title');
|
|
1850
|
+
const content = document.getElementById('viewer-content');
|
|
1851
|
+
|
|
1852
|
+
const chunkName = chunkData.name || chunkData.type || 'Chunk';
|
|
1853
|
+
title.textContent = `${getChunkIcon(chunkData.type)} ${chunkName}`;
|
|
1854
|
+
|
|
1855
|
+
let html = '';
|
|
1856
|
+
|
|
1857
|
+
// Navigation bar with breadcrumbs and back/forward
|
|
1858
|
+
html += renderNavigationBar(chunkData);
|
|
1859
|
+
|
|
1860
|
+
// Track sections for dropdown navigation
|
|
1861
|
+
const sections = [];
|
|
1862
|
+
|
|
1863
|
+
// === ORDER: Docstring (comments), Code, Metadata ===
|
|
1864
|
+
|
|
1865
|
+
// === 1. Docstring Section (Comments) ===
|
|
1866
|
+
if (chunkData.docstring) {
|
|
1867
|
+
sections.push({ id: 'docstring', label: '📖 Docstring' });
|
|
1868
|
+
html += '<div class="viewer-section" data-section="docstring">';
|
|
1869
|
+
html += '<div class="viewer-section-title">📖 Docstring</div>';
|
|
1870
|
+
html += `<div style="color: #8b949e; font-style: italic; padding: 8px 12px; background: #161b22; border-radius: 4px; white-space: pre-wrap;">${escapeHtml(chunkData.docstring)}</div>`;
|
|
1871
|
+
html += '</div>';
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// === 2. Source Code Section ===
|
|
1875
|
+
if (chunkData.content) {
|
|
1876
|
+
sections.push({ id: 'source-code', label: '📝 Source Code' });
|
|
1877
|
+
html += '<div class="viewer-section" data-section="source-code">';
|
|
1878
|
+
html += '<div class="viewer-section-title">📝 Source Code</div>';
|
|
1879
|
+
const langClass = getLanguageClass(chunkData.file_path);
|
|
1880
|
+
html += `<pre><code class="hljs${langClass ? ' language-' + langClass : ''}">${escapeHtml(chunkData.content)}</code></pre>`;
|
|
1881
|
+
html += '</div>';
|
|
1882
|
+
} else {
|
|
1883
|
+
html += '<p style="color: #8b949e; padding: 20px; text-align: center;">No content available for this chunk.</p>';
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// === 3. Metadata Section ===
|
|
1887
|
+
sections.push({ id: 'metadata', label: 'ℹ️ Metadata' });
|
|
1888
|
+
html += '<div class="viewer-section" data-section="metadata">';
|
|
1889
|
+
html += '<div class="viewer-section-title">ℹ️ Metadata</div>';
|
|
1890
|
+
html += '<div class="viewer-info-grid">';
|
|
1891
|
+
|
|
1892
|
+
// Basic info
|
|
1893
|
+
html += `<div class="viewer-info-row">`;
|
|
1894
|
+
html += `<span class="viewer-info-label">Name:</span>`;
|
|
1895
|
+
html += `<span class="viewer-info-value">${escapeHtml(chunkName)}</span>`;
|
|
1896
|
+
html += `</div>`;
|
|
1897
|
+
|
|
1898
|
+
html += `<div class="viewer-info-row">`;
|
|
1899
|
+
html += `<span class="viewer-info-label">Type:</span>`;
|
|
1900
|
+
html += `<span class="viewer-info-value">${escapeHtml(chunkData.type || 'code')}</span>`;
|
|
1901
|
+
html += `</div>`;
|
|
1902
|
+
|
|
1903
|
+
// File path (clickable - navigates to file node)
|
|
1904
|
+
if (chunkData.file_path) {
|
|
1905
|
+
const shortPath = chunkData.file_path.split('/').slice(-3).join('/');
|
|
1906
|
+
const escapedPath = escapeHtml(chunkData.file_path).replace(/'/g, "\\'");
|
|
1907
|
+
html += `<div class="viewer-info-row">`;
|
|
1908
|
+
html += `<span class="viewer-info-label">File:</span>`;
|
|
1909
|
+
html += `<span class="viewer-info-value clickable" onclick="navigateToFileByPath('${escapedPath}')" title="Click to navigate to file: ${escapeHtml(chunkData.file_path)}">.../${escapeHtml(shortPath)}</span>`;
|
|
1910
|
+
html += `</div>`;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// Line numbers
|
|
1914
|
+
if (chunkData.start_line && chunkData.end_line) {
|
|
1915
|
+
html += `<div class="viewer-info-row">`;
|
|
1916
|
+
html += `<span class="viewer-info-label">Lines:</span>`;
|
|
1917
|
+
html += `<span class="viewer-info-value">${chunkData.start_line} - ${chunkData.end_line} (${chunkData.end_line - chunkData.start_line + 1} lines)</span>`;
|
|
1918
|
+
html += `</div>`;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Language
|
|
1922
|
+
if (chunkData.language) {
|
|
1923
|
+
html += `<div class="viewer-info-row">`;
|
|
1924
|
+
html += `<span class="viewer-info-label">Language:</span>`;
|
|
1925
|
+
html += `<span class="viewer-info-value">${escapeHtml(chunkData.language)}</span>`;
|
|
1926
|
+
html += `</div>`;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// Complexity
|
|
1930
|
+
if (chunkData.complexity !== undefined && chunkData.complexity !== null) {
|
|
1931
|
+
const complexityColor = chunkData.complexity > 10 ? '#f85149' : chunkData.complexity > 5 ? '#d29922' : '#3fb950';
|
|
1932
|
+
html += `<div class="viewer-info-row">`;
|
|
1933
|
+
html += `<span class="viewer-info-label">Complexity:</span>`;
|
|
1934
|
+
html += `<span class="viewer-info-value" style="color: ${complexityColor}">${chunkData.complexity.toFixed(1)}</span>`;
|
|
1935
|
+
html += `</div>`;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
html += '</div>';
|
|
1939
|
+
html += '</div>';
|
|
1940
|
+
|
|
1941
|
+
// === 4. External Calls & Callers Section (Cross-file references) ===
|
|
1942
|
+
const chunkId = chunkData.id;
|
|
1943
|
+
const currentFilePath = chunkData.file_path;
|
|
1944
|
+
|
|
1945
|
+
if (chunkId) {
|
|
1946
|
+
// Find all caller relationships
|
|
1947
|
+
const allCallers = allLinks.filter(l => l.type === 'caller' && l.target === chunkId);
|
|
1948
|
+
const allCallees = allLinks.filter(l => l.type === 'caller' && l.source === chunkId);
|
|
1949
|
+
|
|
1950
|
+
// Separate external (different file) from local (same file) relationships
|
|
1951
|
+
// Use Maps to deduplicate by node.id
|
|
1952
|
+
const externalCallersMap = new Map();
|
|
1953
|
+
const localCallersMap = new Map();
|
|
1954
|
+
allCallers.forEach(link => {
|
|
1955
|
+
const callerNode = allNodes.find(n => n.id === link.source);
|
|
1956
|
+
if (callerNode) {
|
|
1957
|
+
if (callerNode.file_path !== currentFilePath) {
|
|
1958
|
+
if (!externalCallersMap.has(callerNode.id)) {
|
|
1959
|
+
externalCallersMap.set(callerNode.id, { link, node: callerNode });
|
|
1960
|
+
}
|
|
1961
|
+
} else {
|
|
1962
|
+
if (!localCallersMap.has(callerNode.id)) {
|
|
1963
|
+
localCallersMap.set(callerNode.id, { link, node: callerNode });
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
});
|
|
1968
|
+
const externalCallers = Array.from(externalCallersMap.values());
|
|
1969
|
+
const localCallers = Array.from(localCallersMap.values());
|
|
1970
|
+
|
|
1971
|
+
const externalCalleesMap = new Map();
|
|
1972
|
+
const localCalleesMap = new Map();
|
|
1973
|
+
allCallees.forEach(link => {
|
|
1974
|
+
const calleeNode = allNodes.find(n => n.id === link.target);
|
|
1975
|
+
if (calleeNode) {
|
|
1976
|
+
if (calleeNode.file_path !== currentFilePath) {
|
|
1977
|
+
if (!externalCalleesMap.has(calleeNode.id)) {
|
|
1978
|
+
externalCalleesMap.set(calleeNode.id, { link, node: calleeNode });
|
|
1979
|
+
}
|
|
1980
|
+
} else {
|
|
1981
|
+
if (!localCalleesMap.has(calleeNode.id)) {
|
|
1982
|
+
localCalleesMap.set(calleeNode.id, { link, node: calleeNode });
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
const externalCallees = Array.from(externalCalleesMap.values());
|
|
1988
|
+
const localCallees = Array.from(localCalleesMap.values());
|
|
1989
|
+
|
|
1990
|
+
// === External Callers Section (functions from other files that call this) ===
|
|
1991
|
+
if (externalCallers.length > 0) {
|
|
1992
|
+
sections.push({ id: 'external-callers', label: '📥 External Callers' });
|
|
1993
|
+
html += '<div class="viewer-section" data-section="external-callers">';
|
|
1994
|
+
html += '<div class="viewer-section-title">📥 External Callers <span style="color: #8b949e; font-weight: normal;">(functions from other files calling this)</span></div>';
|
|
1995
|
+
html += '<div style="display: flex; flex-direction: column; gap: 6px;">';
|
|
1996
|
+
externalCallers.slice(0, 10).forEach(({ link, node }) => {
|
|
1997
|
+
const shortPath = node.file_path ? node.file_path.split('/').slice(-2).join('/') : '';
|
|
1998
|
+
html += `<div class="external-call-item" onclick="focusNodeInTree('${link.source}')" title="${escapeHtml(node.file_path || '')}">`;
|
|
1999
|
+
html += `<span class="external-call-icon">←</span>`;
|
|
2000
|
+
html += `<span class="external-call-name">${escapeHtml(node.name || node.id.substring(0, 8))}</span>`;
|
|
2001
|
+
html += `<span class="external-call-path">${escapeHtml(shortPath)}</span>`;
|
|
2002
|
+
html += `</div>`;
|
|
2003
|
+
});
|
|
2004
|
+
if (externalCallers.length > 10) {
|
|
2005
|
+
html += `<div style="color: #8b949e; font-size: 11px; padding-left: 20px;">+${externalCallers.length - 10} more external callers</div>`;
|
|
2006
|
+
}
|
|
2007
|
+
html += '</div></div>';
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// === External Calls Section (functions in other files this calls) ===
|
|
2011
|
+
if (externalCallees.length > 0) {
|
|
2012
|
+
sections.push({ id: 'external-calls', label: '📤 External Calls' });
|
|
2013
|
+
html += '<div class="viewer-section" data-section="external-calls">';
|
|
2014
|
+
html += '<div class="viewer-section-title">📤 External Calls <span style="color: #8b949e; font-weight: normal;">(functions in other files this calls)</span></div>';
|
|
2015
|
+
html += '<div style="display: flex; flex-direction: column; gap: 6px;">';
|
|
2016
|
+
externalCallees.slice(0, 10).forEach(({ link, node }) => {
|
|
2017
|
+
const shortPath = node.file_path ? node.file_path.split('/').slice(-2).join('/') : '';
|
|
2018
|
+
html += `<div class="external-call-item" onclick="focusNodeInTree('${link.target}')" title="${escapeHtml(node.file_path || '')}">`;
|
|
2019
|
+
html += `<span class="external-call-icon">→</span>`;
|
|
2020
|
+
html += `<span class="external-call-name">${escapeHtml(node.name || node.id.substring(0, 8))}</span>`;
|
|
2021
|
+
html += `<span class="external-call-path">${escapeHtml(shortPath)}</span>`;
|
|
2022
|
+
html += `</div>`;
|
|
2023
|
+
});
|
|
2024
|
+
if (externalCallees.length > 10) {
|
|
2025
|
+
html += `<div style="color: #8b949e; font-size: 11px; padding-left: 20px;">+${externalCallees.length - 10} more external calls</div>`;
|
|
2026
|
+
}
|
|
2027
|
+
html += '</div></div>';
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// === Local (Same-File) Relationships Section ===
|
|
2031
|
+
if (localCallers.length > 0 || localCallees.length > 0) {
|
|
2032
|
+
sections.push({ id: 'local-references', label: '🔗 Local References' });
|
|
2033
|
+
html += '<div class="viewer-section" data-section="local-references">';
|
|
2034
|
+
html += '<div class="viewer-section-title">🔗 Local References <span style="color: #8b949e; font-weight: normal;">(same file)</span></div>';
|
|
2035
|
+
|
|
2036
|
+
if (localCallers.length > 0) {
|
|
2037
|
+
html += '<div style="margin-bottom: 8px;">';
|
|
2038
|
+
html += '<div style="color: #58a6ff; font-size: 11px; margin-bottom: 4px;">Called by:</div>';
|
|
2039
|
+
html += '<div style="display: flex; flex-wrap: wrap; gap: 4px;">';
|
|
2040
|
+
localCallers.slice(0, 8).forEach(({ link, node }) => {
|
|
2041
|
+
html += `<span class="relationship-tag caller" onclick="focusNodeInTree('${link.source}')" title="${escapeHtml(node.name || '')}">${escapeHtml(node.name || node.id.substring(0, 8))}</span>`;
|
|
2042
|
+
});
|
|
2043
|
+
if (localCallers.length > 8) {
|
|
2044
|
+
html += `<span style="color: #8b949e; font-size: 10px;">+${localCallers.length - 8} more</span>`;
|
|
2045
|
+
}
|
|
2046
|
+
html += '</div></div>';
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
if (localCallees.length > 0) {
|
|
2050
|
+
html += '<div>';
|
|
2051
|
+
html += '<div style="color: #f0883e; font-size: 11px; margin-bottom: 4px;">Calls:</div>';
|
|
2052
|
+
html += '<div style="display: flex; flex-wrap: wrap; gap: 4px;">';
|
|
2053
|
+
localCallees.slice(0, 8).forEach(({ link, node }) => {
|
|
2054
|
+
html += `<span class="relationship-tag callee" onclick="focusNodeInTree('${link.target}')" title="${escapeHtml(node.name || '')}">${escapeHtml(node.name || node.id.substring(0, 8))}</span>`;
|
|
2055
|
+
});
|
|
2056
|
+
if (localCallees.length > 8) {
|
|
2057
|
+
html += `<span style="color: #8b949e; font-size: 10px;">+${localCallees.length - 8} more</span>`;
|
|
2058
|
+
}
|
|
2059
|
+
html += '</div></div>';
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
html += '</div>';
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// === Semantically Similar Section ===
|
|
2066
|
+
const semanticLinks = allLinks.filter(l => l.type === 'semantic' && l.source === chunkId);
|
|
2067
|
+
if (semanticLinks.length > 0) {
|
|
2068
|
+
sections.push({ id: 'semantic', label: '🧠 Semantically Similar' });
|
|
2069
|
+
html += '<div class="viewer-section" data-section="semantic">';
|
|
2070
|
+
html += '<div class="viewer-section-title">🧠 Semantically Similar</div>';
|
|
2071
|
+
html += '<div style="display: flex; flex-direction: column; gap: 4px;">';
|
|
2072
|
+
semanticLinks.slice(0, 5).forEach(link => {
|
|
2073
|
+
const similarNode = allNodes.find(n => n.id === link.target);
|
|
2074
|
+
if (similarNode) {
|
|
2075
|
+
const similarity = (link.similarity * 100).toFixed(0);
|
|
2076
|
+
const label = similarNode.name || similarNode.id.substring(0, 8);
|
|
2077
|
+
html += `<div class="semantic-item" onclick="focusNodeInTree('${link.target}')" title="${escapeHtml(similarNode.file_path || '')}">`;
|
|
2078
|
+
html += `<span class="semantic-score">${similarity}%</span>`;
|
|
2079
|
+
html += `<span class="semantic-name">${escapeHtml(label)}</span>`;
|
|
2080
|
+
html += `<span class="semantic-type">${similarNode.type || ''}</span>`;
|
|
2081
|
+
html += `</div>`;
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
html += '</div>';
|
|
2085
|
+
html += '</div>';
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
content.innerHTML = html;
|
|
2090
|
+
|
|
2091
|
+
// Apply syntax highlighting to code blocks
|
|
2092
|
+
content.querySelectorAll('pre code').forEach((block) => {
|
|
2093
|
+
if (typeof hljs !== 'undefined') {
|
|
2094
|
+
hljs.highlightElement(block);
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
// Populate section dropdown for navigation
|
|
2099
|
+
populateSectionDropdown(sections);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Focus on a node in the tree (expand path, scroll, highlight)
|
|
2103
|
+
function focusNodeInTree(nodeId) {
|
|
2104
|
+
console.log(`Focusing on node in tree: ${nodeId}`);
|
|
2105
|
+
|
|
2106
|
+
// Find the node in allNodes (the original data)
|
|
2107
|
+
const targetNodeData = allNodes.find(n => n.id === nodeId);
|
|
2108
|
+
if (!targetNodeData) {
|
|
2109
|
+
console.log(`Node ${nodeId} not found in allNodes`);
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// Find the path to this node in the tree structure
|
|
2114
|
+
// We need to find and expand all ancestors to make the node visible
|
|
2115
|
+
const pathToNode = findPathToNode(treeData, nodeId);
|
|
2116
|
+
|
|
2117
|
+
if (pathToNode.length > 0) {
|
|
2118
|
+
console.log(`Found path to node: ${pathToNode.map(n => n.name).join(' -> ')}`);
|
|
2119
|
+
|
|
2120
|
+
// Expand all nodes along the path (except the target node itself)
|
|
2121
|
+
pathToNode.slice(0, -1).forEach(node => {
|
|
2122
|
+
if (node._children) {
|
|
2123
|
+
// Node is collapsed, expand it
|
|
2124
|
+
console.log(`Expanding ${node.name} to reveal path`);
|
|
2125
|
+
node.children = node._children;
|
|
2126
|
+
node._children = null;
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
// Re-render the tree to show the expanded path
|
|
2131
|
+
renderVisualization();
|
|
2132
|
+
|
|
2133
|
+
// After render, scroll to and highlight the target node
|
|
2134
|
+
setTimeout(() => {
|
|
2135
|
+
highlightNodeInTree(nodeId);
|
|
2136
|
+
}, 100);
|
|
2137
|
+
} else {
|
|
2138
|
+
console.log(`Path to node ${nodeId} not found in tree - it may be orphaned`);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// Display the content in the viewer panel
|
|
2142
|
+
if (chunkTypes.includes(targetNodeData.type)) {
|
|
2143
|
+
displayChunkContent(targetNodeData);
|
|
2144
|
+
} else if (targetNodeData.type === 'file') {
|
|
2145
|
+
displayFileInfo(targetNodeData);
|
|
2146
|
+
} else if (targetNodeData.type === 'directory') {
|
|
2147
|
+
displayDirectoryInfo(targetNodeData);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// Find path from root to a specific node by ID
|
|
2152
|
+
function findPathToNode(node, targetId, path = []) {
|
|
2153
|
+
if (!node) return [];
|
|
2154
|
+
|
|
2155
|
+
// Add current node to path
|
|
2156
|
+
const currentPath = [...path, node];
|
|
2157
|
+
|
|
2158
|
+
// Check if this is the target
|
|
2159
|
+
if (node.id === targetId) {
|
|
2160
|
+
return currentPath;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// Check visible children
|
|
2164
|
+
if (node.children) {
|
|
2165
|
+
for (const child of node.children) {
|
|
2166
|
+
const result = findPathToNode(child, targetId, currentPath);
|
|
2167
|
+
if (result.length > 0) return result;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// Check hidden children
|
|
2172
|
+
if (node._children) {
|
|
2173
|
+
for (const child of node._children) {
|
|
2174
|
+
const result = findPathToNode(child, targetId, currentPath);
|
|
2175
|
+
if (result.length > 0) return result;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
return [];
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// Expand path to a node and highlight it (without triggering content display)
|
|
2183
|
+
function expandAndHighlightNode(nodeId) {
|
|
2184
|
+
console.log('=== EXPAND AND HIGHLIGHT ===');
|
|
2185
|
+
console.log('Target nodeId:', nodeId);
|
|
2186
|
+
|
|
2187
|
+
// Find the path to this node in the tree structure
|
|
2188
|
+
const pathToNode = findPathToNode(treeData, nodeId);
|
|
2189
|
+
|
|
2190
|
+
if (pathToNode.length > 0) {
|
|
2191
|
+
console.log('Found path:', pathToNode.map(n => n.name).join(' -> '));
|
|
2192
|
+
|
|
2193
|
+
// Expand all nodes along the path (except the target node itself)
|
|
2194
|
+
let needsRerender = false;
|
|
2195
|
+
pathToNode.slice(0, -1).forEach(node => {
|
|
2196
|
+
if (node._children) {
|
|
2197
|
+
console.log('Expanding:', node.name);
|
|
2198
|
+
node.children = node._children;
|
|
2199
|
+
node._children = null;
|
|
2200
|
+
needsRerender = true;
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
if (needsRerender) {
|
|
2205
|
+
renderVisualization();
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// Highlight after a short delay to allow render to complete
|
|
2209
|
+
setTimeout(() => {
|
|
2210
|
+
highlightNodeInTree(nodeId);
|
|
2211
|
+
}, 50);
|
|
2212
|
+
} else {
|
|
2213
|
+
console.log('Path not found - trying direct highlight');
|
|
2214
|
+
highlightNodeInTree(nodeId);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// Highlight and scroll to a node in the rendered tree
|
|
2219
|
+
function highlightNodeInTree(nodeId, persistent = true) {
|
|
2220
|
+
console.log('=== HIGHLIGHT NODE ===');
|
|
2221
|
+
console.log('Looking for nodeId:', nodeId);
|
|
2222
|
+
|
|
2223
|
+
// Remove any existing highlight
|
|
2224
|
+
d3.selectAll('.node-highlight').classed('node-highlight', false);
|
|
2225
|
+
if (persistent) {
|
|
2226
|
+
d3.selectAll('.node-selected').classed('node-selected', false);
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// Find and highlight the target node in the rendered SVG
|
|
2230
|
+
const svg = d3.select('#graph');
|
|
2231
|
+
const allNodes = svg.selectAll('.node');
|
|
2232
|
+
console.log('Total nodes in SVG:', allNodes.size());
|
|
2233
|
+
|
|
2234
|
+
const targetNode = allNodes.filter(d => d.data.id === nodeId);
|
|
2235
|
+
console.log('Matching nodes found:', targetNode.size());
|
|
2236
|
+
|
|
2237
|
+
if (!targetNode.empty()) {
|
|
2238
|
+
// Add highlight class (persistent = orange glow that stays)
|
|
2239
|
+
if (persistent) {
|
|
2240
|
+
targetNode.classed('node-selected', true);
|
|
2241
|
+
console.log('Applied .node-selected class');
|
|
2242
|
+
} else {
|
|
2243
|
+
targetNode.classed('node-highlight', true);
|
|
2244
|
+
console.log('Applied .node-highlight class');
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// Pulse the node circle - scale up from current size
|
|
2248
|
+
targetNode.select('circle')
|
|
2249
|
+
.transition()
|
|
2250
|
+
.duration(200)
|
|
2251
|
+
.attr('r', d => getNodeRadius(d) * 1.5) // Grow 50%
|
|
2252
|
+
.transition()
|
|
2253
|
+
.duration(200)
|
|
2254
|
+
.attr('r', d => getNodeRadius(d) * 0.8) // Shrink 20%
|
|
2255
|
+
.transition()
|
|
2256
|
+
.duration(200)
|
|
2257
|
+
.attr('r', d => getNodeRadius(d)); // Return to normal
|
|
2258
|
+
|
|
2259
|
+
// Get the node's position for scrolling
|
|
2260
|
+
const nodeTransform = targetNode.attr('transform');
|
|
2261
|
+
const match = nodeTransform.match(/translate\\(([^,]+),([^)]+)\\)/);
|
|
2262
|
+
if (match) {
|
|
2263
|
+
const x = parseFloat(match[1]);
|
|
2264
|
+
const y = parseFloat(match[2]);
|
|
2265
|
+
|
|
2266
|
+
// Pan the view to center on this node
|
|
2267
|
+
const { width, height } = getViewportDimensions();
|
|
2268
|
+
const zoom = d3.zoom().on('zoom', () => {}); // Get current zoom
|
|
2269
|
+
const svg = d3.select('#graph');
|
|
2270
|
+
|
|
2271
|
+
// Calculate center offset
|
|
2272
|
+
const centerX = width / 2 - x;
|
|
2273
|
+
const centerY = height / 2 - y;
|
|
2274
|
+
|
|
2275
|
+
// Apply smooth transition to center on node
|
|
2276
|
+
svg.transition()
|
|
2277
|
+
.duration(500)
|
|
2278
|
+
.call(
|
|
2279
|
+
d3.zoom().transform,
|
|
2280
|
+
d3.zoomIdentity.translate(centerX, centerY)
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
console.log(`Highlighted node ${nodeId}`);
|
|
2285
|
+
} else {
|
|
2286
|
+
console.log(`Node ${nodeId} not found in rendered tree`);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// Legacy function for backward compatibility
|
|
2291
|
+
function focusNode(nodeId) {
|
|
2292
|
+
focusNodeInTree(nodeId);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function getChunkIcon(chunkType) {
|
|
2296
|
+
const icons = {
|
|
2297
|
+
'function': '⚡',
|
|
2298
|
+
'class': '🏛️',
|
|
2299
|
+
'method': '🔧',
|
|
2300
|
+
'code': '📝',
|
|
2301
|
+
'import': '📦',
|
|
2302
|
+
'comment': '💬',
|
|
2303
|
+
'docstring': '📖'
|
|
2304
|
+
};
|
|
2305
|
+
return icons[chunkType] || '📝';
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function escapeHtml(text) {
|
|
2309
|
+
const div = document.createElement('div');
|
|
2310
|
+
div.textContent = text;
|
|
2311
|
+
return div.innerHTML;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// Get language class for highlight.js based on file extension
|
|
2315
|
+
function getLanguageClass(filePath) {
|
|
2316
|
+
if (!filePath) return '';
|
|
2317
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
2318
|
+
const langMap = {
|
|
2319
|
+
'py': 'python',
|
|
2320
|
+
'js': 'javascript',
|
|
2321
|
+
'ts': 'typescript',
|
|
2322
|
+
'tsx': 'typescript',
|
|
2323
|
+
'jsx': 'javascript',
|
|
2324
|
+
'java': 'java',
|
|
2325
|
+
'go': 'go',
|
|
2326
|
+
'rs': 'rust',
|
|
2327
|
+
'rb': 'ruby',
|
|
2328
|
+
'php': 'php',
|
|
2329
|
+
'c': 'c',
|
|
2330
|
+
'cpp': 'cpp',
|
|
2331
|
+
'cc': 'cpp',
|
|
2332
|
+
'h': 'c',
|
|
2333
|
+
'hpp': 'cpp',
|
|
2334
|
+
'cs': 'csharp',
|
|
2335
|
+
'swift': 'swift',
|
|
2336
|
+
'kt': 'kotlin',
|
|
2337
|
+
'scala': 'scala',
|
|
2338
|
+
'sh': 'bash',
|
|
2339
|
+
'bash': 'bash',
|
|
2340
|
+
'zsh': 'bash',
|
|
2341
|
+
'sql': 'sql',
|
|
2342
|
+
'html': 'html',
|
|
2343
|
+
'htm': 'html',
|
|
2344
|
+
'css': 'css',
|
|
2345
|
+
'scss': 'scss',
|
|
2346
|
+
'less': 'less',
|
|
2347
|
+
'json': 'json',
|
|
2348
|
+
'yaml': 'yaml',
|
|
2349
|
+
'yml': 'yaml',
|
|
2350
|
+
'xml': 'xml',
|
|
2351
|
+
'md': 'markdown',
|
|
2352
|
+
'markdown': 'markdown',
|
|
2353
|
+
'toml': 'ini',
|
|
2354
|
+
'ini': 'ini',
|
|
2355
|
+
'cfg': 'ini',
|
|
2356
|
+
'lua': 'lua',
|
|
2357
|
+
'r': 'r',
|
|
2358
|
+
'dart': 'dart',
|
|
2359
|
+
'ex': 'elixir',
|
|
2360
|
+
'exs': 'elixir',
|
|
2361
|
+
'erl': 'erlang',
|
|
2362
|
+
'hs': 'haskell',
|
|
2363
|
+
'clj': 'clojure',
|
|
2364
|
+
'vim': 'vim',
|
|
2365
|
+
'dockerfile': 'dockerfile'
|
|
2366
|
+
};
|
|
2367
|
+
return langMap[ext] || '';
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// ============================================================================
|
|
2371
|
+
// LAYOUT TOGGLE
|
|
2372
|
+
// ============================================================================
|
|
2373
|
+
|
|
2374
|
+
function toggleCallLines(show) {
|
|
2375
|
+
showCallLines = show;
|
|
2376
|
+
const lineGroup = d3.select('.external-lines-group');
|
|
2377
|
+
if (!lineGroup.empty()) {
|
|
2378
|
+
lineGroup.style('display', show ? 'block' : 'none');
|
|
2379
|
+
}
|
|
2380
|
+
console.log(`Call lines ${show ? 'shown' : 'hidden'}`);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function toggleLayout() {
|
|
2384
|
+
const toggleCheckbox = document.getElementById('layout-toggle');
|
|
2385
|
+
const labels = document.querySelectorAll('.toggle-label');
|
|
2386
|
+
|
|
2387
|
+
// Update layout based on checkbox state
|
|
2388
|
+
currentLayout = toggleCheckbox.checked ? 'circular' : 'linear';
|
|
2389
|
+
|
|
2390
|
+
// Update label highlighting
|
|
2391
|
+
labels.forEach((label, index) => {
|
|
2392
|
+
if (index === 0) {
|
|
2393
|
+
// Linear label (left)
|
|
2394
|
+
label.classList.toggle('active', currentLayout === 'linear');
|
|
2395
|
+
} else {
|
|
2396
|
+
// Circular label (right)
|
|
2397
|
+
label.classList.toggle('active', currentLayout === 'circular');
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
console.log(`Layout switched to: ${currentLayout}`);
|
|
2402
|
+
renderVisualization();
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
// ============================================================================
|
|
2406
|
+
// FILE TYPE FILTER
|
|
2407
|
+
// ============================================================================
|
|
2408
|
+
|
|
2409
|
+
// Code file extensions
|
|
2410
|
+
const codeExtensions = new Set([
|
|
2411
|
+
'.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.c', '.cpp', '.h', '.hpp',
|
|
2412
|
+
'.cs', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.scala', '.r',
|
|
2413
|
+
'.sh', '.bash', '.zsh', '.ps1', '.bat', '.sql', '.html', '.css', '.scss',
|
|
2414
|
+
'.sass', '.less', '.vue', '.svelte', '.astro', '.elm', '.clj', '.ex', '.exs',
|
|
2415
|
+
'.hs', '.ml', '.lua', '.pl', '.pm', '.m', '.mm', '.f', '.f90', '.for',
|
|
2416
|
+
'.asm', '.s', '.v', '.vhd', '.sv', '.nim', '.zig', '.d', '.dart', '.groovy',
|
|
2417
|
+
'.coffee', '.litcoffee', '.purs', '.rkt', '.scm', '.lisp', '.cl'
|
|
2418
|
+
]);
|
|
2419
|
+
|
|
2420
|
+
// Doc file extensions
|
|
2421
|
+
const docExtensions = new Set([
|
|
2422
|
+
'.md', '.markdown', '.rst', '.txt', '.adoc', '.asciidoc', '.org', '.tex',
|
|
2423
|
+
'.rtf', '.doc', '.docx', '.pdf', '.json', '.yaml', '.yml', '.toml', '.ini',
|
|
2424
|
+
'.cfg', '.conf', '.xml', '.csv', '.tsv', '.log', '.man', '.info', '.pod',
|
|
2425
|
+
'.rdoc', '.textile', '.wiki'
|
|
2426
|
+
]);
|
|
2427
|
+
|
|
2428
|
+
function getFileType(filename) {
|
|
2429
|
+
if (!filename) return 'unknown';
|
|
2430
|
+
const ext = '.' + filename.split('.').pop().toLowerCase();
|
|
2431
|
+
if (codeExtensions.has(ext)) return 'code';
|
|
2432
|
+
if (docExtensions.has(ext)) return 'docs';
|
|
2433
|
+
return 'unknown';
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
function setFileFilter(filter) {
|
|
2437
|
+
currentFileFilter = filter;
|
|
2438
|
+
|
|
2439
|
+
// Update button states
|
|
2440
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
2441
|
+
btn.classList.toggle('active', btn.dataset.filter === filter);
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
// Apply filter to the tree
|
|
2445
|
+
applyFileFilter();
|
|
2446
|
+
|
|
2447
|
+
console.log(`File filter set to: ${filter}`);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
function applyFileFilter() {
|
|
2451
|
+
if (!treeData) return;
|
|
2452
|
+
|
|
2453
|
+
console.log('=== APPLYING FILE FILTER (VISIBILITY) ===');
|
|
2454
|
+
console.log('Current filter:', currentFileFilter);
|
|
2455
|
+
|
|
2456
|
+
// Get all node elements in the visualization
|
|
2457
|
+
const nodeElements = d3.selectAll('.node');
|
|
2458
|
+
const linkElements = d3.selectAll('.link');
|
|
2459
|
+
|
|
2460
|
+
if (currentFileFilter === 'all') {
|
|
2461
|
+
// Show everything
|
|
2462
|
+
nodeElements.style('display', null);
|
|
2463
|
+
linkElements.style('display', null);
|
|
2464
|
+
console.log('Showing all nodes');
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// Build set of visible node IDs based on filter
|
|
2469
|
+
const visibleIds = new Set();
|
|
2470
|
+
|
|
2471
|
+
// Recursive function to check if a node or any descendant matches filter
|
|
2472
|
+
function checkNodeAndDescendants(node) {
|
|
2473
|
+
let hasMatchingDescendant = false;
|
|
2474
|
+
|
|
2475
|
+
// Check children (both visible and collapsed)
|
|
2476
|
+
const children = node.children || node._children || [];
|
|
2477
|
+
children.forEach(child => {
|
|
2478
|
+
if (checkNodeAndDescendants(child)) {
|
|
2479
|
+
hasMatchingDescendant = true;
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
|
|
2483
|
+
// Check if this node itself matches
|
|
2484
|
+
let matches = false;
|
|
2485
|
+
if (node.type === 'directory') {
|
|
2486
|
+
// Directories are visible if they have matching descendants
|
|
2487
|
+
matches = hasMatchingDescendant;
|
|
2488
|
+
} else if (node.type === 'file') {
|
|
2489
|
+
const fileType = getFileType(node.name);
|
|
2490
|
+
matches = (fileType === currentFileFilter) || (fileType === 'unknown');
|
|
2491
|
+
} else if (chunkTypes.includes(node.type)) {
|
|
2492
|
+
// Chunks match if their parent file matches
|
|
2493
|
+
// For simplicity, check file_path extension
|
|
2494
|
+
if (node.file_path) {
|
|
2495
|
+
const ext = node.file_path.substring(node.file_path.lastIndexOf('.'));
|
|
2496
|
+
const fileType = getFileType(node.file_path);
|
|
2497
|
+
matches = (fileType === currentFileFilter) || (fileType === 'unknown');
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
if (matches || hasMatchingDescendant) {
|
|
2502
|
+
visibleIds.add(node.id);
|
|
2503
|
+
return true;
|
|
2504
|
+
}
|
|
2505
|
+
return false;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
checkNodeAndDescendants(treeData);
|
|
2509
|
+
console.log(`Filter found ${visibleIds.size} visible nodes`);
|
|
2510
|
+
|
|
2511
|
+
// Apply visibility to DOM elements
|
|
2512
|
+
nodeElements.style('display', function(d) {
|
|
2513
|
+
return visibleIds.has(d.data.id) ? null : 'none';
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
// Hide links where either end is hidden
|
|
2517
|
+
linkElements.style('display', function(d) {
|
|
2518
|
+
const sourceVisible = visibleIds.has(d.source.data.id);
|
|
2519
|
+
const targetVisible = visibleIds.has(d.target.data.id);
|
|
2520
|
+
return (sourceVisible && targetVisible) ? null : 'none';
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
console.log('=== FILTER COMPLETE (VISIBILITY) ===');
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// ============================================================================
|
|
2527
|
+
// VIEWER PANEL CONTROLS
|
|
2528
|
+
// ============================================================================
|
|
2529
|
+
|
|
2530
|
+
let isViewerExpanded = false;
|
|
2531
|
+
|
|
2532
|
+
function openViewerPanel() {
|
|
2533
|
+
const panel = document.getElementById('viewer-panel');
|
|
2534
|
+
const container = document.getElementById('main-container');
|
|
2535
|
+
|
|
2536
|
+
if (!isViewerOpen) {
|
|
2537
|
+
panel.classList.add('open');
|
|
2538
|
+
container.classList.add('viewer-open');
|
|
2539
|
+
isViewerOpen = true;
|
|
2540
|
+
|
|
2541
|
+
// Re-render visualization to adjust to new viewport size
|
|
2542
|
+
setTimeout(() => {
|
|
2543
|
+
renderVisualization();
|
|
2544
|
+
}, 300); // Wait for transition
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
function closeViewerPanel() {
|
|
2549
|
+
const panel = document.getElementById('viewer-panel');
|
|
2550
|
+
const container = document.getElementById('main-container');
|
|
2551
|
+
|
|
2552
|
+
panel.classList.remove('open');
|
|
2553
|
+
panel.classList.remove('expanded');
|
|
2554
|
+
container.classList.remove('viewer-open');
|
|
2555
|
+
isViewerOpen = false;
|
|
2556
|
+
isViewerExpanded = false;
|
|
2557
|
+
|
|
2558
|
+
// Update icon
|
|
2559
|
+
const icon = document.getElementById('expand-icon');
|
|
2560
|
+
if (icon) icon.textContent = '⬅';
|
|
2561
|
+
|
|
2562
|
+
// Re-render visualization to adjust to new viewport size
|
|
2563
|
+
setTimeout(() => {
|
|
2564
|
+
renderVisualization();
|
|
2565
|
+
}, 300); // Wait for transition
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
function toggleViewerExpand() {
|
|
2569
|
+
const panel = document.getElementById('viewer-panel');
|
|
2570
|
+
const icon = document.getElementById('expand-icon');
|
|
2571
|
+
|
|
2572
|
+
isViewerExpanded = !isViewerExpanded;
|
|
2573
|
+
|
|
2574
|
+
if (isViewerExpanded) {
|
|
2575
|
+
panel.classList.add('expanded');
|
|
2576
|
+
if (icon) icon.textContent = '➡';
|
|
2577
|
+
} else {
|
|
2578
|
+
panel.classList.remove('expanded');
|
|
2579
|
+
if (icon) icon.textContent = '⬅';
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// Don't re-render graph on expand - only affects panel width
|
|
2583
|
+
// Graph will adjust on close via closeViewerPanel()
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
function jumpToSection(sectionId) {
|
|
2587
|
+
if (!sectionId) return;
|
|
2588
|
+
|
|
2589
|
+
const viewerContent = document.getElementById('viewer-content');
|
|
2590
|
+
const sectionElement = viewerContent.querySelector(`[data-section="${sectionId}"]`);
|
|
2591
|
+
|
|
2592
|
+
if (sectionElement) {
|
|
2593
|
+
sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
2594
|
+
|
|
2595
|
+
// Briefly highlight the section
|
|
2596
|
+
sectionElement.style.transition = 'background-color 0.3s';
|
|
2597
|
+
sectionElement.style.backgroundColor = 'rgba(88, 166, 255, 0.15)';
|
|
2598
|
+
setTimeout(() => {
|
|
2599
|
+
sectionElement.style.backgroundColor = '';
|
|
2600
|
+
}, 1000);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// Reset dropdown to default
|
|
2604
|
+
const dropdown = document.getElementById('section-dropdown');
|
|
2605
|
+
if (dropdown) dropdown.value = '';
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
function populateSectionDropdown(sections) {
|
|
2609
|
+
const dropdown = document.getElementById('section-dropdown');
|
|
2610
|
+
if (!dropdown) return;
|
|
2611
|
+
|
|
2612
|
+
// Clear existing options except the first one
|
|
2613
|
+
dropdown.innerHTML = '<option value="">Jump to section...</option>';
|
|
2614
|
+
|
|
2615
|
+
// Add section options
|
|
2616
|
+
sections.forEach(section => {
|
|
2617
|
+
const option = document.createElement('option');
|
|
2618
|
+
option.value = section.id;
|
|
2619
|
+
option.textContent = section.label;
|
|
2620
|
+
dropdown.appendChild(option);
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2623
|
+
// Show/hide dropdown based on whether we have sections
|
|
2624
|
+
const sectionNav = document.getElementById('section-nav');
|
|
2625
|
+
if (sectionNav) {
|
|
2626
|
+
sectionNav.style.display = sections.length > 1 ? 'block' : 'none';
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// ============================================================================
|
|
2631
|
+
// NAVIGATION FUNCTIONS
|
|
2632
|
+
// ============================================================================
|
|
2633
|
+
|
|
2634
|
+
function addToNavHistory(item) {
|
|
2635
|
+
// Remove any forward history when adding new item
|
|
2636
|
+
if (navigationIndex < navigationHistory.length - 1) {
|
|
2637
|
+
navigationHistory = navigationHistory.slice(0, navigationIndex + 1);
|
|
2638
|
+
}
|
|
2639
|
+
navigationHistory.push(item);
|
|
2640
|
+
navigationIndex = navigationHistory.length - 1;
|
|
2641
|
+
console.log(`Navigation history: ${navigationHistory.length} items, index: ${navigationIndex}`);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
function goBack() {
|
|
2645
|
+
if (navigationIndex > 0) {
|
|
2646
|
+
navigationIndex--;
|
|
2647
|
+
const item = navigationHistory[navigationIndex];
|
|
2648
|
+
console.log(`Going back to: ${item.type} - ${item.data.name}`);
|
|
2649
|
+
if (item.type === 'directory') {
|
|
2650
|
+
displayDirectoryInfo(item.data, false); // false = don't add to history
|
|
2651
|
+
focusNodeInTree(item.data.id);
|
|
2652
|
+
} else if (item.type === 'file') {
|
|
2653
|
+
displayFileInfo(item.data, false); // false = don't add to history
|
|
2654
|
+
focusNodeInTree(item.data.id);
|
|
2655
|
+
} else if (item.type === 'chunk') {
|
|
2656
|
+
displayChunkContent(item.data, false); // false = don't add to history
|
|
2657
|
+
focusNodeInTree(item.data.id);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function goForward() {
|
|
2663
|
+
if (navigationIndex < navigationHistory.length - 1) {
|
|
2664
|
+
navigationIndex++;
|
|
2665
|
+
const item = navigationHistory[navigationIndex];
|
|
2666
|
+
console.log(`Going forward to: ${item.type} - ${item.data.name}`);
|
|
2667
|
+
if (item.type === 'directory') {
|
|
2668
|
+
displayDirectoryInfo(item.data, false); // false = don't add to history
|
|
2669
|
+
focusNodeInTree(item.data.id);
|
|
2670
|
+
} else if (item.type === 'file') {
|
|
2671
|
+
displayFileInfo(item.data, false); // false = don't add to history
|
|
2672
|
+
focusNodeInTree(item.data.id);
|
|
2673
|
+
} else if (item.type === 'chunk') {
|
|
2674
|
+
displayChunkContent(item.data, false); // false = don't add to history
|
|
2675
|
+
focusNodeInTree(item.data.id);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
function navigateToDirectory(dirData) {
|
|
2681
|
+
console.log(`Navigating to directory: ${dirData.name}`);
|
|
2682
|
+
// Focus on the node in the tree (expand path and highlight)
|
|
2683
|
+
focusNodeInTree(dirData.id);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
function navigateToFile(fileData) {
|
|
2687
|
+
console.log(`Navigating to file: ${fileData.name}`);
|
|
2688
|
+
// Focus on the node in the tree (expand path and highlight)
|
|
2689
|
+
focusNodeInTree(fileData.id);
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
// Navigate to a file by its file path (used when clicking on file paths in chunk metadata)
|
|
2693
|
+
function navigateToFileByPath(filePath) {
|
|
2694
|
+
console.log(`Navigating to file by path: ${filePath}`);
|
|
2695
|
+
|
|
2696
|
+
// Find the file node in allNodes that matches this path
|
|
2697
|
+
const fileNode = allNodes.find(n => {
|
|
2698
|
+
if (n.type !== 'file') return false;
|
|
2699
|
+
// Match against various path properties
|
|
2700
|
+
return n.path === filePath ||
|
|
2701
|
+
n.file_path === filePath ||
|
|
2702
|
+
(n.path && n.path.endsWith(filePath)) ||
|
|
2703
|
+
(n.file_path && n.file_path.endsWith(filePath)) ||
|
|
2704
|
+
filePath.endsWith(n.name);
|
|
2705
|
+
});
|
|
2706
|
+
|
|
2707
|
+
if (fileNode) {
|
|
2708
|
+
console.log(`Found file node: ${fileNode.name} (id: ${fileNode.id})`);
|
|
2709
|
+
focusNodeInTree(fileNode.id);
|
|
2710
|
+
} else {
|
|
2711
|
+
console.log(`File node not found for path: ${filePath}`);
|
|
2712
|
+
// Try to find by just the filename
|
|
2713
|
+
const fileName = filePath.split('/').pop();
|
|
2714
|
+
const fileByName = allNodes.find(n => n.type === 'file' && n.name === fileName);
|
|
2715
|
+
if (fileByName) {
|
|
2716
|
+
console.log(`Found file by name: ${fileByName.name}`);
|
|
2717
|
+
focusNodeInTree(fileByName.id);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
function renderNavigationBar(currentItem) {
|
|
2723
|
+
let html = '<div class="navigation-bar">';
|
|
2724
|
+
|
|
2725
|
+
// Back/Forward buttons
|
|
2726
|
+
const canGoBack = navigationIndex > 0;
|
|
2727
|
+
const canGoForward = navigationIndex < navigationHistory.length - 1;
|
|
2728
|
+
|
|
2729
|
+
html += `<button class="nav-btn ${canGoBack ? '' : 'disabled'}" onclick="goBack()" ${canGoBack ? '' : 'disabled'} title="Go Back">←</button>`;
|
|
2730
|
+
html += `<button class="nav-btn ${canGoForward ? '' : 'disabled'}" onclick="goForward()" ${canGoForward ? '' : 'disabled'} title="Go Forward">→</button>`;
|
|
2731
|
+
|
|
2732
|
+
// Breadcrumb trail
|
|
2733
|
+
html += '<div class="breadcrumb-trail">';
|
|
2734
|
+
|
|
2735
|
+
// Build breadcrumb from path
|
|
2736
|
+
if (currentItem && currentItem.id) {
|
|
2737
|
+
const path = findPathToNode(treeData, currentItem.id);
|
|
2738
|
+
path.forEach((node, index) => {
|
|
2739
|
+
const isLast = index === path.length - 1;
|
|
2740
|
+
const clickable = !isLast;
|
|
2741
|
+
|
|
2742
|
+
if (index > 0) {
|
|
2743
|
+
html += '<span class="breadcrumb-separator">/</span>';
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
if (clickable) {
|
|
2747
|
+
html += `<span class="breadcrumb-item clickable" onclick="focusNodeInTree('${node.id}')">${escapeHtml(node.name)}</span>`;
|
|
2748
|
+
} else {
|
|
2749
|
+
html += `<span class="breadcrumb-item current">${escapeHtml(node.name)}</span>`;
|
|
2750
|
+
}
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
html += '</div>';
|
|
2755
|
+
html += '</div>';
|
|
2756
|
+
|
|
2757
|
+
return html;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
// ============================================================================
|
|
2761
|
+
// SEARCH FUNCTIONALITY
|
|
2762
|
+
// ============================================================================
|
|
2763
|
+
|
|
2764
|
+
let searchDebounceTimer = null;
|
|
2765
|
+
let searchResults = [];
|
|
2766
|
+
let selectedSearchIndex = -1;
|
|
2767
|
+
|
|
2768
|
+
function handleSearchInput(event) {
|
|
2769
|
+
const query = event.target.value.trim();
|
|
2770
|
+
|
|
2771
|
+
// Debounce search - wait 150ms after typing stops
|
|
2772
|
+
clearTimeout(searchDebounceTimer);
|
|
2773
|
+
searchDebounceTimer = setTimeout(() => {
|
|
2774
|
+
performSearch(query);
|
|
2775
|
+
}, 150);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
function handleSearchKeydown(event) {
|
|
2779
|
+
const resultsContainer = document.getElementById('search-results');
|
|
2780
|
+
|
|
2781
|
+
switch(event.key) {
|
|
2782
|
+
case 'ArrowDown':
|
|
2783
|
+
event.preventDefault();
|
|
2784
|
+
if (searchResults.length > 0) {
|
|
2785
|
+
selectedSearchIndex = Math.min(selectedSearchIndex + 1, searchResults.length - 1);
|
|
2786
|
+
updateSearchSelection();
|
|
2787
|
+
}
|
|
2788
|
+
break;
|
|
2789
|
+
case 'ArrowUp':
|
|
2790
|
+
event.preventDefault();
|
|
2791
|
+
if (searchResults.length > 0) {
|
|
2792
|
+
selectedSearchIndex = Math.max(selectedSearchIndex - 1, 0);
|
|
2793
|
+
updateSearchSelection();
|
|
2794
|
+
}
|
|
2795
|
+
break;
|
|
2796
|
+
case 'Enter':
|
|
2797
|
+
event.preventDefault();
|
|
2798
|
+
if (selectedSearchIndex >= 0 && selectedSearchIndex < searchResults.length) {
|
|
2799
|
+
selectSearchResult(searchResults[selectedSearchIndex]);
|
|
2800
|
+
}
|
|
2801
|
+
break;
|
|
2802
|
+
case 'Escape':
|
|
2803
|
+
closeSearchResults();
|
|
2804
|
+
document.getElementById('search-input').blur();
|
|
2805
|
+
break;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
function performSearch(query) {
|
|
2810
|
+
const resultsContainer = document.getElementById('search-results');
|
|
2811
|
+
|
|
2812
|
+
if (!query || query.length < 2) {
|
|
2813
|
+
closeSearchResults();
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
const lowerQuery = query.toLowerCase();
|
|
2818
|
+
|
|
2819
|
+
// Search through all nodes (directories, files, and chunks)
|
|
2820
|
+
searchResults = allNodes
|
|
2821
|
+
.filter(node => {
|
|
2822
|
+
// Match against name
|
|
2823
|
+
const nameMatch = node.name && node.name.toLowerCase().includes(lowerQuery);
|
|
2824
|
+
// Match against file path
|
|
2825
|
+
const pathMatch = node.file_path && node.file_path.toLowerCase().includes(lowerQuery);
|
|
2826
|
+
// Match against ID (useful for finding specific chunks)
|
|
2827
|
+
const idMatch = node.id && node.id.toLowerCase().includes(lowerQuery);
|
|
2828
|
+
return nameMatch || pathMatch || idMatch;
|
|
2829
|
+
})
|
|
2830
|
+
.slice(0, 20); // Limit to 20 results
|
|
2831
|
+
|
|
2832
|
+
// Sort results: exact matches first, then by type priority
|
|
2833
|
+
const typePriority = { 'directory': 1, 'file': 2, 'class': 3, 'function': 4, 'method': 5 };
|
|
2834
|
+
searchResults.sort((a, b) => {
|
|
2835
|
+
// Exact name match gets highest priority
|
|
2836
|
+
const aExact = a.name && a.name.toLowerCase() === lowerQuery ? 0 : 1;
|
|
2837
|
+
const bExact = b.name && b.name.toLowerCase() === lowerQuery ? 0 : 1;
|
|
2838
|
+
if (aExact !== bExact) return aExact - bExact;
|
|
2839
|
+
|
|
2840
|
+
// Then sort by type priority
|
|
2841
|
+
const aPriority = typePriority[a.type] || 10;
|
|
2842
|
+
const bPriority = typePriority[b.type] || 10;
|
|
2843
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
2844
|
+
|
|
2845
|
+
// Finally sort alphabetically
|
|
2846
|
+
return (a.name || '').localeCompare(b.name || '');
|
|
2847
|
+
});
|
|
2848
|
+
|
|
2849
|
+
selectedSearchIndex = searchResults.length > 0 ? 0 : -1;
|
|
2850
|
+
renderSearchResults(query);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
function renderSearchResults(query) {
|
|
2854
|
+
const resultsContainer = document.getElementById('search-results');
|
|
2855
|
+
|
|
2856
|
+
if (searchResults.length === 0) {
|
|
2857
|
+
resultsContainer.innerHTML = '<div class="search-no-results">No results found</div>';
|
|
2858
|
+
resultsContainer.classList.add('visible');
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
let html = '';
|
|
2863
|
+
|
|
2864
|
+
searchResults.forEach((node, index) => {
|
|
2865
|
+
const icon = getSearchResultIcon(node.type);
|
|
2866
|
+
const name = highlightMatch(node.name || node.id.substring(0, 20), query);
|
|
2867
|
+
const path = node.file_path ? node.file_path.split('/').slice(-3).join('/') : '';
|
|
2868
|
+
const type = node.type || 'unknown';
|
|
2869
|
+
const selected = index === selectedSearchIndex ? 'selected' : '';
|
|
2870
|
+
|
|
2871
|
+
html += `<div class="search-result-item ${selected}"
|
|
2872
|
+
data-index="${index}"
|
|
2873
|
+
onclick="selectSearchResultByIndex(${index})"
|
|
2874
|
+
onmouseenter="hoverSearchResult(${index})">`;
|
|
2875
|
+
html += `<span class="search-result-icon">${icon}</span>`;
|
|
2876
|
+
html += `<div class="search-result-info">`;
|
|
2877
|
+
html += `<div class="search-result-name">${name}</div>`;
|
|
2878
|
+
if (path) {
|
|
2879
|
+
html += `<div class="search-result-path">${escapeHtml(path)}</div>`;
|
|
2880
|
+
}
|
|
2881
|
+
html += `</div>`;
|
|
2882
|
+
html += `<span class="search-result-type">${type}</span>`;
|
|
2883
|
+
html += `</div>`;
|
|
2884
|
+
});
|
|
2885
|
+
|
|
2886
|
+
html += '<div class="search-hint">↑↓ Navigate • Enter Select • Esc Close</div>';
|
|
2887
|
+
|
|
2888
|
+
resultsContainer.innerHTML = html;
|
|
2889
|
+
resultsContainer.classList.add('visible');
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function getSearchResultIcon(type) {
|
|
2893
|
+
const icons = {
|
|
2894
|
+
'directory': '📁',
|
|
2895
|
+
'file': '📄',
|
|
2896
|
+
'function': '⚡',
|
|
2897
|
+
'class': '🏛️',
|
|
2898
|
+
'method': '🔧',
|
|
2899
|
+
'module': '📦',
|
|
2900
|
+
'imports': '📦',
|
|
2901
|
+
'text': '📝',
|
|
2902
|
+
'code': '📝'
|
|
2903
|
+
};
|
|
2904
|
+
return icons[type] || '📄';
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
function highlightMatch(text, query) {
|
|
2908
|
+
if (!text || !query) return escapeHtml(text || '');
|
|
2909
|
+
|
|
2910
|
+
const lowerText = text.toLowerCase();
|
|
2911
|
+
const lowerQuery = query.toLowerCase();
|
|
2912
|
+
const index = lowerText.indexOf(lowerQuery);
|
|
2913
|
+
|
|
2914
|
+
if (index === -1) return escapeHtml(text);
|
|
2915
|
+
|
|
2916
|
+
const before = text.substring(0, index);
|
|
2917
|
+
const match = text.substring(index, index + query.length);
|
|
2918
|
+
const after = text.substring(index + query.length);
|
|
2919
|
+
|
|
2920
|
+
return escapeHtml(before) + '<mark>' + escapeHtml(match) + '</mark>' + escapeHtml(after);
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
function updateSearchSelection() {
|
|
2924
|
+
const items = document.querySelectorAll('.search-result-item');
|
|
2925
|
+
items.forEach((item, index) => {
|
|
2926
|
+
item.classList.toggle('selected', index === selectedSearchIndex);
|
|
2927
|
+
});
|
|
2928
|
+
|
|
2929
|
+
// Scroll selected item into view
|
|
2930
|
+
const selected = items[selectedSearchIndex];
|
|
2931
|
+
if (selected) {
|
|
2932
|
+
selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
function hoverSearchResult(index) {
|
|
2937
|
+
selectedSearchIndex = index;
|
|
2938
|
+
updateSearchSelection();
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
function selectSearchResultByIndex(index) {
|
|
2942
|
+
if (index >= 0 && index < searchResults.length) {
|
|
2943
|
+
selectSearchResult(searchResults[index]);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
function selectSearchResult(node) {
|
|
2948
|
+
console.log(`Search selected: ${node.name} (${node.type})`);
|
|
2949
|
+
|
|
2950
|
+
// Close search dropdown
|
|
2951
|
+
closeSearchResults();
|
|
2952
|
+
|
|
2953
|
+
// Clear input
|
|
2954
|
+
document.getElementById('search-input').value = '';
|
|
2955
|
+
|
|
2956
|
+
// Focus on the node in the tree
|
|
2957
|
+
focusNodeInTree(node.id);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
function closeSearchResults() {
|
|
2961
|
+
const resultsContainer = document.getElementById('search-results');
|
|
2962
|
+
resultsContainer.classList.remove('visible');
|
|
2963
|
+
searchResults = [];
|
|
2964
|
+
selectedSearchIndex = -1;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// ============================================================================
|
|
2968
|
+
// THEME TOGGLE
|
|
2969
|
+
// ============================================================================
|
|
2970
|
+
|
|
2971
|
+
function toggleTheme() {
|
|
2972
|
+
const html = document.documentElement;
|
|
2973
|
+
const currentTheme = html.getAttribute('data-theme') || 'dark';
|
|
2974
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
2975
|
+
|
|
2976
|
+
// Update theme
|
|
2977
|
+
if (newTheme === 'light') {
|
|
2978
|
+
html.setAttribute('data-theme', 'light');
|
|
2979
|
+
} else {
|
|
2980
|
+
html.removeAttribute('data-theme');
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// Save preference
|
|
2984
|
+
localStorage.setItem('theme', newTheme);
|
|
2985
|
+
|
|
2986
|
+
// Update icon only
|
|
2987
|
+
const themeIcon = document.getElementById('theme-icon');
|
|
2988
|
+
|
|
2989
|
+
if (newTheme === 'light') {
|
|
2990
|
+
themeIcon.textContent = '☀️';
|
|
2991
|
+
} else {
|
|
2992
|
+
themeIcon.textContent = '🌙';
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
console.log(`Theme toggled to: ${newTheme}`);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
function loadThemePreference() {
|
|
2999
|
+
const savedTheme = localStorage.getItem('theme') || 'dark';
|
|
3000
|
+
const html = document.documentElement;
|
|
3001
|
+
|
|
3002
|
+
if (savedTheme === 'light') {
|
|
3003
|
+
html.setAttribute('data-theme', 'light');
|
|
3004
|
+
const themeIcon = document.getElementById('theme-icon');
|
|
3005
|
+
if (themeIcon) themeIcon.textContent = '☀️';
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
console.log(`Loaded theme preference: ${savedTheme}`);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// ============================================================================
|
|
3012
|
+
// ANALYSIS REPORTS
|
|
3013
|
+
// ============================================================================
|
|
3014
|
+
|
|
3015
|
+
function getComplexityGrade(complexity) {
|
|
3016
|
+
if (complexity === undefined || complexity === null) return 'N/A';
|
|
3017
|
+
if (complexity <= 5) return 'A';
|
|
3018
|
+
if (complexity <= 10) return 'B';
|
|
3019
|
+
if (complexity <= 15) return 'C';
|
|
3020
|
+
if (complexity <= 20) return 'D';
|
|
3021
|
+
return 'F';
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
function getGradeColor(grade) {
|
|
3025
|
+
const colors = {
|
|
3026
|
+
'A': '#2ea043',
|
|
3027
|
+
'B': '#1f6feb',
|
|
3028
|
+
'C': '#d29922',
|
|
3029
|
+
'D': '#f0883e',
|
|
3030
|
+
'F': '#da3633',
|
|
3031
|
+
'N/A': '#6e7681'
|
|
3032
|
+
};
|
|
3033
|
+
return colors[grade] || colors['N/A'];
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
function showComplexityReport() {
|
|
3037
|
+
openViewerPanel();
|
|
3038
|
+
|
|
3039
|
+
const viewerTitle = document.getElementById('viewer-title');
|
|
3040
|
+
const viewerContent = document.getElementById('viewer-content');
|
|
3041
|
+
|
|
3042
|
+
viewerTitle.textContent = '📊 Complexity Report';
|
|
3043
|
+
|
|
3044
|
+
// Collect all chunk nodes with complexity data
|
|
3045
|
+
const chunksWithComplexity = [];
|
|
3046
|
+
|
|
3047
|
+
function collectChunks(node) {
|
|
3048
|
+
if (chunkTypes.includes(node.type)) {
|
|
3049
|
+
const complexity = node.complexity !== undefined ? node.complexity : null;
|
|
3050
|
+
chunksWithComplexity.push({
|
|
3051
|
+
name: node.name,
|
|
3052
|
+
type: node.type,
|
|
3053
|
+
file_path: node.file_path || 'Unknown',
|
|
3054
|
+
start_line: node.start_line || 0,
|
|
3055
|
+
end_line: node.end_line || 0,
|
|
3056
|
+
complexity: complexity,
|
|
3057
|
+
grade: getComplexityGrade(complexity),
|
|
3058
|
+
node: node
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
// Recursively process children
|
|
3063
|
+
const children = node.children || node._children || [];
|
|
3064
|
+
children.forEach(child => collectChunks(child));
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// Start from treeData root
|
|
3068
|
+
if (treeData) {
|
|
3069
|
+
collectChunks(treeData);
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
// Calculate statistics
|
|
3073
|
+
const totalFunctions = chunksWithComplexity.length;
|
|
3074
|
+
const validComplexity = chunksWithComplexity.filter(c => c.complexity !== null);
|
|
3075
|
+
const avgComplexity = validComplexity.length > 0
|
|
3076
|
+
? (validComplexity.reduce((sum, c) => sum + c.complexity, 0) / validComplexity.length).toFixed(2)
|
|
3077
|
+
: 'N/A';
|
|
3078
|
+
|
|
3079
|
+
// Count by grade
|
|
3080
|
+
const gradeCounts = {
|
|
3081
|
+
'A': 0, 'B': 0, 'C': 0, 'D': 0, 'F': 0, 'N/A': 0
|
|
3082
|
+
};
|
|
3083
|
+
chunksWithComplexity.forEach(c => {
|
|
3084
|
+
gradeCounts[c.grade]++;
|
|
3085
|
+
});
|
|
3086
|
+
|
|
3087
|
+
// Sort by complexity (highest first)
|
|
3088
|
+
const sortedChunks = [...chunksWithComplexity].sort((a, b) => {
|
|
3089
|
+
if (a.complexity === null) return 1;
|
|
3090
|
+
if (b.complexity === null) return -1;
|
|
3091
|
+
return b.complexity - a.complexity;
|
|
3092
|
+
});
|
|
3093
|
+
|
|
3094
|
+
// Build HTML
|
|
3095
|
+
let html = '<div class="complexity-report">';
|
|
3096
|
+
|
|
3097
|
+
// Summary Stats
|
|
3098
|
+
html += '<div class="complexity-summary">';
|
|
3099
|
+
html += '<div class="summary-grid">';
|
|
3100
|
+
html += `<div class="summary-card">
|
|
3101
|
+
<div class="summary-label">Total Functions</div>
|
|
3102
|
+
<div class="summary-value">${totalFunctions}</div>
|
|
3103
|
+
</div>`;
|
|
3104
|
+
html += `<div class="summary-card">
|
|
3105
|
+
<div class="summary-label">Average Complexity</div>
|
|
3106
|
+
<div class="summary-value">${avgComplexity}</div>
|
|
3107
|
+
</div>`;
|
|
3108
|
+
html += `<div class="summary-card">
|
|
3109
|
+
<div class="summary-label">With Complexity Data</div>
|
|
3110
|
+
<div class="summary-value">${validComplexity.length}</div>
|
|
3111
|
+
</div>`;
|
|
3112
|
+
html += '</div>';
|
|
3113
|
+
|
|
3114
|
+
// Grade Distribution
|
|
3115
|
+
html += '<div class="grade-distribution">';
|
|
3116
|
+
html += '<div class="distribution-title">Grade Distribution</div>';
|
|
3117
|
+
html += '<div class="distribution-bars">';
|
|
3118
|
+
|
|
3119
|
+
const maxCount = Math.max(...Object.values(gradeCounts));
|
|
3120
|
+
['A', 'B', 'C', 'D', 'F', 'N/A'].forEach(grade => {
|
|
3121
|
+
const count = gradeCounts[grade];
|
|
3122
|
+
const percentage = totalFunctions > 0 ? (count / totalFunctions * 100) : 0;
|
|
3123
|
+
const barWidth = maxCount > 0 ? (count / maxCount * 100) : 0;
|
|
3124
|
+
html += `
|
|
3125
|
+
<div class="distribution-row">
|
|
3126
|
+
<div class="distribution-grade" style="color: ${getGradeColor(grade)}">${grade}</div>
|
|
3127
|
+
<div class="distribution-bar-container">
|
|
3128
|
+
<div class="distribution-bar" style="width: ${barWidth}%; background: ${getGradeColor(grade)}"></div>
|
|
3129
|
+
</div>
|
|
3130
|
+
<div class="distribution-count">${count} (${percentage.toFixed(1)}%)</div>
|
|
3131
|
+
</div>
|
|
3132
|
+
`;
|
|
3133
|
+
});
|
|
3134
|
+
html += '</div></div>';
|
|
3135
|
+
html += '</div>';
|
|
3136
|
+
|
|
3137
|
+
// Complexity Hotspots Table
|
|
3138
|
+
html += '<div class="complexity-hotspots">';
|
|
3139
|
+
html += '<h3 class="section-title">Complexity Hotspots</h3>';
|
|
3140
|
+
|
|
3141
|
+
if (sortedChunks.length === 0) {
|
|
3142
|
+
html += '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No functions found with complexity data.</p>';
|
|
3143
|
+
} else {
|
|
3144
|
+
html += '<div class="hotspots-table-container">';
|
|
3145
|
+
html += '<table class="hotspots-table">';
|
|
3146
|
+
html += `
|
|
3147
|
+
<thead>
|
|
3148
|
+
<tr>
|
|
3149
|
+
<th>Name</th>
|
|
3150
|
+
<th>File</th>
|
|
3151
|
+
<th>Lines</th>
|
|
3152
|
+
<th>Complexity</th>
|
|
3153
|
+
<th>Grade</th>
|
|
3154
|
+
</tr>
|
|
3155
|
+
</thead>
|
|
3156
|
+
<tbody>
|
|
3157
|
+
`;
|
|
3158
|
+
|
|
3159
|
+
sortedChunks.forEach(chunk => {
|
|
3160
|
+
const lines = chunk.end_line > 0 ? chunk.end_line - chunk.start_line + 1 : 'N/A';
|
|
3161
|
+
const complexityDisplay = chunk.complexity !== null ? chunk.complexity.toFixed(1) : 'N/A';
|
|
3162
|
+
const gradeColor = getGradeColor(chunk.grade);
|
|
3163
|
+
|
|
3164
|
+
// Get relative file path
|
|
3165
|
+
const fileName = chunk.file_path.split('/').pop() || chunk.file_path;
|
|
3166
|
+
const lineRange = chunk.start_line > 0 ? `L${chunk.start_line}-${chunk.end_line}` : '';
|
|
3167
|
+
|
|
3168
|
+
html += `
|
|
3169
|
+
<tr class="hotspot-row" onclick='navigateToChunk(${JSON.stringify(chunk.name)})'>
|
|
3170
|
+
<td class="hotspot-name">${escapeHtml(chunk.name)}</td>
|
|
3171
|
+
<td class="hotspot-file" title="${escapeHtml(chunk.file_path)}">${escapeHtml(fileName)}</td>
|
|
3172
|
+
<td class="hotspot-lines">${lines}</td>
|
|
3173
|
+
<td class="hotspot-complexity">${complexityDisplay}</td>
|
|
3174
|
+
<td class="hotspot-grade">
|
|
3175
|
+
<span class="grade-badge" style="background: ${gradeColor}">${chunk.grade}</span>
|
|
3176
|
+
</td>
|
|
3177
|
+
</tr>
|
|
3178
|
+
`;
|
|
3179
|
+
});
|
|
3180
|
+
|
|
3181
|
+
html += '</tbody></table></div>';
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
html += '</div>'; // complexity-hotspots
|
|
3185
|
+
html += '</div>'; // complexity-report
|
|
3186
|
+
|
|
3187
|
+
viewerContent.innerHTML = html;
|
|
3188
|
+
|
|
3189
|
+
// Hide section dropdown for reports (no code sections)
|
|
3190
|
+
const sectionNav = document.getElementById('section-nav');
|
|
3191
|
+
if (sectionNav) {
|
|
3192
|
+
sectionNav.style.display = 'none';
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function navigateToChunk(chunkName) {
|
|
3197
|
+
// Find the chunk node in the tree
|
|
3198
|
+
function findChunk(node) {
|
|
3199
|
+
if (chunkTypes.includes(node.type) && node.name === chunkName) {
|
|
3200
|
+
return node;
|
|
3201
|
+
}
|
|
3202
|
+
const children = node.children || node._children || [];
|
|
3203
|
+
for (const child of children) {
|
|
3204
|
+
const found = findChunk(child);
|
|
3205
|
+
if (found) return found;
|
|
3206
|
+
}
|
|
3207
|
+
return null;
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
if (treeData) {
|
|
3211
|
+
const chunk = findChunk(treeData);
|
|
3212
|
+
if (chunk) {
|
|
3213
|
+
// Display the chunk content
|
|
3214
|
+
displayChunkContent(chunk);
|
|
3215
|
+
|
|
3216
|
+
// Highlight the node in the visualization
|
|
3217
|
+
highlightNode(chunk.id);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
function escapeHtml(text) {
|
|
3223
|
+
const div = document.createElement('div');
|
|
3224
|
+
div.textContent = text;
|
|
3225
|
+
return div.innerHTML;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
function highlightNode(nodeId) {
|
|
3229
|
+
// Remove existing highlights
|
|
3230
|
+
d3.selectAll('.node').classed('node-highlight', false);
|
|
3231
|
+
|
|
3232
|
+
// Add highlight to the target node
|
|
3233
|
+
d3.selectAll('.node')
|
|
3234
|
+
.filter(d => d.data.id === nodeId)
|
|
3235
|
+
.classed('node-highlight', true);
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
// ============================================================================
|
|
3239
|
+
// CODE SMELLS DETECTION AND REPORTING
|
|
3240
|
+
// ============================================================================
|
|
3241
|
+
|
|
3242
|
+
function detectCodeSmells(nodes) {
|
|
3243
|
+
const smells = [];
|
|
3244
|
+
|
|
3245
|
+
function analyzeNode(node) {
|
|
3246
|
+
// Only analyze code chunks
|
|
3247
|
+
if (!chunkTypes.includes(node.type)) {
|
|
3248
|
+
const children = node.children || node._children || [];
|
|
3249
|
+
children.forEach(child => analyzeNode(child));
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
const lineCount = (node.end_line && node.start_line)
|
|
3254
|
+
? node.end_line - node.start_line + 1
|
|
3255
|
+
: 0;
|
|
3256
|
+
const complexity = node.complexity || 0;
|
|
3257
|
+
|
|
3258
|
+
// 1. Long Method - Functions with > 50 lines
|
|
3259
|
+
if (node.type === 'function' || node.type === 'method') {
|
|
3260
|
+
if (lineCount > 100) {
|
|
3261
|
+
smells.push({
|
|
3262
|
+
type: 'Long Method',
|
|
3263
|
+
severity: 'error',
|
|
3264
|
+
node: node,
|
|
3265
|
+
details: `${lineCount} lines (very long)`
|
|
3266
|
+
});
|
|
3267
|
+
} else if (lineCount > 50) {
|
|
3268
|
+
smells.push({
|
|
3269
|
+
type: 'Long Method',
|
|
3270
|
+
severity: 'warning',
|
|
3271
|
+
node: node,
|
|
3272
|
+
details: `${lineCount} lines`
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// 2. High Complexity - Functions with complexity > 15
|
|
3278
|
+
if ((node.type === 'function' || node.type === 'method') && complexity > 0) {
|
|
3279
|
+
if (complexity > 20) {
|
|
3280
|
+
smells.push({
|
|
3281
|
+
type: 'High Complexity',
|
|
3282
|
+
severity: 'error',
|
|
3283
|
+
node: node,
|
|
3284
|
+
details: `Complexity: ${complexity.toFixed(1)} (very complex)`
|
|
3285
|
+
});
|
|
3286
|
+
} else if (complexity > 15) {
|
|
3287
|
+
smells.push({
|
|
3288
|
+
type: 'High Complexity',
|
|
3289
|
+
severity: 'warning',
|
|
3290
|
+
node: node,
|
|
3291
|
+
details: `Complexity: ${complexity.toFixed(1)}`
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
// 3. Deep Nesting - Proxy using complexity > 20
|
|
3297
|
+
if ((node.type === 'function' || node.type === 'method') && complexity > 20) {
|
|
3298
|
+
smells.push({
|
|
3299
|
+
type: 'Deep Nesting',
|
|
3300
|
+
severity: complexity > 25 ? 'error' : 'warning',
|
|
3301
|
+
node: node,
|
|
3302
|
+
details: `Complexity: ${complexity.toFixed(1)} (likely deep nesting)`
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// 4. God Class - Classes with > 20 methods or > 500 lines
|
|
3307
|
+
if (node.type === 'class') {
|
|
3308
|
+
const children = node.children || node._children || [];
|
|
3309
|
+
const methodCount = children.filter(c => c.type === 'method').length;
|
|
3310
|
+
|
|
3311
|
+
if (methodCount > 30 || lineCount > 800) {
|
|
3312
|
+
smells.push({
|
|
3313
|
+
type: 'God Class',
|
|
3314
|
+
severity: 'error',
|
|
3315
|
+
node: node,
|
|
3316
|
+
details: `${methodCount} methods, ${lineCount} lines (very large)`
|
|
3317
|
+
});
|
|
3318
|
+
} else if (methodCount > 20 || lineCount > 500) {
|
|
3319
|
+
smells.push({
|
|
3320
|
+
type: 'God Class',
|
|
3321
|
+
severity: 'warning',
|
|
3322
|
+
node: node,
|
|
3323
|
+
details: `${methodCount} methods, ${lineCount} lines`
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
// Recursively process children
|
|
3329
|
+
const children = node.children || node._children || [];
|
|
3330
|
+
children.forEach(child => analyzeNode(child));
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
if (nodes) {
|
|
3334
|
+
analyzeNode(nodes);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
return smells;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
function showCodeSmells() {
|
|
3341
|
+
openViewerPanel();
|
|
3342
|
+
|
|
3343
|
+
const viewerTitle = document.getElementById('viewer-title');
|
|
3344
|
+
const viewerContent = document.getElementById('viewer-content');
|
|
3345
|
+
|
|
3346
|
+
viewerTitle.textContent = '🔍 Code Smells';
|
|
3347
|
+
|
|
3348
|
+
// Detect all code smells
|
|
3349
|
+
const allSmells = detectCodeSmells(treeData);
|
|
3350
|
+
|
|
3351
|
+
// Count by type and severity
|
|
3352
|
+
const smellCounts = {
|
|
3353
|
+
'Long Method': { total: 0, warning: 0, error: 0 },
|
|
3354
|
+
'High Complexity': { total: 0, warning: 0, error: 0 },
|
|
3355
|
+
'Deep Nesting': { total: 0, warning: 0, error: 0 },
|
|
3356
|
+
'God Class': { total: 0, warning: 0, error: 0 }
|
|
3357
|
+
};
|
|
3358
|
+
|
|
3359
|
+
let totalWarnings = 0;
|
|
3360
|
+
let totalErrors = 0;
|
|
3361
|
+
|
|
3362
|
+
allSmells.forEach(smell => {
|
|
3363
|
+
smellCounts[smell.type].total++;
|
|
3364
|
+
smellCounts[smell.type][smell.severity]++;
|
|
3365
|
+
if (smell.severity === 'warning') totalWarnings++;
|
|
3366
|
+
if (smell.severity === 'error') totalErrors++;
|
|
3367
|
+
});
|
|
3368
|
+
|
|
3369
|
+
// Build HTML
|
|
3370
|
+
let html = '<div class="code-smells-report">';
|
|
3371
|
+
|
|
3372
|
+
// Summary Cards
|
|
3373
|
+
html += '<div class="smell-summary-grid">';
|
|
3374
|
+
html += `
|
|
3375
|
+
<div class="smell-summary-card">
|
|
3376
|
+
<div class="smell-card-header">
|
|
3377
|
+
<span class="smell-card-icon">🔍</span>
|
|
3378
|
+
<div class="smell-card-title">Total Smells</div>
|
|
3379
|
+
</div>
|
|
3380
|
+
<div class="smell-card-count">${allSmells.length}</div>
|
|
3381
|
+
</div>
|
|
3382
|
+
<div class="smell-summary-card warning">
|
|
3383
|
+
<div class="smell-card-header">
|
|
3384
|
+
<span class="smell-card-icon">⚠️</span>
|
|
3385
|
+
<div class="smell-card-title">Warnings</div>
|
|
3386
|
+
</div>
|
|
3387
|
+
<div class="smell-card-count" style="color: var(--warning)">${totalWarnings}</div>
|
|
3388
|
+
</div>
|
|
3389
|
+
<div class="smell-summary-card error">
|
|
3390
|
+
<div class="smell-card-header">
|
|
3391
|
+
<span class="smell-card-icon">🚨</span>
|
|
3392
|
+
<div class="smell-card-title">Errors</div>
|
|
3393
|
+
</div>
|
|
3394
|
+
<div class="smell-card-count" style="color: var(--error)">${totalErrors}</div>
|
|
3395
|
+
</div>
|
|
3396
|
+
`;
|
|
3397
|
+
html += '</div>';
|
|
3398
|
+
|
|
3399
|
+
// Filters
|
|
3400
|
+
html += '<div class="smell-filters">';
|
|
3401
|
+
html += '<div class="filter-title">Filter by Type</div>';
|
|
3402
|
+
html += '<div class="filter-checkboxes">';
|
|
3403
|
+
|
|
3404
|
+
Object.keys(smellCounts).forEach(type => {
|
|
3405
|
+
const count = smellCounts[type].total;
|
|
3406
|
+
html += `
|
|
3407
|
+
<div class="filter-checkbox-item">
|
|
3408
|
+
<input type="checkbox" id="filter-${type.replace(/\\s+/g, '-')}"
|
|
3409
|
+
checked onchange="filterCodeSmells()">
|
|
3410
|
+
<label class="filter-checkbox-label" for="filter-${type.replace(/\\s+/g, '-')}">${type}</label>
|
|
3411
|
+
<span class="filter-checkbox-count">${count}</span>
|
|
3412
|
+
</div>
|
|
3413
|
+
`;
|
|
3414
|
+
});
|
|
3415
|
+
|
|
3416
|
+
html += '</div></div>';
|
|
3417
|
+
|
|
3418
|
+
// Smells Table
|
|
3419
|
+
html += '<div id="smells-table-wrapper">';
|
|
3420
|
+
html += '<h3 class="section-title">Detected Code Smells</h3>';
|
|
3421
|
+
|
|
3422
|
+
if (allSmells.length === 0) {
|
|
3423
|
+
html += '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No code smells detected! Great job! 🎉</p>';
|
|
3424
|
+
} else {
|
|
3425
|
+
html += '<div class="smells-table-container">';
|
|
3426
|
+
html += '<table class="smells-table">';
|
|
3427
|
+
html += `
|
|
3428
|
+
<thead>
|
|
3429
|
+
<tr>
|
|
3430
|
+
<th>Type</th>
|
|
3431
|
+
<th>Severity</th>
|
|
3432
|
+
<th>Name</th>
|
|
3433
|
+
<th>File</th>
|
|
3434
|
+
<th>Details</th>
|
|
3435
|
+
</tr>
|
|
3436
|
+
</thead>
|
|
3437
|
+
<tbody id="smells-table-body">
|
|
3438
|
+
`;
|
|
3439
|
+
|
|
3440
|
+
// Sort by severity (error first) then by type
|
|
3441
|
+
const sortedSmells = [...allSmells].sort((a, b) => {
|
|
3442
|
+
if (a.severity !== b.severity) {
|
|
3443
|
+
return a.severity === 'error' ? -1 : 1;
|
|
3444
|
+
}
|
|
3445
|
+
return a.type.localeCompare(b.type);
|
|
3446
|
+
});
|
|
3447
|
+
|
|
3448
|
+
sortedSmells.forEach(smell => {
|
|
3449
|
+
const fileName = smell.node.file_path ? smell.node.file_path.split('/').pop() : 'Unknown';
|
|
3450
|
+
const severityIcon = smell.severity === 'error' ? '🚨' : '⚠️';
|
|
3451
|
+
|
|
3452
|
+
html += `
|
|
3453
|
+
<tr class="smell-row" data-smell-type="${smell.type.replace(/\\s+/g, '-')}"
|
|
3454
|
+
onclick='navigateToChunk(${JSON.stringify(smell.node.name)})'>
|
|
3455
|
+
<td><span class="smell-type-badge">${escapeHtml(smell.type)}</span></td>
|
|
3456
|
+
<td><span class="severity-badge ${smell.severity}">${severityIcon} ${smell.severity.toUpperCase()}</span></td>
|
|
3457
|
+
<td class="smell-name">${escapeHtml(smell.node.name)}</td>
|
|
3458
|
+
<td class="smell-file" title="${escapeHtml(smell.node.file_path || '')}">${escapeHtml(fileName)}</td>
|
|
3459
|
+
<td class="smell-details">${escapeHtml(smell.details)}</td>
|
|
3460
|
+
</tr>
|
|
3461
|
+
`;
|
|
3462
|
+
});
|
|
3463
|
+
|
|
3464
|
+
html += '</tbody></table></div>';
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
html += '</div>'; // smells-table-wrapper
|
|
3468
|
+
html += '</div>'; // code-smells-report
|
|
3469
|
+
|
|
3470
|
+
viewerContent.innerHTML = html;
|
|
3471
|
+
|
|
3472
|
+
// Hide section dropdown for reports (no code sections)
|
|
3473
|
+
const sectionNav = document.getElementById('section-nav');
|
|
3474
|
+
if (sectionNav) {
|
|
3475
|
+
sectionNav.style.display = 'none';
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
function filterCodeSmells() {
|
|
3480
|
+
const rows = document.querySelectorAll('.smell-row');
|
|
3481
|
+
|
|
3482
|
+
rows.forEach(row => {
|
|
3483
|
+
const smellType = row.getAttribute('data-smell-type');
|
|
3484
|
+
const checkbox = document.getElementById(`filter-${smellType}`);
|
|
3485
|
+
|
|
3486
|
+
if (checkbox && checkbox.checked) {
|
|
3487
|
+
row.style.display = '';
|
|
3488
|
+
} else {
|
|
3489
|
+
row.style.display = 'none';
|
|
3490
|
+
}
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
// ============================================================================
|
|
3495
|
+
// DEPENDENCIES ANALYSIS AND REPORTING
|
|
3496
|
+
// ============================================================================
|
|
3497
|
+
|
|
3498
|
+
function showDependencies() {
|
|
3499
|
+
openViewerPanel();
|
|
3500
|
+
|
|
3501
|
+
const viewerTitle = document.getElementById('viewer-title');
|
|
3502
|
+
const viewerContent = document.getElementById('viewer-content');
|
|
3503
|
+
|
|
3504
|
+
viewerTitle.textContent = '🔗 Code Structure';
|
|
3505
|
+
|
|
3506
|
+
// Code file extensions (exclude docs like .md, .txt, .rst)
|
|
3507
|
+
const codeExtensions = ['.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.swift', '.kt', '.scala', '.ex', '.exs', '.clj', '.vue', '.svelte'];
|
|
3508
|
+
|
|
3509
|
+
function isCodeFile(filePath) {
|
|
3510
|
+
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
|
|
3511
|
+
return codeExtensions.includes(ext);
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// Build directory structure from nodes (code files only)
|
|
3515
|
+
const dirStructure = new Map();
|
|
3516
|
+
|
|
3517
|
+
allNodes.forEach(node => {
|
|
3518
|
+
if (node.type === 'file' && node.file_path && isCodeFile(node.file_path)) {
|
|
3519
|
+
const parts = node.file_path.split('/');
|
|
3520
|
+
const fileName = parts.pop();
|
|
3521
|
+
const dirPath = parts.join('/') || '/';
|
|
3522
|
+
|
|
3523
|
+
if (!dirStructure.has(dirPath)) {
|
|
3524
|
+
dirStructure.set(dirPath, []);
|
|
3525
|
+
}
|
|
3526
|
+
dirStructure.get(dirPath).push({
|
|
3527
|
+
name: fileName,
|
|
3528
|
+
path: node.file_path,
|
|
3529
|
+
chunks: allNodes.filter(n => n.file_path === node.file_path && n.type !== 'file').length
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
});
|
|
3533
|
+
|
|
3534
|
+
// Calculate stats
|
|
3535
|
+
const totalDirs = dirStructure.size;
|
|
3536
|
+
const totalFiles = Array.from(dirStructure.values()).reduce((sum, files) => sum + files.length, 0);
|
|
3537
|
+
const totalChunks = allNodes.filter(n => n.type !== 'file' && n.type !== 'directory').length;
|
|
3538
|
+
|
|
3539
|
+
let html = `
|
|
3540
|
+
<div class="report-section">
|
|
3541
|
+
<h3>📁 Directory Overview</h3>
|
|
3542
|
+
<p style="color: var(--text-secondary); margin-bottom: 15px;">Showing code organization by directory structure.</p>
|
|
3543
|
+
<div class="metrics-grid">
|
|
3544
|
+
<div class="metric-card">
|
|
3545
|
+
<div class="metric-value">${totalDirs}</div>
|
|
3546
|
+
<div class="metric-label">Directories</div>
|
|
3547
|
+
</div>
|
|
3548
|
+
<div class="metric-card">
|
|
3549
|
+
<div class="metric-value">${totalFiles}</div>
|
|
3550
|
+
<div class="metric-label">Files</div>
|
|
3551
|
+
</div>
|
|
3552
|
+
<div class="metric-card">
|
|
3553
|
+
<div class="metric-value">${totalChunks}</div>
|
|
3554
|
+
<div class="metric-label">Code Chunks</div>
|
|
3555
|
+
</div>
|
|
3556
|
+
</div>
|
|
3557
|
+
</div>
|
|
3558
|
+
<div class="report-section">
|
|
3559
|
+
<h3>📂 Directory Structure</h3>
|
|
3560
|
+
`;
|
|
3561
|
+
|
|
3562
|
+
// Sort directories
|
|
3563
|
+
const sortedDirs = Array.from(dirStructure.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
3564
|
+
|
|
3565
|
+
sortedDirs.forEach(([dir, files]) => {
|
|
3566
|
+
const dirDisplay = dir === '/' ? 'Root' : dir;
|
|
3567
|
+
const totalChunksInDir = files.reduce((sum, f) => sum + f.chunks, 0);
|
|
3568
|
+
|
|
3569
|
+
html += `
|
|
3570
|
+
<div class="dependency-item" style="margin-bottom: 20px; padding: 12px; background: var(--bg-secondary); border-radius: 6px; border-left: 3px solid var(--accent-blue);">
|
|
3571
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
3572
|
+
<strong style="color: var(--accent-blue);">📁 ${escapeHtml(dirDisplay)}</strong>
|
|
3573
|
+
<span style="color: var(--text-secondary); font-size: 12px;">${files.length} files, ${totalChunksInDir} chunks</span>
|
|
3574
|
+
</div>
|
|
3575
|
+
<ul style="margin: 0; padding: 0 0 0 20px; list-style: none;">
|
|
3576
|
+
`;
|
|
3577
|
+
|
|
3578
|
+
files.sort((a, b) => a.name.localeCompare(b.name)).forEach(file => {
|
|
3579
|
+
html += `<li style="padding: 4px 0; color: var(--text-primary);">
|
|
3580
|
+
📄 ${escapeHtml(file.name)}
|
|
3581
|
+
<span style="color: var(--text-secondary); font-size: 11px;">(${file.chunks} chunks)</span>
|
|
3582
|
+
</li>`;
|
|
3583
|
+
});
|
|
3584
|
+
|
|
3585
|
+
html += `</ul></div>`;
|
|
3586
|
+
});
|
|
3587
|
+
|
|
3588
|
+
html += '</div>';
|
|
3589
|
+
|
|
3590
|
+
viewerContent.innerHTML = html;
|
|
3591
|
+
|
|
3592
|
+
// Hide section dropdown for reports (no code sections)
|
|
3593
|
+
const sectionNav = document.getElementById('section-nav');
|
|
3594
|
+
if (sectionNav) {
|
|
3595
|
+
sectionNav.style.display = 'none';
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
function buildFileDependencyGraph() {
|
|
3600
|
+
// Map: file_path -> { dependsOn: Set<file_path>, usedBy: Set<file_path> }
|
|
3601
|
+
const fileDeps = new Map();
|
|
3602
|
+
|
|
3603
|
+
allLinks.forEach(link => {
|
|
3604
|
+
if (link.type === 'caller') {
|
|
3605
|
+
const sourceNode = allNodes.find(n => n.id === link.source);
|
|
3606
|
+
const targetNode = allNodes.find(n => n.id === link.target);
|
|
3607
|
+
|
|
3608
|
+
if (sourceNode && targetNode && sourceNode.file_path && targetNode.file_path) {
|
|
3609
|
+
// Skip self-references (same file)
|
|
3610
|
+
if (sourceNode.file_path === targetNode.file_path) {
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
// sourceNode calls targetNode → source depends on target
|
|
3615
|
+
if (!fileDeps.has(sourceNode.file_path)) {
|
|
3616
|
+
fileDeps.set(sourceNode.file_path, { dependsOn: new Set(), usedBy: new Set() });
|
|
3617
|
+
}
|
|
3618
|
+
if (!fileDeps.has(targetNode.file_path)) {
|
|
3619
|
+
fileDeps.set(targetNode.file_path, { dependsOn: new Set(), usedBy: new Set() });
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
fileDeps.get(sourceNode.file_path).dependsOn.add(targetNode.file_path);
|
|
3623
|
+
fileDeps.get(targetNode.file_path).usedBy.add(sourceNode.file_path);
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
});
|
|
3627
|
+
|
|
3628
|
+
return fileDeps;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
function findCircularDeps(fileDeps) {
|
|
3632
|
+
// Simple cycle detection using DFS
|
|
3633
|
+
const cycles = [];
|
|
3634
|
+
const visited = new Set();
|
|
3635
|
+
const recStack = new Set();
|
|
3636
|
+
const pathStack = [];
|
|
3637
|
+
|
|
3638
|
+
function dfs(filePath) {
|
|
3639
|
+
visited.add(filePath);
|
|
3640
|
+
recStack.add(filePath);
|
|
3641
|
+
pathStack.push(filePath);
|
|
3642
|
+
|
|
3643
|
+
const deps = fileDeps.get(filePath);
|
|
3644
|
+
if (deps && deps.dependsOn) {
|
|
3645
|
+
for (const depFile of deps.dependsOn) {
|
|
3646
|
+
if (!visited.has(depFile)) {
|
|
3647
|
+
dfs(depFile);
|
|
3648
|
+
} else if (recStack.has(depFile)) {
|
|
3649
|
+
// Found a cycle
|
|
3650
|
+
const cycleStartIndex = pathStack.indexOf(depFile);
|
|
3651
|
+
if (cycleStartIndex !== -1) {
|
|
3652
|
+
const cycle = pathStack.slice(cycleStartIndex);
|
|
3653
|
+
cycle.push(depFile); // Complete the cycle
|
|
3654
|
+
// Check if this cycle is already recorded
|
|
3655
|
+
const cycleStr = cycle.sort().join('|');
|
|
3656
|
+
if (!cycles.some(c => c.sort().join('|') === cycleStr)) {
|
|
3657
|
+
cycles.push([...cycle]);
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
pathStack.pop();
|
|
3665
|
+
recStack.delete(filePath);
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
for (const filePath of fileDeps.keys()) {
|
|
3669
|
+
if (!visited.has(filePath)) {
|
|
3670
|
+
dfs(filePath);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
return cycles;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
function toggleDependencyDetails(rowId, index) {
|
|
3678
|
+
const detailsRow = document.getElementById(`${rowId}-details`);
|
|
3679
|
+
const btn = document.querySelector(`#${rowId} .expand-btn`);
|
|
3680
|
+
|
|
3681
|
+
if (detailsRow.style.display === 'none') {
|
|
3682
|
+
detailsRow.style.display = '';
|
|
3683
|
+
btn.textContent = '▲';
|
|
3684
|
+
} else {
|
|
3685
|
+
detailsRow.style.display = 'none';
|
|
3686
|
+
btn.textContent = '▼';
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
// ============================================================================
|
|
3691
|
+
// TRENDS / METRICS SNAPSHOT
|
|
3692
|
+
// ============================================================================
|
|
3693
|
+
|
|
3694
|
+
function showTrends() {
|
|
3695
|
+
openViewerPanel();
|
|
3696
|
+
|
|
3697
|
+
const viewerTitle = document.getElementById('viewer-title');
|
|
3698
|
+
const viewerContent = document.getElementById('viewer-content');
|
|
3699
|
+
|
|
3700
|
+
viewerTitle.textContent = '📈 Codebase Metrics Snapshot';
|
|
3701
|
+
|
|
3702
|
+
// Calculate metrics from current codebase
|
|
3703
|
+
const metrics = calculateCodebaseMetrics();
|
|
3704
|
+
|
|
3705
|
+
// Build HTML
|
|
3706
|
+
let html = '<div class="trends-report">';
|
|
3707
|
+
|
|
3708
|
+
// Snapshot Banner
|
|
3709
|
+
html += '<div class="snapshot-banner">';
|
|
3710
|
+
html += '<div class="snapshot-header">';
|
|
3711
|
+
html += '<div class="snapshot-icon">📊</div>';
|
|
3712
|
+
html += '<div class="snapshot-info">';
|
|
3713
|
+
html += '<div class="snapshot-title">Codebase Metrics Snapshot</div>';
|
|
3714
|
+
html += `<div class="snapshot-timestamp">Generated: ${new Date().toLocaleString('en-US', {
|
|
3715
|
+
month: 'short',
|
|
3716
|
+
day: 'numeric',
|
|
3717
|
+
year: 'numeric',
|
|
3718
|
+
hour: 'numeric',
|
|
3719
|
+
minute: '2-digit',
|
|
3720
|
+
hour12: true
|
|
3721
|
+
})}</div>`;
|
|
3722
|
+
html += '</div></div>';
|
|
3723
|
+
html += '<div class="snapshot-description">';
|
|
3724
|
+
html += 'This snapshot serves as the baseline for future trend tracking. ';
|
|
3725
|
+
html += 'With git history analysis, you could track how these metrics evolve over time.';
|
|
3726
|
+
html += '</div>';
|
|
3727
|
+
html += '</div>';
|
|
3728
|
+
|
|
3729
|
+
// Key Metrics Cards
|
|
3730
|
+
html += '<div class="metrics-section">';
|
|
3731
|
+
html += '<h3 class="section-title">Key Metrics</h3>';
|
|
3732
|
+
html += '<div class="metrics-grid">';
|
|
3733
|
+
|
|
3734
|
+
html += `<div class="metric-card">
|
|
3735
|
+
<div class="metric-icon">📝</div>
|
|
3736
|
+
<div class="metric-value">${metrics.totalLines.toLocaleString()}</div>
|
|
3737
|
+
<div class="metric-label">Lines of Code</div>
|
|
3738
|
+
</div>`;
|
|
3739
|
+
|
|
3740
|
+
html += `<div class="metric-card">
|
|
3741
|
+
<div class="metric-icon">⚡</div>
|
|
3742
|
+
<div class="metric-value">${metrics.totalFunctions}</div>
|
|
3743
|
+
<div class="metric-label">Functions/Methods</div>
|
|
3744
|
+
</div>`;
|
|
3745
|
+
|
|
3746
|
+
html += `<div class="metric-card">
|
|
3747
|
+
<div class="metric-icon">🎯</div>
|
|
3748
|
+
<div class="metric-value">${metrics.totalClasses}</div>
|
|
3749
|
+
<div class="metric-label">Classes</div>
|
|
3750
|
+
</div>`;
|
|
3751
|
+
|
|
3752
|
+
html += `<div class="metric-card">
|
|
3753
|
+
<div class="metric-icon">📄</div>
|
|
3754
|
+
<div class="metric-value">${metrics.totalFiles}</div>
|
|
3755
|
+
<div class="metric-label">Files</div>
|
|
3756
|
+
</div>`;
|
|
3757
|
+
|
|
3758
|
+
html += '</div></div>';
|
|
3759
|
+
|
|
3760
|
+
// Code Health Score
|
|
3761
|
+
html += '<div class="health-section">';
|
|
3762
|
+
html += '<h3 class="section-title">Code Health Score</h3>';
|
|
3763
|
+
html += '<div class="health-card">';
|
|
3764
|
+
|
|
3765
|
+
const healthInfo = getHealthScoreInfo(metrics.healthScore);
|
|
3766
|
+
html += `<div class="health-score-display">
|
|
3767
|
+
<div class="health-score-value">${metrics.healthScore}/100</div>
|
|
3768
|
+
<div class="health-score-label">${healthInfo.emoji} ${healthInfo.label}</div>
|
|
3769
|
+
</div>`;
|
|
3770
|
+
|
|
3771
|
+
html += '<div class="health-progress-container">';
|
|
3772
|
+
html += `<div class="health-progress-bar" style="width: ${metrics.healthScore}%; background: ${healthInfo.color}"></div>`;
|
|
3773
|
+
html += '</div>';
|
|
3774
|
+
|
|
3775
|
+
html += `<div class="health-description">${healthInfo.description}</div>`;
|
|
3776
|
+
html += '</div></div>';
|
|
3777
|
+
|
|
3778
|
+
// Complexity Distribution
|
|
3779
|
+
html += '<div class="distribution-section">';
|
|
3780
|
+
html += '<h3 class="section-title">Complexity Distribution</h3>';
|
|
3781
|
+
html += '<div class="distribution-chart">';
|
|
3782
|
+
|
|
3783
|
+
const complexityDist = metrics.complexityDistribution;
|
|
3784
|
+
const maxPct = Math.max(...Object.values(complexityDist));
|
|
3785
|
+
|
|
3786
|
+
['A', 'B', 'C', 'D', 'F'].forEach(grade => {
|
|
3787
|
+
const pct = complexityDist[grade] || 0;
|
|
3788
|
+
const barWidth = maxPct > 0 ? (pct / maxPct * 100) : 0;
|
|
3789
|
+
const color = getGradeColor(grade);
|
|
3790
|
+
const range = getComplexityRange(grade);
|
|
3791
|
+
|
|
3792
|
+
html += `<div class="distribution-bar-row">
|
|
3793
|
+
<div class="distribution-bar-label">
|
|
3794
|
+
<span class="distribution-grade" style="color: ${color}">${grade}</span>
|
|
3795
|
+
<span class="distribution-range">${range}</span>
|
|
3796
|
+
</div>
|
|
3797
|
+
<div class="distribution-bar-container">
|
|
3798
|
+
<div class="distribution-bar-fill" style="width: ${barWidth}%; background: ${color}"></div>
|
|
3799
|
+
</div>
|
|
3800
|
+
<div class="distribution-bar-value">${pct.toFixed(1)}%</div>
|
|
3801
|
+
</div>`;
|
|
3802
|
+
});
|
|
3803
|
+
|
|
3804
|
+
html += '</div></div>';
|
|
3805
|
+
|
|
3806
|
+
// Function Size Distribution
|
|
3807
|
+
html += '<div class="size-distribution-section">';
|
|
3808
|
+
html += '<h3 class="section-title">Function Size Distribution</h3>';
|
|
3809
|
+
html += '<div class="distribution-chart">';
|
|
3810
|
+
|
|
3811
|
+
const sizeDist = metrics.sizeDistribution;
|
|
3812
|
+
const maxSizePct = Math.max(...Object.values(sizeDist));
|
|
3813
|
+
|
|
3814
|
+
[
|
|
3815
|
+
{ key: 'small', label: 'Small (1-20 lines)', color: '#238636' },
|
|
3816
|
+
{ key: 'medium', label: 'Medium (21-50 lines)', color: '#1f6feb' },
|
|
3817
|
+
{ key: 'large', label: 'Large (51-100 lines)', color: '#d29922' },
|
|
3818
|
+
{ key: 'veryLarge', label: 'Very Large (100+ lines)', color: '#da3633' }
|
|
3819
|
+
].forEach(({ key, label, color }) => {
|
|
3820
|
+
const pct = sizeDist[key] || 0;
|
|
3821
|
+
const barWidth = maxSizePct > 0 ? (pct / maxSizePct * 100) : 0;
|
|
3822
|
+
|
|
3823
|
+
html += `<div class="distribution-bar-row">
|
|
3824
|
+
<div class="distribution-bar-label">
|
|
3825
|
+
<span class="size-label">${label}</span>
|
|
3826
|
+
</div>
|
|
3827
|
+
<div class="distribution-bar-container">
|
|
3828
|
+
<div class="distribution-bar-fill" style="width: ${barWidth}%; background: ${color}"></div>
|
|
3829
|
+
</div>
|
|
3830
|
+
<div class="distribution-bar-value">${pct.toFixed(1)}%</div>
|
|
3831
|
+
</div>`;
|
|
3832
|
+
});
|
|
3833
|
+
|
|
3834
|
+
html += '</div></div>';
|
|
3835
|
+
|
|
3836
|
+
// Historical Trends Section
|
|
3837
|
+
html += '<div class="trends-section">';
|
|
3838
|
+
html += '<h3 class="section-title">📊 Historical Trends</h3>';
|
|
3839
|
+
|
|
3840
|
+
// Check if trend data is available
|
|
3841
|
+
if (window.graphTrendData && window.graphTrendData.entries && window.graphTrendData.entries.length > 0) {
|
|
3842
|
+
const trendEntries = window.graphTrendData.entries;
|
|
3843
|
+
|
|
3844
|
+
// Render trend charts
|
|
3845
|
+
html += '<div class="trends-container">';
|
|
3846
|
+
html += '<div id="health-score-chart" class="trend-chart"></div>';
|
|
3847
|
+
html += '<div id="complexity-chart" class="trend-chart"></div>';
|
|
3848
|
+
html += '<div id="files-chunks-chart" class="trend-chart"></div>';
|
|
3849
|
+
html += '</div>';
|
|
3850
|
+
|
|
3851
|
+
html += `<div class="trend-info">Showing ${trendEntries.length} data points from ${trendEntries[0].date} to ${trendEntries[trendEntries.length - 1].date}</div>`;
|
|
3852
|
+
} else {
|
|
3853
|
+
// No trend data yet - show placeholder
|
|
3854
|
+
html += '<div class="future-placeholder">';
|
|
3855
|
+
html += '<div class="future-icon">📊</div>';
|
|
3856
|
+
html += '<div class="future-title">No Historical Data Yet</div>';
|
|
3857
|
+
html += '<div class="future-description">';
|
|
3858
|
+
html += 'Trend data will be collected automatically after each indexing operation. ';
|
|
3859
|
+
html += 'Run <code>mcp-vector-search index</code> to generate the first snapshot.';
|
|
3860
|
+
html += '</div>';
|
|
3861
|
+
html += '</div>';
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
html += '</div>'; // trends-section
|
|
3865
|
+
|
|
3866
|
+
html += '</div>'; // trends-report
|
|
3867
|
+
|
|
3868
|
+
viewerContent.innerHTML = html;
|
|
3869
|
+
|
|
3870
|
+
// Hide section dropdown for reports (no code sections)
|
|
3871
|
+
const sectionNav = document.getElementById('section-nav');
|
|
3872
|
+
if (sectionNav) {
|
|
3873
|
+
sectionNav.style.display = 'none';
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
// Render D3 charts if trend data is available
|
|
3877
|
+
if (window.graphTrendData && window.graphTrendData.entries && window.graphTrendData.entries.length > 0) {
|
|
3878
|
+
renderTrendCharts(window.graphTrendData.entries);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
// ============================================================================
|
|
3883
|
+
// REMEDIATION REPORT GENERATION
|
|
3884
|
+
// ============================================================================
|
|
3885
|
+
|
|
3886
|
+
function generateRemediationReport() {
|
|
3887
|
+
// Gather complexity data (metrics are stored directly on nodes, not in a metrics object)
|
|
3888
|
+
const complexityData = [];
|
|
3889
|
+
allNodes.forEach(node => {
|
|
3890
|
+
if (node.complexity !== undefined && node.complexity !== null) {
|
|
3891
|
+
const complexity = node.complexity;
|
|
3892
|
+
let grade = 'A';
|
|
3893
|
+
if (complexity > 40) grade = 'F';
|
|
3894
|
+
else if (complexity > 30) grade = 'D';
|
|
3895
|
+
else if (complexity > 20) grade = 'C';
|
|
3896
|
+
else if (complexity > 10) grade = 'B';
|
|
3897
|
+
|
|
3898
|
+
// Calculate lines from start_line and end_line
|
|
3899
|
+
const lines = (node.end_line && node.start_line) ? (node.end_line - node.start_line + 1) : 0;
|
|
3900
|
+
|
|
3901
|
+
if (grade !== 'A' && grade !== 'B') { // Only include C, D, F
|
|
3902
|
+
complexityData.push({
|
|
3903
|
+
name: node.name || node.id,
|
|
3904
|
+
file: node.file_path || 'Unknown',
|
|
3905
|
+
type: node.type || 'unknown',
|
|
3906
|
+
complexity: complexity,
|
|
3907
|
+
grade: grade,
|
|
3908
|
+
lines: lines
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
});
|
|
3913
|
+
|
|
3914
|
+
// Gather code smell data
|
|
3915
|
+
const smells = [];
|
|
3916
|
+
allNodes.forEach(node => {
|
|
3917
|
+
const file = node.file_path || 'Unknown';
|
|
3918
|
+
const name = node.name || node.id;
|
|
3919
|
+
const lines = (node.end_line && node.start_line) ? (node.end_line - node.start_line + 1) : 0;
|
|
3920
|
+
const complexity = node.complexity || 0;
|
|
3921
|
+
const depth = node.depth || 0;
|
|
3922
|
+
|
|
3923
|
+
// Long Method
|
|
3924
|
+
if (lines > 50) {
|
|
3925
|
+
smells.push({
|
|
3926
|
+
file, name,
|
|
3927
|
+
smell: 'Long Method',
|
|
3928
|
+
severity: lines > 100 ? 'error' : 'warning',
|
|
3929
|
+
detail: `${lines} lines (recommended: <50)`
|
|
3930
|
+
});
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
// High Complexity
|
|
3934
|
+
if (complexity > 15) {
|
|
3935
|
+
smells.push({
|
|
3936
|
+
file, name,
|
|
3937
|
+
smell: 'High Complexity',
|
|
3938
|
+
severity: complexity > 25 ? 'error' : 'warning',
|
|
3939
|
+
detail: `Complexity: ${complexity} (recommended: <15)`
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
// Deep Nesting (using depth field)
|
|
3944
|
+
if (depth > 4) {
|
|
3945
|
+
smells.push({
|
|
3946
|
+
file, name,
|
|
3947
|
+
smell: 'Deep Nesting',
|
|
3948
|
+
severity: depth > 6 ? 'error' : 'warning',
|
|
3949
|
+
detail: `Depth: ${depth} (recommended: <4)`
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
// God Class (for classes only)
|
|
3954
|
+
if (node.type === 'class' && lines > 300) {
|
|
3955
|
+
smells.push({
|
|
3956
|
+
file, name,
|
|
3957
|
+
smell: 'God Class',
|
|
3958
|
+
severity: 'error',
|
|
3959
|
+
detail: `${lines} lines - consider breaking into smaller classes`
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
});
|
|
3963
|
+
|
|
3964
|
+
// Sort by severity (errors first) then by file
|
|
3965
|
+
complexityData.sort((a, b) => {
|
|
3966
|
+
const gradeOrder = { F: 0, D: 1, C: 2 };
|
|
3967
|
+
return (gradeOrder[a.grade] || 99) - (gradeOrder[b.grade] || 99);
|
|
3968
|
+
});
|
|
3969
|
+
|
|
3970
|
+
smells.sort((a, b) => {
|
|
3971
|
+
if (a.severity !== b.severity) {
|
|
3972
|
+
return a.severity === 'error' ? -1 : 1;
|
|
3973
|
+
}
|
|
3974
|
+
return a.file.localeCompare(b.file);
|
|
3975
|
+
});
|
|
3976
|
+
|
|
3977
|
+
// Generate Markdown
|
|
3978
|
+
const date = new Date().toISOString().split('T')[0];
|
|
3979
|
+
let markdown = `# Code Remediation Report
|
|
3980
|
+
Generated: ${date}
|
|
3981
|
+
|
|
3982
|
+
## Summary
|
|
3983
|
+
|
|
3984
|
+
- **High Complexity Items**: ${complexityData.length}
|
|
3985
|
+
- **Code Smells Detected**: ${smells.length}
|
|
3986
|
+
- **Critical Issues (Errors)**: ${smells.filter(s => s.severity === 'error').length}
|
|
3987
|
+
|
|
3988
|
+
---
|
|
3989
|
+
|
|
3990
|
+
## 🔴 Priority: High Complexity Code
|
|
3991
|
+
|
|
3992
|
+
These functions/methods have complexity scores that make them difficult to maintain and test.
|
|
3993
|
+
|
|
3994
|
+
| Grade | Name | File | Complexity | Lines |
|
|
3995
|
+
|-------|------|------|------------|-------|
|
|
3996
|
+
`;
|
|
3997
|
+
|
|
3998
|
+
complexityData.forEach(item => {
|
|
3999
|
+
const gradeEmoji = item.grade === 'F' ? '🔴' : item.grade === 'D' ? '🟠' : '🟡';
|
|
4000
|
+
markdown += `| ${gradeEmoji} ${item.grade} | \\`${item.name}\\` | ${item.file} | ${item.complexity} | ${item.lines} |\\n`;
|
|
4001
|
+
});
|
|
4002
|
+
|
|
4003
|
+
markdown += `
|
|
4004
|
+
---
|
|
4005
|
+
|
|
4006
|
+
## 🔍 Code Smells
|
|
4007
|
+
|
|
4008
|
+
### Critical Issues (Errors)
|
|
4009
|
+
|
|
4010
|
+
`;
|
|
4011
|
+
|
|
4012
|
+
const errors = smells.filter(s => s.severity === 'error');
|
|
4013
|
+
if (errors.length === 0) {
|
|
4014
|
+
markdown += '_No critical issues found._\\n';
|
|
4015
|
+
} else {
|
|
4016
|
+
markdown += '| Smell | Name | File | Detail |\\n|-------|------|------|--------|\\n';
|
|
4017
|
+
errors.forEach(s => {
|
|
4018
|
+
markdown += `| 🔴 ${s.smell} | \\`${s.name}\\` | ${s.file} | ${s.detail} |\\n`;
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
markdown += `
|
|
4023
|
+
### Warnings
|
|
4024
|
+
|
|
4025
|
+
`;
|
|
4026
|
+
|
|
4027
|
+
const warnings = smells.filter(s => s.severity === 'warning');
|
|
4028
|
+
if (warnings.length === 0) {
|
|
4029
|
+
markdown += '_No warnings found._\\n';
|
|
4030
|
+
} else {
|
|
4031
|
+
markdown += '| Smell | Name | File | Detail |\\n|-------|------|------|--------|\\n';
|
|
4032
|
+
warnings.forEach(s => {
|
|
4033
|
+
markdown += `| 🟡 ${s.smell} | \\`${s.name}\\` | ${s.file} | ${s.detail} |\\n`;
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
markdown += `
|
|
4038
|
+
---
|
|
4039
|
+
|
|
4040
|
+
## Recommended Actions
|
|
4041
|
+
|
|
4042
|
+
1. **Start with Grade F items** - These have the highest complexity and are hardest to maintain
|
|
4043
|
+
2. **Address Critical code smells** - God Classes and deeply nested code should be refactored
|
|
4044
|
+
3. **Break down long methods** - Extract helper functions to reduce complexity
|
|
4045
|
+
4. **Add tests before refactoring** - Ensure behavior is preserved
|
|
4046
|
+
|
|
4047
|
+
---
|
|
4048
|
+
|
|
4049
|
+
_Generated by MCP Vector Search Visualization_
|
|
4050
|
+
`;
|
|
4051
|
+
|
|
4052
|
+
// Save the file with dialog (or fallback to download)
|
|
4053
|
+
const blob = new Blob([markdown], { type: 'text/markdown' });
|
|
4054
|
+
const defaultFilename = `remediation-report-${date}.md`;
|
|
4055
|
+
|
|
4056
|
+
async function saveWithDialog() {
|
|
4057
|
+
try {
|
|
4058
|
+
// Use File System Access API if available (shows save dialog)
|
|
4059
|
+
if ('showSaveFilePicker' in window) {
|
|
4060
|
+
const handle = await window.showSaveFilePicker({
|
|
4061
|
+
suggestedName: defaultFilename,
|
|
4062
|
+
types: [{
|
|
4063
|
+
description: 'Markdown files',
|
|
4064
|
+
accept: { 'text/markdown': ['.md'] }
|
|
4065
|
+
}]
|
|
4066
|
+
});
|
|
4067
|
+
const writable = await handle.createWritable();
|
|
4068
|
+
await writable.write(blob);
|
|
4069
|
+
await writable.close();
|
|
4070
|
+
return handle.name;
|
|
4071
|
+
}
|
|
4072
|
+
} catch (err) {
|
|
4073
|
+
if (err.name === 'AbortError') {
|
|
4074
|
+
return null; // User cancelled
|
|
4075
|
+
}
|
|
4076
|
+
console.warn('Save dialog failed, falling back to download:', err);
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
// Fallback: standard download
|
|
4080
|
+
const url = URL.createObjectURL(blob);
|
|
4081
|
+
const a = document.createElement('a');
|
|
4082
|
+
a.href = url;
|
|
4083
|
+
a.download = defaultFilename;
|
|
4084
|
+
document.body.appendChild(a);
|
|
4085
|
+
a.click();
|
|
4086
|
+
document.body.removeChild(a);
|
|
4087
|
+
URL.revokeObjectURL(url);
|
|
4088
|
+
return defaultFilename;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
saveWithDialog().then(savedFilename => {
|
|
4092
|
+
if (savedFilename === null) {
|
|
4093
|
+
// User cancelled - don't show confirmation
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
// Show confirmation
|
|
4098
|
+
openViewerPanel();
|
|
4099
|
+
document.getElementById('viewer-title').textContent = '📋 Report Saved';
|
|
4100
|
+
document.getElementById('viewer-content').innerHTML = `
|
|
4101
|
+
<div class="report-section">
|
|
4102
|
+
<h3>✅ Remediation Report Generated</h3>
|
|
4103
|
+
<p>The report has been saved as <code>${escapeHtml(savedFilename)}</code></p>
|
|
4104
|
+
<div class="metrics-grid">
|
|
4105
|
+
<div class="metric-card">
|
|
4106
|
+
<div class="metric-value">${complexityData.length}</div>
|
|
4107
|
+
<div class="metric-label">Complexity Issues</div>
|
|
4108
|
+
</div>
|
|
4109
|
+
<div class="metric-card">
|
|
4110
|
+
<div class="metric-value">${smells.length}</div>
|
|
4111
|
+
<div class="metric-label">Code Smells</div>
|
|
4112
|
+
</div>
|
|
4113
|
+
<div class="metric-card">
|
|
4114
|
+
<div class="metric-value">${errors.length}</div>
|
|
4115
|
+
<div class="metric-label">Critical Errors</div>
|
|
4116
|
+
</div>
|
|
4117
|
+
</div>
|
|
4118
|
+
<p style="margin-top: 15px; color: var(--text-secondary);">Share this report with your team for prioritized remediation.</p>
|
|
4119
|
+
</div>
|
|
4120
|
+
`;
|
|
4121
|
+
|
|
4122
|
+
// Hide section dropdown for reports (no code sections)
|
|
4123
|
+
const sectionNav = document.getElementById('section-nav');
|
|
4124
|
+
if (sectionNav) {
|
|
4125
|
+
sectionNav.style.display = 'none';
|
|
4126
|
+
}
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
// Render trend line charts using D3
|
|
4131
|
+
function renderTrendCharts(entries) {
|
|
4132
|
+
// Chart dimensions
|
|
4133
|
+
const margin = {top: 20, right: 30, bottom: 40, left: 50};
|
|
4134
|
+
const width = 600 - margin.left - margin.right;
|
|
4135
|
+
const height = 250 - margin.top - margin.bottom;
|
|
4136
|
+
|
|
4137
|
+
// Parse dates
|
|
4138
|
+
const parseDate = d3.timeParse('%Y-%m-%d');
|
|
4139
|
+
entries.forEach(d => {
|
|
4140
|
+
d.parsedDate = parseDate(d.date);
|
|
4141
|
+
});
|
|
4142
|
+
|
|
4143
|
+
// 1. Health Score Chart
|
|
4144
|
+
renderLineChart('#health-score-chart', entries, {
|
|
4145
|
+
title: 'Health Score Over Time',
|
|
4146
|
+
width, height, margin,
|
|
4147
|
+
yAccessor: d => d.metrics.health_score || 0,
|
|
4148
|
+
yLabel: 'Health Score',
|
|
4149
|
+
color: '#238636',
|
|
4150
|
+
yDomain: [0, 100]
|
|
4151
|
+
});
|
|
4152
|
+
|
|
4153
|
+
// 2. Average Complexity Chart
|
|
4154
|
+
renderLineChart('#complexity-chart', entries, {
|
|
4155
|
+
title: 'Average Complexity Over Time',
|
|
4156
|
+
width, height, margin,
|
|
4157
|
+
yAccessor: d => d.metrics.avg_complexity || 0,
|
|
4158
|
+
yLabel: 'Avg Complexity',
|
|
4159
|
+
color: '#d29922',
|
|
4160
|
+
yDomain: [0, d3.max(entries, d => d.metrics.avg_complexity || 0) * 1.1]
|
|
4161
|
+
});
|
|
4162
|
+
|
|
4163
|
+
// 3. Files and Chunks Chart (dual line)
|
|
4164
|
+
renderDualLineChart('#files-chunks-chart', entries, {
|
|
4165
|
+
title: 'Files and Chunks Over Time',
|
|
4166
|
+
width, height, margin,
|
|
4167
|
+
y1Accessor: d => d.metrics.total_files || 0,
|
|
4168
|
+
y2Accessor: d => d.metrics.total_chunks || 0,
|
|
4169
|
+
y1Label: 'Files',
|
|
4170
|
+
y2Label: 'Chunks',
|
|
4171
|
+
color1: '#1f6feb',
|
|
4172
|
+
color2: '#8957e5'
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
// Render single line chart
|
|
4177
|
+
function renderLineChart(selector, data, config) {
|
|
4178
|
+
const svg = d3.select(selector)
|
|
4179
|
+
.append('svg')
|
|
4180
|
+
.attr('width', config.width + config.margin.left + config.margin.right)
|
|
4181
|
+
.attr('height', config.height + config.margin.top + config.margin.bottom)
|
|
4182
|
+
.append('g')
|
|
4183
|
+
.attr('transform', `translate(${config.margin.left},${config.margin.top})`);
|
|
4184
|
+
|
|
4185
|
+
// Add title
|
|
4186
|
+
svg.append('text')
|
|
4187
|
+
.attr('x', config.width / 2)
|
|
4188
|
+
.attr('y', -5)
|
|
4189
|
+
.attr('text-anchor', 'middle')
|
|
4190
|
+
.style('font-size', '14px')
|
|
4191
|
+
.style('font-weight', 'bold')
|
|
4192
|
+
.text(config.title);
|
|
4193
|
+
|
|
4194
|
+
// Create scales
|
|
4195
|
+
const xScale = d3.scaleTime()
|
|
4196
|
+
.domain(d3.extent(data, d => d.parsedDate))
|
|
4197
|
+
.range([0, config.width]);
|
|
4198
|
+
|
|
4199
|
+
const yScale = d3.scaleLinear()
|
|
4200
|
+
.domain(config.yDomain || [0, d3.max(data, config.yAccessor)])
|
|
4201
|
+
.range([config.height, 0]);
|
|
4202
|
+
|
|
4203
|
+
// Create line generator
|
|
4204
|
+
const line = d3.line()
|
|
4205
|
+
.x(d => xScale(d.parsedDate))
|
|
4206
|
+
.y(d => yScale(config.yAccessor(d)))
|
|
4207
|
+
.curve(d3.curveMonotoneX);
|
|
4208
|
+
|
|
4209
|
+
// Add X axis
|
|
4210
|
+
svg.append('g')
|
|
4211
|
+
.attr('transform', `translate(0,${config.height})`)
|
|
4212
|
+
.call(d3.axisBottom(xScale).ticks(5).tickFormat(d3.timeFormat('%b %d')))
|
|
4213
|
+
.selectAll('text')
|
|
4214
|
+
.style('font-size', '11px');
|
|
4215
|
+
|
|
4216
|
+
// Add Y axis
|
|
4217
|
+
svg.append('g')
|
|
4218
|
+
.call(d3.axisLeft(yScale).ticks(5))
|
|
4219
|
+
.selectAll('text')
|
|
4220
|
+
.style('font-size', '11px');
|
|
4221
|
+
|
|
4222
|
+
// Add Y axis label
|
|
4223
|
+
svg.append('text')
|
|
4224
|
+
.attr('transform', 'rotate(-90)')
|
|
4225
|
+
.attr('y', 0 - config.margin.left + 10)
|
|
4226
|
+
.attr('x', 0 - (config.height / 2))
|
|
4227
|
+
.attr('dy', '1em')
|
|
4228
|
+
.style('text-anchor', 'middle')
|
|
4229
|
+
.style('font-size', '12px')
|
|
4230
|
+
.text(config.yLabel);
|
|
4231
|
+
|
|
4232
|
+
// Add line path
|
|
4233
|
+
svg.append('path')
|
|
4234
|
+
.datum(data)
|
|
4235
|
+
.attr('fill', 'none')
|
|
4236
|
+
.attr('stroke', config.color)
|
|
4237
|
+
.attr('stroke-width', 2)
|
|
4238
|
+
.attr('d', line);
|
|
4239
|
+
|
|
4240
|
+
// Add dots
|
|
4241
|
+
svg.selectAll('.dot')
|
|
4242
|
+
.data(data)
|
|
4243
|
+
.enter().append('circle')
|
|
4244
|
+
.attr('class', 'dot')
|
|
4245
|
+
.attr('cx', d => xScale(d.parsedDate))
|
|
4246
|
+
.attr('cy', d => yScale(config.yAccessor(d)))
|
|
4247
|
+
.attr('r', 4)
|
|
4248
|
+
.attr('fill', config.color)
|
|
4249
|
+
.style('cursor', 'pointer')
|
|
4250
|
+
.append('title')
|
|
4251
|
+
.text(d => `${d.date}: ${config.yAccessor(d).toFixed(1)}`);
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
// Render dual line chart (two Y axes)
|
|
4255
|
+
function renderDualLineChart(selector, data, config) {
|
|
4256
|
+
const svg = d3.select(selector)
|
|
4257
|
+
.append('svg')
|
|
4258
|
+
.attr('width', config.width + config.margin.left + config.margin.right)
|
|
4259
|
+
.attr('height', config.height + config.margin.top + config.margin.bottom)
|
|
4260
|
+
.append('g')
|
|
4261
|
+
.attr('transform', `translate(${config.margin.left},${config.margin.top})`);
|
|
4262
|
+
|
|
4263
|
+
// Add title
|
|
4264
|
+
svg.append('text')
|
|
4265
|
+
.attr('x', config.width / 2)
|
|
4266
|
+
.attr('y', -5)
|
|
4267
|
+
.attr('text-anchor', 'middle')
|
|
4268
|
+
.style('font-size', '14px')
|
|
4269
|
+
.style('font-weight', 'bold')
|
|
4270
|
+
.text(config.title);
|
|
4271
|
+
|
|
4272
|
+
// Create scales
|
|
4273
|
+
const xScale = d3.scaleTime()
|
|
4274
|
+
.domain(d3.extent(data, d => d.parsedDate))
|
|
4275
|
+
.range([0, config.width]);
|
|
4276
|
+
|
|
4277
|
+
const y1Scale = d3.scaleLinear()
|
|
4278
|
+
.domain([0, d3.max(data, config.y1Accessor) * 1.1])
|
|
4279
|
+
.range([config.height, 0]);
|
|
4280
|
+
|
|
4281
|
+
const y2Scale = d3.scaleLinear()
|
|
4282
|
+
.domain([0, d3.max(data, config.y2Accessor) * 1.1])
|
|
4283
|
+
.range([config.height, 0]);
|
|
4284
|
+
|
|
4285
|
+
// Create line generators
|
|
4286
|
+
const line1 = d3.line()
|
|
4287
|
+
.x(d => xScale(d.parsedDate))
|
|
4288
|
+
.y(d => y1Scale(config.y1Accessor(d)))
|
|
4289
|
+
.curve(d3.curveMonotoneX);
|
|
4290
|
+
|
|
4291
|
+
const line2 = d3.line()
|
|
4292
|
+
.x(d => xScale(d.parsedDate))
|
|
4293
|
+
.y(d => y2Scale(config.y2Accessor(d)))
|
|
4294
|
+
.curve(d3.curveMonotoneX);
|
|
4295
|
+
|
|
4296
|
+
// Add X axis
|
|
4297
|
+
svg.append('g')
|
|
4298
|
+
.attr('transform', `translate(0,${config.height})`)
|
|
4299
|
+
.call(d3.axisBottom(xScale).ticks(5).tickFormat(d3.timeFormat('%b %d')))
|
|
4300
|
+
.selectAll('text')
|
|
4301
|
+
.style('font-size', '11px');
|
|
4302
|
+
|
|
4303
|
+
// Add Y1 axis (left)
|
|
4304
|
+
svg.append('g')
|
|
4305
|
+
.call(d3.axisLeft(y1Scale).ticks(5))
|
|
4306
|
+
.selectAll('text')
|
|
4307
|
+
.style('font-size', '11px')
|
|
4308
|
+
.style('fill', config.color1);
|
|
4309
|
+
|
|
4310
|
+
// Add Y2 axis (right)
|
|
4311
|
+
svg.append('g')
|
|
4312
|
+
.attr('transform', `translate(${config.width},0)`)
|
|
4313
|
+
.call(d3.axisRight(y2Scale).ticks(5))
|
|
4314
|
+
.selectAll('text')
|
|
4315
|
+
.style('font-size', '11px')
|
|
4316
|
+
.style('fill', config.color2);
|
|
4317
|
+
|
|
4318
|
+
// Add line 1
|
|
4319
|
+
svg.append('path')
|
|
4320
|
+
.datum(data)
|
|
4321
|
+
.attr('fill', 'none')
|
|
4322
|
+
.attr('stroke', config.color1)
|
|
4323
|
+
.attr('stroke-width', 2)
|
|
4324
|
+
.attr('d', line1);
|
|
4325
|
+
|
|
4326
|
+
// Add line 2
|
|
4327
|
+
svg.append('path')
|
|
4328
|
+
.datum(data)
|
|
4329
|
+
.attr('fill', 'none')
|
|
4330
|
+
.attr('stroke', config.color2)
|
|
4331
|
+
.attr('stroke-width', 2)
|
|
4332
|
+
.attr('stroke-dasharray', '5,5')
|
|
4333
|
+
.attr('d', line2);
|
|
4334
|
+
|
|
4335
|
+
// Add legend
|
|
4336
|
+
const legend = svg.append('g')
|
|
4337
|
+
.attr('transform', `translate(${config.width - 120}, 10)`);
|
|
4338
|
+
|
|
4339
|
+
legend.append('line')
|
|
4340
|
+
.attr('x1', 0).attr('x2', 20)
|
|
4341
|
+
.attr('y1', 5).attr('y2', 5)
|
|
4342
|
+
.attr('stroke', config.color1)
|
|
4343
|
+
.attr('stroke-width', 2);
|
|
4344
|
+
legend.append('text')
|
|
4345
|
+
.attr('x', 25).attr('y', 9)
|
|
4346
|
+
.style('font-size', '11px')
|
|
4347
|
+
.text(config.y1Label);
|
|
4348
|
+
|
|
4349
|
+
legend.append('line')
|
|
4350
|
+
.attr('x1', 0).attr('x2', 20)
|
|
4351
|
+
.attr('y1', 20).attr('y2', 20)
|
|
4352
|
+
.attr('stroke', config.color2)
|
|
4353
|
+
.attr('stroke-width', 2)
|
|
4354
|
+
.attr('stroke-dasharray', '5,5');
|
|
4355
|
+
legend.append('text')
|
|
4356
|
+
.attr('x', 25).attr('y', 24)
|
|
4357
|
+
.style('font-size', '11px')
|
|
4358
|
+
.text(config.y2Label);
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
function calculateCodebaseMetrics() {
|
|
4362
|
+
const metrics = {
|
|
4363
|
+
totalLines: 0,
|
|
4364
|
+
totalFunctions: 0,
|
|
4365
|
+
totalClasses: 0,
|
|
4366
|
+
totalFiles: 0,
|
|
4367
|
+
complexityDistribution: { A: 0, B: 0, C: 0, D: 0, F: 0 },
|
|
4368
|
+
sizeDistribution: { small: 0, medium: 0, large: 0, veryLarge: 0 },
|
|
4369
|
+
healthScore: 0
|
|
4370
|
+
};
|
|
4371
|
+
|
|
4372
|
+
const chunksWithComplexity = [];
|
|
4373
|
+
|
|
4374
|
+
function analyzeNode(node) {
|
|
4375
|
+
// Count files
|
|
4376
|
+
if (node.type === 'file') {
|
|
4377
|
+
metrics.totalFiles++;
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
// Count classes
|
|
4381
|
+
if (node.type === 'class') {
|
|
4382
|
+
metrics.totalClasses++;
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
// Count functions and methods
|
|
4386
|
+
if (node.type === 'function' || node.type === 'method') {
|
|
4387
|
+
metrics.totalFunctions++;
|
|
4388
|
+
|
|
4389
|
+
// Calculate lines
|
|
4390
|
+
const lineCount = (node.end_line && node.start_line)
|
|
4391
|
+
? node.end_line - node.start_line + 1
|
|
4392
|
+
: 0;
|
|
4393
|
+
metrics.totalLines += lineCount;
|
|
4394
|
+
|
|
4395
|
+
// Size distribution
|
|
4396
|
+
if (lineCount <= 20) {
|
|
4397
|
+
metrics.sizeDistribution.small++;
|
|
4398
|
+
} else if (lineCount <= 50) {
|
|
4399
|
+
metrics.sizeDistribution.medium++;
|
|
4400
|
+
} else if (lineCount <= 100) {
|
|
4401
|
+
metrics.sizeDistribution.large++;
|
|
4402
|
+
} else {
|
|
4403
|
+
metrics.sizeDistribution.veryLarge++;
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
// Complexity distribution
|
|
4407
|
+
if (node.complexity !== undefined && node.complexity !== null) {
|
|
4408
|
+
const grade = getComplexityGrade(node.complexity);
|
|
4409
|
+
if (grade in metrics.complexityDistribution) {
|
|
4410
|
+
metrics.complexityDistribution[grade]++;
|
|
4411
|
+
}
|
|
4412
|
+
chunksWithComplexity.push(node);
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
// Recursively process children
|
|
4417
|
+
const children = node.children || node._children || [];
|
|
4418
|
+
children.forEach(child => analyzeNode(child));
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
if (treeData) {
|
|
4422
|
+
analyzeNode(treeData);
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
// Convert complexity counts to percentages
|
|
4426
|
+
const totalWithComplexity = chunksWithComplexity.length;
|
|
4427
|
+
if (totalWithComplexity > 0) {
|
|
4428
|
+
Object.keys(metrics.complexityDistribution).forEach(grade => {
|
|
4429
|
+
metrics.complexityDistribution[grade] =
|
|
4430
|
+
(metrics.complexityDistribution[grade] / totalWithComplexity) * 100;
|
|
4431
|
+
});
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
// Convert size counts to percentages
|
|
4435
|
+
const totalFuncs = metrics.totalFunctions;
|
|
4436
|
+
if (totalFuncs > 0) {
|
|
4437
|
+
Object.keys(metrics.sizeDistribution).forEach(size => {
|
|
4438
|
+
metrics.sizeDistribution[size] =
|
|
4439
|
+
(metrics.sizeDistribution[size] / totalFuncs) * 100;
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
// Calculate health score
|
|
4444
|
+
metrics.healthScore = calculateHealthScore(chunksWithComplexity);
|
|
4445
|
+
|
|
4446
|
+
return metrics;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
function calculateHealthScore(chunks) {
|
|
4450
|
+
if (chunks.length === 0) return 100;
|
|
4451
|
+
|
|
4452
|
+
let score = 0;
|
|
4453
|
+
chunks.forEach(chunk => {
|
|
4454
|
+
const grade = getComplexityGrade(chunk.complexity);
|
|
4455
|
+
const gradeScores = { A: 100, B: 80, C: 60, D: 40, F: 20 };
|
|
4456
|
+
score += gradeScores[grade] || 50;
|
|
4457
|
+
});
|
|
4458
|
+
|
|
4459
|
+
return Math.round(score / chunks.length);
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
function getHealthScoreInfo(score) {
|
|
4463
|
+
if (score >= 80) {
|
|
4464
|
+
return {
|
|
4465
|
+
emoji: '🟢',
|
|
4466
|
+
label: 'Excellent',
|
|
4467
|
+
color: '#238636',
|
|
4468
|
+
description: 'Your codebase has excellent complexity distribution with most code in the A-B range.'
|
|
4469
|
+
};
|
|
4470
|
+
} else if (score >= 60) {
|
|
4471
|
+
return {
|
|
4472
|
+
emoji: '🟡',
|
|
4473
|
+
label: 'Good',
|
|
4474
|
+
color: '#d29922',
|
|
4475
|
+
description: 'Your codebase is in good shape, but could benefit from refactoring some complex functions.'
|
|
4476
|
+
};
|
|
4477
|
+
} else if (score >= 40) {
|
|
4478
|
+
return {
|
|
4479
|
+
emoji: '🟠',
|
|
4480
|
+
label: 'Needs Attention',
|
|
4481
|
+
color: '#f0883e',
|
|
4482
|
+
description: 'Your codebase has significant complexity issues that should be addressed soon.'
|
|
4483
|
+
};
|
|
4484
|
+
} else {
|
|
4485
|
+
return {
|
|
4486
|
+
emoji: '🔴',
|
|
4487
|
+
label: 'Critical',
|
|
4488
|
+
color: '#da3633',
|
|
4489
|
+
description: 'Your codebase has critical complexity issues requiring immediate refactoring.'
|
|
4490
|
+
};
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
function getComplexityRange(grade) {
|
|
4495
|
+
const ranges = {
|
|
4496
|
+
'A': '1-5',
|
|
4497
|
+
'B': '6-10',
|
|
4498
|
+
'C': '11-15',
|
|
4499
|
+
'D': '16-20',
|
|
4500
|
+
'F': '21+'
|
|
4501
|
+
};
|
|
4502
|
+
return ranges[grade] || '';
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
function showComingSoon(reportName) {
|
|
4506
|
+
alert(`${reportName} - Coming Soon!\n\nThis feature will display detailed ${reportName.toLowerCase()} in a future release.`);
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
// ============================================================================
|
|
4510
|
+
// INITIALIZATION
|
|
4511
|
+
// ============================================================================
|
|
4512
|
+
|
|
4513
|
+
// Load data and initialize UI when page loads
|
|
4514
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
4515
|
+
console.log('=== PAGE INITIALIZATION ===');
|
|
4516
|
+
console.log('DOMContentLoaded event fired');
|
|
4517
|
+
|
|
4518
|
+
// Load theme preference before anything else
|
|
4519
|
+
loadThemePreference();
|
|
4520
|
+
|
|
4521
|
+
// Initialize toggle label highlighting
|
|
4522
|
+
const labels = document.querySelectorAll('.toggle-label');
|
|
4523
|
+
console.log(`Found ${labels.length} toggle labels`);
|
|
4524
|
+
if (labels[0]) {
|
|
4525
|
+
labels[0].classList.add('active');
|
|
4526
|
+
console.log('Activated first toggle label (linear mode)');
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
// Close search results when clicking outside
|
|
4530
|
+
document.addEventListener('click', (event) => {
|
|
4531
|
+
const searchContainer = document.querySelector('.search-container');
|
|
4532
|
+
if (searchContainer && !searchContainer.contains(event.target)) {
|
|
4533
|
+
closeSearchResults();
|
|
4534
|
+
}
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4537
|
+
// Load graph data
|
|
4538
|
+
console.log('Calling loadGraphData()...');
|
|
4539
|
+
loadGraphData();
|
|
4540
|
+
console.log('=== END PAGE INITIALIZATION ===');
|
|
4541
|
+
});
|
|
4542
|
+
"""
|