mcp-vector-search 0.12.6__py3-none-any.whl → 1.0.3__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 +2 -2
- mcp_vector_search/analysis/__init__.py +64 -0
- mcp_vector_search/analysis/collectors/__init__.py +39 -0
- mcp_vector_search/analysis/collectors/base.py +164 -0
- mcp_vector_search/analysis/collectors/complexity.py +743 -0
- mcp_vector_search/analysis/metrics.py +341 -0
- mcp_vector_search/analysis/reporters/__init__.py +5 -0
- mcp_vector_search/analysis/reporters/console.py +222 -0
- mcp_vector_search/cli/commands/analyze.py +408 -0
- mcp_vector_search/cli/commands/chat.py +1262 -0
- mcp_vector_search/cli/commands/index.py +21 -3
- 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 +30 -7
- mcp_vector_search/cli/commands/setup.py +1133 -0
- mcp_vector_search/cli/commands/status.py +37 -2
- 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 +276 -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 +29 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +714 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +311 -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 +180 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +2507 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +1313 -0
- mcp_vector_search/cli/commands/visualize.py.original +2536 -0
- mcp_vector_search/cli/didyoumean.py +22 -2
- mcp_vector_search/cli/main.py +115 -159
- mcp_vector_search/cli/output.py +24 -8
- 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 +185 -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 +369 -94
- mcp_vector_search/core/exceptions.py +11 -0
- mcp_vector_search/core/git_hooks.py +4 -4
- mcp_vector_search/core/indexer.py +221 -4
- mcp_vector_search/core/llm_client.py +751 -0
- mcp_vector_search/core/models.py +3 -0
- mcp_vector_search/core/project.py +17 -0
- mcp_vector_search/core/scheduler.py +11 -11
- mcp_vector_search/core/search.py +179 -29
- mcp_vector_search/mcp/server.py +24 -5
- mcp_vector_search/utils/__init__.py +2 -0
- 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.0.3.dist-info}/METADATA +182 -52
- mcp_vector_search-1.0.3.dist-info/RECORD +97 -0
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.0.3.dist-info}/WHEEL +1 -1
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.0.3.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.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,2507 @@
|
|
|
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
|
+
// Chunk types for code nodes (function, class, method, text, imports, module)
|
|
54
|
+
const chunkTypes = ['function', 'class', 'method', 'text', 'imports', 'module'];
|
|
55
|
+
|
|
56
|
+
// Size scaling configuration
|
|
57
|
+
const sizeConfig = {
|
|
58
|
+
minRadius: 12, // Minimum node radius (50% larger for readability)
|
|
59
|
+
maxRadius: 24, // Maximum node radius
|
|
60
|
+
chunkMinRadius: 9, // Minimum for chunks (50% larger for readability)
|
|
61
|
+
chunkMaxRadius: 16 // Maximum for chunks
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Dynamic dimensions that update when viewer opens/closes
|
|
65
|
+
function getViewportDimensions() {
|
|
66
|
+
const container = document.getElementById('main-container');
|
|
67
|
+
return {
|
|
68
|
+
width: container.clientWidth,
|
|
69
|
+
height: container.clientHeight
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const margin = {top: 40, right: 120, bottom: 20, left: 120};
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// DATA LOADING
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
async function loadGraphData() {
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch('/api/graph');
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
allNodes = data.nodes || [];
|
|
84
|
+
allLinks = data.links || [];
|
|
85
|
+
|
|
86
|
+
console.log(`Loaded ${allNodes.length} nodes and ${allLinks.length} links`);
|
|
87
|
+
|
|
88
|
+
// DEBUG: Log first few nodes to see actual structure
|
|
89
|
+
console.log('=== SAMPLE NODE STRUCTURE ===');
|
|
90
|
+
if (allNodes.length > 0) {
|
|
91
|
+
console.log('First node:', JSON.stringify(allNodes[0], null, 2));
|
|
92
|
+
if (allNodes.length > 1) {
|
|
93
|
+
console.log('Second node:', JSON.stringify(allNodes[1], null, 2));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Count node types
|
|
98
|
+
const typeCounts = {};
|
|
99
|
+
allNodes.forEach(node => {
|
|
100
|
+
const type = node.type || 'undefined';
|
|
101
|
+
typeCounts[type] = (typeCounts[type] || 0) + 1;
|
|
102
|
+
});
|
|
103
|
+
console.log('Node type counts:', typeCounts);
|
|
104
|
+
console.log('=== END SAMPLE NODE STRUCTURE ===');
|
|
105
|
+
|
|
106
|
+
buildTreeStructure();
|
|
107
|
+
renderVisualization();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Failed to load graph data:', error);
|
|
110
|
+
document.body.innerHTML =
|
|
111
|
+
'<div style="color: red; padding: 20px; font-family: Arial;">Error loading visualization data. Check console for details.</div>';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// TREE STRUCTURE BUILDING
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
function buildTreeStructure() {
|
|
120
|
+
// Include directories, files, AND chunks (function, class, method, text, imports, module)
|
|
121
|
+
const treeNodes = allNodes.filter(node => {
|
|
122
|
+
const type = node.type;
|
|
123
|
+
return type === 'directory' || type === 'file' || chunkTypes.includes(type);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log(`Filtered to ${treeNodes.length} tree nodes (directories, files, and chunks)`);
|
|
127
|
+
|
|
128
|
+
// Count node types for debugging
|
|
129
|
+
const dirCount = treeNodes.filter(n => n.type === 'directory').length;
|
|
130
|
+
const fileCount = treeNodes.filter(n => n.type === 'file').length;
|
|
131
|
+
const chunkCount = treeNodes.filter(n => chunkTypes.includes(n.type)).length;
|
|
132
|
+
console.log(`Node breakdown: ${dirCount} directories, ${fileCount} files, ${chunkCount} chunks`);
|
|
133
|
+
|
|
134
|
+
// Create lookup maps
|
|
135
|
+
const nodeMap = new Map();
|
|
136
|
+
treeNodes.forEach(node => {
|
|
137
|
+
nodeMap.set(node.id, {
|
|
138
|
+
...node,
|
|
139
|
+
children: []
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Build parent-child relationships
|
|
144
|
+
const parentMap = new Map();
|
|
145
|
+
|
|
146
|
+
// DEBUG: Analyze link structure
|
|
147
|
+
console.log('=== LINK STRUCTURE DEBUG ===');
|
|
148
|
+
console.log(`Total links: ${allLinks.length}`);
|
|
149
|
+
|
|
150
|
+
// Get unique link types (handle undefined)
|
|
151
|
+
const linkTypes = [...new Set(allLinks.map(l => l.type || 'undefined'))];
|
|
152
|
+
console.log('Link types found:', linkTypes);
|
|
153
|
+
|
|
154
|
+
// Count links by type
|
|
155
|
+
const linkTypeCounts = {};
|
|
156
|
+
allLinks.forEach(link => {
|
|
157
|
+
const type = link.type || 'undefined';
|
|
158
|
+
linkTypeCounts[type] = (linkTypeCounts[type] || 0) + 1;
|
|
159
|
+
});
|
|
160
|
+
console.log('Link type counts:', linkTypeCounts);
|
|
161
|
+
|
|
162
|
+
// Sample first few links
|
|
163
|
+
console.log('Sample links (first 5):');
|
|
164
|
+
allLinks.slice(0, 5).forEach((link, i) => {
|
|
165
|
+
console.log(` Link ${i}:`, JSON.stringify(link, null, 2));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Check if links have properties we expect
|
|
169
|
+
if (allLinks.length > 0) {
|
|
170
|
+
const firstLink = allLinks[0];
|
|
171
|
+
console.log('Link properties:', Object.keys(firstLink));
|
|
172
|
+
}
|
|
173
|
+
console.log('=== END LINK STRUCTURE DEBUG ===');
|
|
174
|
+
|
|
175
|
+
// Build parent-child relationships from links
|
|
176
|
+
// Process all containment and hierarchy links to establish the tree structure
|
|
177
|
+
console.log('=== BUILDING TREE RELATIONSHIPS ===');
|
|
178
|
+
|
|
179
|
+
let relationshipsProcessed = {
|
|
180
|
+
dir_hierarchy: 0,
|
|
181
|
+
dir_containment: 0,
|
|
182
|
+
file_containment: 0,
|
|
183
|
+
chunk_containment: 0 // undefined links = chunk-to-chunk (class -> method)
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let relationshipsMatched = {
|
|
187
|
+
dir_hierarchy: 0,
|
|
188
|
+
dir_containment: 0,
|
|
189
|
+
file_containment: 0,
|
|
190
|
+
chunk_containment: 0
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Process all relationship links
|
|
194
|
+
allLinks.forEach(link => {
|
|
195
|
+
const linkType = link.type;
|
|
196
|
+
|
|
197
|
+
// Determine relationship category
|
|
198
|
+
let category = null;
|
|
199
|
+
if (linkType === 'dir_hierarchy') {
|
|
200
|
+
category = 'dir_hierarchy';
|
|
201
|
+
} else if (linkType === 'dir_containment') {
|
|
202
|
+
category = 'dir_containment';
|
|
203
|
+
} else if (linkType === 'file_containment') {
|
|
204
|
+
category = 'file_containment';
|
|
205
|
+
} else if (linkType === undefined || linkType === 'undefined') {
|
|
206
|
+
// Undefined links are chunk-to-chunk (e.g., class -> method)
|
|
207
|
+
category = 'chunk_containment';
|
|
208
|
+
} else {
|
|
209
|
+
// Skip semantic, caller, and other non-hierarchical links
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
relationshipsProcessed[category]++;
|
|
214
|
+
|
|
215
|
+
// Get parent and child nodes from the map
|
|
216
|
+
const parentNode = nodeMap.get(link.source);
|
|
217
|
+
const childNode = nodeMap.get(link.target);
|
|
218
|
+
|
|
219
|
+
// Both nodes must exist in our tree node set
|
|
220
|
+
if (!parentNode || !childNode) {
|
|
221
|
+
if (relationshipsProcessed[category] <= 3) { // Log first few misses
|
|
222
|
+
console.log(`${category} link skipped - parent: ${link.source} (exists: ${!!parentNode}), child: ${link.target} (exists: ${!!childNode})`);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Establish parent-child relationship
|
|
228
|
+
// Add child to parent's children array
|
|
229
|
+
parentNode.children.push(childNode);
|
|
230
|
+
|
|
231
|
+
// Record the parent in parentMap (used to identify root nodes)
|
|
232
|
+
parentMap.set(link.target, link.source);
|
|
233
|
+
|
|
234
|
+
relationshipsMatched[category]++;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
console.log('Relationship processing summary:');
|
|
238
|
+
console.log(` dir_hierarchy: ${relationshipsMatched.dir_hierarchy}/${relationshipsProcessed.dir_hierarchy} matched`);
|
|
239
|
+
console.log(` dir_containment: ${relationshipsMatched.dir_containment}/${relationshipsProcessed.dir_containment} matched`);
|
|
240
|
+
console.log(` file_containment: ${relationshipsMatched.file_containment}/${relationshipsProcessed.file_containment} matched`);
|
|
241
|
+
console.log(` chunk_containment: ${relationshipsMatched.chunk_containment}/${relationshipsProcessed.chunk_containment} matched`);
|
|
242
|
+
console.log(` Total parent-child links: ${parentMap.size}`);
|
|
243
|
+
console.log('=== END TREE RELATIONSHIPS ===');
|
|
244
|
+
|
|
245
|
+
// Find root nodes (nodes with no parents)
|
|
246
|
+
// IMPORTANT: Exclude chunk types from roots - they should only appear as children of files
|
|
247
|
+
// Orphaned chunks (without file_containment links) are excluded from the tree
|
|
248
|
+
const rootNodes = treeNodes
|
|
249
|
+
.filter(node => !parentMap.has(node.id))
|
|
250
|
+
.filter(node => !chunkTypes.includes(node.type)) // Exclude orphaned chunks
|
|
251
|
+
.map(node => nodeMap.get(node.id))
|
|
252
|
+
.filter(node => node !== undefined);
|
|
253
|
+
|
|
254
|
+
console.log('=== ROOT NODE ANALYSIS ===');
|
|
255
|
+
console.log(`Found ${rootNodes.length} root nodes (directories and files only)`);
|
|
256
|
+
|
|
257
|
+
// DEBUG: Count root node types
|
|
258
|
+
const rootTypeCounts = {};
|
|
259
|
+
rootNodes.forEach(node => {
|
|
260
|
+
const type = node.type || 'undefined';
|
|
261
|
+
rootTypeCounts[type] = (rootTypeCounts[type] || 0) + 1;
|
|
262
|
+
});
|
|
263
|
+
console.log('Root node type breakdown:', rootTypeCounts);
|
|
264
|
+
|
|
265
|
+
// If we have chunk nodes as roots, something went wrong
|
|
266
|
+
const chunkRoots = rootNodes.filter(n => chunkTypes.includes(n.type)).length;
|
|
267
|
+
if (chunkRoots > 0) {
|
|
268
|
+
console.warn(`WARNING: ${chunkRoots} chunk nodes are roots - they should be children of files!`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If we have file nodes as roots (except for top-level files), might be missing dir_containment
|
|
272
|
+
const fileRoots = rootNodes.filter(n => n.type === 'file').length;
|
|
273
|
+
if (fileRoots > 0) {
|
|
274
|
+
console.log(`INFO: ${fileRoots} file nodes are roots (this is normal for files not in subdirectories)`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log('=== END ROOT NODE ANALYSIS ===');
|
|
278
|
+
|
|
279
|
+
// Create virtual root if multiple roots
|
|
280
|
+
if (rootNodes.length === 0) {
|
|
281
|
+
console.error('No root nodes found!');
|
|
282
|
+
treeData = {name: 'Empty', id: 'root', type: 'directory', children: []};
|
|
283
|
+
} else if (rootNodes.length === 1) {
|
|
284
|
+
treeData = rootNodes[0];
|
|
285
|
+
} else {
|
|
286
|
+
treeData = {
|
|
287
|
+
name: 'Project Root',
|
|
288
|
+
id: 'virtual-root',
|
|
289
|
+
type: 'directory',
|
|
290
|
+
children: rootNodes
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Collapse single-child chains to make the tree more compact
|
|
295
|
+
// - Directory with single directory child: src -> mcp_vector_search becomes "src/mcp_vector_search"
|
|
296
|
+
// - File with single chunk child: promote the chunk's children to the file level
|
|
297
|
+
function collapseSingleChildChains(node) {
|
|
298
|
+
if (!node || !node.children) return;
|
|
299
|
+
|
|
300
|
+
// First, recursively process all children
|
|
301
|
+
node.children.forEach(child => collapseSingleChildChains(child));
|
|
302
|
+
|
|
303
|
+
// Case 1: Directory with single directory child - combine names
|
|
304
|
+
if (node.type === 'directory' && node.children.length === 1) {
|
|
305
|
+
const onlyChild = node.children[0];
|
|
306
|
+
if (onlyChild.type === 'directory') {
|
|
307
|
+
// Merge: combine names with "/"
|
|
308
|
+
console.log(`Collapsing dir chain: ${node.name} + ${onlyChild.name}`);
|
|
309
|
+
node.name = `${node.name}/${onlyChild.name}`;
|
|
310
|
+
// Take the child's children as our own
|
|
311
|
+
node.children = onlyChild.children || [];
|
|
312
|
+
node._children = onlyChild._children || null;
|
|
313
|
+
// Preserve the deepest node's id for any link references
|
|
314
|
+
node.collapsed_ids = node.collapsed_ids || [node.id];
|
|
315
|
+
node.collapsed_ids.push(onlyChild.id);
|
|
316
|
+
|
|
317
|
+
// Recursively check again in case there's another single child
|
|
318
|
+
collapseSingleChildChains(node);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Case 2: File with single chunk child - promote chunk's children to file
|
|
323
|
+
// This handles files where there's just one L1 (e.g., imports or a single class)
|
|
324
|
+
if (node.type === 'file' && node.children && node.children.length === 1) {
|
|
325
|
+
const onlyChild = node.children[0];
|
|
326
|
+
if (chunkTypes.includes(onlyChild.type)) {
|
|
327
|
+
// If the chunk has children, promote them to the file level
|
|
328
|
+
const chunkChildren = onlyChild.children || onlyChild._children || [];
|
|
329
|
+
if (chunkChildren.length > 0) {
|
|
330
|
+
console.log(`Promoting ${chunkChildren.length} children from ${onlyChild.type} to file ${node.name}`);
|
|
331
|
+
// Replace the single chunk with its children
|
|
332
|
+
node.children = chunkChildren;
|
|
333
|
+
// Store info about the collapsed chunk
|
|
334
|
+
node.collapsed_chunk = {
|
|
335
|
+
type: onlyChild.type,
|
|
336
|
+
name: onlyChild.name,
|
|
337
|
+
id: onlyChild.id
|
|
338
|
+
};
|
|
339
|
+
} else {
|
|
340
|
+
// Collapse file+chunk into combined name (like directory chains)
|
|
341
|
+
console.log(`Collapsing file+chunk: ${node.name}/${onlyChild.name}`);
|
|
342
|
+
node.name = `${node.name}/${onlyChild.name}`;
|
|
343
|
+
node.children = null; // Remove chunk child - now a leaf node
|
|
344
|
+
node._children = null;
|
|
345
|
+
node.collapsed_ids = node.collapsed_ids || [node.id];
|
|
346
|
+
node.collapsed_ids.push(onlyChild.id);
|
|
347
|
+
|
|
348
|
+
// Store chunk data for display when clicked
|
|
349
|
+
node.collapsed_chunk = {
|
|
350
|
+
type: onlyChild.type,
|
|
351
|
+
name: onlyChild.name,
|
|
352
|
+
id: onlyChild.id,
|
|
353
|
+
content: onlyChild.content,
|
|
354
|
+
start_line: onlyChild.start_line,
|
|
355
|
+
end_line: onlyChild.end_line,
|
|
356
|
+
complexity: onlyChild.complexity
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Apply single-child chain collapsing to all root children
|
|
364
|
+
console.log('=== COLLAPSING SINGLE-CHILD CHAINS ===');
|
|
365
|
+
if (treeData.children) {
|
|
366
|
+
treeData.children.forEach(child => collapseSingleChildChains(child));
|
|
367
|
+
}
|
|
368
|
+
console.log('=== END COLLAPSING SINGLE-CHILD CHAINS ===');
|
|
369
|
+
|
|
370
|
+
// Collapse all directories and files by default
|
|
371
|
+
function collapseAll(node) {
|
|
372
|
+
if (node.children && node.children.length > 0) {
|
|
373
|
+
// First, recursively process all descendants
|
|
374
|
+
node.children.forEach(child => collapseAll(child));
|
|
375
|
+
|
|
376
|
+
// Then collapse this node (move children to _children)
|
|
377
|
+
node._children = node.children;
|
|
378
|
+
node.children = null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Collapse all child nodes of the root (but keep root's direct children visible initially)
|
|
383
|
+
// This way, only the root level (first level) is visible, all deeper levels are collapsed
|
|
384
|
+
if (treeData.children) {
|
|
385
|
+
treeData.children.forEach(child => {
|
|
386
|
+
// Collapse all descendants of each root child, but keep the root children themselves visible
|
|
387
|
+
if (child.children && child.children.length > 0) {
|
|
388
|
+
child.children.forEach(grandchild => collapseAll(grandchild));
|
|
389
|
+
// Move children to _children to collapse
|
|
390
|
+
child._children = child.children;
|
|
391
|
+
child.children = null;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
console.log('Tree structure built with all directories and files collapsed');
|
|
397
|
+
|
|
398
|
+
// Calculate line counts for all nodes (for proportional node rendering)
|
|
399
|
+
allLineCounts = []; // Reset for fresh calculation
|
|
400
|
+
calculateNodeSizes(treeData);
|
|
401
|
+
calculatePercentiles(); // Calculate 20th/80th percentile thresholds
|
|
402
|
+
console.log('Node sizes calculated with percentile-based sizing');
|
|
403
|
+
|
|
404
|
+
// DEBUG: Check a few file nodes to see if they have chunks in _children
|
|
405
|
+
console.log('=== POST-COLLAPSE FILE CHECK ===');
|
|
406
|
+
let filesChecked = 0;
|
|
407
|
+
let filesWithChunks = 0;
|
|
408
|
+
|
|
409
|
+
function checkFilesRecursive(node) {
|
|
410
|
+
if (node.type === 'file') {
|
|
411
|
+
filesChecked++;
|
|
412
|
+
const chunkCount = (node._children || []).length;
|
|
413
|
+
if (chunkCount > 0) {
|
|
414
|
+
filesWithChunks++;
|
|
415
|
+
console.log(`File ${node.name} has ${chunkCount} chunks in _children`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check both visible and hidden children
|
|
420
|
+
const childrenToCheck = node.children || node._children || [];
|
|
421
|
+
childrenToCheck.forEach(child => checkFilesRecursive(child));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
checkFilesRecursive(treeData);
|
|
425
|
+
console.log(`Checked ${filesChecked} files, ${filesWithChunks} have chunks`);
|
|
426
|
+
console.log('=== END POST-COLLAPSE FILE CHECK ===');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ============================================================================
|
|
430
|
+
// NODE SIZE CALCULATION
|
|
431
|
+
// ============================================================================
|
|
432
|
+
|
|
433
|
+
// Global variables for size scaling - now tracking line counts
|
|
434
|
+
let globalMinLines = Infinity;
|
|
435
|
+
let globalMaxLines = 0;
|
|
436
|
+
let allLineCounts = []; // Collect all line counts for percentile calculation
|
|
437
|
+
|
|
438
|
+
function calculateNodeSizes(node) {
|
|
439
|
+
if (!node) return 0;
|
|
440
|
+
|
|
441
|
+
// For chunks: use line count directly
|
|
442
|
+
if (chunkTypes.includes(node.type)) {
|
|
443
|
+
const lineCount = (node.start_line && node.end_line)
|
|
444
|
+
? node.end_line - node.start_line + 1
|
|
445
|
+
: 1;
|
|
446
|
+
node._lineCount = lineCount;
|
|
447
|
+
allLineCounts.push(lineCount);
|
|
448
|
+
|
|
449
|
+
if (lineCount > 0) {
|
|
450
|
+
globalMinLines = Math.min(globalMinLines, lineCount);
|
|
451
|
+
globalMaxLines = Math.max(globalMaxLines, lineCount);
|
|
452
|
+
}
|
|
453
|
+
return lineCount;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// For files and directories: sum of children line counts
|
|
457
|
+
const children = node.children || node._children || [];
|
|
458
|
+
let totalLines = 0;
|
|
459
|
+
|
|
460
|
+
children.forEach(child => {
|
|
461
|
+
totalLines += calculateNodeSizes(child);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
node._lineCount = totalLines || 1; // Minimum 1 for empty dirs/files
|
|
465
|
+
|
|
466
|
+
// DON'T add files/directories to allLineCounts - they skew percentiles
|
|
467
|
+
// Only chunks should affect percentile calculation since only chunks use percentile sizing
|
|
468
|
+
// (Files and directories use separate minRadius/maxRadius, not chunkMinRadius/chunkMaxRadius)
|
|
469
|
+
|
|
470
|
+
if (node._lineCount > 0) {
|
|
471
|
+
globalMinLines = Math.min(globalMinLines, node._lineCount);
|
|
472
|
+
globalMaxLines = Math.max(globalMaxLines, node._lineCount);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return node._lineCount;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Calculate percentile thresholds after all nodes are processed
|
|
479
|
+
let percentile20 = 0;
|
|
480
|
+
let percentile80 = 0;
|
|
481
|
+
|
|
482
|
+
function calculatePercentiles() {
|
|
483
|
+
if (allLineCounts.length === 0) return;
|
|
484
|
+
|
|
485
|
+
const sorted = [...allLineCounts].sort((a, b) => a - b);
|
|
486
|
+
const p20Index = Math.floor(sorted.length * 0.20);
|
|
487
|
+
const p80Index = Math.floor(sorted.length * 0.80);
|
|
488
|
+
|
|
489
|
+
percentile20 = sorted[p20Index] || 1;
|
|
490
|
+
percentile80 = sorted[p80Index] || sorted[sorted.length - 1] || 1;
|
|
491
|
+
|
|
492
|
+
console.log(`Line count percentiles (chunks only): 20th=${percentile20}, 80th=${percentile80}, range=${percentile80 - percentile20}`);
|
|
493
|
+
console.log(`Total chunks: ${allLineCounts.length}, min=${globalMinLines}, max=${globalMaxLines}`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Count external calls for a node
|
|
497
|
+
function getExternalCallCounts(nodeData) {
|
|
498
|
+
if (!nodeData.id) return { inbound: 0, outbound: 0, inboundNodes: [], outboundNodes: [] };
|
|
499
|
+
|
|
500
|
+
const nodeFilePath = nodeData.file_path;
|
|
501
|
+
const inboundNodes = []; // Array of {id, name, file_path}
|
|
502
|
+
const outboundNodes = []; // Array of {id, name, file_path}
|
|
503
|
+
|
|
504
|
+
// Use a Set to deduplicate by source/target node
|
|
505
|
+
const inboundSeen = new Set();
|
|
506
|
+
const outboundSeen = new Set();
|
|
507
|
+
|
|
508
|
+
allLinks.forEach(link => {
|
|
509
|
+
if (link.type === 'caller') {
|
|
510
|
+
if (link.target === nodeData.id) {
|
|
511
|
+
// Something calls this node
|
|
512
|
+
const callerNode = allNodes.find(n => n.id === link.source);
|
|
513
|
+
if (callerNode && callerNode.file_path !== nodeFilePath && !inboundSeen.has(callerNode.id)) {
|
|
514
|
+
inboundSeen.add(callerNode.id);
|
|
515
|
+
inboundNodes.push({ id: callerNode.id, name: callerNode.name, file_path: callerNode.file_path });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (link.source === nodeData.id) {
|
|
519
|
+
// This node calls something
|
|
520
|
+
const calleeNode = allNodes.find(n => n.id === link.target);
|
|
521
|
+
if (calleeNode && calleeNode.file_path !== nodeFilePath && !outboundSeen.has(calleeNode.id)) {
|
|
522
|
+
outboundSeen.add(calleeNode.id);
|
|
523
|
+
outboundNodes.push({ id: calleeNode.id, name: calleeNode.name, file_path: calleeNode.file_path });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
inbound: inboundNodes.length,
|
|
531
|
+
outbound: outboundNodes.length,
|
|
532
|
+
inboundNodes,
|
|
533
|
+
outboundNodes
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Store external call data for line drawing
|
|
538
|
+
let externalCallData = [];
|
|
539
|
+
|
|
540
|
+
function collectExternalCallData() {
|
|
541
|
+
externalCallData = [];
|
|
542
|
+
|
|
543
|
+
allNodes.forEach(nodeData => {
|
|
544
|
+
if (!chunkTypes.includes(nodeData.type)) return;
|
|
545
|
+
|
|
546
|
+
const counts = getExternalCallCounts(nodeData);
|
|
547
|
+
if (counts.inbound > 0 || counts.outbound > 0) {
|
|
548
|
+
externalCallData.push({
|
|
549
|
+
nodeId: nodeData.id,
|
|
550
|
+
inboundNodes: counts.inboundNodes,
|
|
551
|
+
outboundNodes: counts.outboundNodes
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function drawExternalCallLines(svg, root) {
|
|
558
|
+
// Remove existing external call lines
|
|
559
|
+
svg.selectAll('.external-call-line').remove();
|
|
560
|
+
|
|
561
|
+
// Build a map of node positions from the tree (visible nodes only)
|
|
562
|
+
const nodePositions = new Map();
|
|
563
|
+
root.descendants().forEach(d => {
|
|
564
|
+
nodePositions.set(d.data.id, { x: d.x, y: d.y, node: d });
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Build a map from node ID to tree node (for parent traversal)
|
|
568
|
+
const treeNodeMap = new Map();
|
|
569
|
+
root.descendants().forEach(d => {
|
|
570
|
+
treeNodeMap.set(d.data.id, d);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Helper: Find position for a node, falling back to visible ancestors
|
|
574
|
+
// If the target node is not visible (collapsed), find its closest visible ancestor
|
|
575
|
+
function getPositionWithFallback(nodeId) {
|
|
576
|
+
// First check if node is directly visible
|
|
577
|
+
if (nodePositions.has(nodeId)) {
|
|
578
|
+
return nodePositions.get(nodeId);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Node not visible - try to find via tree traversal
|
|
582
|
+
// Strategy 1: Find by file_path matching in visible nodes
|
|
583
|
+
const targetNode = allNodes.find(n => n.id === nodeId);
|
|
584
|
+
if (!targetNode) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Strategy 2: Look for the file that contains this chunk
|
|
589
|
+
if (targetNode.file_path) {
|
|
590
|
+
// Look for visible file nodes that match this path
|
|
591
|
+
for (const [id, pos] of nodePositions) {
|
|
592
|
+
const visibleNode = allNodes.find(n => n.id === id);
|
|
593
|
+
if (visibleNode) {
|
|
594
|
+
// Check if this is the file containing our chunk
|
|
595
|
+
if (visibleNode.type === 'file' &&
|
|
596
|
+
(visibleNode.path === targetNode.file_path ||
|
|
597
|
+
visibleNode.file_path === targetNode.file_path ||
|
|
598
|
+
visibleNode.name === targetNode.file_path.split('/').pop())) {
|
|
599
|
+
return pos;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Strategy 3: Look for directory containing the file
|
|
605
|
+
const pathParts = targetNode.file_path.split('/');
|
|
606
|
+
// Go from most specific (file's directory) to least specific (root)
|
|
607
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
608
|
+
const dirName = pathParts[i];
|
|
609
|
+
if (!dirName) continue;
|
|
610
|
+
|
|
611
|
+
// Find a visible directory with this name
|
|
612
|
+
for (const [id, pos] of nodePositions) {
|
|
613
|
+
const visibleNode = allNodes.find(n => n.id === id);
|
|
614
|
+
if (visibleNode && visibleNode.type === 'directory' && visibleNode.name === dirName) {
|
|
615
|
+
return pos;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Create a group for external call lines (behind nodes)
|
|
625
|
+
let lineGroup = svg.select('.external-lines-group');
|
|
626
|
+
if (lineGroup.empty()) {
|
|
627
|
+
lineGroup = svg.insert('g', ':first-child')
|
|
628
|
+
.attr('class', 'external-lines-group');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Respect the toggle state
|
|
632
|
+
lineGroup.style('display', showCallLines ? 'block' : 'none');
|
|
633
|
+
|
|
634
|
+
console.log(`[CallLines] Drawing lines for ${externalCallData.length} nodes with external calls`);
|
|
635
|
+
|
|
636
|
+
let linesDrawn = 0;
|
|
637
|
+
externalCallData.forEach(data => {
|
|
638
|
+
const sourcePos = getPositionWithFallback(data.nodeId);
|
|
639
|
+
if (!sourcePos) {
|
|
640
|
+
console.log(`[CallLines] No source position for ${data.nodeId}`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Draw lines to inbound nodes (callers) - dashed blue (fainter)
|
|
645
|
+
data.inboundNodes.forEach(caller => {
|
|
646
|
+
const targetPos = getPositionWithFallback(caller.id);
|
|
647
|
+
if (targetPos) {
|
|
648
|
+
lineGroup.append('path')
|
|
649
|
+
.attr('class', 'external-call-line inbound-line')
|
|
650
|
+
.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}`)
|
|
651
|
+
.attr('fill', 'none')
|
|
652
|
+
.attr('stroke', '#58a6ff')
|
|
653
|
+
.attr('stroke-width', 1)
|
|
654
|
+
.attr('stroke-dasharray', '4,2')
|
|
655
|
+
.attr('opacity', 0.35)
|
|
656
|
+
.attr('pointer-events', 'none');
|
|
657
|
+
linesDrawn++;
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Draw lines to outbound nodes (callees) - dashed orange (fainter)
|
|
662
|
+
data.outboundNodes.forEach(callee => {
|
|
663
|
+
const targetPos = getPositionWithFallback(callee.id);
|
|
664
|
+
if (targetPos) {
|
|
665
|
+
lineGroup.append('path')
|
|
666
|
+
.attr('class', 'external-call-line outbound-line')
|
|
667
|
+
.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}`)
|
|
668
|
+
.attr('fill', 'none')
|
|
669
|
+
.attr('stroke', '#f0883e')
|
|
670
|
+
.attr('stroke-width', 1)
|
|
671
|
+
.attr('stroke-dasharray', '4,2')
|
|
672
|
+
.attr('opacity', 0.35)
|
|
673
|
+
.attr('pointer-events', 'none');
|
|
674
|
+
linesDrawn++;
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
console.log(`[CallLines] Drew ${linesDrawn} call lines`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Draw external call lines for CIRCULAR layout
|
|
683
|
+
// Converts polar coordinates (angle, radius) to Cartesian (x, y)
|
|
684
|
+
function drawExternalCallLinesCircular(svg, root) {
|
|
685
|
+
// Remove existing external call lines
|
|
686
|
+
svg.selectAll('.external-call-line').remove();
|
|
687
|
+
|
|
688
|
+
// Helper: Convert polar to Cartesian coordinates
|
|
689
|
+
// In D3 radial layout: d.x = angle (radians), d.y = radius
|
|
690
|
+
function polarToCartesian(angle, radius) {
|
|
691
|
+
return {
|
|
692
|
+
x: radius * Math.cos(angle - Math.PI / 2),
|
|
693
|
+
y: radius * Math.sin(angle - Math.PI / 2)
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Build a map of node positions from the tree (visible nodes only)
|
|
698
|
+
const nodePositions = new Map();
|
|
699
|
+
root.descendants().forEach(d => {
|
|
700
|
+
const cartesian = polarToCartesian(d.x, d.y);
|
|
701
|
+
nodePositions.set(d.data.id, { x: cartesian.x, y: cartesian.y, angle: d.x, radius: d.y, node: d });
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Helper: Find position for a node, falling back to visible ancestors
|
|
705
|
+
function getPositionWithFallback(nodeId) {
|
|
706
|
+
// First check if node is directly visible
|
|
707
|
+
if (nodePositions.has(nodeId)) {
|
|
708
|
+
return nodePositions.get(nodeId);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Node not visible - try to find via tree traversal
|
|
712
|
+
const targetNode = allNodes.find(n => n.id === nodeId);
|
|
713
|
+
if (!targetNode) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Look for the file that contains this chunk
|
|
718
|
+
if (targetNode.file_path) {
|
|
719
|
+
// Look for visible file nodes that match this path
|
|
720
|
+
for (const [id, pos] of nodePositions) {
|
|
721
|
+
const visibleNode = allNodes.find(n => n.id === id);
|
|
722
|
+
if (visibleNode) {
|
|
723
|
+
if (visibleNode.type === 'file' &&
|
|
724
|
+
(visibleNode.path === targetNode.file_path ||
|
|
725
|
+
visibleNode.file_path === targetNode.file_path ||
|
|
726
|
+
visibleNode.name === targetNode.file_path.split('/').pop())) {
|
|
727
|
+
return pos;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Look for directory containing the file
|
|
733
|
+
const pathParts = targetNode.file_path.split('/');
|
|
734
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
735
|
+
const dirName = pathParts[i];
|
|
736
|
+
if (!dirName) continue;
|
|
737
|
+
|
|
738
|
+
for (const [id, pos] of nodePositions) {
|
|
739
|
+
const visibleNode = allNodes.find(n => n.id === id);
|
|
740
|
+
if (visibleNode && visibleNode.type === 'directory' && visibleNode.name === dirName) {
|
|
741
|
+
return pos;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Create a group for external call lines (behind nodes)
|
|
751
|
+
let lineGroup = svg.select('.external-lines-group');
|
|
752
|
+
if (lineGroup.empty()) {
|
|
753
|
+
lineGroup = svg.insert('g', ':first-child')
|
|
754
|
+
.attr('class', 'external-lines-group');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Respect the toggle state
|
|
758
|
+
lineGroup.style('display', showCallLines ? 'block' : 'none');
|
|
759
|
+
|
|
760
|
+
console.log(`[CallLines Circular] Drawing lines for ${externalCallData.length} nodes with external calls`);
|
|
761
|
+
|
|
762
|
+
let linesDrawn = 0;
|
|
763
|
+
externalCallData.forEach(data => {
|
|
764
|
+
const sourcePos = getPositionWithFallback(data.nodeId);
|
|
765
|
+
if (!sourcePos) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Draw lines to inbound nodes (callers) - dashed blue (fainter)
|
|
770
|
+
data.inboundNodes.forEach(caller => {
|
|
771
|
+
const targetPos = getPositionWithFallback(caller.id);
|
|
772
|
+
if (targetPos) {
|
|
773
|
+
// Use quadratic bezier curves that go through the center for circular layout
|
|
774
|
+
const midX = (sourcePos.x + targetPos.x) / 2 * 0.3;
|
|
775
|
+
const midY = (sourcePos.y + targetPos.y) / 2 * 0.3;
|
|
776
|
+
|
|
777
|
+
lineGroup.append('path')
|
|
778
|
+
.attr('class', 'external-call-line inbound-line')
|
|
779
|
+
.attr('d', `M${targetPos.x},${targetPos.y} Q${midX},${midY} ${sourcePos.x},${sourcePos.y}`)
|
|
780
|
+
.attr('fill', 'none')
|
|
781
|
+
.attr('stroke', '#58a6ff')
|
|
782
|
+
.attr('stroke-width', 1)
|
|
783
|
+
.attr('stroke-dasharray', '4,2')
|
|
784
|
+
.attr('opacity', 0.35)
|
|
785
|
+
.attr('pointer-events', 'none');
|
|
786
|
+
linesDrawn++;
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// Draw lines to outbound nodes (callees) - dashed orange (fainter)
|
|
791
|
+
data.outboundNodes.forEach(callee => {
|
|
792
|
+
const targetPos = getPositionWithFallback(callee.id);
|
|
793
|
+
if (targetPos) {
|
|
794
|
+
// Use quadratic bezier curves that go through the center for circular layout
|
|
795
|
+
const midX = (sourcePos.x + targetPos.x) / 2 * 0.3;
|
|
796
|
+
const midY = (sourcePos.y + targetPos.y) / 2 * 0.3;
|
|
797
|
+
|
|
798
|
+
lineGroup.append('path')
|
|
799
|
+
.attr('class', 'external-call-line outbound-line')
|
|
800
|
+
.attr('d', `M${sourcePos.x},${sourcePos.y} Q${midX},${midY} ${targetPos.x},${targetPos.y}`)
|
|
801
|
+
.attr('fill', 'none')
|
|
802
|
+
.attr('stroke', '#f0883e')
|
|
803
|
+
.attr('stroke-width', 1)
|
|
804
|
+
.attr('stroke-dasharray', '4,2')
|
|
805
|
+
.attr('opacity', 0.35)
|
|
806
|
+
.attr('pointer-events', 'none');
|
|
807
|
+
linesDrawn++;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
console.log(`[CallLines Circular] Drew ${linesDrawn} call lines`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Get color based on complexity (darker = more complex)
|
|
816
|
+
// Uses HSL color model for smooth gradients
|
|
817
|
+
function getComplexityColor(d, baseHue) {
|
|
818
|
+
const nodeData = d.data;
|
|
819
|
+
const complexity = nodeData.complexity;
|
|
820
|
+
|
|
821
|
+
// If no complexity data, return a default based on type
|
|
822
|
+
if (complexity === undefined || complexity === null) {
|
|
823
|
+
// Default colors for non-complex nodes
|
|
824
|
+
if (nodeData.type === 'directory') {
|
|
825
|
+
return nodeData._children ? '#f39c12' : '#3498db'; // Orange/Blue
|
|
826
|
+
} else if (nodeData.type === 'file') {
|
|
827
|
+
return nodeData._children ? '#95a5a6' : '#ecf0f1'; // Gray/White
|
|
828
|
+
} else if (chunkTypes.includes(nodeData.type)) {
|
|
829
|
+
return '#9b59b6'; // Default purple
|
|
830
|
+
}
|
|
831
|
+
return '#95a5a6';
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Complexity ranges: 0-5 (low), 5-10 (medium), 10-20 (high), 20+ (very high)
|
|
835
|
+
// Map to lightness: 70% (light) to 30% (dark)
|
|
836
|
+
const maxComplexity = 25; // Cap for scaling
|
|
837
|
+
const normalizedComplexity = Math.min(complexity, maxComplexity) / maxComplexity;
|
|
838
|
+
|
|
839
|
+
// Lightness goes from 65% (low complexity) to 35% (high complexity)
|
|
840
|
+
const lightness = 65 - (normalizedComplexity * 30);
|
|
841
|
+
|
|
842
|
+
// Saturation increases slightly with complexity (60% to 80%)
|
|
843
|
+
const saturation = 60 + (normalizedComplexity * 20);
|
|
844
|
+
|
|
845
|
+
return `hsl(${baseHue}, ${saturation}%, ${lightness}%)`;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Get node fill color with complexity shading
|
|
849
|
+
function getNodeFillColor(d) {
|
|
850
|
+
const nodeData = d.data;
|
|
851
|
+
|
|
852
|
+
if (nodeData.type === 'directory') {
|
|
853
|
+
// Orange (30°) if collapsed, Blue (210°) if expanded
|
|
854
|
+
const hue = nodeData._children ? 30 : 210;
|
|
855
|
+
// Directories aggregate complexity from children
|
|
856
|
+
const avgComplexity = calculateAverageComplexity(nodeData);
|
|
857
|
+
if (avgComplexity > 0) {
|
|
858
|
+
const lightness = 55 - (Math.min(avgComplexity, 15) / 15) * 20;
|
|
859
|
+
return `hsl(${hue}, 70%, ${lightness}%)`;
|
|
860
|
+
}
|
|
861
|
+
return nodeData._children ? '#f39c12' : '#3498db';
|
|
862
|
+
} else if (nodeData.type === 'file') {
|
|
863
|
+
// Gray files, but show complexity if available
|
|
864
|
+
const avgComplexity = calculateAverageComplexity(nodeData);
|
|
865
|
+
if (avgComplexity > 0) {
|
|
866
|
+
// Gray hue (0° with 0 saturation) to slight red tint for complexity
|
|
867
|
+
const saturation = Math.min(avgComplexity, 15) * 2; // 0-30%
|
|
868
|
+
const lightness = 70 - (Math.min(avgComplexity, 15) / 15) * 25;
|
|
869
|
+
return `hsl(0, ${saturation}%, ${lightness}%)`;
|
|
870
|
+
}
|
|
871
|
+
return nodeData._children ? '#95a5a6' : '#ecf0f1';
|
|
872
|
+
} else if (chunkTypes.includes(nodeData.type)) {
|
|
873
|
+
// Purple (280°) for chunks, darker with higher complexity
|
|
874
|
+
return getComplexityColor(d, 280);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return '#95a5a6';
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Calculate average complexity for a node (recursively for dirs/files)
|
|
881
|
+
function calculateAverageComplexity(node) {
|
|
882
|
+
if (chunkTypes.includes(node.type)) {
|
|
883
|
+
return node.complexity || 0;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const children = node.children || node._children || [];
|
|
887
|
+
if (children.length === 0) return 0;
|
|
888
|
+
|
|
889
|
+
let totalComplexity = 0;
|
|
890
|
+
let count = 0;
|
|
891
|
+
|
|
892
|
+
children.forEach(child => {
|
|
893
|
+
if (chunkTypes.includes(child.type) && child.complexity) {
|
|
894
|
+
totalComplexity += child.complexity;
|
|
895
|
+
count++;
|
|
896
|
+
} else {
|
|
897
|
+
const childAvg = calculateAverageComplexity(child);
|
|
898
|
+
if (childAvg > 0) {
|
|
899
|
+
totalComplexity += childAvg;
|
|
900
|
+
count++;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
return count > 0 ? totalComplexity / count : 0;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function getNodeRadius(d) {
|
|
909
|
+
const nodeData = d.data;
|
|
910
|
+
const lineCount = nodeData._lineCount || 1;
|
|
911
|
+
|
|
912
|
+
// Determine min/max based on node type
|
|
913
|
+
let minR, maxR;
|
|
914
|
+
if (chunkTypes.includes(nodeData.type)) {
|
|
915
|
+
minR = sizeConfig.chunkMinRadius;
|
|
916
|
+
maxR = sizeConfig.chunkMaxRadius;
|
|
917
|
+
} else {
|
|
918
|
+
minR = sizeConfig.minRadius;
|
|
919
|
+
maxR = sizeConfig.maxRadius;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Percentile-based relative sizing:
|
|
923
|
+
// - Below 20th percentile → minimum size
|
|
924
|
+
// - Above 80th percentile → maximum size
|
|
925
|
+
// - Between 20th-80th → linear interpolation
|
|
926
|
+
|
|
927
|
+
if (percentile80 <= percentile20) {
|
|
928
|
+
return (minR + maxR) / 2; // Default if no range
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
let normalized;
|
|
932
|
+
if (lineCount <= percentile20) {
|
|
933
|
+
// Below 20th percentile - use minimum size
|
|
934
|
+
normalized = 0;
|
|
935
|
+
} else if (lineCount >= percentile80) {
|
|
936
|
+
// Above 80th percentile - use maximum size
|
|
937
|
+
normalized = 1;
|
|
938
|
+
} else {
|
|
939
|
+
// Linear interpolation between 20th and 80th percentile
|
|
940
|
+
normalized = (lineCount - percentile20) / (percentile80 - percentile20);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Scale to radius range
|
|
944
|
+
return minR + (normalized * (maxR - minR));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ============================================================================
|
|
948
|
+
// VISUALIZATION RENDERING
|
|
949
|
+
// ============================================================================
|
|
950
|
+
|
|
951
|
+
function renderVisualization() {
|
|
952
|
+
console.log('=== RENDER VISUALIZATION ===');
|
|
953
|
+
console.log(`Current layout: ${currentLayout}`);
|
|
954
|
+
console.log(`Tree data exists: ${treeData !== null}`);
|
|
955
|
+
if (treeData) {
|
|
956
|
+
console.log(`Root node: ${treeData.name}, children: ${(treeData.children || []).length}, _children: ${(treeData._children || []).length}`);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Clear existing content
|
|
960
|
+
const graphElement = d3.select('#graph');
|
|
961
|
+
console.log(`Graph element found: ${!graphElement.empty()}`);
|
|
962
|
+
graphElement.selectAll('*').remove();
|
|
963
|
+
|
|
964
|
+
if (currentLayout === 'linear') {
|
|
965
|
+
console.log('Calling renderLinearTree()...');
|
|
966
|
+
renderLinearTree();
|
|
967
|
+
} else {
|
|
968
|
+
console.log('Calling renderCircularTree()...');
|
|
969
|
+
renderCircularTree();
|
|
970
|
+
}
|
|
971
|
+
console.log('=== END RENDER VISUALIZATION ===');
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ============================================================================
|
|
975
|
+
// LINEAR TREE LAYOUT
|
|
976
|
+
// ============================================================================
|
|
977
|
+
|
|
978
|
+
function renderLinearTree() {
|
|
979
|
+
console.log('=== RENDER LINEAR TREE ===');
|
|
980
|
+
const { width, height } = getViewportDimensions();
|
|
981
|
+
console.log(`Viewport dimensions: ${width}x${height}`);
|
|
982
|
+
|
|
983
|
+
const svg = d3.select('#graph')
|
|
984
|
+
.attr('width', width)
|
|
985
|
+
.attr('height', height);
|
|
986
|
+
|
|
987
|
+
const g = svg.append('g')
|
|
988
|
+
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
989
|
+
|
|
990
|
+
// Create tree layout
|
|
991
|
+
// For horizontal tree: size is [height, width] where height controls vertical spread
|
|
992
|
+
const treeLayout = d3.tree()
|
|
993
|
+
.size([height - margin.top - margin.bottom, width - margin.left - margin.right]);
|
|
994
|
+
|
|
995
|
+
console.log(`Tree layout size: ${height - margin.top - margin.bottom} x ${width - margin.left - margin.right}`);
|
|
996
|
+
|
|
997
|
+
// Create hierarchy from tree data
|
|
998
|
+
// D3 hierarchy automatically respects children vs _children
|
|
999
|
+
console.log('Creating D3 hierarchy...');
|
|
1000
|
+
const root = d3.hierarchy(treeData, d => d.children);
|
|
1001
|
+
console.log(`Hierarchy created: ${root.descendants().length} nodes`);
|
|
1002
|
+
|
|
1003
|
+
// Apply tree layout
|
|
1004
|
+
console.log('Applying tree layout...');
|
|
1005
|
+
treeLayout(root);
|
|
1006
|
+
console.log('Tree layout applied');
|
|
1007
|
+
|
|
1008
|
+
// Add zoom behavior
|
|
1009
|
+
const zoom = d3.zoom()
|
|
1010
|
+
.scaleExtent([0.1, 3])
|
|
1011
|
+
.on('zoom', (event) => {
|
|
1012
|
+
g.attr('transform', `translate(${margin.left},${margin.top}) ${event.transform}`);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
svg.call(zoom);
|
|
1016
|
+
|
|
1017
|
+
// Draw links
|
|
1018
|
+
const links = root.links();
|
|
1019
|
+
console.log(`Drawing ${links.length} links`);
|
|
1020
|
+
g.selectAll('.link')
|
|
1021
|
+
.data(links)
|
|
1022
|
+
.enter()
|
|
1023
|
+
.append('path')
|
|
1024
|
+
.attr('class', 'link')
|
|
1025
|
+
.attr('d', d3.linkHorizontal()
|
|
1026
|
+
.x(d => d.y)
|
|
1027
|
+
.y(d => d.x))
|
|
1028
|
+
.attr('fill', 'none')
|
|
1029
|
+
.attr('stroke', '#ccc')
|
|
1030
|
+
.attr('stroke-width', 1.5);
|
|
1031
|
+
|
|
1032
|
+
// Draw nodes
|
|
1033
|
+
const descendants = root.descendants();
|
|
1034
|
+
console.log(`Drawing ${descendants.length} nodes`);
|
|
1035
|
+
const nodes = g.selectAll('.node')
|
|
1036
|
+
.data(descendants)
|
|
1037
|
+
.enter()
|
|
1038
|
+
.append('g')
|
|
1039
|
+
.attr('class', 'node')
|
|
1040
|
+
.attr('transform', d => `translate(${d.y},${d.x})`)
|
|
1041
|
+
.on('click', handleNodeClick)
|
|
1042
|
+
.style('cursor', 'pointer');
|
|
1043
|
+
|
|
1044
|
+
console.log(`Created ${nodes.size()} node elements`);
|
|
1045
|
+
|
|
1046
|
+
// Node circles - sized proportionally to content, colored by complexity
|
|
1047
|
+
nodes.append('circle')
|
|
1048
|
+
.attr('r', d => getNodeRadius(d)) // Dynamic size based on content
|
|
1049
|
+
.attr('fill', d => getNodeFillColor(d)) // Complexity-based coloring
|
|
1050
|
+
.attr('stroke', '#fff')
|
|
1051
|
+
.attr('stroke-width', 2);
|
|
1052
|
+
|
|
1053
|
+
// Add external call arrow indicators (only for chunk nodes)
|
|
1054
|
+
nodes.each(function(d) {
|
|
1055
|
+
const node = d3.select(this);
|
|
1056
|
+
const nodeData = d.data;
|
|
1057
|
+
|
|
1058
|
+
// Only add indicators for code chunks (functions, classes, methods)
|
|
1059
|
+
if (!chunkTypes.includes(nodeData.type)) return;
|
|
1060
|
+
|
|
1061
|
+
const counts = getExternalCallCounts(nodeData);
|
|
1062
|
+
const radius = getNodeRadius(d);
|
|
1063
|
+
|
|
1064
|
+
// Inbound arrow: ← before the node (functions from other files call this)
|
|
1065
|
+
if (counts.inbound > 0) {
|
|
1066
|
+
node.append('text')
|
|
1067
|
+
.attr('class', 'call-indicator inbound')
|
|
1068
|
+
.attr('x', -(radius + 8))
|
|
1069
|
+
.attr('y', 5)
|
|
1070
|
+
.attr('text-anchor', 'end')
|
|
1071
|
+
.attr('fill', '#58a6ff')
|
|
1072
|
+
.attr('font-size', '14px')
|
|
1073
|
+
.attr('font-weight', 'bold')
|
|
1074
|
+
.attr('cursor', 'pointer')
|
|
1075
|
+
.text(counts.inbound > 1 ? `${counts.inbound}←` : '←')
|
|
1076
|
+
.append('title')
|
|
1077
|
+
.text(`Called by ${counts.inbound} external function(s):\n${counts.inboundNodes.map(n => n.name).join(', ')}`);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Outbound arrow: → after the label (this calls functions in other files)
|
|
1081
|
+
if (counts.outbound > 0) {
|
|
1082
|
+
// Get approximate label width
|
|
1083
|
+
const labelText = nodeData.name || '';
|
|
1084
|
+
const labelWidth = labelText.length * 7;
|
|
1085
|
+
|
|
1086
|
+
node.append('text')
|
|
1087
|
+
.attr('class', 'call-indicator outbound')
|
|
1088
|
+
.attr('x', radius + labelWidth + 16)
|
|
1089
|
+
.attr('y', 5)
|
|
1090
|
+
.attr('text-anchor', 'start')
|
|
1091
|
+
.attr('fill', '#f0883e')
|
|
1092
|
+
.attr('font-size', '14px')
|
|
1093
|
+
.attr('font-weight', 'bold')
|
|
1094
|
+
.attr('cursor', 'pointer')
|
|
1095
|
+
.text(counts.outbound > 1 ? `→${counts.outbound}` : '→')
|
|
1096
|
+
.append('title')
|
|
1097
|
+
.text(`Calls ${counts.outbound} external function(s):\n${counts.outboundNodes.map(n => n.name).join(', ')}`);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
// Collect and draw external call lines
|
|
1102
|
+
collectExternalCallData();
|
|
1103
|
+
drawExternalCallLines(g, root);
|
|
1104
|
+
|
|
1105
|
+
// Node labels - positioned to the right of node, left-aligned
|
|
1106
|
+
// Use transform to position text, as x attribute can have rendering issues
|
|
1107
|
+
const labels = nodes.append('text')
|
|
1108
|
+
.attr('class', 'node-label')
|
|
1109
|
+
.attr('transform', d => `translate(${getNodeRadius(d) + 6}, 0)`)
|
|
1110
|
+
.attr('dominant-baseline', 'middle')
|
|
1111
|
+
.attr('text-anchor', 'start')
|
|
1112
|
+
.text(d => d.data.name)
|
|
1113
|
+
.style('font-size', d => chunkTypes.includes(d.data.type) ? '15px' : '18px')
|
|
1114
|
+
.style('font-family', 'Arial, sans-serif')
|
|
1115
|
+
.style('fill', d => chunkTypes.includes(d.data.type) ? '#bb86fc' : '#adbac7')
|
|
1116
|
+
.style('cursor', 'pointer')
|
|
1117
|
+
.style('pointer-events', 'all')
|
|
1118
|
+
.on('click', handleNodeClick);
|
|
1119
|
+
|
|
1120
|
+
console.log(`Created ${labels.size()} label elements`);
|
|
1121
|
+
console.log('=== END RENDER LINEAR TREE ===');
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// ============================================================================
|
|
1125
|
+
// CIRCULAR TREE LAYOUT
|
|
1126
|
+
// ============================================================================
|
|
1127
|
+
|
|
1128
|
+
function renderCircularTree() {
|
|
1129
|
+
const { width, height } = getViewportDimensions();
|
|
1130
|
+
const svg = d3.select('#graph')
|
|
1131
|
+
.attr('width', width)
|
|
1132
|
+
.attr('height', height);
|
|
1133
|
+
|
|
1134
|
+
const radius = Math.min(width, height) / 2 - 100;
|
|
1135
|
+
|
|
1136
|
+
const g = svg.append('g')
|
|
1137
|
+
.attr('transform', `translate(${width/2},${height/2})`);
|
|
1138
|
+
|
|
1139
|
+
// Create radial tree layout
|
|
1140
|
+
const treeLayout = d3.tree()
|
|
1141
|
+
.size([2 * Math.PI, radius])
|
|
1142
|
+
.separation((a, b) => (a.parent === b.parent ? 1 : 2) / a.depth);
|
|
1143
|
+
|
|
1144
|
+
// Create hierarchy
|
|
1145
|
+
// D3 hierarchy automatically respects children vs _children
|
|
1146
|
+
const root = d3.hierarchy(treeData, d => d.children);
|
|
1147
|
+
|
|
1148
|
+
// Apply layout
|
|
1149
|
+
treeLayout(root);
|
|
1150
|
+
|
|
1151
|
+
// Add zoom behavior
|
|
1152
|
+
const zoom = d3.zoom()
|
|
1153
|
+
.scaleExtent([0.1, 3])
|
|
1154
|
+
.on('zoom', (event) => {
|
|
1155
|
+
g.attr('transform', `translate(${width/2},${height/2}) ${event.transform}`);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
svg.call(zoom);
|
|
1159
|
+
|
|
1160
|
+
// Draw links
|
|
1161
|
+
g.selectAll('.link')
|
|
1162
|
+
.data(root.links())
|
|
1163
|
+
.enter()
|
|
1164
|
+
.append('path')
|
|
1165
|
+
.attr('class', 'link')
|
|
1166
|
+
.attr('d', d3.linkRadial()
|
|
1167
|
+
.angle(d => d.x)
|
|
1168
|
+
.radius(d => d.y))
|
|
1169
|
+
.attr('fill', 'none')
|
|
1170
|
+
.attr('stroke', '#ccc')
|
|
1171
|
+
.attr('stroke-width', 1.5);
|
|
1172
|
+
|
|
1173
|
+
// Draw nodes
|
|
1174
|
+
const nodes = g.selectAll('.node')
|
|
1175
|
+
.data(root.descendants())
|
|
1176
|
+
.enter()
|
|
1177
|
+
.append('g')
|
|
1178
|
+
.attr('class', 'node')
|
|
1179
|
+
.attr('transform', d => `
|
|
1180
|
+
rotate(${d.x * 180 / Math.PI - 90})
|
|
1181
|
+
translate(${d.y},0)
|
|
1182
|
+
`)
|
|
1183
|
+
.on('click', handleNodeClick)
|
|
1184
|
+
.style('cursor', 'pointer');
|
|
1185
|
+
|
|
1186
|
+
// Node circles - sized proportionally to content, colored by complexity
|
|
1187
|
+
nodes.append('circle')
|
|
1188
|
+
.attr('r', d => getNodeRadius(d)) // Dynamic size based on content
|
|
1189
|
+
.attr('fill', d => getNodeFillColor(d)) // Complexity-based coloring
|
|
1190
|
+
.attr('stroke', '#fff')
|
|
1191
|
+
.attr('stroke-width', 2);
|
|
1192
|
+
|
|
1193
|
+
// Add external call arrow indicators (only for chunk nodes)
|
|
1194
|
+
nodes.each(function(d) {
|
|
1195
|
+
const node = d3.select(this);
|
|
1196
|
+
const nodeData = d.data;
|
|
1197
|
+
|
|
1198
|
+
if (!chunkTypes.includes(nodeData.type)) return;
|
|
1199
|
+
|
|
1200
|
+
const counts = getExternalCallCounts(nodeData);
|
|
1201
|
+
const radius = getNodeRadius(d);
|
|
1202
|
+
|
|
1203
|
+
// Inbound indicator
|
|
1204
|
+
if (counts.inbound > 0) {
|
|
1205
|
+
node.append('text')
|
|
1206
|
+
.attr('x', 0)
|
|
1207
|
+
.attr('y', -(radius + 8))
|
|
1208
|
+
.attr('text-anchor', 'middle')
|
|
1209
|
+
.attr('fill', '#58a6ff')
|
|
1210
|
+
.attr('font-size', '10px')
|
|
1211
|
+
.attr('font-weight', 'bold')
|
|
1212
|
+
.text(counts.inbound > 1 ? `↓${counts.inbound}` : '↓')
|
|
1213
|
+
.append('title')
|
|
1214
|
+
.text(`Called by ${counts.inbound} external function(s)`);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Outbound indicator
|
|
1218
|
+
if (counts.outbound > 0) {
|
|
1219
|
+
node.append('text')
|
|
1220
|
+
.attr('x', 0)
|
|
1221
|
+
.attr('y', radius + 12)
|
|
1222
|
+
.attr('text-anchor', 'middle')
|
|
1223
|
+
.attr('fill', '#f0883e')
|
|
1224
|
+
.attr('font-size', '10px')
|
|
1225
|
+
.attr('font-weight', 'bold')
|
|
1226
|
+
.text(counts.outbound > 1 ? `↑${counts.outbound}` : '↑')
|
|
1227
|
+
.append('title')
|
|
1228
|
+
.text(`Calls ${counts.outbound} external function(s)`);
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
// Node labels - positioned to the right of node, left-aligned
|
|
1233
|
+
// Use transform to position text, as x attribute can have rendering issues
|
|
1234
|
+
nodes.append('text')
|
|
1235
|
+
.attr('class', 'node-label')
|
|
1236
|
+
.attr('transform', d => {
|
|
1237
|
+
const offset = getNodeRadius(d) + 6;
|
|
1238
|
+
const rotate = d.x >= Math.PI ? 'rotate(180)' : '';
|
|
1239
|
+
return `translate(${offset}, 0) ${rotate}`;
|
|
1240
|
+
})
|
|
1241
|
+
.attr('dominant-baseline', 'middle')
|
|
1242
|
+
.attr('text-anchor', d => d.x >= Math.PI ? 'end' : 'start')
|
|
1243
|
+
.text(d => d.data.name)
|
|
1244
|
+
.style('font-size', d => chunkTypes.includes(d.data.type) ? '15px' : '18px')
|
|
1245
|
+
.style('font-family', 'Arial, sans-serif')
|
|
1246
|
+
.style('fill', d => chunkTypes.includes(d.data.type) ? '#bb86fc' : '#adbac7')
|
|
1247
|
+
.style('cursor', 'pointer')
|
|
1248
|
+
.style('pointer-events', 'all')
|
|
1249
|
+
.on('click', handleNodeClick);
|
|
1250
|
+
|
|
1251
|
+
// Collect and draw external call lines (circular version)
|
|
1252
|
+
collectExternalCallData();
|
|
1253
|
+
drawExternalCallLinesCircular(g, root);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// ============================================================================
|
|
1257
|
+
// INTERACTION HANDLERS
|
|
1258
|
+
// ============================================================================
|
|
1259
|
+
|
|
1260
|
+
function handleNodeClick(event, d) {
|
|
1261
|
+
event.stopPropagation();
|
|
1262
|
+
|
|
1263
|
+
const nodeData = d.data;
|
|
1264
|
+
|
|
1265
|
+
console.log('=== NODE CLICK DEBUG ===');
|
|
1266
|
+
console.log(`Clicked node: ${nodeData.name} (type: ${nodeData.type}, id: ${nodeData.id})`);
|
|
1267
|
+
console.log(`Has children: ${nodeData.children ? nodeData.children.length : 0}`);
|
|
1268
|
+
console.log(`Has _children: ${nodeData._children ? nodeData._children.length : 0}`);
|
|
1269
|
+
|
|
1270
|
+
if (nodeData.type === 'directory') {
|
|
1271
|
+
// Toggle directory: swap children <-> _children
|
|
1272
|
+
if (nodeData.children) {
|
|
1273
|
+
// Currently expanded - collapse it
|
|
1274
|
+
console.log('Collapsing directory');
|
|
1275
|
+
nodeData._children = nodeData.children;
|
|
1276
|
+
nodeData.children = null;
|
|
1277
|
+
} else if (nodeData._children) {
|
|
1278
|
+
// Currently collapsed - expand it
|
|
1279
|
+
console.log('Expanding directory');
|
|
1280
|
+
nodeData.children = nodeData._children;
|
|
1281
|
+
nodeData._children = null;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Re-render to show/hide children
|
|
1285
|
+
renderVisualization();
|
|
1286
|
+
|
|
1287
|
+
// Don't auto-open viewer panel for directories - just expand/collapse
|
|
1288
|
+
} else if (nodeData.type === 'file') {
|
|
1289
|
+
// Check if this file has a collapsed chunk (single chunk with no children)
|
|
1290
|
+
if (nodeData.collapsed_chunk) {
|
|
1291
|
+
console.log(`Collapsed file+chunk: ${nodeData.name}, showing content directly`);
|
|
1292
|
+
displayChunkContent({
|
|
1293
|
+
...nodeData.collapsed_chunk,
|
|
1294
|
+
name: nodeData.collapsed_chunk.name,
|
|
1295
|
+
type: nodeData.collapsed_chunk.type
|
|
1296
|
+
});
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Check if this is a single-chunk file - skip to content directly
|
|
1301
|
+
const childrenArray = nodeData._children || nodeData.children;
|
|
1302
|
+
|
|
1303
|
+
if (childrenArray && childrenArray.length === 1) {
|
|
1304
|
+
const onlyChild = childrenArray[0];
|
|
1305
|
+
|
|
1306
|
+
if (chunkTypes.includes(onlyChild.type)) {
|
|
1307
|
+
console.log(`Single-chunk file: ${nodeData.name}, showing content directly`);
|
|
1308
|
+
|
|
1309
|
+
// Expand the file visually (for tree consistency)
|
|
1310
|
+
if (nodeData._children) {
|
|
1311
|
+
nodeData.children = nodeData._children;
|
|
1312
|
+
nodeData._children = null;
|
|
1313
|
+
renderVisualization();
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Auto-display the chunk content
|
|
1317
|
+
displayChunkContent(onlyChild);
|
|
1318
|
+
return; // Skip normal file toggle behavior
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Continue with existing toggle logic for multi-chunk files
|
|
1323
|
+
// Toggle file: swap children <-> _children
|
|
1324
|
+
if (nodeData.children) {
|
|
1325
|
+
// Currently expanded - collapse it
|
|
1326
|
+
console.log('Collapsing file');
|
|
1327
|
+
nodeData._children = nodeData.children;
|
|
1328
|
+
nodeData.children = null;
|
|
1329
|
+
} else if (nodeData._children) {
|
|
1330
|
+
// Currently collapsed - expand it
|
|
1331
|
+
console.log('Expanding file');
|
|
1332
|
+
nodeData.children = nodeData._children;
|
|
1333
|
+
nodeData._children = null;
|
|
1334
|
+
} else {
|
|
1335
|
+
console.log('WARNING: File has neither children nor _children!');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Re-render to show/hide children
|
|
1339
|
+
renderVisualization();
|
|
1340
|
+
|
|
1341
|
+
// Don't auto-open viewer panel for files - just expand/collapse
|
|
1342
|
+
} else if (chunkTypes.includes(nodeData.type)) {
|
|
1343
|
+
// Chunks can have children too (e.g., imports -> functions, class -> methods)
|
|
1344
|
+
// If chunk has children, toggle expand/collapse
|
|
1345
|
+
if (nodeData.children || nodeData._children) {
|
|
1346
|
+
if (nodeData.children) {
|
|
1347
|
+
// Currently expanded - collapse it
|
|
1348
|
+
console.log(`Collapsing ${nodeData.type} chunk`);
|
|
1349
|
+
nodeData._children = nodeData.children;
|
|
1350
|
+
nodeData.children = null;
|
|
1351
|
+
} else if (nodeData._children) {
|
|
1352
|
+
// Currently collapsed - expand it
|
|
1353
|
+
console.log(`Expanding ${nodeData.type} chunk to show ${nodeData._children.length} children`);
|
|
1354
|
+
nodeData.children = nodeData._children;
|
|
1355
|
+
nodeData._children = null;
|
|
1356
|
+
}
|
|
1357
|
+
// Re-render to show/hide children
|
|
1358
|
+
renderVisualization();
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Also show chunk content in side panel
|
|
1362
|
+
console.log('Displaying chunk content');
|
|
1363
|
+
displayChunkContent(nodeData);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
console.log('=== END NODE CLICK DEBUG ===');
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function displayDirectoryInfo(dirData, addToHistory = true) {
|
|
1370
|
+
openViewerPanel();
|
|
1371
|
+
|
|
1372
|
+
// Add to navigation history
|
|
1373
|
+
if (addToHistory) {
|
|
1374
|
+
addToNavHistory({type: 'directory', data: dirData});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const title = document.getElementById('viewer-title');
|
|
1378
|
+
const content = document.getElementById('viewer-content');
|
|
1379
|
+
|
|
1380
|
+
title.textContent = `📁 ${dirData.name}`;
|
|
1381
|
+
|
|
1382
|
+
// Count children
|
|
1383
|
+
const children = dirData.children || dirData._children || [];
|
|
1384
|
+
const dirs = children.filter(c => c.type === 'directory').length;
|
|
1385
|
+
const files = children.filter(c => c.type === 'file').length;
|
|
1386
|
+
|
|
1387
|
+
let html = '';
|
|
1388
|
+
|
|
1389
|
+
// Navigation bar with breadcrumbs and back/forward
|
|
1390
|
+
html += renderNavigationBar(dirData);
|
|
1391
|
+
|
|
1392
|
+
html += '<div class="viewer-section">';
|
|
1393
|
+
html += '<div class="viewer-section-title">Directory Information</div>';
|
|
1394
|
+
html += '<div class="viewer-info-grid">';
|
|
1395
|
+
html += `<div class="viewer-info-row">`;
|
|
1396
|
+
html += `<span class="viewer-info-label">Name:</span>`;
|
|
1397
|
+
html += `<span class="viewer-info-value">${escapeHtml(dirData.name)}</span>`;
|
|
1398
|
+
html += `</div>`;
|
|
1399
|
+
html += `<div class="viewer-info-row">`;
|
|
1400
|
+
html += `<span class="viewer-info-label">Subdirectories:</span>`;
|
|
1401
|
+
html += `<span class="viewer-info-value">${dirs}</span>`;
|
|
1402
|
+
html += `</div>`;
|
|
1403
|
+
html += `<div class="viewer-info-row">`;
|
|
1404
|
+
html += `<span class="viewer-info-label">Files:</span>`;
|
|
1405
|
+
html += `<span class="viewer-info-value">${files}</span>`;
|
|
1406
|
+
html += `</div>`;
|
|
1407
|
+
html += `<div class="viewer-info-row">`;
|
|
1408
|
+
html += `<span class="viewer-info-label">Total Items:</span>`;
|
|
1409
|
+
html += `<span class="viewer-info-value">${children.length}</span>`;
|
|
1410
|
+
html += `</div>`;
|
|
1411
|
+
html += '</div>';
|
|
1412
|
+
html += '</div>';
|
|
1413
|
+
|
|
1414
|
+
if (children.length > 0) {
|
|
1415
|
+
html += '<div class="viewer-section">';
|
|
1416
|
+
html += '<div class="viewer-section-title">Contents</div>';
|
|
1417
|
+
html += '<div class="dir-list">';
|
|
1418
|
+
|
|
1419
|
+
// Sort: directories first, then files
|
|
1420
|
+
const sortedChildren = [...children].sort((a, b) => {
|
|
1421
|
+
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
|
1422
|
+
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
|
1423
|
+
return a.name.localeCompare(b.name);
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
sortedChildren.forEach(child => {
|
|
1427
|
+
const icon = child.type === 'directory' ? '📁' : '📄';
|
|
1428
|
+
const type = child.type === 'directory' ? 'dir' : 'file';
|
|
1429
|
+
const childData = JSON.stringify(child).replace(/"/g, '"');
|
|
1430
|
+
const clickHandler = child.type === 'directory'
|
|
1431
|
+
? `navigateToDirectory(${childData})`
|
|
1432
|
+
: `navigateToFile(${childData})`;
|
|
1433
|
+
html += `<div class="dir-list-item clickable" onclick="${clickHandler}">`;
|
|
1434
|
+
html += `<span class="dir-icon">${icon}</span>`;
|
|
1435
|
+
html += `<span class="dir-name">${escapeHtml(child.name)}</span>`;
|
|
1436
|
+
html += `<span class="dir-type">${type}</span>`;
|
|
1437
|
+
html += `<span class="dir-arrow">→</span>`;
|
|
1438
|
+
html += `</div>`;
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
html += '</div>';
|
|
1442
|
+
html += '</div>';
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
content.innerHTML = html;
|
|
1446
|
+
|
|
1447
|
+
// Hide section dropdown for directories (no code sections)
|
|
1448
|
+
populateSectionDropdown([]);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function displayFileInfo(fileData, addToHistory = true) {
|
|
1452
|
+
openViewerPanel();
|
|
1453
|
+
|
|
1454
|
+
// Add to navigation history
|
|
1455
|
+
if (addToHistory) {
|
|
1456
|
+
addToNavHistory({type: 'file', data: fileData});
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const title = document.getElementById('viewer-title');
|
|
1460
|
+
const content = document.getElementById('viewer-content');
|
|
1461
|
+
|
|
1462
|
+
title.textContent = `📄 ${fileData.name}`;
|
|
1463
|
+
|
|
1464
|
+
// Get chunks
|
|
1465
|
+
const chunks = fileData.children || fileData._children || [];
|
|
1466
|
+
|
|
1467
|
+
let html = '';
|
|
1468
|
+
|
|
1469
|
+
// Navigation bar with breadcrumbs and back/forward
|
|
1470
|
+
html += renderNavigationBar(fileData);
|
|
1471
|
+
|
|
1472
|
+
html += '<div class="viewer-section">';
|
|
1473
|
+
html += '<div class="viewer-section-title">File Information</div>';
|
|
1474
|
+
html += '<div class="viewer-info-grid">';
|
|
1475
|
+
html += `<div class="viewer-info-row">`;
|
|
1476
|
+
html += `<span class="viewer-info-label">Name:</span>`;
|
|
1477
|
+
html += `<span class="viewer-info-value">${escapeHtml(fileData.name)}</span>`;
|
|
1478
|
+
html += `</div>`;
|
|
1479
|
+
html += `<div class="viewer-info-row">`;
|
|
1480
|
+
html += `<span class="viewer-info-label">Chunks:</span>`;
|
|
1481
|
+
html += `<span class="viewer-info-value">${chunks.length}</span>`;
|
|
1482
|
+
html += `</div>`;
|
|
1483
|
+
if (fileData.path) {
|
|
1484
|
+
html += `<div class="viewer-info-row">`;
|
|
1485
|
+
html += `<span class="viewer-info-label">Path:</span>`;
|
|
1486
|
+
html += `<span class="viewer-info-value" style="word-break: break-all;">${escapeHtml(fileData.path)}</span>`;
|
|
1487
|
+
html += `</div>`;
|
|
1488
|
+
}
|
|
1489
|
+
html += '</div>';
|
|
1490
|
+
html += '</div>';
|
|
1491
|
+
|
|
1492
|
+
if (chunks.length > 0) {
|
|
1493
|
+
html += '<div class="viewer-section">';
|
|
1494
|
+
html += '<div class="viewer-section-title">Code Chunks</div>';
|
|
1495
|
+
html += '<div class="chunk-list">';
|
|
1496
|
+
|
|
1497
|
+
chunks.forEach(chunk => {
|
|
1498
|
+
const icon = getChunkIcon(chunk.type);
|
|
1499
|
+
const chunkName = chunk.name || chunk.type || 'chunk';
|
|
1500
|
+
const lines = chunk.start_line && chunk.end_line
|
|
1501
|
+
? `Lines ${chunk.start_line}-${chunk.end_line}`
|
|
1502
|
+
: 'Unknown lines';
|
|
1503
|
+
|
|
1504
|
+
html += `<div class="chunk-list-item" onclick="displayChunkContent(${JSON.stringify(chunk).replace(/"/g, '"')})">`;
|
|
1505
|
+
html += `<span class="chunk-icon">${icon}</span>`;
|
|
1506
|
+
html += `<div class="chunk-info">`;
|
|
1507
|
+
html += `<div class="chunk-name">${escapeHtml(chunkName)}</div>`;
|
|
1508
|
+
html += `<div class="chunk-meta">${lines} • ${chunk.type || 'code'}</div>`;
|
|
1509
|
+
html += `</div>`;
|
|
1510
|
+
html += `</div>`;
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
html += '</div>';
|
|
1514
|
+
html += '</div>';
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
content.innerHTML = html;
|
|
1518
|
+
|
|
1519
|
+
// Hide section dropdown for files (no code sections in file view)
|
|
1520
|
+
populateSectionDropdown([]);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function displayChunkContent(chunkData, addToHistory = true) {
|
|
1524
|
+
openViewerPanel();
|
|
1525
|
+
|
|
1526
|
+
// Add to navigation history
|
|
1527
|
+
if (addToHistory) {
|
|
1528
|
+
addToNavHistory({type: 'chunk', data: chunkData});
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const title = document.getElementById('viewer-title');
|
|
1532
|
+
const content = document.getElementById('viewer-content');
|
|
1533
|
+
|
|
1534
|
+
const chunkName = chunkData.name || chunkData.type || 'Chunk';
|
|
1535
|
+
title.textContent = `${getChunkIcon(chunkData.type)} ${chunkName}`;
|
|
1536
|
+
|
|
1537
|
+
let html = '';
|
|
1538
|
+
|
|
1539
|
+
// Navigation bar with breadcrumbs and back/forward
|
|
1540
|
+
html += renderNavigationBar(chunkData);
|
|
1541
|
+
|
|
1542
|
+
// Track sections for dropdown navigation
|
|
1543
|
+
const sections = [];
|
|
1544
|
+
|
|
1545
|
+
// === ORDER: Docstring (comments), Code, Metadata ===
|
|
1546
|
+
|
|
1547
|
+
// === 1. Docstring Section (Comments) ===
|
|
1548
|
+
if (chunkData.docstring) {
|
|
1549
|
+
sections.push({ id: 'docstring', label: '📖 Docstring' });
|
|
1550
|
+
html += '<div class="viewer-section" data-section="docstring">';
|
|
1551
|
+
html += '<div class="viewer-section-title">📖 Docstring</div>';
|
|
1552
|
+
html += `<div style="color: #8b949e; font-style: italic; padding: 8px 12px; background: #161b22; border-radius: 4px; white-space: pre-wrap;">${escapeHtml(chunkData.docstring)}</div>`;
|
|
1553
|
+
html += '</div>';
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// === 2. Source Code Section ===
|
|
1557
|
+
if (chunkData.content) {
|
|
1558
|
+
sections.push({ id: 'source-code', label: '📝 Source Code' });
|
|
1559
|
+
html += '<div class="viewer-section" data-section="source-code">';
|
|
1560
|
+
html += '<div class="viewer-section-title">📝 Source Code</div>';
|
|
1561
|
+
const langClass = getLanguageClass(chunkData.file_path);
|
|
1562
|
+
html += `<pre><code class="hljs${langClass ? ' language-' + langClass : ''}">${escapeHtml(chunkData.content)}</code></pre>`;
|
|
1563
|
+
html += '</div>';
|
|
1564
|
+
} else {
|
|
1565
|
+
html += '<p style="color: #8b949e; padding: 20px; text-align: center;">No content available for this chunk.</p>';
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// === 3. Metadata Section ===
|
|
1569
|
+
sections.push({ id: 'metadata', label: 'ℹ️ Metadata' });
|
|
1570
|
+
html += '<div class="viewer-section" data-section="metadata">';
|
|
1571
|
+
html += '<div class="viewer-section-title">ℹ️ Metadata</div>';
|
|
1572
|
+
html += '<div class="viewer-info-grid">';
|
|
1573
|
+
|
|
1574
|
+
// Basic info
|
|
1575
|
+
html += `<div class="viewer-info-row">`;
|
|
1576
|
+
html += `<span class="viewer-info-label">Name:</span>`;
|
|
1577
|
+
html += `<span class="viewer-info-value">${escapeHtml(chunkName)}</span>`;
|
|
1578
|
+
html += `</div>`;
|
|
1579
|
+
|
|
1580
|
+
html += `<div class="viewer-info-row">`;
|
|
1581
|
+
html += `<span class="viewer-info-label">Type:</span>`;
|
|
1582
|
+
html += `<span class="viewer-info-value">${escapeHtml(chunkData.type || 'code')}</span>`;
|
|
1583
|
+
html += `</div>`;
|
|
1584
|
+
|
|
1585
|
+
// File path (clickable - navigates to file node)
|
|
1586
|
+
if (chunkData.file_path) {
|
|
1587
|
+
const shortPath = chunkData.file_path.split('/').slice(-3).join('/');
|
|
1588
|
+
const escapedPath = escapeHtml(chunkData.file_path).replace(/'/g, "\\'");
|
|
1589
|
+
html += `<div class="viewer-info-row">`;
|
|
1590
|
+
html += `<span class="viewer-info-label">File:</span>`;
|
|
1591
|
+
html += `<span class="viewer-info-value clickable" onclick="navigateToFileByPath('${escapedPath}')" title="Click to navigate to file: ${escapeHtml(chunkData.file_path)}">.../${escapeHtml(shortPath)}</span>`;
|
|
1592
|
+
html += `</div>`;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// Line numbers
|
|
1596
|
+
if (chunkData.start_line && chunkData.end_line) {
|
|
1597
|
+
html += `<div class="viewer-info-row">`;
|
|
1598
|
+
html += `<span class="viewer-info-label">Lines:</span>`;
|
|
1599
|
+
html += `<span class="viewer-info-value">${chunkData.start_line} - ${chunkData.end_line} (${chunkData.end_line - chunkData.start_line + 1} lines)</span>`;
|
|
1600
|
+
html += `</div>`;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Language
|
|
1604
|
+
if (chunkData.language) {
|
|
1605
|
+
html += `<div class="viewer-info-row">`;
|
|
1606
|
+
html += `<span class="viewer-info-label">Language:</span>`;
|
|
1607
|
+
html += `<span class="viewer-info-value">${escapeHtml(chunkData.language)}</span>`;
|
|
1608
|
+
html += `</div>`;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// Complexity
|
|
1612
|
+
if (chunkData.complexity !== undefined && chunkData.complexity !== null) {
|
|
1613
|
+
const complexityColor = chunkData.complexity > 10 ? '#f85149' : chunkData.complexity > 5 ? '#d29922' : '#3fb950';
|
|
1614
|
+
html += `<div class="viewer-info-row">`;
|
|
1615
|
+
html += `<span class="viewer-info-label">Complexity:</span>`;
|
|
1616
|
+
html += `<span class="viewer-info-value" style="color: ${complexityColor}">${chunkData.complexity.toFixed(1)}</span>`;
|
|
1617
|
+
html += `</div>`;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
html += '</div>';
|
|
1621
|
+
html += '</div>';
|
|
1622
|
+
|
|
1623
|
+
// === 4. External Calls & Callers Section (Cross-file references) ===
|
|
1624
|
+
const chunkId = chunkData.id;
|
|
1625
|
+
const currentFilePath = chunkData.file_path;
|
|
1626
|
+
|
|
1627
|
+
if (chunkId) {
|
|
1628
|
+
// Find all caller relationships
|
|
1629
|
+
const allCallers = allLinks.filter(l => l.type === 'caller' && l.target === chunkId);
|
|
1630
|
+
const allCallees = allLinks.filter(l => l.type === 'caller' && l.source === chunkId);
|
|
1631
|
+
|
|
1632
|
+
// Separate external (different file) from local (same file) relationships
|
|
1633
|
+
// Use Maps to deduplicate by node.id
|
|
1634
|
+
const externalCallersMap = new Map();
|
|
1635
|
+
const localCallersMap = new Map();
|
|
1636
|
+
allCallers.forEach(link => {
|
|
1637
|
+
const callerNode = allNodes.find(n => n.id === link.source);
|
|
1638
|
+
if (callerNode) {
|
|
1639
|
+
if (callerNode.file_path !== currentFilePath) {
|
|
1640
|
+
if (!externalCallersMap.has(callerNode.id)) {
|
|
1641
|
+
externalCallersMap.set(callerNode.id, { link, node: callerNode });
|
|
1642
|
+
}
|
|
1643
|
+
} else {
|
|
1644
|
+
if (!localCallersMap.has(callerNode.id)) {
|
|
1645
|
+
localCallersMap.set(callerNode.id, { link, node: callerNode });
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
});
|
|
1650
|
+
const externalCallers = Array.from(externalCallersMap.values());
|
|
1651
|
+
const localCallers = Array.from(localCallersMap.values());
|
|
1652
|
+
|
|
1653
|
+
const externalCalleesMap = new Map();
|
|
1654
|
+
const localCalleesMap = new Map();
|
|
1655
|
+
allCallees.forEach(link => {
|
|
1656
|
+
const calleeNode = allNodes.find(n => n.id === link.target);
|
|
1657
|
+
if (calleeNode) {
|
|
1658
|
+
if (calleeNode.file_path !== currentFilePath) {
|
|
1659
|
+
if (!externalCalleesMap.has(calleeNode.id)) {
|
|
1660
|
+
externalCalleesMap.set(calleeNode.id, { link, node: calleeNode });
|
|
1661
|
+
}
|
|
1662
|
+
} else {
|
|
1663
|
+
if (!localCalleesMap.has(calleeNode.id)) {
|
|
1664
|
+
localCalleesMap.set(calleeNode.id, { link, node: calleeNode });
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
const externalCallees = Array.from(externalCalleesMap.values());
|
|
1670
|
+
const localCallees = Array.from(localCalleesMap.values());
|
|
1671
|
+
|
|
1672
|
+
// === External Callers Section (functions from other files that call this) ===
|
|
1673
|
+
if (externalCallers.length > 0) {
|
|
1674
|
+
sections.push({ id: 'external-callers', label: '📥 External Callers' });
|
|
1675
|
+
html += '<div class="viewer-section" data-section="external-callers">';
|
|
1676
|
+
html += '<div class="viewer-section-title">📥 External Callers <span style="color: #8b949e; font-weight: normal;">(functions from other files calling this)</span></div>';
|
|
1677
|
+
html += '<div style="display: flex; flex-direction: column; gap: 6px;">';
|
|
1678
|
+
externalCallers.slice(0, 10).forEach(({ link, node }) => {
|
|
1679
|
+
const shortPath = node.file_path ? node.file_path.split('/').slice(-2).join('/') : '';
|
|
1680
|
+
html += `<div class="external-call-item" onclick="focusNodeInTree('${link.source}')" title="${escapeHtml(node.file_path || '')}">`;
|
|
1681
|
+
html += `<span class="external-call-icon">←</span>`;
|
|
1682
|
+
html += `<span class="external-call-name">${escapeHtml(node.name || node.id.substring(0, 8))}</span>`;
|
|
1683
|
+
html += `<span class="external-call-path">${escapeHtml(shortPath)}</span>`;
|
|
1684
|
+
html += `</div>`;
|
|
1685
|
+
});
|
|
1686
|
+
if (externalCallers.length > 10) {
|
|
1687
|
+
html += `<div style="color: #8b949e; font-size: 11px; padding-left: 20px;">+${externalCallers.length - 10} more external callers</div>`;
|
|
1688
|
+
}
|
|
1689
|
+
html += '</div></div>';
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// === External Calls Section (functions in other files this calls) ===
|
|
1693
|
+
if (externalCallees.length > 0) {
|
|
1694
|
+
sections.push({ id: 'external-calls', label: '📤 External Calls' });
|
|
1695
|
+
html += '<div class="viewer-section" data-section="external-calls">';
|
|
1696
|
+
html += '<div class="viewer-section-title">📤 External Calls <span style="color: #8b949e; font-weight: normal;">(functions in other files this calls)</span></div>';
|
|
1697
|
+
html += '<div style="display: flex; flex-direction: column; gap: 6px;">';
|
|
1698
|
+
externalCallees.slice(0, 10).forEach(({ link, node }) => {
|
|
1699
|
+
const shortPath = node.file_path ? node.file_path.split('/').slice(-2).join('/') : '';
|
|
1700
|
+
html += `<div class="external-call-item" onclick="focusNodeInTree('${link.target}')" title="${escapeHtml(node.file_path || '')}">`;
|
|
1701
|
+
html += `<span class="external-call-icon">→</span>`;
|
|
1702
|
+
html += `<span class="external-call-name">${escapeHtml(node.name || node.id.substring(0, 8))}</span>`;
|
|
1703
|
+
html += `<span class="external-call-path">${escapeHtml(shortPath)}</span>`;
|
|
1704
|
+
html += `</div>`;
|
|
1705
|
+
});
|
|
1706
|
+
if (externalCallees.length > 10) {
|
|
1707
|
+
html += `<div style="color: #8b949e; font-size: 11px; padding-left: 20px;">+${externalCallees.length - 10} more external calls</div>`;
|
|
1708
|
+
}
|
|
1709
|
+
html += '</div></div>';
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// === Local (Same-File) Relationships Section ===
|
|
1713
|
+
if (localCallers.length > 0 || localCallees.length > 0) {
|
|
1714
|
+
sections.push({ id: 'local-references', label: '🔗 Local References' });
|
|
1715
|
+
html += '<div class="viewer-section" data-section="local-references">';
|
|
1716
|
+
html += '<div class="viewer-section-title">🔗 Local References <span style="color: #8b949e; font-weight: normal;">(same file)</span></div>';
|
|
1717
|
+
|
|
1718
|
+
if (localCallers.length > 0) {
|
|
1719
|
+
html += '<div style="margin-bottom: 8px;">';
|
|
1720
|
+
html += '<div style="color: #58a6ff; font-size: 11px; margin-bottom: 4px;">Called by:</div>';
|
|
1721
|
+
html += '<div style="display: flex; flex-wrap: wrap; gap: 4px;">';
|
|
1722
|
+
localCallers.slice(0, 8).forEach(({ link, node }) => {
|
|
1723
|
+
html += `<span class="relationship-tag caller" onclick="focusNodeInTree('${link.source}')" title="${escapeHtml(node.name || '')}">${escapeHtml(node.name || node.id.substring(0, 8))}</span>`;
|
|
1724
|
+
});
|
|
1725
|
+
if (localCallers.length > 8) {
|
|
1726
|
+
html += `<span style="color: #8b949e; font-size: 10px;">+${localCallers.length - 8} more</span>`;
|
|
1727
|
+
}
|
|
1728
|
+
html += '</div></div>';
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (localCallees.length > 0) {
|
|
1732
|
+
html += '<div>';
|
|
1733
|
+
html += '<div style="color: #f0883e; font-size: 11px; margin-bottom: 4px;">Calls:</div>';
|
|
1734
|
+
html += '<div style="display: flex; flex-wrap: wrap; gap: 4px;">';
|
|
1735
|
+
localCallees.slice(0, 8).forEach(({ link, node }) => {
|
|
1736
|
+
html += `<span class="relationship-tag callee" onclick="focusNodeInTree('${link.target}')" title="${escapeHtml(node.name || '')}">${escapeHtml(node.name || node.id.substring(0, 8))}</span>`;
|
|
1737
|
+
});
|
|
1738
|
+
if (localCallees.length > 8) {
|
|
1739
|
+
html += `<span style="color: #8b949e; font-size: 10px;">+${localCallees.length - 8} more</span>`;
|
|
1740
|
+
}
|
|
1741
|
+
html += '</div></div>';
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
html += '</div>';
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// === Semantically Similar Section ===
|
|
1748
|
+
const semanticLinks = allLinks.filter(l => l.type === 'semantic' && l.source === chunkId);
|
|
1749
|
+
if (semanticLinks.length > 0) {
|
|
1750
|
+
sections.push({ id: 'semantic', label: '🧠 Semantically Similar' });
|
|
1751
|
+
html += '<div class="viewer-section" data-section="semantic">';
|
|
1752
|
+
html += '<div class="viewer-section-title">🧠 Semantically Similar</div>';
|
|
1753
|
+
html += '<div style="display: flex; flex-direction: column; gap: 4px;">';
|
|
1754
|
+
semanticLinks.slice(0, 5).forEach(link => {
|
|
1755
|
+
const similarNode = allNodes.find(n => n.id === link.target);
|
|
1756
|
+
if (similarNode) {
|
|
1757
|
+
const similarity = (link.similarity * 100).toFixed(0);
|
|
1758
|
+
const label = similarNode.name || similarNode.id.substring(0, 8);
|
|
1759
|
+
html += `<div class="semantic-item" onclick="focusNodeInTree('${link.target}')" title="${escapeHtml(similarNode.file_path || '')}">`;
|
|
1760
|
+
html += `<span class="semantic-score">${similarity}%</span>`;
|
|
1761
|
+
html += `<span class="semantic-name">${escapeHtml(label)}</span>`;
|
|
1762
|
+
html += `<span class="semantic-type">${similarNode.type || ''}</span>`;
|
|
1763
|
+
html += `</div>`;
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
html += '</div>';
|
|
1767
|
+
html += '</div>';
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
content.innerHTML = html;
|
|
1772
|
+
|
|
1773
|
+
// Apply syntax highlighting to code blocks
|
|
1774
|
+
content.querySelectorAll('pre code').forEach((block) => {
|
|
1775
|
+
if (typeof hljs !== 'undefined') {
|
|
1776
|
+
hljs.highlightElement(block);
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
// Populate section dropdown for navigation
|
|
1781
|
+
populateSectionDropdown(sections);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Focus on a node in the tree (expand path, scroll, highlight)
|
|
1785
|
+
function focusNodeInTree(nodeId) {
|
|
1786
|
+
console.log(`Focusing on node in tree: ${nodeId}`);
|
|
1787
|
+
|
|
1788
|
+
// Find the node in allNodes (the original data)
|
|
1789
|
+
const targetNodeData = allNodes.find(n => n.id === nodeId);
|
|
1790
|
+
if (!targetNodeData) {
|
|
1791
|
+
console.log(`Node ${nodeId} not found in allNodes`);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// Find the path to this node in the tree structure
|
|
1796
|
+
// We need to find and expand all ancestors to make the node visible
|
|
1797
|
+
const pathToNode = findPathToNode(treeData, nodeId);
|
|
1798
|
+
|
|
1799
|
+
if (pathToNode.length > 0) {
|
|
1800
|
+
console.log(`Found path to node: ${pathToNode.map(n => n.name).join(' -> ')}`);
|
|
1801
|
+
|
|
1802
|
+
// Expand all nodes along the path (except the target node itself)
|
|
1803
|
+
pathToNode.slice(0, -1).forEach(node => {
|
|
1804
|
+
if (node._children) {
|
|
1805
|
+
// Node is collapsed, expand it
|
|
1806
|
+
console.log(`Expanding ${node.name} to reveal path`);
|
|
1807
|
+
node.children = node._children;
|
|
1808
|
+
node._children = null;
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
// Re-render the tree to show the expanded path
|
|
1813
|
+
renderVisualization();
|
|
1814
|
+
|
|
1815
|
+
// After render, scroll to and highlight the target node
|
|
1816
|
+
setTimeout(() => {
|
|
1817
|
+
highlightNodeInTree(nodeId);
|
|
1818
|
+
}, 100);
|
|
1819
|
+
} else {
|
|
1820
|
+
console.log(`Path to node ${nodeId} not found in tree - it may be orphaned`);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Display the content in the viewer panel
|
|
1824
|
+
if (chunkTypes.includes(targetNodeData.type)) {
|
|
1825
|
+
displayChunkContent(targetNodeData);
|
|
1826
|
+
} else if (targetNodeData.type === 'file') {
|
|
1827
|
+
displayFileInfo(targetNodeData);
|
|
1828
|
+
} else if (targetNodeData.type === 'directory') {
|
|
1829
|
+
displayDirectoryInfo(targetNodeData);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// Find path from root to a specific node by ID
|
|
1834
|
+
function findPathToNode(node, targetId, path = []) {
|
|
1835
|
+
if (!node) return [];
|
|
1836
|
+
|
|
1837
|
+
// Add current node to path
|
|
1838
|
+
const currentPath = [...path, node];
|
|
1839
|
+
|
|
1840
|
+
// Check if this is the target
|
|
1841
|
+
if (node.id === targetId) {
|
|
1842
|
+
return currentPath;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// Check visible children
|
|
1846
|
+
if (node.children) {
|
|
1847
|
+
for (const child of node.children) {
|
|
1848
|
+
const result = findPathToNode(child, targetId, currentPath);
|
|
1849
|
+
if (result.length > 0) return result;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Check hidden children
|
|
1854
|
+
if (node._children) {
|
|
1855
|
+
for (const child of node._children) {
|
|
1856
|
+
const result = findPathToNode(child, targetId, currentPath);
|
|
1857
|
+
if (result.length > 0) return result;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
return [];
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// Highlight and scroll to a node in the rendered tree
|
|
1865
|
+
function highlightNodeInTree(nodeId) {
|
|
1866
|
+
// Remove any existing highlight
|
|
1867
|
+
d3.selectAll('.node-highlight').classed('node-highlight', false);
|
|
1868
|
+
|
|
1869
|
+
// Find and highlight the target node in the rendered SVG
|
|
1870
|
+
const svg = d3.select('#graph');
|
|
1871
|
+
const targetNode = svg.selectAll('.node')
|
|
1872
|
+
.filter(d => d.data.id === nodeId);
|
|
1873
|
+
|
|
1874
|
+
if (!targetNode.empty()) {
|
|
1875
|
+
// Add highlight class
|
|
1876
|
+
targetNode.classed('node-highlight', true);
|
|
1877
|
+
|
|
1878
|
+
// Pulse the node circle - scale up from current size
|
|
1879
|
+
targetNode.select('circle')
|
|
1880
|
+
.transition()
|
|
1881
|
+
.duration(200)
|
|
1882
|
+
.attr('r', d => getNodeRadius(d) * 1.5) // Grow 50%
|
|
1883
|
+
.transition()
|
|
1884
|
+
.duration(200)
|
|
1885
|
+
.attr('r', d => getNodeRadius(d) * 0.8) // Shrink 20%
|
|
1886
|
+
.transition()
|
|
1887
|
+
.duration(200)
|
|
1888
|
+
.attr('r', d => getNodeRadius(d)); // Return to normal
|
|
1889
|
+
|
|
1890
|
+
// Get the node's position for scrolling
|
|
1891
|
+
const nodeTransform = targetNode.attr('transform');
|
|
1892
|
+
const match = nodeTransform.match(/translate\\(([^,]+),([^)]+)\\)/);
|
|
1893
|
+
if (match) {
|
|
1894
|
+
const x = parseFloat(match[1]);
|
|
1895
|
+
const y = parseFloat(match[2]);
|
|
1896
|
+
|
|
1897
|
+
// Pan the view to center on this node
|
|
1898
|
+
const { width, height } = getViewportDimensions();
|
|
1899
|
+
const zoom = d3.zoom().on('zoom', () => {}); // Get current zoom
|
|
1900
|
+
const svg = d3.select('#graph');
|
|
1901
|
+
|
|
1902
|
+
// Calculate center offset
|
|
1903
|
+
const centerX = width / 2 - x;
|
|
1904
|
+
const centerY = height / 2 - y;
|
|
1905
|
+
|
|
1906
|
+
// Apply smooth transition to center on node
|
|
1907
|
+
svg.transition()
|
|
1908
|
+
.duration(500)
|
|
1909
|
+
.call(
|
|
1910
|
+
d3.zoom().transform,
|
|
1911
|
+
d3.zoomIdentity.translate(centerX, centerY)
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
console.log(`Highlighted node ${nodeId}`);
|
|
1916
|
+
} else {
|
|
1917
|
+
console.log(`Node ${nodeId} not found in rendered tree`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Legacy function for backward compatibility
|
|
1922
|
+
function focusNode(nodeId) {
|
|
1923
|
+
focusNodeInTree(nodeId);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function getChunkIcon(chunkType) {
|
|
1927
|
+
const icons = {
|
|
1928
|
+
'function': '⚡',
|
|
1929
|
+
'class': '🏛️',
|
|
1930
|
+
'method': '🔧',
|
|
1931
|
+
'code': '📝',
|
|
1932
|
+
'import': '📦',
|
|
1933
|
+
'comment': '💬',
|
|
1934
|
+
'docstring': '📖'
|
|
1935
|
+
};
|
|
1936
|
+
return icons[chunkType] || '📝';
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function escapeHtml(text) {
|
|
1940
|
+
const div = document.createElement('div');
|
|
1941
|
+
div.textContent = text;
|
|
1942
|
+
return div.innerHTML;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// Get language class for highlight.js based on file extension
|
|
1946
|
+
function getLanguageClass(filePath) {
|
|
1947
|
+
if (!filePath) return '';
|
|
1948
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
1949
|
+
const langMap = {
|
|
1950
|
+
'py': 'python',
|
|
1951
|
+
'js': 'javascript',
|
|
1952
|
+
'ts': 'typescript',
|
|
1953
|
+
'tsx': 'typescript',
|
|
1954
|
+
'jsx': 'javascript',
|
|
1955
|
+
'java': 'java',
|
|
1956
|
+
'go': 'go',
|
|
1957
|
+
'rs': 'rust',
|
|
1958
|
+
'rb': 'ruby',
|
|
1959
|
+
'php': 'php',
|
|
1960
|
+
'c': 'c',
|
|
1961
|
+
'cpp': 'cpp',
|
|
1962
|
+
'cc': 'cpp',
|
|
1963
|
+
'h': 'c',
|
|
1964
|
+
'hpp': 'cpp',
|
|
1965
|
+
'cs': 'csharp',
|
|
1966
|
+
'swift': 'swift',
|
|
1967
|
+
'kt': 'kotlin',
|
|
1968
|
+
'scala': 'scala',
|
|
1969
|
+
'sh': 'bash',
|
|
1970
|
+
'bash': 'bash',
|
|
1971
|
+
'zsh': 'bash',
|
|
1972
|
+
'sql': 'sql',
|
|
1973
|
+
'html': 'html',
|
|
1974
|
+
'htm': 'html',
|
|
1975
|
+
'css': 'css',
|
|
1976
|
+
'scss': 'scss',
|
|
1977
|
+
'less': 'less',
|
|
1978
|
+
'json': 'json',
|
|
1979
|
+
'yaml': 'yaml',
|
|
1980
|
+
'yml': 'yaml',
|
|
1981
|
+
'xml': 'xml',
|
|
1982
|
+
'md': 'markdown',
|
|
1983
|
+
'markdown': 'markdown',
|
|
1984
|
+
'toml': 'ini',
|
|
1985
|
+
'ini': 'ini',
|
|
1986
|
+
'cfg': 'ini',
|
|
1987
|
+
'lua': 'lua',
|
|
1988
|
+
'r': 'r',
|
|
1989
|
+
'dart': 'dart',
|
|
1990
|
+
'ex': 'elixir',
|
|
1991
|
+
'exs': 'elixir',
|
|
1992
|
+
'erl': 'erlang',
|
|
1993
|
+
'hs': 'haskell',
|
|
1994
|
+
'clj': 'clojure',
|
|
1995
|
+
'vim': 'vim',
|
|
1996
|
+
'dockerfile': 'dockerfile'
|
|
1997
|
+
};
|
|
1998
|
+
return langMap[ext] || '';
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// ============================================================================
|
|
2002
|
+
// LAYOUT TOGGLE
|
|
2003
|
+
// ============================================================================
|
|
2004
|
+
|
|
2005
|
+
function toggleCallLines(show) {
|
|
2006
|
+
showCallLines = show;
|
|
2007
|
+
const lineGroup = d3.select('.external-lines-group');
|
|
2008
|
+
if (!lineGroup.empty()) {
|
|
2009
|
+
lineGroup.style('display', show ? 'block' : 'none');
|
|
2010
|
+
}
|
|
2011
|
+
console.log(`Call lines ${show ? 'shown' : 'hidden'}`);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function toggleLayout() {
|
|
2015
|
+
const toggleCheckbox = document.getElementById('layout-toggle');
|
|
2016
|
+
const labels = document.querySelectorAll('.toggle-label');
|
|
2017
|
+
|
|
2018
|
+
// Update layout based on checkbox state
|
|
2019
|
+
currentLayout = toggleCheckbox.checked ? 'circular' : 'linear';
|
|
2020
|
+
|
|
2021
|
+
// Update label highlighting
|
|
2022
|
+
labels.forEach((label, index) => {
|
|
2023
|
+
if (index === 0) {
|
|
2024
|
+
// Linear label (left)
|
|
2025
|
+
label.classList.toggle('active', currentLayout === 'linear');
|
|
2026
|
+
} else {
|
|
2027
|
+
// Circular label (right)
|
|
2028
|
+
label.classList.toggle('active', currentLayout === 'circular');
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
console.log(`Layout switched to: ${currentLayout}`);
|
|
2033
|
+
renderVisualization();
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// ============================================================================
|
|
2037
|
+
// VIEWER PANEL CONTROLS
|
|
2038
|
+
// ============================================================================
|
|
2039
|
+
|
|
2040
|
+
let isViewerExpanded = false;
|
|
2041
|
+
|
|
2042
|
+
function openViewerPanel() {
|
|
2043
|
+
const panel = document.getElementById('viewer-panel');
|
|
2044
|
+
const container = document.getElementById('main-container');
|
|
2045
|
+
|
|
2046
|
+
if (!isViewerOpen) {
|
|
2047
|
+
panel.classList.add('open');
|
|
2048
|
+
container.classList.add('viewer-open');
|
|
2049
|
+
isViewerOpen = true;
|
|
2050
|
+
|
|
2051
|
+
// Re-render visualization to adjust to new viewport size
|
|
2052
|
+
setTimeout(() => {
|
|
2053
|
+
renderVisualization();
|
|
2054
|
+
}, 300); // Wait for transition
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function closeViewerPanel() {
|
|
2059
|
+
const panel = document.getElementById('viewer-panel');
|
|
2060
|
+
const container = document.getElementById('main-container');
|
|
2061
|
+
|
|
2062
|
+
panel.classList.remove('open');
|
|
2063
|
+
panel.classList.remove('expanded');
|
|
2064
|
+
container.classList.remove('viewer-open');
|
|
2065
|
+
isViewerOpen = false;
|
|
2066
|
+
isViewerExpanded = false;
|
|
2067
|
+
|
|
2068
|
+
// Update icon
|
|
2069
|
+
const icon = document.getElementById('expand-icon');
|
|
2070
|
+
if (icon) icon.textContent = '⬅';
|
|
2071
|
+
|
|
2072
|
+
// Re-render visualization to adjust to new viewport size
|
|
2073
|
+
setTimeout(() => {
|
|
2074
|
+
renderVisualization();
|
|
2075
|
+
}, 300); // Wait for transition
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
function toggleViewerExpand() {
|
|
2079
|
+
const panel = document.getElementById('viewer-panel');
|
|
2080
|
+
const icon = document.getElementById('expand-icon');
|
|
2081
|
+
|
|
2082
|
+
isViewerExpanded = !isViewerExpanded;
|
|
2083
|
+
|
|
2084
|
+
if (isViewerExpanded) {
|
|
2085
|
+
panel.classList.add('expanded');
|
|
2086
|
+
if (icon) icon.textContent = '➡';
|
|
2087
|
+
} else {
|
|
2088
|
+
panel.classList.remove('expanded');
|
|
2089
|
+
if (icon) icon.textContent = '⬅';
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// Don't re-render graph on expand - only affects panel width
|
|
2093
|
+
// Graph will adjust on close via closeViewerPanel()
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
function jumpToSection(sectionId) {
|
|
2097
|
+
if (!sectionId) return;
|
|
2098
|
+
|
|
2099
|
+
const viewerContent = document.getElementById('viewer-content');
|
|
2100
|
+
const sectionElement = viewerContent.querySelector(`[data-section="${sectionId}"]`);
|
|
2101
|
+
|
|
2102
|
+
if (sectionElement) {
|
|
2103
|
+
sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
2104
|
+
|
|
2105
|
+
// Briefly highlight the section
|
|
2106
|
+
sectionElement.style.transition = 'background-color 0.3s';
|
|
2107
|
+
sectionElement.style.backgroundColor = 'rgba(88, 166, 255, 0.15)';
|
|
2108
|
+
setTimeout(() => {
|
|
2109
|
+
sectionElement.style.backgroundColor = '';
|
|
2110
|
+
}, 1000);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// Reset dropdown to default
|
|
2114
|
+
const dropdown = document.getElementById('section-dropdown');
|
|
2115
|
+
if (dropdown) dropdown.value = '';
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function populateSectionDropdown(sections) {
|
|
2119
|
+
const dropdown = document.getElementById('section-dropdown');
|
|
2120
|
+
if (!dropdown) return;
|
|
2121
|
+
|
|
2122
|
+
// Clear existing options except the first one
|
|
2123
|
+
dropdown.innerHTML = '<option value="">Jump to section...</option>';
|
|
2124
|
+
|
|
2125
|
+
// Add section options
|
|
2126
|
+
sections.forEach(section => {
|
|
2127
|
+
const option = document.createElement('option');
|
|
2128
|
+
option.value = section.id;
|
|
2129
|
+
option.textContent = section.label;
|
|
2130
|
+
dropdown.appendChild(option);
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
// Show/hide dropdown based on whether we have sections
|
|
2134
|
+
const sectionNav = document.getElementById('section-nav');
|
|
2135
|
+
if (sectionNav) {
|
|
2136
|
+
sectionNav.style.display = sections.length > 1 ? 'block' : 'none';
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// ============================================================================
|
|
2141
|
+
// NAVIGATION FUNCTIONS
|
|
2142
|
+
// ============================================================================
|
|
2143
|
+
|
|
2144
|
+
function addToNavHistory(item) {
|
|
2145
|
+
// Remove any forward history when adding new item
|
|
2146
|
+
if (navigationIndex < navigationHistory.length - 1) {
|
|
2147
|
+
navigationHistory = navigationHistory.slice(0, navigationIndex + 1);
|
|
2148
|
+
}
|
|
2149
|
+
navigationHistory.push(item);
|
|
2150
|
+
navigationIndex = navigationHistory.length - 1;
|
|
2151
|
+
console.log(`Navigation history: ${navigationHistory.length} items, index: ${navigationIndex}`);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
function goBack() {
|
|
2155
|
+
if (navigationIndex > 0) {
|
|
2156
|
+
navigationIndex--;
|
|
2157
|
+
const item = navigationHistory[navigationIndex];
|
|
2158
|
+
console.log(`Going back to: ${item.type} - ${item.data.name}`);
|
|
2159
|
+
if (item.type === 'directory') {
|
|
2160
|
+
displayDirectoryInfo(item.data, false); // false = don't add to history
|
|
2161
|
+
focusNodeInTree(item.data.id);
|
|
2162
|
+
} else if (item.type === 'file') {
|
|
2163
|
+
displayFileInfo(item.data, false); // false = don't add to history
|
|
2164
|
+
focusNodeInTree(item.data.id);
|
|
2165
|
+
} else if (item.type === 'chunk') {
|
|
2166
|
+
displayChunkContent(item.data, false); // false = don't add to history
|
|
2167
|
+
focusNodeInTree(item.data.id);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function goForward() {
|
|
2173
|
+
if (navigationIndex < navigationHistory.length - 1) {
|
|
2174
|
+
navigationIndex++;
|
|
2175
|
+
const item = navigationHistory[navigationIndex];
|
|
2176
|
+
console.log(`Going forward to: ${item.type} - ${item.data.name}`);
|
|
2177
|
+
if (item.type === 'directory') {
|
|
2178
|
+
displayDirectoryInfo(item.data, false); // false = don't add to history
|
|
2179
|
+
focusNodeInTree(item.data.id);
|
|
2180
|
+
} else if (item.type === 'file') {
|
|
2181
|
+
displayFileInfo(item.data, false); // false = don't add to history
|
|
2182
|
+
focusNodeInTree(item.data.id);
|
|
2183
|
+
} else if (item.type === 'chunk') {
|
|
2184
|
+
displayChunkContent(item.data, false); // false = don't add to history
|
|
2185
|
+
focusNodeInTree(item.data.id);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
function navigateToDirectory(dirData) {
|
|
2191
|
+
console.log(`Navigating to directory: ${dirData.name}`);
|
|
2192
|
+
// Focus on the node in the tree (expand path and highlight)
|
|
2193
|
+
focusNodeInTree(dirData.id);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
function navigateToFile(fileData) {
|
|
2197
|
+
console.log(`Navigating to file: ${fileData.name}`);
|
|
2198
|
+
// Focus on the node in the tree (expand path and highlight)
|
|
2199
|
+
focusNodeInTree(fileData.id);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// Navigate to a file by its file path (used when clicking on file paths in chunk metadata)
|
|
2203
|
+
function navigateToFileByPath(filePath) {
|
|
2204
|
+
console.log(`Navigating to file by path: ${filePath}`);
|
|
2205
|
+
|
|
2206
|
+
// Find the file node in allNodes that matches this path
|
|
2207
|
+
const fileNode = allNodes.find(n => {
|
|
2208
|
+
if (n.type !== 'file') return false;
|
|
2209
|
+
// Match against various path properties
|
|
2210
|
+
return n.path === filePath ||
|
|
2211
|
+
n.file_path === filePath ||
|
|
2212
|
+
(n.path && n.path.endsWith(filePath)) ||
|
|
2213
|
+
(n.file_path && n.file_path.endsWith(filePath)) ||
|
|
2214
|
+
filePath.endsWith(n.name);
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
if (fileNode) {
|
|
2218
|
+
console.log(`Found file node: ${fileNode.name} (id: ${fileNode.id})`);
|
|
2219
|
+
focusNodeInTree(fileNode.id);
|
|
2220
|
+
} else {
|
|
2221
|
+
console.log(`File node not found for path: ${filePath}`);
|
|
2222
|
+
// Try to find by just the filename
|
|
2223
|
+
const fileName = filePath.split('/').pop();
|
|
2224
|
+
const fileByName = allNodes.find(n => n.type === 'file' && n.name === fileName);
|
|
2225
|
+
if (fileByName) {
|
|
2226
|
+
console.log(`Found file by name: ${fileByName.name}`);
|
|
2227
|
+
focusNodeInTree(fileByName.id);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function renderNavigationBar(currentItem) {
|
|
2233
|
+
let html = '<div class="navigation-bar">';
|
|
2234
|
+
|
|
2235
|
+
// Back/Forward buttons
|
|
2236
|
+
const canGoBack = navigationIndex > 0;
|
|
2237
|
+
const canGoForward = navigationIndex < navigationHistory.length - 1;
|
|
2238
|
+
|
|
2239
|
+
html += `<button class="nav-btn ${canGoBack ? '' : 'disabled'}" onclick="goBack()" ${canGoBack ? '' : 'disabled'} title="Go Back">←</button>`;
|
|
2240
|
+
html += `<button class="nav-btn ${canGoForward ? '' : 'disabled'}" onclick="goForward()" ${canGoForward ? '' : 'disabled'} title="Go Forward">→</button>`;
|
|
2241
|
+
|
|
2242
|
+
// Breadcrumb trail
|
|
2243
|
+
html += '<div class="breadcrumb-trail">';
|
|
2244
|
+
|
|
2245
|
+
// Build breadcrumb from path
|
|
2246
|
+
if (currentItem && currentItem.id) {
|
|
2247
|
+
const path = findPathToNode(treeData, currentItem.id);
|
|
2248
|
+
path.forEach((node, index) => {
|
|
2249
|
+
const isLast = index === path.length - 1;
|
|
2250
|
+
const clickable = !isLast;
|
|
2251
|
+
|
|
2252
|
+
if (index > 0) {
|
|
2253
|
+
html += '<span class="breadcrumb-separator">/</span>';
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
if (clickable) {
|
|
2257
|
+
html += `<span class="breadcrumb-item clickable" onclick="focusNodeInTree('${node.id}')">${escapeHtml(node.name)}</span>`;
|
|
2258
|
+
} else {
|
|
2259
|
+
html += `<span class="breadcrumb-item current">${escapeHtml(node.name)}</span>`;
|
|
2260
|
+
}
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
html += '</div>';
|
|
2265
|
+
html += '</div>';
|
|
2266
|
+
|
|
2267
|
+
return html;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// ============================================================================
|
|
2271
|
+
// SEARCH FUNCTIONALITY
|
|
2272
|
+
// ============================================================================
|
|
2273
|
+
|
|
2274
|
+
let searchDebounceTimer = null;
|
|
2275
|
+
let searchResults = [];
|
|
2276
|
+
let selectedSearchIndex = -1;
|
|
2277
|
+
|
|
2278
|
+
function handleSearchInput(event) {
|
|
2279
|
+
const query = event.target.value.trim();
|
|
2280
|
+
|
|
2281
|
+
// Debounce search - wait 150ms after typing stops
|
|
2282
|
+
clearTimeout(searchDebounceTimer);
|
|
2283
|
+
searchDebounceTimer = setTimeout(() => {
|
|
2284
|
+
performSearch(query);
|
|
2285
|
+
}, 150);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
function handleSearchKeydown(event) {
|
|
2289
|
+
const resultsContainer = document.getElementById('search-results');
|
|
2290
|
+
|
|
2291
|
+
switch(event.key) {
|
|
2292
|
+
case 'ArrowDown':
|
|
2293
|
+
event.preventDefault();
|
|
2294
|
+
if (searchResults.length > 0) {
|
|
2295
|
+
selectedSearchIndex = Math.min(selectedSearchIndex + 1, searchResults.length - 1);
|
|
2296
|
+
updateSearchSelection();
|
|
2297
|
+
}
|
|
2298
|
+
break;
|
|
2299
|
+
case 'ArrowUp':
|
|
2300
|
+
event.preventDefault();
|
|
2301
|
+
if (searchResults.length > 0) {
|
|
2302
|
+
selectedSearchIndex = Math.max(selectedSearchIndex - 1, 0);
|
|
2303
|
+
updateSearchSelection();
|
|
2304
|
+
}
|
|
2305
|
+
break;
|
|
2306
|
+
case 'Enter':
|
|
2307
|
+
event.preventDefault();
|
|
2308
|
+
if (selectedSearchIndex >= 0 && selectedSearchIndex < searchResults.length) {
|
|
2309
|
+
selectSearchResult(searchResults[selectedSearchIndex]);
|
|
2310
|
+
}
|
|
2311
|
+
break;
|
|
2312
|
+
case 'Escape':
|
|
2313
|
+
closeSearchResults();
|
|
2314
|
+
document.getElementById('search-input').blur();
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function performSearch(query) {
|
|
2320
|
+
const resultsContainer = document.getElementById('search-results');
|
|
2321
|
+
|
|
2322
|
+
if (!query || query.length < 2) {
|
|
2323
|
+
closeSearchResults();
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
const lowerQuery = query.toLowerCase();
|
|
2328
|
+
|
|
2329
|
+
// Search through all nodes (directories, files, and chunks)
|
|
2330
|
+
searchResults = allNodes
|
|
2331
|
+
.filter(node => {
|
|
2332
|
+
// Match against name
|
|
2333
|
+
const nameMatch = node.name && node.name.toLowerCase().includes(lowerQuery);
|
|
2334
|
+
// Match against file path
|
|
2335
|
+
const pathMatch = node.file_path && node.file_path.toLowerCase().includes(lowerQuery);
|
|
2336
|
+
// Match against ID (useful for finding specific chunks)
|
|
2337
|
+
const idMatch = node.id && node.id.toLowerCase().includes(lowerQuery);
|
|
2338
|
+
return nameMatch || pathMatch || idMatch;
|
|
2339
|
+
})
|
|
2340
|
+
.slice(0, 20); // Limit to 20 results
|
|
2341
|
+
|
|
2342
|
+
// Sort results: exact matches first, then by type priority
|
|
2343
|
+
const typePriority = { 'directory': 1, 'file': 2, 'class': 3, 'function': 4, 'method': 5 };
|
|
2344
|
+
searchResults.sort((a, b) => {
|
|
2345
|
+
// Exact name match gets highest priority
|
|
2346
|
+
const aExact = a.name && a.name.toLowerCase() === lowerQuery ? 0 : 1;
|
|
2347
|
+
const bExact = b.name && b.name.toLowerCase() === lowerQuery ? 0 : 1;
|
|
2348
|
+
if (aExact !== bExact) return aExact - bExact;
|
|
2349
|
+
|
|
2350
|
+
// Then sort by type priority
|
|
2351
|
+
const aPriority = typePriority[a.type] || 10;
|
|
2352
|
+
const bPriority = typePriority[b.type] || 10;
|
|
2353
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
2354
|
+
|
|
2355
|
+
// Finally sort alphabetically
|
|
2356
|
+
return (a.name || '').localeCompare(b.name || '');
|
|
2357
|
+
});
|
|
2358
|
+
|
|
2359
|
+
selectedSearchIndex = searchResults.length > 0 ? 0 : -1;
|
|
2360
|
+
renderSearchResults(query);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function renderSearchResults(query) {
|
|
2364
|
+
const resultsContainer = document.getElementById('search-results');
|
|
2365
|
+
|
|
2366
|
+
if (searchResults.length === 0) {
|
|
2367
|
+
resultsContainer.innerHTML = '<div class="search-no-results">No results found</div>';
|
|
2368
|
+
resultsContainer.classList.add('visible');
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
let html = '';
|
|
2373
|
+
|
|
2374
|
+
searchResults.forEach((node, index) => {
|
|
2375
|
+
const icon = getSearchResultIcon(node.type);
|
|
2376
|
+
const name = highlightMatch(node.name || node.id.substring(0, 20), query);
|
|
2377
|
+
const path = node.file_path ? node.file_path.split('/').slice(-3).join('/') : '';
|
|
2378
|
+
const type = node.type || 'unknown';
|
|
2379
|
+
const selected = index === selectedSearchIndex ? 'selected' : '';
|
|
2380
|
+
|
|
2381
|
+
html += `<div class="search-result-item ${selected}"
|
|
2382
|
+
data-index="${index}"
|
|
2383
|
+
onclick="selectSearchResultByIndex(${index})"
|
|
2384
|
+
onmouseenter="hoverSearchResult(${index})">`;
|
|
2385
|
+
html += `<span class="search-result-icon">${icon}</span>`;
|
|
2386
|
+
html += `<div class="search-result-info">`;
|
|
2387
|
+
html += `<div class="search-result-name">${name}</div>`;
|
|
2388
|
+
if (path) {
|
|
2389
|
+
html += `<div class="search-result-path">${escapeHtml(path)}</div>`;
|
|
2390
|
+
}
|
|
2391
|
+
html += `</div>`;
|
|
2392
|
+
html += `<span class="search-result-type">${type}</span>`;
|
|
2393
|
+
html += `</div>`;
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
html += '<div class="search-hint">↑↓ Navigate • Enter Select • Esc Close</div>';
|
|
2397
|
+
|
|
2398
|
+
resultsContainer.innerHTML = html;
|
|
2399
|
+
resultsContainer.classList.add('visible');
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function getSearchResultIcon(type) {
|
|
2403
|
+
const icons = {
|
|
2404
|
+
'directory': '📁',
|
|
2405
|
+
'file': '📄',
|
|
2406
|
+
'function': '⚡',
|
|
2407
|
+
'class': '🏛️',
|
|
2408
|
+
'method': '🔧',
|
|
2409
|
+
'module': '📦',
|
|
2410
|
+
'imports': '📦',
|
|
2411
|
+
'text': '📝',
|
|
2412
|
+
'code': '📝'
|
|
2413
|
+
};
|
|
2414
|
+
return icons[type] || '📄';
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
function highlightMatch(text, query) {
|
|
2418
|
+
if (!text || !query) return escapeHtml(text || '');
|
|
2419
|
+
|
|
2420
|
+
const lowerText = text.toLowerCase();
|
|
2421
|
+
const lowerQuery = query.toLowerCase();
|
|
2422
|
+
const index = lowerText.indexOf(lowerQuery);
|
|
2423
|
+
|
|
2424
|
+
if (index === -1) return escapeHtml(text);
|
|
2425
|
+
|
|
2426
|
+
const before = text.substring(0, index);
|
|
2427
|
+
const match = text.substring(index, index + query.length);
|
|
2428
|
+
const after = text.substring(index + query.length);
|
|
2429
|
+
|
|
2430
|
+
return escapeHtml(before) + '<mark>' + escapeHtml(match) + '</mark>' + escapeHtml(after);
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
function updateSearchSelection() {
|
|
2434
|
+
const items = document.querySelectorAll('.search-result-item');
|
|
2435
|
+
items.forEach((item, index) => {
|
|
2436
|
+
item.classList.toggle('selected', index === selectedSearchIndex);
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
// Scroll selected item into view
|
|
2440
|
+
const selected = items[selectedSearchIndex];
|
|
2441
|
+
if (selected) {
|
|
2442
|
+
selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
function hoverSearchResult(index) {
|
|
2447
|
+
selectedSearchIndex = index;
|
|
2448
|
+
updateSearchSelection();
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function selectSearchResultByIndex(index) {
|
|
2452
|
+
if (index >= 0 && index < searchResults.length) {
|
|
2453
|
+
selectSearchResult(searchResults[index]);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function selectSearchResult(node) {
|
|
2458
|
+
console.log(`Search selected: ${node.name} (${node.type})`);
|
|
2459
|
+
|
|
2460
|
+
// Close search dropdown
|
|
2461
|
+
closeSearchResults();
|
|
2462
|
+
|
|
2463
|
+
// Clear input
|
|
2464
|
+
document.getElementById('search-input').value = '';
|
|
2465
|
+
|
|
2466
|
+
// Focus on the node in the tree
|
|
2467
|
+
focusNodeInTree(node.id);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function closeSearchResults() {
|
|
2471
|
+
const resultsContainer = document.getElementById('search-results');
|
|
2472
|
+
resultsContainer.classList.remove('visible');
|
|
2473
|
+
searchResults = [];
|
|
2474
|
+
selectedSearchIndex = -1;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// ============================================================================
|
|
2478
|
+
// INITIALIZATION
|
|
2479
|
+
// ============================================================================
|
|
2480
|
+
|
|
2481
|
+
// Load data and initialize UI when page loads
|
|
2482
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2483
|
+
console.log('=== PAGE INITIALIZATION ===');
|
|
2484
|
+
console.log('DOMContentLoaded event fired');
|
|
2485
|
+
|
|
2486
|
+
// Initialize toggle label highlighting
|
|
2487
|
+
const labels = document.querySelectorAll('.toggle-label');
|
|
2488
|
+
console.log(`Found ${labels.length} toggle labels`);
|
|
2489
|
+
if (labels[0]) {
|
|
2490
|
+
labels[0].classList.add('active');
|
|
2491
|
+
console.log('Activated first toggle label (linear mode)');
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// Close search results when clicking outside
|
|
2495
|
+
document.addEventListener('click', (event) => {
|
|
2496
|
+
const searchContainer = document.querySelector('.search-container');
|
|
2497
|
+
if (searchContainer && !searchContainer.contains(event.target)) {
|
|
2498
|
+
closeSearchResults();
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
// Load graph data
|
|
2503
|
+
console.log('Calling loadGraphData()...');
|
|
2504
|
+
loadGraphData();
|
|
2505
|
+
console.log('=== END PAGE INITIALIZATION ===');
|
|
2506
|
+
});
|
|
2507
|
+
"""
|