claude-mpm 4.2.2__py3-none-any.whl → 4.2.4__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/monitor.py +1 -1
- claude_mpm/core/config.py +2 -2
- claude_mpm/dashboard/static/css/code-tree.css +220 -1
- claude_mpm/dashboard/static/css/dashboard.css +286 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +1833 -89
- claude_mpm/dashboard/static/js/socket-client.js +11 -8
- claude_mpm/dashboard/templates/index.html +41 -40
- claude_mpm/services/agents/deployment/agent_template_builder.py +17 -4
- claude_mpm/services/dashboard/stable_server.py +482 -0
- claude_mpm/services/socketio/client_proxy.py +16 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +27 -5
- claude_mpm/services/socketio/monitor_server.py +2 -2
- claude_mpm/services/socketio/server/core.py +62 -0
- claude_mpm/tools/code_tree_analyzer.py +95 -17
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/RECORD +21 -20
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.2.dist-info → claude_mpm-4.2.4.dist-info}/top_level.txt +0 -0
|
@@ -32,7 +32,7 @@ class CodeTree {
|
|
|
32
32
|
lines: 0
|
|
33
33
|
};
|
|
34
34
|
// Radial layout settings
|
|
35
|
-
this.isRadialLayout =
|
|
35
|
+
this.isRadialLayout = false; // Toggle for radial vs linear layout - defaulting to linear for better readability
|
|
36
36
|
this.margin = {top: 20, right: 20, bottom: 20, left: 20};
|
|
37
37
|
this.width = 960 - this.margin.left - this.margin.right;
|
|
38
38
|
this.height = 600 - this.margin.top - this.margin.bottom;
|
|
@@ -48,10 +48,17 @@ class CodeTree {
|
|
|
48
48
|
this.socket = null;
|
|
49
49
|
this.autoDiscovered = false; // Track if auto-discovery has been done
|
|
50
50
|
this.zoom = null; // Store zoom behavior
|
|
51
|
+
|
|
52
|
+
// Structured data properties
|
|
53
|
+
this.structuredDataContent = null;
|
|
54
|
+
this.selectedASTItem = null;
|
|
51
55
|
this.activeNode = null; // Track currently active node
|
|
52
56
|
this.loadingNodes = new Set(); // Track nodes that are loading
|
|
53
57
|
this.bulkLoadMode = false; // Track bulk loading preference
|
|
54
58
|
this.expandedPaths = new Set(); // Track which paths are expanded
|
|
59
|
+
this.focusedNode = null; // Track the currently focused directory
|
|
60
|
+
this.horizontalNodes = new Set(); // Track nodes that should have horizontal text
|
|
61
|
+
this.centralSpine = new Set(); // Track the main path through the tree
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
/**
|
|
@@ -87,6 +94,7 @@ class CodeTree {
|
|
|
87
94
|
this.setupControls();
|
|
88
95
|
this.initializeTreeData();
|
|
89
96
|
this.subscribeToEvents();
|
|
97
|
+
this.initializeStructuredData();
|
|
90
98
|
|
|
91
99
|
// Set initial status message
|
|
92
100
|
const breadcrumbContent = document.getElementById('breadcrumb-content');
|
|
@@ -166,20 +174,8 @@ class CodeTree {
|
|
|
166
174
|
});
|
|
167
175
|
}
|
|
168
176
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
expandBtn.addEventListener('click', () => this.expandAll());
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const collapseBtn = document.getElementById('code-collapse-all');
|
|
175
|
-
if (collapseBtn) {
|
|
176
|
-
collapseBtn.addEventListener('click', () => this.collapseAll());
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const resetZoomBtn = document.getElementById('code-reset-zoom');
|
|
180
|
-
if (resetZoomBtn) {
|
|
181
|
-
resetZoomBtn.addEventListener('click', () => this.resetZoom());
|
|
182
|
-
}
|
|
177
|
+
// Note: Expand/collapse/reset buttons are now handled by the tree controls toolbar
|
|
178
|
+
// which is created dynamically in addTreeControls()
|
|
183
179
|
|
|
184
180
|
const toggleLegendBtn = document.getElementById('code-toggle-legend');
|
|
185
181
|
if (toggleLegendBtn) {
|
|
@@ -372,14 +368,27 @@ class CodeTree {
|
|
|
372
368
|
});
|
|
373
369
|
}
|
|
374
370
|
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
371
|
+
// Enable zoom and pan functionality for better navigation
|
|
372
|
+
this.zoom = d3.zoom()
|
|
373
|
+
.scaleExtent([0.1, 3]) // Allow zoom from 10% to 300%
|
|
374
|
+
.on('zoom', (event) => {
|
|
375
|
+
// Apply zoom transform to the tree group
|
|
376
|
+
this.treeGroup.attr('transform', event.transform);
|
|
377
|
+
|
|
378
|
+
// Keep text size constant by applying inverse scaling
|
|
379
|
+
this.adjustTextSizeForZoom(event.transform.k);
|
|
380
|
+
|
|
381
|
+
// Update zoom level display
|
|
382
|
+
this.updateZoomLevel(event.transform.k);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Apply zoom behavior to SVG
|
|
386
|
+
this.svg.call(this.zoom);
|
|
387
|
+
|
|
388
|
+
// Add keyboard shortcuts for zoom
|
|
389
|
+
this.addZoomKeyboardShortcuts();
|
|
390
|
+
|
|
391
|
+
console.log('[CodeTree] Zoom and pan functionality enabled');
|
|
383
392
|
|
|
384
393
|
// Add controls overlay
|
|
385
394
|
this.addVisualizationControls();
|
|
@@ -416,12 +425,11 @@ class CodeTree {
|
|
|
416
425
|
initializeTreeData() {
|
|
417
426
|
const workingDir = this.getWorkingDirectory();
|
|
418
427
|
const dirName = workingDir ? workingDir.split('/').pop() || 'Project Root' : 'Project Root';
|
|
419
|
-
|
|
420
|
-
// Use
|
|
421
|
-
// The actual working directory is retrieved via getWorkingDirectory() when needed
|
|
428
|
+
|
|
429
|
+
// Use absolute path for consistency with API expectations
|
|
422
430
|
this.treeData = {
|
|
423
431
|
name: dirName,
|
|
424
|
-
path: '.', //
|
|
432
|
+
path: workingDir || '.', // Use working directory or fallback to '.'
|
|
425
433
|
type: 'root',
|
|
426
434
|
children: [],
|
|
427
435
|
loaded: false,
|
|
@@ -531,7 +539,7 @@ class CodeTree {
|
|
|
531
539
|
const dirName = workingDir.split('/').pop() || 'Project Root';
|
|
532
540
|
this.treeData = {
|
|
533
541
|
name: dirName,
|
|
534
|
-
path:
|
|
542
|
+
path: workingDir, // Use absolute path for consistency with API expectations
|
|
535
543
|
type: 'root',
|
|
536
544
|
children: [],
|
|
537
545
|
loaded: false,
|
|
@@ -549,10 +557,7 @@ class CodeTree {
|
|
|
549
557
|
this.updateBreadcrumb(`Discovering structure in ${dirName}...`, 'info');
|
|
550
558
|
|
|
551
559
|
// Get selected languages from checkboxes
|
|
552
|
-
const selectedLanguages =
|
|
553
|
-
document.querySelectorAll('.language-checkbox:checked').forEach(cb => {
|
|
554
|
-
selectedLanguages.push(cb.value);
|
|
555
|
-
});
|
|
560
|
+
const selectedLanguages = this.getSelectedLanguages();
|
|
556
561
|
|
|
557
562
|
// Get ignore patterns
|
|
558
563
|
const ignorePatterns = document.getElementById('ignore-patterns')?.value || '';
|
|
@@ -643,7 +648,37 @@ class CodeTree {
|
|
|
643
648
|
.attr('title', 'Toggle between radial and linear layouts')
|
|
644
649
|
.text('◎')
|
|
645
650
|
.on('click', () => this.toggleLayout());
|
|
646
|
-
|
|
651
|
+
|
|
652
|
+
// Zoom In
|
|
653
|
+
toolbar.append('button')
|
|
654
|
+
.attr('class', 'tree-control-btn')
|
|
655
|
+
.attr('title', 'Zoom in')
|
|
656
|
+
.text('🔍+')
|
|
657
|
+
.on('click', () => this.zoomIn());
|
|
658
|
+
|
|
659
|
+
// Zoom Out
|
|
660
|
+
toolbar.append('button')
|
|
661
|
+
.attr('class', 'tree-control-btn')
|
|
662
|
+
.attr('title', 'Zoom out')
|
|
663
|
+
.text('🔍-')
|
|
664
|
+
.on('click', () => this.zoomOut());
|
|
665
|
+
|
|
666
|
+
// Reset Zoom
|
|
667
|
+
toolbar.append('button')
|
|
668
|
+
.attr('class', 'tree-control-btn')
|
|
669
|
+
.attr('title', 'Reset zoom to fit tree')
|
|
670
|
+
.text('⌂')
|
|
671
|
+
.on('click', () => this.resetZoom());
|
|
672
|
+
|
|
673
|
+
// Zoom Level Display
|
|
674
|
+
toolbar.append('span')
|
|
675
|
+
.attr('class', 'zoom-level-display')
|
|
676
|
+
.attr('id', 'zoom-level-display')
|
|
677
|
+
.text('100%')
|
|
678
|
+
.style('margin-left', '8px')
|
|
679
|
+
.style('font-size', '11px')
|
|
680
|
+
.style('color', '#718096');
|
|
681
|
+
|
|
647
682
|
// Path Search
|
|
648
683
|
const searchInput = toolbar.append('input')
|
|
649
684
|
.attr('class', 'tree-control-btn')
|
|
@@ -948,11 +983,44 @@ class CodeTree {
|
|
|
948
983
|
this.socket.on('code:top_level:discovered', (data) => this.onTopLevelDiscovered(data));
|
|
949
984
|
this.socket.on('code:directory:discovered', (data) => this.onDirectoryDiscovered(data));
|
|
950
985
|
this.socket.on('code:file:discovered', (data) => this.onFileDiscovered(data));
|
|
951
|
-
this.socket.on('code:file:analyzed', (data) =>
|
|
986
|
+
this.socket.on('code:file:analyzed', (data) => {
|
|
987
|
+
console.log('📨 [SOCKET] Received code:file:analyzed event');
|
|
988
|
+
this.onFileAnalyzed(data);
|
|
989
|
+
});
|
|
952
990
|
this.socket.on('code:node:found', (data) => this.onNodeFound(data));
|
|
953
991
|
|
|
954
992
|
// Progress updates
|
|
955
993
|
this.socket.on('code:analysis:progress', (data) => this.onProgressUpdate(data));
|
|
994
|
+
|
|
995
|
+
// Error handling
|
|
996
|
+
this.socket.on('code:analysis:error', (data) => {
|
|
997
|
+
console.error('❌ [FILE ANALYSIS] Analysis error:', data);
|
|
998
|
+
this.showNotification(`Analysis error: ${data.error || 'Unknown error'}`, 'error');
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// Generic error handling
|
|
1002
|
+
this.socket.on('error', (error) => {
|
|
1003
|
+
console.error('❌ [SOCKET] Socket error:', error);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
// Socket connection status
|
|
1007
|
+
this.socket.on('connect', () => {
|
|
1008
|
+
console.log('✅ [SOCKET] Connected to server, analysis service should be available');
|
|
1009
|
+
this.connectionStable = true;
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
this.socket.on('disconnect', () => {
|
|
1013
|
+
console.log('❌ [SOCKET] Disconnected from server - disabling AST analysis');
|
|
1014
|
+
this.connectionStable = false;
|
|
1015
|
+
// Clear any pending analysis timeouts
|
|
1016
|
+
if (this.analysisTimeouts) {
|
|
1017
|
+
this.analysisTimeouts.forEach((timeout, path) => {
|
|
1018
|
+
clearTimeout(timeout);
|
|
1019
|
+
this.loadingNodes.delete(path);
|
|
1020
|
+
});
|
|
1021
|
+
this.analysisTimeouts.clear();
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
956
1024
|
|
|
957
1025
|
// Lazy loading responses
|
|
958
1026
|
this.socket.on('code:directory:contents', (data) => {
|
|
@@ -1136,10 +1204,11 @@ class CodeTree {
|
|
|
1136
1204
|
// Add to events display
|
|
1137
1205
|
this.addEventToDisplay(`📁 Found ${(data.items || []).length} top-level items in project root`, 'info');
|
|
1138
1206
|
|
|
1139
|
-
// The root node
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1207
|
+
// The root node should receive the children
|
|
1208
|
+
const workingDir = this.getWorkingDirectory();
|
|
1209
|
+
const rootNode = this.findNodeByPath(workingDir);
|
|
1210
|
+
|
|
1211
|
+
console.log(`🔎 Looking for root node with path "${workingDir}", found:`, rootNode ? {
|
|
1143
1212
|
name: rootNode.name,
|
|
1144
1213
|
path: rootNode.path,
|
|
1145
1214
|
currentChildren: rootNode.children ? rootNode.children.length : 0
|
|
@@ -1150,14 +1219,16 @@ class CodeTree {
|
|
|
1150
1219
|
|
|
1151
1220
|
// Update the root node with discovered children
|
|
1152
1221
|
rootNode.children = data.items.map(child => {
|
|
1153
|
-
//
|
|
1154
|
-
|
|
1155
|
-
|
|
1222
|
+
// CRITICAL FIX: Use consistent path format that matches API expectations
|
|
1223
|
+
// The API expects absolute paths, so construct them properly
|
|
1224
|
+
const workingDir = this.getWorkingDirectory();
|
|
1225
|
+
const childPath = workingDir ? `${workingDir}/${child.name}`.replace(/\/+/g, '/') : child.name;
|
|
1226
|
+
|
|
1156
1227
|
console.log(` Adding child: ${child.name} with path: ${childPath}`);
|
|
1157
|
-
|
|
1228
|
+
|
|
1158
1229
|
return {
|
|
1159
1230
|
name: child.name,
|
|
1160
|
-
path: childPath, //
|
|
1231
|
+
path: childPath, // Use absolute path for consistency
|
|
1161
1232
|
type: child.type,
|
|
1162
1233
|
loaded: child.type === 'directory' ? false : undefined, // Explicitly false for directories
|
|
1163
1234
|
analyzed: child.type === 'file' ? false : undefined,
|
|
@@ -1470,6 +1541,45 @@ class CodeTree {
|
|
|
1470
1541
|
* Handle file analyzed event
|
|
1471
1542
|
*/
|
|
1472
1543
|
onFileAnalyzed(data) {
|
|
1544
|
+
console.log('✅ [FILE ANALYSIS] Received analysis result:', {
|
|
1545
|
+
path: data.path,
|
|
1546
|
+
elements: data.elements ? data.elements.length : 0,
|
|
1547
|
+
complexity: data.complexity,
|
|
1548
|
+
lines: data.lines,
|
|
1549
|
+
stats: data.stats,
|
|
1550
|
+
elementsDetail: data.elements,
|
|
1551
|
+
fullData: data
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
// Debug: Show elements in detail
|
|
1555
|
+
if (data.elements && data.elements.length > 0) {
|
|
1556
|
+
console.log('🔍 [AST ELEMENTS] Found elements:', data.elements.map(elem => ({
|
|
1557
|
+
name: elem.name,
|
|
1558
|
+
type: elem.type,
|
|
1559
|
+
line: elem.line,
|
|
1560
|
+
methods: elem.methods ? elem.methods.length : 0
|
|
1561
|
+
})));
|
|
1562
|
+
} else {
|
|
1563
|
+
const fileName = data.path.split('/').pop();
|
|
1564
|
+
console.log('⚠️ [AST ELEMENTS] No elements found in analysis result');
|
|
1565
|
+
|
|
1566
|
+
// Show user-friendly message for files with no AST elements
|
|
1567
|
+
if (fileName.endsWith('__init__.py')) {
|
|
1568
|
+
this.showNotification(`${fileName} is empty or contains only imports`, 'info');
|
|
1569
|
+
this.updateBreadcrumb(`${fileName} - no code elements to display`, 'info');
|
|
1570
|
+
} else {
|
|
1571
|
+
this.showNotification(`${fileName} contains no classes or functions`, 'info');
|
|
1572
|
+
this.updateBreadcrumb(`${fileName} - no AST elements found`, 'info');
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Clear analysis timeout
|
|
1577
|
+
if (this.analysisTimeouts && this.analysisTimeouts.has(data.path)) {
|
|
1578
|
+
clearTimeout(this.analysisTimeouts.get(data.path));
|
|
1579
|
+
this.analysisTimeouts.delete(data.path);
|
|
1580
|
+
console.log('⏰ [FILE ANALYSIS] Cleared timeout for:', data.path);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1473
1583
|
// Remove loading pulse if this file was being analyzed
|
|
1474
1584
|
const d3Node = this.findD3NodeByPath(data.path);
|
|
1475
1585
|
if (d3Node && this.loadingNodes.has(data.path)) {
|
|
@@ -1484,13 +1594,14 @@ class CodeTree {
|
|
|
1484
1594
|
|
|
1485
1595
|
const fileNode = this.findNodeByPath(data.path);
|
|
1486
1596
|
if (fileNode) {
|
|
1597
|
+
console.log('🔍 [FILE NODE] Found file node for:', data.path);
|
|
1487
1598
|
fileNode.analyzed = true;
|
|
1488
1599
|
fileNode.complexity = data.complexity || 0;
|
|
1489
1600
|
fileNode.lines = data.lines || 0;
|
|
1490
|
-
|
|
1601
|
+
|
|
1491
1602
|
// Add code elements as children
|
|
1492
1603
|
if (data.elements && Array.isArray(data.elements)) {
|
|
1493
|
-
|
|
1604
|
+
const children = data.elements.map(elem => ({
|
|
1494
1605
|
name: elem.name,
|
|
1495
1606
|
type: elem.type.toLowerCase(),
|
|
1496
1607
|
path: `${data.path}#${elem.name}`,
|
|
@@ -1506,8 +1617,17 @@ class CodeTree {
|
|
|
1506
1617
|
docstring: m.docstring || ''
|
|
1507
1618
|
})) : []
|
|
1508
1619
|
}));
|
|
1620
|
+
|
|
1621
|
+
fileNode.children = children;
|
|
1622
|
+
console.log('✅ [FILE NODE] Added children to file node:', {
|
|
1623
|
+
filePath: data.path,
|
|
1624
|
+
childrenCount: children.length,
|
|
1625
|
+
children: children.map(c => ({ name: c.name, type: c.type }))
|
|
1626
|
+
});
|
|
1627
|
+
} else {
|
|
1628
|
+
console.log('⚠️ [FILE NODE] No elements to add as children');
|
|
1509
1629
|
}
|
|
1510
|
-
|
|
1630
|
+
|
|
1511
1631
|
// Update stats
|
|
1512
1632
|
if (data.stats) {
|
|
1513
1633
|
this.stats.classes += data.stats.classes || 0;
|
|
@@ -1515,13 +1635,48 @@ class CodeTree {
|
|
|
1515
1635
|
this.stats.methods += data.stats.methods || 0;
|
|
1516
1636
|
this.stats.lines += data.stats.lines || 0;
|
|
1517
1637
|
}
|
|
1518
|
-
|
|
1638
|
+
|
|
1519
1639
|
this.updateStats();
|
|
1520
|
-
|
|
1640
|
+
|
|
1641
|
+
// CRITICAL FIX: Recreate D3 hierarchy to include new children
|
|
1642
|
+
if (this.root && fileNode.children && fileNode.children.length > 0) {
|
|
1643
|
+
console.log('🔄 [FILE NODE] Recreating D3 hierarchy to include AST children');
|
|
1644
|
+
|
|
1645
|
+
// Store the old root for expansion state preservation
|
|
1646
|
+
const oldRoot = this.root;
|
|
1647
|
+
|
|
1648
|
+
// Recreate the D3 hierarchy with updated data
|
|
1649
|
+
this.root = d3.hierarchy(this.treeData);
|
|
1650
|
+
this.root.x0 = this.height / 2;
|
|
1651
|
+
this.root.y0 = 0;
|
|
1652
|
+
|
|
1653
|
+
// Preserve expansion state from old tree
|
|
1654
|
+
this.preserveExpansionState(oldRoot, this.root);
|
|
1655
|
+
|
|
1656
|
+
// Find the updated file node in the new hierarchy
|
|
1657
|
+
const updatedFileNode = this.findD3NodeByPath(data.path);
|
|
1658
|
+
if (updatedFileNode) {
|
|
1659
|
+
// Ensure the file node is expanded to show its AST children
|
|
1660
|
+
if (updatedFileNode.children && updatedFileNode.children.length > 0) {
|
|
1661
|
+
updatedFileNode._children = null;
|
|
1662
|
+
updatedFileNode.data.expanded = true;
|
|
1663
|
+
console.log('✅ [FILE NODE] File node expanded to show AST children:', {
|
|
1664
|
+
path: data.path,
|
|
1665
|
+
childrenCount: updatedFileNode.children.length,
|
|
1666
|
+
childNames: updatedFileNode.children.map(c => c.data.name)
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Update the visualization with the new hierarchy
|
|
1672
|
+
this.update(this.root);
|
|
1673
|
+
} else if (this.root) {
|
|
1521
1674
|
this.update(this.root);
|
|
1522
1675
|
}
|
|
1523
|
-
|
|
1676
|
+
|
|
1524
1677
|
this.updateBreadcrumb(`Analyzed: ${data.path}`, 'success');
|
|
1678
|
+
} else {
|
|
1679
|
+
console.error('❌ [FILE NODE] Could not find file node for path:', data.path);
|
|
1525
1680
|
}
|
|
1526
1681
|
}
|
|
1527
1682
|
|
|
@@ -1965,12 +2120,12 @@ class CodeTree {
|
|
|
1965
2120
|
* Update statistics display
|
|
1966
2121
|
*/
|
|
1967
2122
|
updateStats() {
|
|
1968
|
-
// Update stats display - use correct IDs from
|
|
2123
|
+
// Update stats display - use correct IDs from corner controls
|
|
1969
2124
|
const statsElements = {
|
|
1970
|
-
'
|
|
1971
|
-
'
|
|
1972
|
-
'
|
|
1973
|
-
'
|
|
2125
|
+
'stats-files': this.stats.files,
|
|
2126
|
+
'stats-classes': this.stats.classes,
|
|
2127
|
+
'stats-functions': this.stats.functions,
|
|
2128
|
+
'stats-methods': this.stats.methods
|
|
1974
2129
|
};
|
|
1975
2130
|
|
|
1976
2131
|
for (const [id, value] of Object.entries(statsElements)) {
|
|
@@ -2001,6 +2156,145 @@ class CodeTree {
|
|
|
2001
2156
|
}
|
|
2002
2157
|
}
|
|
2003
2158
|
|
|
2159
|
+
/**
|
|
2160
|
+
* Analyze file using HTTP fallback when SocketIO fails
|
|
2161
|
+
*/
|
|
2162
|
+
async analyzeFileHTTP(filePath, fileName, d3Node) {
|
|
2163
|
+
console.log('🌐 [HTTP FALLBACK] Analyzing file via HTTP:', filePath);
|
|
2164
|
+
console.log('🌐 [HTTP FALLBACK] File name:', fileName);
|
|
2165
|
+
|
|
2166
|
+
try {
|
|
2167
|
+
// For now, create mock AST data since we don't have an HTTP endpoint yet
|
|
2168
|
+
// This demonstrates the structure and can be replaced with real HTTP call
|
|
2169
|
+
const mockAnalysisResult = this.createMockAnalysisData(filePath, fileName);
|
|
2170
|
+
console.log('🌐 [HTTP FALLBACK] Created mock data:', mockAnalysisResult);
|
|
2171
|
+
|
|
2172
|
+
// Simulate network delay
|
|
2173
|
+
setTimeout(() => {
|
|
2174
|
+
console.log('✅ [HTTP FALLBACK] Mock analysis complete for:', fileName);
|
|
2175
|
+
console.log('✅ [HTTP FALLBACK] Calling onFileAnalyzed with:', mockAnalysisResult);
|
|
2176
|
+
this.onFileAnalyzed(mockAnalysisResult);
|
|
2177
|
+
}, 1000);
|
|
2178
|
+
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
console.error('❌ [HTTP FALLBACK] Analysis failed:', error);
|
|
2181
|
+
this.showNotification(`Analysis failed: ${error.message}`, 'error');
|
|
2182
|
+
this.loadingNodes.delete(filePath);
|
|
2183
|
+
this.removeLoadingPulse(d3Node);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
/**
|
|
2188
|
+
* Create mock analysis data for demonstration
|
|
2189
|
+
*/
|
|
2190
|
+
createMockAnalysisData(filePath, fileName) {
|
|
2191
|
+
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
2192
|
+
console.log('🔍 [MOCK DATA] Creating mock data for file:', fileName, 'extension:', ext);
|
|
2193
|
+
|
|
2194
|
+
// Create realistic mock data based on file type
|
|
2195
|
+
let elements = [];
|
|
2196
|
+
|
|
2197
|
+
if (ext === 'py') {
|
|
2198
|
+
elements = [
|
|
2199
|
+
{
|
|
2200
|
+
name: 'ExampleClass',
|
|
2201
|
+
type: 'class',
|
|
2202
|
+
line: 10,
|
|
2203
|
+
complexity: 3,
|
|
2204
|
+
docstring: 'Example class for demonstration',
|
|
2205
|
+
methods: [
|
|
2206
|
+
{ name: '__init__', type: 'method', line: 12, complexity: 1 },
|
|
2207
|
+
{ name: 'example_method', type: 'method', line: 18, complexity: 2 }
|
|
2208
|
+
]
|
|
2209
|
+
},
|
|
2210
|
+
{
|
|
2211
|
+
name: 'example_function',
|
|
2212
|
+
type: 'function',
|
|
2213
|
+
line: 25,
|
|
2214
|
+
complexity: 2,
|
|
2215
|
+
docstring: 'Example function'
|
|
2216
|
+
}
|
|
2217
|
+
];
|
|
2218
|
+
} else if (ext === 'js' || ext === 'ts') {
|
|
2219
|
+
elements = [
|
|
2220
|
+
{
|
|
2221
|
+
name: 'ExampleClass',
|
|
2222
|
+
type: 'class',
|
|
2223
|
+
line: 5,
|
|
2224
|
+
complexity: 2,
|
|
2225
|
+
methods: [
|
|
2226
|
+
{ name: 'constructor', type: 'method', line: 6, complexity: 1 },
|
|
2227
|
+
{ name: 'exampleMethod', type: 'method', line: 10, complexity: 2 }
|
|
2228
|
+
]
|
|
2229
|
+
},
|
|
2230
|
+
{
|
|
2231
|
+
name: 'exampleFunction',
|
|
2232
|
+
type: 'function',
|
|
2233
|
+
line: 20,
|
|
2234
|
+
complexity: 1
|
|
2235
|
+
}
|
|
2236
|
+
];
|
|
2237
|
+
} else {
|
|
2238
|
+
// For other file types, create at least one element to show it's working
|
|
2239
|
+
elements = [
|
|
2240
|
+
{
|
|
2241
|
+
name: 'mock_element',
|
|
2242
|
+
type: 'function',
|
|
2243
|
+
line: 1,
|
|
2244
|
+
complexity: 1,
|
|
2245
|
+
docstring: `Mock element for ${fileName}`
|
|
2246
|
+
}
|
|
2247
|
+
];
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
console.log('🔍 [MOCK DATA] Created elements:', elements);
|
|
2251
|
+
|
|
2252
|
+
return {
|
|
2253
|
+
path: filePath,
|
|
2254
|
+
elements: elements,
|
|
2255
|
+
complexity: elements.reduce((sum, elem) => sum + (elem.complexity || 1), 0),
|
|
2256
|
+
lines: 50,
|
|
2257
|
+
stats: {
|
|
2258
|
+
classes: elements.filter(e => e.type === 'class').length,
|
|
2259
|
+
functions: elements.filter(e => e.type === 'function').length,
|
|
2260
|
+
methods: elements.reduce((sum, e) => sum + (e.methods ? e.methods.length : 0), 0),
|
|
2261
|
+
lines: 50
|
|
2262
|
+
}
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
/**
|
|
2267
|
+
* Get selected languages from checkboxes with fallback
|
|
2268
|
+
*/
|
|
2269
|
+
getSelectedLanguages() {
|
|
2270
|
+
const selectedLanguages = [];
|
|
2271
|
+
const checkboxes = document.querySelectorAll('.language-checkbox:checked');
|
|
2272
|
+
|
|
2273
|
+
console.log('🔍 [LANGUAGE] Found checkboxes:', checkboxes.length);
|
|
2274
|
+
console.log('🔍 [LANGUAGE] All language checkboxes:', document.querySelectorAll('.language-checkbox').length);
|
|
2275
|
+
|
|
2276
|
+
checkboxes.forEach(cb => {
|
|
2277
|
+
console.log('🔍 [LANGUAGE] Checked language:', cb.value);
|
|
2278
|
+
selectedLanguages.push(cb.value);
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
// Fallback: if no languages are selected, default to common ones
|
|
2282
|
+
if (selectedLanguages.length === 0) {
|
|
2283
|
+
console.warn('⚠️ [LANGUAGE] No languages selected, using defaults');
|
|
2284
|
+
selectedLanguages.push('python', 'javascript', 'typescript');
|
|
2285
|
+
|
|
2286
|
+
// Also check the checkboxes programmatically
|
|
2287
|
+
document.querySelectorAll('.language-checkbox').forEach(cb => {
|
|
2288
|
+
if (['python', 'javascript', 'typescript'].includes(cb.value)) {
|
|
2289
|
+
cb.checked = true;
|
|
2290
|
+
console.log('✅ [LANGUAGE] Auto-checked:', cb.value);
|
|
2291
|
+
}
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
return selectedLanguages;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2004
2298
|
/**
|
|
2005
2299
|
* Detect language from file extension
|
|
2006
2300
|
*/
|
|
@@ -2082,6 +2376,172 @@ class CodeTree {
|
|
|
2082
2376
|
return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)];
|
|
2083
2377
|
}
|
|
2084
2378
|
|
|
2379
|
+
/**
|
|
2380
|
+
* Apply horizontal text to the central spine of the tree
|
|
2381
|
+
*/
|
|
2382
|
+
applySingletonHorizontalLayout(nodes) {
|
|
2383
|
+
if (this.isRadialLayout) return; // Only apply to linear layout
|
|
2384
|
+
|
|
2385
|
+
// Clear previous horizontal nodes tracking
|
|
2386
|
+
this.horizontalNodes.clear();
|
|
2387
|
+
this.centralSpine.clear();
|
|
2388
|
+
|
|
2389
|
+
// Find the central spine - the main path through the tree
|
|
2390
|
+
this.identifyCentralSpine(nodes);
|
|
2391
|
+
|
|
2392
|
+
// Mark all central spine nodes for horizontal text
|
|
2393
|
+
this.centralSpine.forEach(path => {
|
|
2394
|
+
this.horizontalNodes.add(path);
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
console.log(`🎯 [SPINE] Central spine nodes:`, Array.from(this.centralSpine));
|
|
2398
|
+
console.log(`📝 [TEXT] Horizontal text nodes:`, Array.from(this.horizontalNodes));
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
/**
|
|
2402
|
+
* Identify the central spine of the tree (main path from root to deepest/most important nodes)
|
|
2403
|
+
*/
|
|
2404
|
+
identifyCentralSpine(nodes) {
|
|
2405
|
+
if (!nodes || nodes.length === 0) return;
|
|
2406
|
+
|
|
2407
|
+
// Start with the root node
|
|
2408
|
+
const rootNode = nodes.find(node => node.depth === 0);
|
|
2409
|
+
if (!rootNode) {
|
|
2410
|
+
console.warn('🎯 [SPINE] No root node found!');
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
this.centralSpine.add(rootNode.data.path);
|
|
2415
|
+
console.log(`🎯 [SPINE] Starting spine with root: ${rootNode.data.name} (${rootNode.data.path})`);
|
|
2416
|
+
|
|
2417
|
+
// Follow the main path through the tree
|
|
2418
|
+
let currentNode = rootNode;
|
|
2419
|
+
while (currentNode && currentNode.children && currentNode.children.length > 0) {
|
|
2420
|
+
// Choose the "main" child - prioritize directories, then by name
|
|
2421
|
+
const mainChild = this.selectMainChild(currentNode.children);
|
|
2422
|
+
if (mainChild) {
|
|
2423
|
+
this.centralSpine.add(mainChild.data.path);
|
|
2424
|
+
console.log(`🎯 [SPINE] Adding to spine: ${mainChild.data.name}`);
|
|
2425
|
+
currentNode = mainChild;
|
|
2426
|
+
} else {
|
|
2427
|
+
break;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
/**
|
|
2433
|
+
* Select the main child to continue the central spine
|
|
2434
|
+
*/
|
|
2435
|
+
selectMainChild(children) {
|
|
2436
|
+
if (!children || children.length === 0) return null;
|
|
2437
|
+
|
|
2438
|
+
// If only one child, it's the main path
|
|
2439
|
+
if (children.length === 1) return children[0];
|
|
2440
|
+
|
|
2441
|
+
// Prioritize directories over files
|
|
2442
|
+
const directories = children.filter(child => child.data.type === 'directory');
|
|
2443
|
+
if (directories.length === 1) return directories[0];
|
|
2444
|
+
|
|
2445
|
+
// If multiple directories, choose the first one (could be enhanced with better logic)
|
|
2446
|
+
if (directories.length > 0) return directories[0];
|
|
2447
|
+
|
|
2448
|
+
// Fallback to first child
|
|
2449
|
+
return children[0];
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/**
|
|
2453
|
+
* Find chains of singleton nodes (nodes with only one child)
|
|
2454
|
+
*/
|
|
2455
|
+
findSingletonChains(nodes) {
|
|
2456
|
+
const chains = [];
|
|
2457
|
+
const processed = new Set();
|
|
2458
|
+
|
|
2459
|
+
nodes.forEach(node => {
|
|
2460
|
+
if (processed.has(node)) return;
|
|
2461
|
+
|
|
2462
|
+
// Start a new chain if this node has exactly one child
|
|
2463
|
+
if (node.children && node.children.length === 1) {
|
|
2464
|
+
const chain = [node];
|
|
2465
|
+
let current = node.children[0];
|
|
2466
|
+
|
|
2467
|
+
console.log(`🔍 [CHAIN] Starting singleton chain with: ${node.data.name} (depth: ${node.depth})`);
|
|
2468
|
+
|
|
2469
|
+
// Follow the chain of singletons
|
|
2470
|
+
while (current && current.children && current.children.length === 1) {
|
|
2471
|
+
chain.push(current);
|
|
2472
|
+
processed.add(current);
|
|
2473
|
+
console.log(`🔍 [CHAIN] Adding to chain: ${current.data.name} (depth: ${current.depth})`);
|
|
2474
|
+
current = current.children[0];
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// Add the final node if it exists (even if it has multiple children or no children)
|
|
2478
|
+
if (current) {
|
|
2479
|
+
chain.push(current);
|
|
2480
|
+
processed.add(current);
|
|
2481
|
+
console.log(`🔍 [CHAIN] Final node in chain: ${current.data.name} (depth: ${current.depth})`);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// Only create horizontal layout for chains of 2 or more nodes
|
|
2485
|
+
if (chain.length >= 2) {
|
|
2486
|
+
console.log(`✅ [CHAIN] Created horizontal chain:`, chain.map(n => n.data.name));
|
|
2487
|
+
chains.push(chain);
|
|
2488
|
+
processed.add(node);
|
|
2489
|
+
} else {
|
|
2490
|
+
console.log(`❌ [CHAIN] Chain too short (${chain.length}), skipping`);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
|
|
2495
|
+
return chains;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
/**
|
|
2499
|
+
* Layout a chain of nodes horizontally with parent in center
|
|
2500
|
+
*/
|
|
2501
|
+
layoutChainHorizontally(chain) {
|
|
2502
|
+
if (chain.length < 2) return;
|
|
2503
|
+
|
|
2504
|
+
const horizontalSpacing = 150; // Spacing between nodes in horizontal chain
|
|
2505
|
+
const parentNode = chain[0];
|
|
2506
|
+
const originalX = parentNode.x;
|
|
2507
|
+
const originalY = parentNode.y;
|
|
2508
|
+
|
|
2509
|
+
// CRITICAL: In D3 tree layout for linear mode:
|
|
2510
|
+
// - d.x controls VERTICAL position (up-down)
|
|
2511
|
+
// - d.y controls HORIZONTAL position (left-right)
|
|
2512
|
+
// To make singleton chains horizontal, we need to adjust d.x (vertical) to be the same
|
|
2513
|
+
// and spread out d.y (horizontal) positions
|
|
2514
|
+
|
|
2515
|
+
if (chain.length === 2) {
|
|
2516
|
+
// Simple case: parent and one child side by side
|
|
2517
|
+
const centerY = originalY;
|
|
2518
|
+
parentNode.y = centerY - horizontalSpacing / 2; // Parent to the left
|
|
2519
|
+
chain[1].y = centerY + horizontalSpacing / 2; // Child to the right
|
|
2520
|
+
chain[1].x = originalX; // Same vertical level as parent
|
|
2521
|
+
} else {
|
|
2522
|
+
// Multiple nodes: center the parent in the horizontal chain
|
|
2523
|
+
const totalWidth = (chain.length - 1) * horizontalSpacing;
|
|
2524
|
+
const startY = originalY - (totalWidth / 2);
|
|
2525
|
+
|
|
2526
|
+
chain.forEach((node, index) => {
|
|
2527
|
+
node.y = startY + (index * horizontalSpacing); // Spread horizontally
|
|
2528
|
+
node.x = originalX; // All at same vertical level
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// Mark all nodes in this chain as needing horizontal text
|
|
2533
|
+
chain.forEach(node => {
|
|
2534
|
+
this.horizontalNodes.add(node.data.path);
|
|
2535
|
+
console.log(`📝 [TEXT] Marking node for horizontal text: ${node.data.name} (${node.data.path})`);
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
console.log(`🔄 [LAYOUT] Horizontal chain of ${chain.length} nodes:`,
|
|
2539
|
+
chain.map(n => ({ name: n.data.name, vertical: n.x, horizontal: n.y })));
|
|
2540
|
+
console.log(`📝 [TEXT] Total horizontal nodes:`, Array.from(this.horizontalNodes));
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
|
|
2544
|
+
|
|
2085
2545
|
/**
|
|
2086
2546
|
* Update D3 tree visualization
|
|
2087
2547
|
*/
|
|
@@ -2095,6 +2555,9 @@ class CodeTree {
|
|
|
2095
2555
|
const nodes = treeData.descendants();
|
|
2096
2556
|
const links = treeData.descendants().slice(1);
|
|
2097
2557
|
|
|
2558
|
+
// Apply horizontal layout for singleton chains
|
|
2559
|
+
this.applySingletonHorizontalLayout(nodes);
|
|
2560
|
+
|
|
2098
2561
|
if (this.isRadialLayout) {
|
|
2099
2562
|
// Radial layout adjustments
|
|
2100
2563
|
nodes.forEach(d => {
|
|
@@ -2173,20 +2636,42 @@ class CodeTree {
|
|
|
2173
2636
|
|
|
2174
2637
|
// Add labels for nodes with smart positioning
|
|
2175
2638
|
nodeEnter.append('text')
|
|
2176
|
-
.attr('class',
|
|
2639
|
+
.attr('class', d => {
|
|
2640
|
+
// Add horizontal-text class for root node
|
|
2641
|
+
const baseClass = 'node-label';
|
|
2642
|
+
if (d.depth === 0) {
|
|
2643
|
+
console.log(`📝 [TEXT] ✅ Adding horizontal-text class to root: ${d.data.name}`);
|
|
2644
|
+
return `${baseClass} horizontal-text`;
|
|
2645
|
+
}
|
|
2646
|
+
return baseClass;
|
|
2647
|
+
})
|
|
2177
2648
|
.attr('dy', '.35em')
|
|
2178
2649
|
.attr('x', d => {
|
|
2179
2650
|
if (this.isRadialLayout) {
|
|
2180
2651
|
// For radial layout, initial position
|
|
2181
2652
|
return 0;
|
|
2653
|
+
} else if (d.depth === 0 || this.horizontalNodes.has(d.data.path)) {
|
|
2654
|
+
// Root node or horizontal nodes: center text above the node
|
|
2655
|
+
console.log(`📝 [TEXT] ✅ HORIZONTAL positioning for: ${d.data.name} (depth: ${d.depth}, path: ${d.data.path})`);
|
|
2656
|
+
console.log(`📝 [TEXT] ✅ Root check: depth === 0 = ${d.depth === 0}`);
|
|
2657
|
+
console.log(`📝 [TEXT] ✅ Horizontal set check: ${this.horizontalNodes.has(d.data.path)}`);
|
|
2658
|
+
return 0;
|
|
2182
2659
|
} else {
|
|
2183
2660
|
// Linear layout: standard positioning
|
|
2661
|
+
console.log(`📝 [TEXT] Positioning vertical text for: ${d.data.name} (depth: ${d.depth}, path: ${d.data.path})`);
|
|
2184
2662
|
return d.children || d._children ? -13 : 13;
|
|
2185
2663
|
}
|
|
2186
2664
|
})
|
|
2665
|
+
.attr('y', d => {
|
|
2666
|
+
// For root node or horizontal nodes, position text above the node
|
|
2667
|
+
return (d.depth === 0 || this.horizontalNodes.has(d.data.path)) ? -20 : 0;
|
|
2668
|
+
})
|
|
2187
2669
|
.attr('text-anchor', d => {
|
|
2188
2670
|
if (this.isRadialLayout) {
|
|
2189
2671
|
return 'start'; // Will be adjusted in update
|
|
2672
|
+
} else if (d.depth === 0 || this.horizontalNodes.has(d.data.path)) {
|
|
2673
|
+
// Root node or horizontal nodes: center the text
|
|
2674
|
+
return 'middle';
|
|
2190
2675
|
} else {
|
|
2191
2676
|
// Linear layout: standard anchoring
|
|
2192
2677
|
return d.children || d._children ? 'end' : 'start';
|
|
@@ -2203,6 +2688,22 @@ class CodeTree {
|
|
|
2203
2688
|
.style('font-size', '12px')
|
|
2204
2689
|
.style('font-family', '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif')
|
|
2205
2690
|
.style('text-shadow', '1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8)')
|
|
2691
|
+
.style('writing-mode', d => {
|
|
2692
|
+
// Force horizontal writing mode for root node
|
|
2693
|
+
if (d.depth === 0) {
|
|
2694
|
+
console.log(`📝 [TEXT] ✅ Setting horizontal writing-mode for root: ${d.data.name}`);
|
|
2695
|
+
return 'horizontal-tb';
|
|
2696
|
+
}
|
|
2697
|
+
return null;
|
|
2698
|
+
})
|
|
2699
|
+
.style('text-orientation', d => {
|
|
2700
|
+
// Force mixed text orientation for root node
|
|
2701
|
+
if (d.depth === 0) {
|
|
2702
|
+
console.log(`📝 [TEXT] ✅ Setting mixed text-orientation for root: ${d.data.name}`);
|
|
2703
|
+
return 'mixed';
|
|
2704
|
+
}
|
|
2705
|
+
return null;
|
|
2706
|
+
})
|
|
2206
2707
|
.on('click', (event, d) => this.onNodeClick(event, d)) // CRITICAL FIX: Add click handler to labels
|
|
2207
2708
|
.style('cursor', 'pointer');
|
|
2208
2709
|
|
|
@@ -2301,6 +2802,7 @@ class CodeTree {
|
|
|
2301
2802
|
|
|
2302
2803
|
// Update text labels with proper rotation for radial layout
|
|
2303
2804
|
const isRadial = this.isRadialLayout; // Capture the layout type
|
|
2805
|
+
const horizontalNodes = this.horizontalNodes; // Capture horizontal nodes set
|
|
2304
2806
|
nodeUpdate.select('text.node-label')
|
|
2305
2807
|
.style('fill-opacity', 1)
|
|
2306
2808
|
.style('fill', '#333')
|
|
@@ -2331,12 +2833,26 @@ class CodeTree {
|
|
|
2331
2833
|
.attr('dy', '.35em');
|
|
2332
2834
|
}
|
|
2333
2835
|
} else {
|
|
2334
|
-
// Linear layout -
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2836
|
+
// Linear layout - handle root node and horizontal nodes differently
|
|
2837
|
+
const isHorizontal = d.depth === 0 || horizontalNodes.has(d.data.path);
|
|
2838
|
+
|
|
2839
|
+
if (isHorizontal) {
|
|
2840
|
+
// Root node or horizontal nodes: text above the node, centered
|
|
2841
|
+
selection
|
|
2842
|
+
.attr('transform', null)
|
|
2843
|
+
.attr('x', 0)
|
|
2844
|
+
.attr('y', -20)
|
|
2845
|
+
.attr('text-anchor', 'middle')
|
|
2846
|
+
.attr('dy', '.35em');
|
|
2847
|
+
} else {
|
|
2848
|
+
// Regular linear layout - no rotation needed
|
|
2849
|
+
selection
|
|
2850
|
+
.attr('transform', null)
|
|
2851
|
+
.attr('x', d.children || d._children ? -13 : 13)
|
|
2852
|
+
.attr('y', 0)
|
|
2853
|
+
.attr('text-anchor', d.children || d._children ? 'end' : 'start')
|
|
2854
|
+
.attr('dy', '.35em');
|
|
2855
|
+
}
|
|
2340
2856
|
}
|
|
2341
2857
|
});
|
|
2342
2858
|
|
|
@@ -2404,6 +2920,14 @@ class CodeTree {
|
|
|
2404
2920
|
d.x0 = d.x;
|
|
2405
2921
|
d.y0 = d.y;
|
|
2406
2922
|
});
|
|
2923
|
+
|
|
2924
|
+
// Apply current zoom level to maintain consistent text size
|
|
2925
|
+
if (this.zoom) {
|
|
2926
|
+
const currentTransform = d3.zoomTransform(this.svg.node());
|
|
2927
|
+
if (currentTransform.k !== 1) {
|
|
2928
|
+
this.adjustTextSizeForZoom(currentTransform.k);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2407
2931
|
}
|
|
2408
2932
|
|
|
2409
2933
|
/**
|
|
@@ -2578,16 +3102,21 @@ class CodeTree {
|
|
|
2578
3102
|
* Handle node click - implement lazy loading with enhanced visual feedback
|
|
2579
3103
|
*/
|
|
2580
3104
|
onNodeClick(event, d) {
|
|
3105
|
+
const clickId = Date.now() + Math.random();
|
|
2581
3106
|
// DEBUG: Log all clicks to verify handler is working
|
|
2582
|
-
console.log(
|
|
3107
|
+
console.log(`🖱️ [NODE CLICK] Clicked on node (ID: ${clickId}):`, {
|
|
2583
3108
|
name: d?.data?.name,
|
|
2584
3109
|
path: d?.data?.path,
|
|
2585
3110
|
type: d?.data?.type,
|
|
2586
3111
|
loaded: d?.data?.loaded,
|
|
2587
3112
|
hasChildren: !!(d?.children || d?._children),
|
|
2588
|
-
dataChildren: d?.data?.children?.length || 0
|
|
3113
|
+
dataChildren: d?.data?.children?.length || 0,
|
|
3114
|
+
loadingNodesSize: this.loadingNodes ? this.loadingNodes.size : 'undefined'
|
|
2589
3115
|
});
|
|
2590
|
-
|
|
3116
|
+
|
|
3117
|
+
// Update structured data with clicked node
|
|
3118
|
+
this.updateStructuredData(d);
|
|
3119
|
+
|
|
2591
3120
|
// Handle node click interaction
|
|
2592
3121
|
|
|
2593
3122
|
// Check event parameter
|
|
@@ -2686,11 +3215,8 @@ class CodeTree {
|
|
|
2686
3215
|
|
|
2687
3216
|
|
|
2688
3217
|
// Get selected languages from checkboxes
|
|
2689
|
-
const selectedLanguages =
|
|
2690
|
-
|
|
2691
|
-
checkboxes.forEach(cb => {
|
|
2692
|
-
selectedLanguages.push(cb.value);
|
|
2693
|
-
});
|
|
3218
|
+
const selectedLanguages = this.getSelectedLanguages();
|
|
3219
|
+
console.log('🔍 [LANGUAGE] Selected languages:', selectedLanguages);
|
|
2694
3220
|
|
|
2695
3221
|
// Get ignore patterns
|
|
2696
3222
|
const ignorePatternsElement = document.getElementById('ignore-patterns');
|
|
@@ -2710,12 +3236,45 @@ class CodeTree {
|
|
|
2710
3236
|
shouldLoad: d.data.type === 'directory' && !d.data.loaded
|
|
2711
3237
|
});
|
|
2712
3238
|
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
this.
|
|
2716
|
-
|
|
3239
|
+
console.log('✅ [SUBDIRECTORY LOADING] Load check passed, proceeding with loading logic');
|
|
3240
|
+
console.log('🔍 [SUBDIRECTORY LOADING] Initial loading state:', {
|
|
3241
|
+
loadingNodesSize: this.loadingNodes ? this.loadingNodes.size : 'undefined',
|
|
3242
|
+
loadingNodesContent: Array.from(this.loadingNodes || [])
|
|
3243
|
+
});
|
|
3244
|
+
|
|
3245
|
+
try {
|
|
3246
|
+
// Debug the path and loadingNodes state
|
|
3247
|
+
console.log('🔍 [SUBDIRECTORY LOADING] Checking for duplicates:', {
|
|
3248
|
+
path: d.data.path,
|
|
3249
|
+
pathType: typeof d.data.path,
|
|
3250
|
+
loadingNodesType: typeof this.loadingNodes,
|
|
3251
|
+
loadingNodesSize: this.loadingNodes ? this.loadingNodes.size : 'undefined',
|
|
3252
|
+
hasMethod: this.loadingNodes && typeof this.loadingNodes.has === 'function'
|
|
3253
|
+
});
|
|
3254
|
+
|
|
3255
|
+
// Prevent duplicate requests
|
|
3256
|
+
const isDuplicate = this.loadingNodes && this.loadingNodes.has(d.data.path);
|
|
3257
|
+
console.log('🔍 [SUBDIRECTORY LOADING] Duplicate check result:', {
|
|
3258
|
+
isDuplicate: isDuplicate,
|
|
3259
|
+
loadingNodesContent: Array.from(this.loadingNodes || []),
|
|
3260
|
+
pathBeingChecked: d.data.path
|
|
3261
|
+
});
|
|
3262
|
+
|
|
3263
|
+
if (isDuplicate) {
|
|
3264
|
+
console.warn('⚠️ [SUBDIRECTORY LOADING] Duplicate request detected, but proceeding anyway:', {
|
|
3265
|
+
path: d.data.path,
|
|
3266
|
+
name: d.data.name,
|
|
3267
|
+
loadingNodesSize: this.loadingNodes.size,
|
|
3268
|
+
loadingNodesContent: Array.from(this.loadingNodes),
|
|
3269
|
+
pathInSet: this.loadingNodes.has(d.data.path)
|
|
3270
|
+
});
|
|
3271
|
+
// Remove the existing entry and proceed
|
|
3272
|
+
this.loadingNodes.delete(d.data.path);
|
|
3273
|
+
console.log('🧹 [SUBDIRECTORY LOADING] Removed duplicate entry, proceeding with fresh request');
|
|
2717
3274
|
}
|
|
2718
|
-
|
|
3275
|
+
|
|
3276
|
+
console.log('✅ [SUBDIRECTORY LOADING] No duplicate request, proceeding to mark as loading');
|
|
3277
|
+
|
|
2719
3278
|
// Mark as loading immediately to prevent duplicate requests
|
|
2720
3279
|
d.data.loaded = 'loading';
|
|
2721
3280
|
this.loadingNodes.add(d.data.path);
|
|
@@ -2788,10 +3347,13 @@ class CodeTree {
|
|
|
2788
3347
|
if (data.exists && data.is_directory && data.contents) {
|
|
2789
3348
|
const node = this.findNodeByPath(d.data.path);
|
|
2790
3349
|
if (node) {
|
|
3350
|
+
console.log('🔧 [SUBDIRECTORY LOADING] Creating children with paths:',
|
|
3351
|
+
data.contents.map(item => ({ name: item.name, path: item.path })));
|
|
3352
|
+
|
|
2791
3353
|
// Add children to the node
|
|
2792
3354
|
node.children = data.contents.map(item => ({
|
|
2793
3355
|
name: item.name,
|
|
2794
|
-
path:
|
|
3356
|
+
path: item.path, // Use the full path from API response
|
|
2795
3357
|
type: item.is_directory ? 'directory' : 'file',
|
|
2796
3358
|
loaded: item.is_directory ? false : undefined,
|
|
2797
3359
|
analyzed: !item.is_directory ? false : undefined,
|
|
@@ -2817,8 +3379,15 @@ class CodeTree {
|
|
|
2817
3379
|
}
|
|
2818
3380
|
|
|
2819
3381
|
this.update(updatedD3Node || this.root);
|
|
3382
|
+
|
|
3383
|
+
// Focus on the newly loaded directory for better UX
|
|
3384
|
+
if (updatedD3Node && data.contents.length > 0) {
|
|
3385
|
+
setTimeout(() => {
|
|
3386
|
+
this.focusOnDirectory(updatedD3Node);
|
|
3387
|
+
}, 500); // Small delay to let the update animation complete
|
|
3388
|
+
}
|
|
2820
3389
|
}
|
|
2821
|
-
|
|
3390
|
+
|
|
2822
3391
|
this.updateBreadcrumb(`Loaded ${data.contents.length} items`, 'success');
|
|
2823
3392
|
this.showNotification(`Loaded ${data.contents.length} items from ${d.data.name}`, 'success');
|
|
2824
3393
|
}
|
|
@@ -2870,12 +3439,37 @@ class CodeTree {
|
|
|
2870
3439
|
d.data.loaded = false;
|
|
2871
3440
|
}
|
|
2872
3441
|
}, 100); // 100ms delay to ensure visual effects render first
|
|
2873
|
-
|
|
3442
|
+
|
|
3443
|
+
} catch (error) {
|
|
3444
|
+
console.error('❌ [SUBDIRECTORY LOADING] Error in directory loading logic:', {
|
|
3445
|
+
error: error.message,
|
|
3446
|
+
stack: error.stack,
|
|
3447
|
+
path: d.data.path,
|
|
3448
|
+
nodeData: d.data
|
|
3449
|
+
});
|
|
3450
|
+
this.showNotification(`Error loading directory: ${error.message}`, 'error');
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
2874
3453
|
// For files that haven't been analyzed, request analysis
|
|
2875
3454
|
else if (d.data.type === 'file' && !d.data.analyzed) {
|
|
2876
3455
|
// Only analyze files of selected languages
|
|
2877
3456
|
const fileLanguage = this.detectLanguage(d.data.path);
|
|
3457
|
+
console.log('🔍 [FILE ANALYSIS] Language check:', {
|
|
3458
|
+
fileName: d.data.name,
|
|
3459
|
+
filePath: d.data.path,
|
|
3460
|
+
detectedLanguage: fileLanguage,
|
|
3461
|
+
selectedLanguages: selectedLanguages,
|
|
3462
|
+
isLanguageSelected: selectedLanguages.includes(fileLanguage),
|
|
3463
|
+
shouldAnalyze: selectedLanguages.includes(fileLanguage) || fileLanguage === 'unknown'
|
|
3464
|
+
});
|
|
3465
|
+
|
|
2878
3466
|
if (!selectedLanguages.includes(fileLanguage) && fileLanguage !== 'unknown') {
|
|
3467
|
+
console.warn('⚠️ [FILE ANALYSIS] Skipping file:', {
|
|
3468
|
+
fileName: d.data.name,
|
|
3469
|
+
detectedLanguage: fileLanguage,
|
|
3470
|
+
selectedLanguages: selectedLanguages,
|
|
3471
|
+
reason: `${fileLanguage} not in selected languages`
|
|
3472
|
+
});
|
|
2879
3473
|
this.showNotification(`Skipping ${d.data.name} - ${fileLanguage} not selected`, 'warning');
|
|
2880
3474
|
return;
|
|
2881
3475
|
}
|
|
@@ -2891,14 +3485,43 @@ class CodeTree {
|
|
|
2891
3485
|
|
|
2892
3486
|
// Delay the socket request to ensure visual effects are rendered
|
|
2893
3487
|
setTimeout(() => {
|
|
2894
|
-
|
|
2895
|
-
|
|
3488
|
+
console.log('🚀 [FILE ANALYSIS] Sending analysis request:', {
|
|
3489
|
+
fileName: d.data.name,
|
|
3490
|
+
originalPath: d.data.path,
|
|
3491
|
+
fullPath: fullPath,
|
|
3492
|
+
hasSocket: !!this.socket,
|
|
3493
|
+
socketConnected: this.socket?.connected
|
|
3494
|
+
});
|
|
3495
|
+
|
|
3496
|
+
if (this.socket && this.socket.connected) {
|
|
3497
|
+
console.log('📡 [FILE ANALYSIS] Using SocketIO for analysis:', {
|
|
3498
|
+
event: 'code:analyze:file',
|
|
3499
|
+
path: fullPath,
|
|
3500
|
+
socketConnected: this.socket.connected,
|
|
3501
|
+
socketId: this.socket.id
|
|
3502
|
+
});
|
|
3503
|
+
|
|
2896
3504
|
this.socket.emit('code:analyze:file', {
|
|
2897
3505
|
path: fullPath
|
|
2898
3506
|
});
|
|
2899
|
-
|
|
3507
|
+
|
|
3508
|
+
// Set a shorter timeout since we have a stable server
|
|
3509
|
+
const analysisTimeout = setTimeout(() => {
|
|
3510
|
+
console.warn('⏰ [FILE ANALYSIS] SocketIO timeout, trying HTTP fallback for:', fullPath);
|
|
3511
|
+
this.analyzeFileHTTP(fullPath, d.data.name, d3.select(event.target.closest('g')));
|
|
3512
|
+
}, 5000); // 5 second timeout
|
|
3513
|
+
|
|
3514
|
+
// Store timeout ID for cleanup
|
|
3515
|
+
if (!this.analysisTimeouts) this.analysisTimeouts = new Map();
|
|
3516
|
+
this.analysisTimeouts.set(fullPath, analysisTimeout);
|
|
3517
|
+
|
|
3518
|
+
this.updateBreadcrumb(`Analyzing ${d.data.name}...`, 'info');
|
|
3519
|
+
this.showNotification(`Analyzing: ${d.data.name}`, 'info');
|
|
3520
|
+
} else {
|
|
3521
|
+
console.log('🔄 [FILE ANALYSIS] SocketIO unavailable, using HTTP fallback');
|
|
2900
3522
|
this.updateBreadcrumb(`Analyzing ${d.data.name}...`, 'info');
|
|
2901
3523
|
this.showNotification(`Analyzing: ${d.data.name}`, 'info');
|
|
3524
|
+
this.analyzeFileHTTP(fullPath, d.data.name, d3.select(event.target.closest('g')));
|
|
2902
3525
|
}
|
|
2903
3526
|
}, 100); // 100ms delay to ensure visual effects render first
|
|
2904
3527
|
}
|
|
@@ -3215,14 +3838,294 @@ class CodeTree {
|
|
|
3215
3838
|
}
|
|
3216
3839
|
|
|
3217
3840
|
/**
|
|
3218
|
-
*
|
|
3841
|
+
* Focus on a specific directory, hiding parent directories and showing only its contents
|
|
3219
3842
|
*/
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
console.log('[
|
|
3224
|
-
|
|
3225
|
-
|
|
3843
|
+
focusOnDirectory(node) {
|
|
3844
|
+
if (!node || node.data.type !== 'directory') return;
|
|
3845
|
+
|
|
3846
|
+
console.log('🎯 [FOCUS] Focusing on directory:', node.data.path);
|
|
3847
|
+
|
|
3848
|
+
// Store the focused node
|
|
3849
|
+
this.focusedNode = node;
|
|
3850
|
+
|
|
3851
|
+
// Create a temporary root for display purposes
|
|
3852
|
+
const focusedRoot = {
|
|
3853
|
+
...node.data,
|
|
3854
|
+
name: `📁 ${node.data.name}`,
|
|
3855
|
+
children: node.data.children || []
|
|
3856
|
+
};
|
|
3857
|
+
|
|
3858
|
+
// Create new D3 hierarchy with focused node as root
|
|
3859
|
+
const tempRoot = d3.hierarchy(focusedRoot);
|
|
3860
|
+
tempRoot.x0 = this.height / 2;
|
|
3861
|
+
tempRoot.y0 = 0;
|
|
3862
|
+
|
|
3863
|
+
// Store original root for restoration
|
|
3864
|
+
if (!this.originalRoot) {
|
|
3865
|
+
this.originalRoot = this.root;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// Update with focused view
|
|
3869
|
+
this.root = tempRoot;
|
|
3870
|
+
this.update(this.root);
|
|
3871
|
+
|
|
3872
|
+
// Add visual styling for focused mode
|
|
3873
|
+
d3.select('#code-tree-container').classed('focused', true);
|
|
3874
|
+
|
|
3875
|
+
// Update breadcrumb to show focused path
|
|
3876
|
+
this.updateBreadcrumb(`Focused on: ${node.data.name}`, 'info');
|
|
3877
|
+
this.showNotification(`Focused on directory: ${node.data.name}`, 'info');
|
|
3878
|
+
|
|
3879
|
+
// Add back button to toolbar
|
|
3880
|
+
this.addBackButton();
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
/**
|
|
3884
|
+
* Return to the full tree view from focused directory view
|
|
3885
|
+
*/
|
|
3886
|
+
unfocusDirectory() {
|
|
3887
|
+
if (!this.originalRoot) return;
|
|
3888
|
+
|
|
3889
|
+
console.log('🔙 [FOCUS] Returning to full tree view');
|
|
3890
|
+
|
|
3891
|
+
// Restore original root
|
|
3892
|
+
this.root = this.originalRoot;
|
|
3893
|
+
this.originalRoot = null;
|
|
3894
|
+
this.focusedNode = null;
|
|
3895
|
+
|
|
3896
|
+
// Update display
|
|
3897
|
+
this.update(this.root);
|
|
3898
|
+
|
|
3899
|
+
// Remove visual styling for focused mode
|
|
3900
|
+
d3.select('#code-tree-container').classed('focused', false);
|
|
3901
|
+
|
|
3902
|
+
// Remove back button
|
|
3903
|
+
this.removeBackButton();
|
|
3904
|
+
|
|
3905
|
+
this.updateBreadcrumb('Full tree view restored', 'success');
|
|
3906
|
+
this.showNotification('Returned to full tree view', 'success');
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
/**
|
|
3910
|
+
* Add back button to return from focused view
|
|
3911
|
+
*/
|
|
3912
|
+
addBackButton() {
|
|
3913
|
+
// Remove existing back button
|
|
3914
|
+
d3.select('#tree-back-button').remove();
|
|
3915
|
+
|
|
3916
|
+
const toolbar = d3.select('.tree-controls-toolbar');
|
|
3917
|
+
if (toolbar.empty()) return;
|
|
3918
|
+
|
|
3919
|
+
toolbar.insert('button', ':first-child')
|
|
3920
|
+
.attr('id', 'tree-back-button')
|
|
3921
|
+
.attr('class', 'tree-control-btn back-btn')
|
|
3922
|
+
.attr('title', 'Return to full tree view')
|
|
3923
|
+
.text('← Back')
|
|
3924
|
+
.on('click', () => this.unfocusDirectory());
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
/**
|
|
3928
|
+
* Remove back button
|
|
3929
|
+
*/
|
|
3930
|
+
removeBackButton() {
|
|
3931
|
+
d3.select('#tree-back-button').remove();
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
/**
|
|
3935
|
+
* Reset zoom to fit the tree
|
|
3936
|
+
*/
|
|
3937
|
+
resetZoom() {
|
|
3938
|
+
if (!this.svg || !this.zoom) return;
|
|
3939
|
+
|
|
3940
|
+
// Calculate bounds of the tree
|
|
3941
|
+
const bounds = this.treeGroup.node().getBBox();
|
|
3942
|
+
const fullWidth = this.width;
|
|
3943
|
+
const fullHeight = this.height;
|
|
3944
|
+
const width = bounds.width;
|
|
3945
|
+
const height = bounds.height;
|
|
3946
|
+
const midX = bounds.x + width / 2;
|
|
3947
|
+
const midY = bounds.y + height / 2;
|
|
3948
|
+
|
|
3949
|
+
if (width === 0 || height === 0) return; // Nothing to fit
|
|
3950
|
+
|
|
3951
|
+
// Calculate scale to fit tree in view with some padding
|
|
3952
|
+
const scale = Math.min(fullWidth / width, fullHeight / height) * 0.9;
|
|
3953
|
+
|
|
3954
|
+
// Calculate translate to center the tree
|
|
3955
|
+
const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];
|
|
3956
|
+
|
|
3957
|
+
// Apply the transform with smooth transition
|
|
3958
|
+
this.svg.transition()
|
|
3959
|
+
.duration(750)
|
|
3960
|
+
.call(this.zoom.transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
|
|
3961
|
+
|
|
3962
|
+
this.showNotification('Zoom reset to fit tree', 'info');
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
/**
|
|
3966
|
+
* Zoom in by a fixed factor
|
|
3967
|
+
*/
|
|
3968
|
+
zoomIn() {
|
|
3969
|
+
if (!this.svg || !this.zoom) return;
|
|
3970
|
+
|
|
3971
|
+
this.svg.transition()
|
|
3972
|
+
.duration(300)
|
|
3973
|
+
.call(this.zoom.scaleBy, 1.5);
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
/**
|
|
3977
|
+
* Zoom out by a fixed factor
|
|
3978
|
+
*/
|
|
3979
|
+
zoomOut() {
|
|
3980
|
+
if (!this.svg || !this.zoom) return;
|
|
3981
|
+
|
|
3982
|
+
this.svg.transition()
|
|
3983
|
+
.duration(300)
|
|
3984
|
+
.call(this.zoom.scaleBy, 1 / 1.5);
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
/**
|
|
3988
|
+
* Update zoom level display
|
|
3989
|
+
*/
|
|
3990
|
+
updateZoomLevel(scale) {
|
|
3991
|
+
const zoomDisplay = document.getElementById('zoom-level-display');
|
|
3992
|
+
if (zoomDisplay) {
|
|
3993
|
+
zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
/**
|
|
3998
|
+
* Adjust text size to remain constant during zoom
|
|
3999
|
+
*/
|
|
4000
|
+
adjustTextSizeForZoom(zoomScale) {
|
|
4001
|
+
if (!this.treeGroup) return;
|
|
4002
|
+
|
|
4003
|
+
// Calculate the inverse scale to keep text at consistent size
|
|
4004
|
+
const textScale = 1 / zoomScale;
|
|
4005
|
+
|
|
4006
|
+
// Apply inverse scaling to all text elements
|
|
4007
|
+
this.treeGroup.selectAll('text')
|
|
4008
|
+
.style('font-size', `${12 * textScale}px`)
|
|
4009
|
+
.attr('transform', function() {
|
|
4010
|
+
// Get existing transform if any
|
|
4011
|
+
const existingTransform = d3.select(this).attr('transform') || '';
|
|
4012
|
+
// Remove any existing scale transforms and add the new one
|
|
4013
|
+
const cleanTransform = existingTransform.replace(/scale\([^)]*\)/g, '').trim();
|
|
4014
|
+
return cleanTransform ? `${cleanTransform} scale(${textScale})` : `scale(${textScale})`;
|
|
4015
|
+
});
|
|
4016
|
+
|
|
4017
|
+
// Also adjust other UI elements that should maintain size
|
|
4018
|
+
this.treeGroup.selectAll('.expand-icon')
|
|
4019
|
+
.style('font-size', `${12 * textScale}px`)
|
|
4020
|
+
.attr('transform', function() {
|
|
4021
|
+
const existingTransform = d3.select(this).attr('transform') || '';
|
|
4022
|
+
const cleanTransform = existingTransform.replace(/scale\([^)]*\)/g, '').trim();
|
|
4023
|
+
return cleanTransform ? `${cleanTransform} scale(${textScale})` : `scale(${textScale})`;
|
|
4024
|
+
});
|
|
4025
|
+
|
|
4026
|
+
// Adjust item count badges
|
|
4027
|
+
this.treeGroup.selectAll('.item-count-badge')
|
|
4028
|
+
.style('font-size', `${10 * textScale}px`)
|
|
4029
|
+
.attr('transform', function() {
|
|
4030
|
+
const existingTransform = d3.select(this).attr('transform') || '';
|
|
4031
|
+
const cleanTransform = existingTransform.replace(/scale\([^)]*\)/g, '').trim();
|
|
4032
|
+
return cleanTransform ? `${cleanTransform} scale(${textScale})` : `scale(${textScale})`;
|
|
4033
|
+
});
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
/**
|
|
4037
|
+
* Add keyboard shortcuts for zoom functionality
|
|
4038
|
+
*/
|
|
4039
|
+
addZoomKeyboardShortcuts() {
|
|
4040
|
+
// Only add shortcuts when the code tab is active
|
|
4041
|
+
document.addEventListener('keydown', (event) => {
|
|
4042
|
+
// Check if code tab is active
|
|
4043
|
+
const codeTab = document.getElementById('code-tab');
|
|
4044
|
+
if (!codeTab || !codeTab.classList.contains('active')) {
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
// Prevent shortcuts when typing in input fields
|
|
4049
|
+
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
// Handle zoom shortcuts
|
|
4054
|
+
if (event.ctrlKey || event.metaKey) {
|
|
4055
|
+
switch (event.key) {
|
|
4056
|
+
case '=':
|
|
4057
|
+
case '+':
|
|
4058
|
+
event.preventDefault();
|
|
4059
|
+
this.zoomIn();
|
|
4060
|
+
break;
|
|
4061
|
+
case '-':
|
|
4062
|
+
event.preventDefault();
|
|
4063
|
+
this.zoomOut();
|
|
4064
|
+
break;
|
|
4065
|
+
case '0':
|
|
4066
|
+
event.preventDefault();
|
|
4067
|
+
this.resetZoom();
|
|
4068
|
+
break;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
/**
|
|
4075
|
+
* Check if a file path represents a source file that should show source viewer
|
|
4076
|
+
*/
|
|
4077
|
+
isSourceFile(path) {
|
|
4078
|
+
if (!path) return false;
|
|
4079
|
+
const sourceExtensions = ['.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.cpp', '.c', '.h', '.cs', '.php', '.rb', '.go', '.rs', '.swift'];
|
|
4080
|
+
return sourceExtensions.some(ext => path.toLowerCase().endsWith(ext));
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
/**
|
|
4084
|
+
* Show hierarchical source viewer for a source file
|
|
4085
|
+
*/
|
|
4086
|
+
async showSourceViewer(node) {
|
|
4087
|
+
console.log('📄 [SOURCE VIEWER] Showing source for:', node.data.path);
|
|
4088
|
+
|
|
4089
|
+
// Create source viewer container
|
|
4090
|
+
const sourceViewer = document.createElement('div');
|
|
4091
|
+
sourceViewer.className = 'source-viewer';
|
|
4092
|
+
|
|
4093
|
+
// Create header
|
|
4094
|
+
const header = document.createElement('div');
|
|
4095
|
+
header.className = 'source-viewer-header';
|
|
4096
|
+
header.innerHTML = `
|
|
4097
|
+
<span>📄 ${node.data.name || 'Source File'}</span>
|
|
4098
|
+
<div class="source-viewer-controls">
|
|
4099
|
+
<button class="source-control-btn" id="expand-all-source" title="Expand all">⬇</button>
|
|
4100
|
+
<button class="source-control-btn" id="collapse-all-source" title="Collapse all">⬆</button>
|
|
4101
|
+
</div>
|
|
4102
|
+
`;
|
|
4103
|
+
|
|
4104
|
+
// Create content container
|
|
4105
|
+
const content = document.createElement('div');
|
|
4106
|
+
content.className = 'source-viewer-content';
|
|
4107
|
+
content.id = 'source-viewer-content';
|
|
4108
|
+
|
|
4109
|
+
sourceViewer.appendChild(header);
|
|
4110
|
+
sourceViewer.appendChild(content);
|
|
4111
|
+
this.structuredDataContent.appendChild(sourceViewer);
|
|
4112
|
+
|
|
4113
|
+
// Add control event listeners
|
|
4114
|
+
document.getElementById('expand-all-source')?.addEventListener('click', () => this.expandAllSource());
|
|
4115
|
+
document.getElementById('collapse-all-source')?.addEventListener('click', () => this.collapseAllSource());
|
|
4116
|
+
|
|
4117
|
+
// Load and display source code
|
|
4118
|
+
try {
|
|
4119
|
+
await this.loadSourceContent(node, content);
|
|
4120
|
+
} catch (error) {
|
|
4121
|
+
console.error('Failed to load source content:', error);
|
|
4122
|
+
content.innerHTML = `
|
|
4123
|
+
<div class="ast-data-placeholder">
|
|
4124
|
+
<div class="ast-placeholder-icon">❌</div>
|
|
4125
|
+
<div class="ast-placeholder-text">Failed to load source file</div>
|
|
4126
|
+
</div>
|
|
4127
|
+
`;
|
|
4128
|
+
}
|
|
3226
4129
|
}
|
|
3227
4130
|
|
|
3228
4131
|
/**
|
|
@@ -3384,6 +4287,35 @@ class CodeTree {
|
|
|
3384
4287
|
}
|
|
3385
4288
|
}
|
|
3386
4289
|
|
|
4290
|
+
/**
|
|
4291
|
+
* Debug function to clear loading state (for troubleshooting)
|
|
4292
|
+
*/
|
|
4293
|
+
clearLoadingState() {
|
|
4294
|
+
console.log('🧹 [DEBUG] Clearing loading state:', {
|
|
4295
|
+
loadingNodesBefore: Array.from(this.loadingNodes),
|
|
4296
|
+
size: this.loadingNodes.size
|
|
4297
|
+
});
|
|
4298
|
+
this.loadingNodes.clear();
|
|
4299
|
+
|
|
4300
|
+
// Also reset any nodes marked as 'loading'
|
|
4301
|
+
this.resetLoadingFlags(this.treeData);
|
|
4302
|
+
|
|
4303
|
+
console.log('✅ [DEBUG] Loading state cleared');
|
|
4304
|
+
this.showNotification('Loading state cleared', 'info');
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
/**
|
|
4308
|
+
* Recursively reset loading flags in tree data
|
|
4309
|
+
*/
|
|
4310
|
+
resetLoadingFlags(node) {
|
|
4311
|
+
if (node.loaded === 'loading') {
|
|
4312
|
+
node.loaded = false;
|
|
4313
|
+
}
|
|
4314
|
+
if (node.children) {
|
|
4315
|
+
node.children.forEach(child => this.resetLoadingFlags(child));
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
|
|
3387
4319
|
/**
|
|
3388
4320
|
* Export tree data
|
|
3389
4321
|
*/
|
|
@@ -3428,7 +4360,7 @@ class CodeTree {
|
|
|
3428
4360
|
if (ticker) {
|
|
3429
4361
|
ticker.textContent = message;
|
|
3430
4362
|
ticker.className = `ticker ticker-${type}`;
|
|
3431
|
-
|
|
4363
|
+
|
|
3432
4364
|
// Auto-hide after 5 seconds for non-error messages
|
|
3433
4365
|
if (type !== 'error') {
|
|
3434
4366
|
setTimeout(() => {
|
|
@@ -3441,6 +4373,790 @@ class CodeTree {
|
|
|
3441
4373
|
}
|
|
3442
4374
|
}
|
|
3443
4375
|
}
|
|
4376
|
+
|
|
4377
|
+
/**
|
|
4378
|
+
* Initialize the structured data integration
|
|
4379
|
+
*/
|
|
4380
|
+
initializeStructuredData() {
|
|
4381
|
+
this.structuredDataContent = document.getElementById('module-data-content');
|
|
4382
|
+
|
|
4383
|
+
if (!this.structuredDataContent) {
|
|
4384
|
+
console.warn('Structured data content element not found');
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
console.log('✅ Structured data integration initialized');
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
/**
|
|
4392
|
+
* Update structured data with node information
|
|
4393
|
+
*/
|
|
4394
|
+
updateStructuredData(node) {
|
|
4395
|
+
if (!this.structuredDataContent) {
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
console.log('🔍 [STRUCTURED DATA] Updating with node:', {
|
|
4400
|
+
name: node?.data?.name,
|
|
4401
|
+
type: node?.data?.type,
|
|
4402
|
+
hasChildren: !!(node?.children || node?._children),
|
|
4403
|
+
dataChildren: node?.data?.children?.length || 0
|
|
4404
|
+
});
|
|
4405
|
+
|
|
4406
|
+
// Clear previous content
|
|
4407
|
+
this.structuredDataContent.innerHTML = '';
|
|
4408
|
+
|
|
4409
|
+
// Check if this is a source file that should show source viewer
|
|
4410
|
+
if (node.data.type === 'file' && this.isSourceFile(node.data.path)) {
|
|
4411
|
+
this.showSourceViewer(node);
|
|
4412
|
+
} else {
|
|
4413
|
+
// Show children or functions for non-source files
|
|
4414
|
+
const children = node.children || node._children || [];
|
|
4415
|
+
const dataChildren = node.data.children || [];
|
|
4416
|
+
|
|
4417
|
+
if (children.length > 0 || dataChildren.length > 0) {
|
|
4418
|
+
this.showASTNodeChildren(node);
|
|
4419
|
+
} else if (node.data.type === 'file' && node.data.analyzed) {
|
|
4420
|
+
this.showASTFileDetails(node);
|
|
4421
|
+
} else {
|
|
4422
|
+
this.showASTNodeDetails(node);
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
/**
|
|
4428
|
+
* Show child nodes in structured data
|
|
4429
|
+
*/
|
|
4430
|
+
showASTNodeChildren(node) {
|
|
4431
|
+
const children = node.children || node._children || [];
|
|
4432
|
+
const dataChildren = node.data.children || [];
|
|
4433
|
+
|
|
4434
|
+
// Use D3 children if available, otherwise use data children
|
|
4435
|
+
const childrenToShow = children.length > 0 ? children : dataChildren;
|
|
4436
|
+
|
|
4437
|
+
if (childrenToShow.length === 0) {
|
|
4438
|
+
this.showASTEmptyState('No children found');
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
// Create header
|
|
4443
|
+
const header = document.createElement('div');
|
|
4444
|
+
header.className = 'structured-view-header';
|
|
4445
|
+
header.innerHTML = `<h4>${this.getNodeIcon(node.data.type)} ${node.data.name || 'Node'} - Children (${childrenToShow.length})</h4>`;
|
|
4446
|
+
this.structuredDataContent.appendChild(header);
|
|
4447
|
+
|
|
4448
|
+
childrenToShow.forEach((child, index) => {
|
|
4449
|
+
const childData = child.data || child;
|
|
4450
|
+
const item = this.createASTDataViewerItem(childData, index);
|
|
4451
|
+
this.structuredDataContent.appendChild(item);
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
/**
|
|
4456
|
+
* Show file details in structured data
|
|
4457
|
+
*/
|
|
4458
|
+
showASTFileDetails(node) {
|
|
4459
|
+
// Create header
|
|
4460
|
+
const header = document.createElement('div');
|
|
4461
|
+
header.className = 'structured-view-header';
|
|
4462
|
+
header.innerHTML = `<h4>${this.getNodeIcon(node.data.type)} ${node.data.name || 'File'} - Details</h4>`;
|
|
4463
|
+
this.structuredDataContent.appendChild(header);
|
|
4464
|
+
|
|
4465
|
+
const details = [];
|
|
4466
|
+
|
|
4467
|
+
if (node.data.language) {
|
|
4468
|
+
details.push({ label: 'Language', value: node.data.language });
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
if (node.data.lines) {
|
|
4472
|
+
details.push({ label: 'Lines', value: node.data.lines });
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
if (node.data.complexity !== undefined) {
|
|
4476
|
+
details.push({ label: 'Complexity', value: node.data.complexity });
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
if (node.data.size) {
|
|
4480
|
+
details.push({ label: 'Size', value: this.formatFileSize(node.data.size) });
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
if (details.length === 0) {
|
|
4484
|
+
this.showASTEmptyState('No details available');
|
|
4485
|
+
return;
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
details.forEach((detail, index) => {
|
|
4489
|
+
const item = this.createASTDetailItem(detail, index);
|
|
4490
|
+
this.structuredDataContent.appendChild(item);
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
/**
|
|
4495
|
+
* Show basic node details in structured data
|
|
4496
|
+
*/
|
|
4497
|
+
showASTNodeDetails(node) {
|
|
4498
|
+
// Create header
|
|
4499
|
+
const header = document.createElement('div');
|
|
4500
|
+
header.className = 'structured-view-header';
|
|
4501
|
+
header.innerHTML = `<h4>${this.getNodeIcon(node.data.type)} ${node.data.name || 'Node'} - Details</h4>`;
|
|
4502
|
+
this.structuredDataContent.appendChild(header);
|
|
4503
|
+
|
|
4504
|
+
const details = [];
|
|
4505
|
+
|
|
4506
|
+
details.push({ label: 'Type', value: node.data.type || 'unknown' });
|
|
4507
|
+
details.push({ label: 'Path', value: node.data.path || 'unknown' });
|
|
4508
|
+
|
|
4509
|
+
if (node.data.line) {
|
|
4510
|
+
details.push({ label: 'Line', value: node.data.line });
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
details.forEach((detail, index) => {
|
|
4514
|
+
const item = this.createASTDetailItem(detail, index);
|
|
4515
|
+
this.structuredDataContent.appendChild(item);
|
|
4516
|
+
});
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
/**
|
|
4520
|
+
* Create an AST data viewer item for a child node
|
|
4521
|
+
*/
|
|
4522
|
+
createASTDataViewerItem(childData, index) {
|
|
4523
|
+
const item = document.createElement('div');
|
|
4524
|
+
item.className = 'ast-data-viewer-item';
|
|
4525
|
+
item.dataset.index = index;
|
|
4526
|
+
|
|
4527
|
+
const header = document.createElement('div');
|
|
4528
|
+
header.className = 'ast-data-item-header';
|
|
4529
|
+
|
|
4530
|
+
const name = document.createElement('div');
|
|
4531
|
+
name.className = 'ast-data-item-name';
|
|
4532
|
+
name.innerHTML = `${this.getNodeIcon(childData.type)} ${childData.name || 'Unknown'}`;
|
|
4533
|
+
|
|
4534
|
+
const type = document.createElement('div');
|
|
4535
|
+
type.className = `ast-data-item-type ${childData.type || 'unknown'}`;
|
|
4536
|
+
type.textContent = childData.type || 'unknown';
|
|
4537
|
+
|
|
4538
|
+
header.appendChild(name);
|
|
4539
|
+
header.appendChild(type);
|
|
4540
|
+
|
|
4541
|
+
const details = document.createElement('div');
|
|
4542
|
+
details.className = 'ast-data-item-details';
|
|
4543
|
+
|
|
4544
|
+
const detailParts = [];
|
|
4545
|
+
|
|
4546
|
+
if (childData.line) {
|
|
4547
|
+
detailParts.push(`<span class="ast-data-item-line">Line ${childData.line}</span>`);
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
if (childData.complexity !== undefined) {
|
|
4551
|
+
const complexityLevel = this.getComplexityLevel(childData.complexity);
|
|
4552
|
+
detailParts.push(`<span class="ast-data-item-complexity">
|
|
4553
|
+
<span class="ast-complexity-indicator ${complexityLevel}"></span>
|
|
4554
|
+
Complexity: ${childData.complexity}
|
|
4555
|
+
</span>`);
|
|
4556
|
+
}
|
|
4557
|
+
|
|
4558
|
+
if (childData.docstring) {
|
|
4559
|
+
detailParts.push(`<div style="margin-top: 4px; font-style: italic;">${childData.docstring}</div>`);
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
details.innerHTML = detailParts.join(' ');
|
|
4563
|
+
|
|
4564
|
+
item.appendChild(header);
|
|
4565
|
+
item.appendChild(details);
|
|
4566
|
+
|
|
4567
|
+
// Add click handler to select item
|
|
4568
|
+
item.addEventListener('click', () => {
|
|
4569
|
+
this.selectASTDataViewerItem(item);
|
|
4570
|
+
});
|
|
4571
|
+
|
|
4572
|
+
return item;
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4575
|
+
/**
|
|
4576
|
+
* Create a detail item for simple key-value pairs
|
|
4577
|
+
*/
|
|
4578
|
+
createASTDetailItem(detail, index) {
|
|
4579
|
+
const item = document.createElement('div');
|
|
4580
|
+
item.className = 'ast-data-viewer-item';
|
|
4581
|
+
item.dataset.index = index;
|
|
4582
|
+
|
|
4583
|
+
const header = document.createElement('div');
|
|
4584
|
+
header.className = 'ast-data-item-header';
|
|
4585
|
+
|
|
4586
|
+
const name = document.createElement('div');
|
|
4587
|
+
name.className = 'ast-data-item-name';
|
|
4588
|
+
name.textContent = detail.label;
|
|
4589
|
+
|
|
4590
|
+
const value = document.createElement('div');
|
|
4591
|
+
value.className = 'ast-data-item-details';
|
|
4592
|
+
value.textContent = detail.value;
|
|
4593
|
+
|
|
4594
|
+
header.appendChild(name);
|
|
4595
|
+
item.appendChild(header);
|
|
4596
|
+
item.appendChild(value);
|
|
4597
|
+
|
|
4598
|
+
return item;
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
/**
|
|
4602
|
+
* Show empty state in structured data
|
|
4603
|
+
*/
|
|
4604
|
+
showASTEmptyState(message) {
|
|
4605
|
+
this.structuredDataContent.innerHTML = `
|
|
4606
|
+
<div class="ast-data-placeholder">
|
|
4607
|
+
<div class="ast-placeholder-icon">📭</div>
|
|
4608
|
+
<div class="ast-placeholder-text">${message}</div>
|
|
4609
|
+
</div>
|
|
4610
|
+
`;
|
|
4611
|
+
}
|
|
4612
|
+
|
|
4613
|
+
/**
|
|
4614
|
+
* Select an AST data viewer item
|
|
4615
|
+
*/
|
|
4616
|
+
selectASTDataViewerItem(item) {
|
|
4617
|
+
// Remove previous selection
|
|
4618
|
+
const previousSelected = this.structuredDataContent.querySelector('.ast-data-viewer-item.selected');
|
|
4619
|
+
if (previousSelected) {
|
|
4620
|
+
previousSelected.classList.remove('selected');
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4623
|
+
// Select new item
|
|
4624
|
+
item.classList.add('selected');
|
|
4625
|
+
this.selectedASTItem = item;
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4628
|
+
/**
|
|
4629
|
+
* Get icon for node type
|
|
4630
|
+
*/
|
|
4631
|
+
getNodeIcon(type) {
|
|
4632
|
+
const icons = {
|
|
4633
|
+
'directory': '📁',
|
|
4634
|
+
'file': '📄',
|
|
4635
|
+
'class': '🏛️',
|
|
4636
|
+
'function': '⚡',
|
|
4637
|
+
'method': '🔧',
|
|
4638
|
+
'variable': '📦',
|
|
4639
|
+
'import': '📥',
|
|
4640
|
+
'module': '📦'
|
|
4641
|
+
};
|
|
4642
|
+
return icons[type] || '📄';
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4645
|
+
/**
|
|
4646
|
+
* Get complexity level for styling
|
|
4647
|
+
*/
|
|
4648
|
+
getComplexityLevel(complexity) {
|
|
4649
|
+
if (complexity <= 5) return 'low';
|
|
4650
|
+
if (complexity <= 10) return 'medium';
|
|
4651
|
+
return 'high';
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
/**
|
|
4655
|
+
* Format file size for display
|
|
4656
|
+
*/
|
|
4657
|
+
formatFileSize(bytes) {
|
|
4658
|
+
if (bytes === 0) return '0 B';
|
|
4659
|
+
const k = 1024;
|
|
4660
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
4661
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
4662
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4665
|
+
/**
|
|
4666
|
+
* Load source content and render with AST integration
|
|
4667
|
+
*/
|
|
4668
|
+
async loadSourceContent(node, contentContainer) {
|
|
4669
|
+
// Try to read the file content
|
|
4670
|
+
const sourceContent = await this.readSourceFile(node.data.path);
|
|
4671
|
+
if (!sourceContent) {
|
|
4672
|
+
throw new Error('Could not read source file');
|
|
4673
|
+
}
|
|
4674
|
+
|
|
4675
|
+
// Get AST elements for this file
|
|
4676
|
+
const astElements = node.data.children || [];
|
|
4677
|
+
|
|
4678
|
+
// Parse and render source with AST integration
|
|
4679
|
+
this.renderSourceWithAST(sourceContent, astElements, contentContainer, node);
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
/**
|
|
4683
|
+
* Read source file content
|
|
4684
|
+
*/
|
|
4685
|
+
async readSourceFile(filePath) {
|
|
4686
|
+
try {
|
|
4687
|
+
console.log('📖 [SOURCE READER] Reading file:', filePath);
|
|
4688
|
+
|
|
4689
|
+
// Make API call to read the actual file content
|
|
4690
|
+
const response = await fetch(`/api/file/read?path=${encodeURIComponent(filePath)}`);
|
|
4691
|
+
|
|
4692
|
+
if (!response.ok) {
|
|
4693
|
+
const error = await response.json();
|
|
4694
|
+
console.error('Failed to read file:', error);
|
|
4695
|
+
// Fall back to placeholder for errors
|
|
4696
|
+
return this.generatePlaceholderSource(filePath);
|
|
4697
|
+
}
|
|
4698
|
+
|
|
4699
|
+
const data = await response.json();
|
|
4700
|
+
console.log('📖 [SOURCE READER] Read', data.lines, 'lines from', data.name);
|
|
4701
|
+
return data.content;
|
|
4702
|
+
|
|
4703
|
+
} catch (error) {
|
|
4704
|
+
console.error('Failed to read source file:', error);
|
|
4705
|
+
// Fall back to placeholder on error
|
|
4706
|
+
return this.generatePlaceholderSource(filePath);
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
|
|
4710
|
+
/**
|
|
4711
|
+
* Generate placeholder source content for demonstration
|
|
4712
|
+
*/
|
|
4713
|
+
generatePlaceholderSource(filePath) {
|
|
4714
|
+
const fileName = filePath.split('/').pop();
|
|
4715
|
+
|
|
4716
|
+
if (fileName.endsWith('.py')) {
|
|
4717
|
+
return `"""
|
|
4718
|
+
${fileName}
|
|
4719
|
+
Generated placeholder content for demonstration
|
|
4720
|
+
"""
|
|
4721
|
+
|
|
4722
|
+
import os
|
|
4723
|
+
import sys
|
|
4724
|
+
from typing import List, Dict, Optional
|
|
4725
|
+
|
|
4726
|
+
class ExampleClass:
|
|
4727
|
+
"""Example class with methods."""
|
|
4728
|
+
|
|
4729
|
+
def __init__(self, name: str):
|
|
4730
|
+
"""Initialize the example class."""
|
|
4731
|
+
self.name = name
|
|
4732
|
+
self.data = {}
|
|
4733
|
+
|
|
4734
|
+
def process_data(self, items: List[str]) -> Dict[str, int]:
|
|
4735
|
+
"""Process a list of items and return counts."""
|
|
4736
|
+
result = {}
|
|
4737
|
+
for item in items:
|
|
4738
|
+
result[item] = result.get(item, 0) + 1
|
|
4739
|
+
return result
|
|
4740
|
+
|
|
4741
|
+
def get_summary(self) -> str:
|
|
4742
|
+
"""Get a summary of the processed data."""
|
|
4743
|
+
if not self.data:
|
|
4744
|
+
return "No data processed"
|
|
4745
|
+
return f"Processed {len(self.data)} items"
|
|
4746
|
+
|
|
4747
|
+
def main():
|
|
4748
|
+
"""Main function."""
|
|
4749
|
+
example = ExampleClass("demo")
|
|
4750
|
+
items = ["a", "b", "a", "c", "b", "a"]
|
|
4751
|
+
result = example.process_data(items)
|
|
4752
|
+
print(example.get_summary())
|
|
4753
|
+
return result
|
|
4754
|
+
|
|
4755
|
+
if __name__ == "__main__":
|
|
4756
|
+
main()
|
|
4757
|
+
`;
|
|
4758
|
+
} else {
|
|
4759
|
+
return `// ${fileName}
|
|
4760
|
+
// Generated placeholder content for demonstration
|
|
4761
|
+
|
|
4762
|
+
class ExampleClass {
|
|
4763
|
+
constructor(name) {
|
|
4764
|
+
this.name = name;
|
|
4765
|
+
this.data = {};
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
processData(items) {
|
|
4769
|
+
const result = {};
|
|
4770
|
+
for (const item of items) {
|
|
4771
|
+
result[item] = (result[item] || 0) + 1;
|
|
4772
|
+
}
|
|
4773
|
+
return result;
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
getSummary() {
|
|
4777
|
+
if (Object.keys(this.data).length === 0) {
|
|
4778
|
+
return "No data processed";
|
|
4779
|
+
}
|
|
4780
|
+
return \`Processed \${Object.keys(this.data).length} items\`;
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
function main() {
|
|
4785
|
+
const example = new ExampleClass("demo");
|
|
4786
|
+
const items = ["a", "b", "a", "c", "b", "a"];
|
|
4787
|
+
const result = example.processData(items);
|
|
4788
|
+
console.log(example.getSummary());
|
|
4789
|
+
return result;
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
main();
|
|
4793
|
+
`;
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
|
|
4797
|
+
/**
|
|
4798
|
+
* Render source code with AST integration and collapsible sections
|
|
4799
|
+
*/
|
|
4800
|
+
renderSourceWithAST(sourceContent, astElements, container, node) {
|
|
4801
|
+
const lines = sourceContent.split('\n');
|
|
4802
|
+
const astMap = this.createASTLineMap(astElements);
|
|
4803
|
+
|
|
4804
|
+
console.log('🎨 [SOURCE RENDERER] Rendering source with AST:', {
|
|
4805
|
+
lines: lines.length,
|
|
4806
|
+
astElements: astElements.length,
|
|
4807
|
+
astMap: Object.keys(astMap).length
|
|
4808
|
+
});
|
|
4809
|
+
|
|
4810
|
+
// Create line elements with AST integration
|
|
4811
|
+
lines.forEach((line, index) => {
|
|
4812
|
+
const lineNumber = index + 1;
|
|
4813
|
+
const lineElement = this.createSourceLine(line, lineNumber, astMap[lineNumber], node);
|
|
4814
|
+
container.appendChild(lineElement);
|
|
4815
|
+
});
|
|
4816
|
+
|
|
4817
|
+
// Store reference for expand/collapse operations
|
|
4818
|
+
this.currentSourceContainer = container;
|
|
4819
|
+
this.currentASTElements = astElements;
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
/**
|
|
4823
|
+
* Create AST line mapping for quick lookup
|
|
4824
|
+
*/
|
|
4825
|
+
createASTLineMap(astElements) {
|
|
4826
|
+
const lineMap = {};
|
|
4827
|
+
|
|
4828
|
+
astElements.forEach(element => {
|
|
4829
|
+
if (element.line) {
|
|
4830
|
+
if (!lineMap[element.line]) {
|
|
4831
|
+
lineMap[element.line] = [];
|
|
4832
|
+
}
|
|
4833
|
+
lineMap[element.line].push(element);
|
|
4834
|
+
}
|
|
4835
|
+
});
|
|
4836
|
+
|
|
4837
|
+
return lineMap;
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
/**
|
|
4841
|
+
* Create a source line element with AST integration
|
|
4842
|
+
*/
|
|
4843
|
+
createSourceLine(content, lineNumber, astElements, node) {
|
|
4844
|
+
const lineDiv = document.createElement('div');
|
|
4845
|
+
lineDiv.className = 'source-line';
|
|
4846
|
+
lineDiv.dataset.lineNumber = lineNumber;
|
|
4847
|
+
|
|
4848
|
+
// Check if this line has AST elements
|
|
4849
|
+
const hasAST = astElements && astElements.length > 0;
|
|
4850
|
+
if (hasAST) {
|
|
4851
|
+
lineDiv.classList.add('ast-element');
|
|
4852
|
+
lineDiv.dataset.astElements = JSON.stringify(astElements);
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
// Determine if this line should be collapsible
|
|
4856
|
+
const isCollapsible = this.isCollapsibleLine(content, astElements);
|
|
4857
|
+
if (isCollapsible) {
|
|
4858
|
+
lineDiv.classList.add('collapsible');
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
// Create line number
|
|
4862
|
+
const lineNumberSpan = document.createElement('span');
|
|
4863
|
+
lineNumberSpan.className = 'line-number';
|
|
4864
|
+
lineNumberSpan.textContent = lineNumber;
|
|
4865
|
+
|
|
4866
|
+
// Create collapse indicator
|
|
4867
|
+
const collapseIndicator = document.createElement('span');
|
|
4868
|
+
collapseIndicator.className = 'collapse-indicator';
|
|
4869
|
+
if (isCollapsible) {
|
|
4870
|
+
collapseIndicator.classList.add('expanded');
|
|
4871
|
+
collapseIndicator.addEventListener('click', (e) => {
|
|
4872
|
+
e.stopPropagation();
|
|
4873
|
+
this.toggleSourceSection(lineDiv);
|
|
4874
|
+
});
|
|
4875
|
+
} else {
|
|
4876
|
+
collapseIndicator.classList.add('none');
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4879
|
+
// Create line content with syntax highlighting
|
|
4880
|
+
const lineContentSpan = document.createElement('span');
|
|
4881
|
+
lineContentSpan.className = 'line-content';
|
|
4882
|
+
lineContentSpan.innerHTML = this.applySyntaxHighlighting(content);
|
|
4883
|
+
|
|
4884
|
+
// Add click handler for AST integration
|
|
4885
|
+
if (hasAST) {
|
|
4886
|
+
lineDiv.addEventListener('click', () => {
|
|
4887
|
+
this.onSourceLineClick(lineDiv, astElements, node);
|
|
4888
|
+
});
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
lineDiv.appendChild(lineNumberSpan);
|
|
4892
|
+
lineDiv.appendChild(collapseIndicator);
|
|
4893
|
+
lineDiv.appendChild(lineContentSpan);
|
|
4894
|
+
|
|
4895
|
+
return lineDiv;
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4898
|
+
/**
|
|
4899
|
+
* Check if a line should be collapsible (function/class definitions)
|
|
4900
|
+
*/
|
|
4901
|
+
isCollapsibleLine(content, astElements) {
|
|
4902
|
+
const trimmed = content.trim();
|
|
4903
|
+
|
|
4904
|
+
// Python patterns
|
|
4905
|
+
if (trimmed.startsWith('def ') || trimmed.startsWith('class ') ||
|
|
4906
|
+
trimmed.startsWith('async def ')) {
|
|
4907
|
+
return true;
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4910
|
+
// JavaScript patterns
|
|
4911
|
+
if (trimmed.includes('function ') || trimmed.includes('class ') ||
|
|
4912
|
+
trimmed.includes('=> {') || trimmed.match(/^\s*\w+\s*\([^)]*\)\s*{/)) {
|
|
4913
|
+
return true;
|
|
4914
|
+
}
|
|
4915
|
+
|
|
4916
|
+
// Check AST elements for function/class definitions
|
|
4917
|
+
if (astElements) {
|
|
4918
|
+
return astElements.some(el =>
|
|
4919
|
+
el.type === 'function' || el.type === 'class' ||
|
|
4920
|
+
el.type === 'method' || el.type === 'FunctionDef' ||
|
|
4921
|
+
el.type === 'ClassDef'
|
|
4922
|
+
);
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4925
|
+
return false;
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
/**
|
|
4929
|
+
* Apply basic syntax highlighting
|
|
4930
|
+
*/
|
|
4931
|
+
applySyntaxHighlighting(content) {
|
|
4932
|
+
// First, properly escape HTML entities
|
|
4933
|
+
let highlighted = content
|
|
4934
|
+
.replace(/&/g, '&')
|
|
4935
|
+
.replace(/</g, '<')
|
|
4936
|
+
.replace(/>/g, '>');
|
|
4937
|
+
|
|
4938
|
+
// Store markers for where we'll insert spans
|
|
4939
|
+
const replacements = [];
|
|
4940
|
+
|
|
4941
|
+
// Python and JavaScript keywords (combined)
|
|
4942
|
+
const keywords = /\b(def|class|import|from|if|else|elif|for|while|try|except|finally|with|as|return|yield|lambda|async|await|function|const|let|var|catch|export)\b/g;
|
|
4943
|
+
|
|
4944
|
+
// Find all matches first without replacing
|
|
4945
|
+
let match;
|
|
4946
|
+
|
|
4947
|
+
// Keywords
|
|
4948
|
+
while ((match = keywords.exec(highlighted)) !== null) {
|
|
4949
|
+
replacements.push({
|
|
4950
|
+
start: match.index,
|
|
4951
|
+
end: match.index + match[0].length,
|
|
4952
|
+
replacement: `<span class="keyword">${match[0]}</span>`
|
|
4953
|
+
});
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
// Strings - simple pattern for now
|
|
4957
|
+
const stringPattern = /(["'`])([^"'`]*?)\1/g;
|
|
4958
|
+
while ((match = stringPattern.exec(highlighted)) !== null) {
|
|
4959
|
+
replacements.push({
|
|
4960
|
+
start: match.index,
|
|
4961
|
+
end: match.index + match[0].length,
|
|
4962
|
+
replacement: `<span class="string">${match[0]}</span>`
|
|
4963
|
+
});
|
|
4964
|
+
}
|
|
4965
|
+
|
|
4966
|
+
// Comments
|
|
4967
|
+
const commentPattern = /(#.*$|\/\/.*$)/gm;
|
|
4968
|
+
while ((match = commentPattern.exec(highlighted)) !== null) {
|
|
4969
|
+
replacements.push({
|
|
4970
|
+
start: match.index,
|
|
4971
|
+
end: match.index + match[0].length,
|
|
4972
|
+
replacement: `<span class="comment">${match[0]}</span>`
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4976
|
+
// Sort replacements by start position (reverse order to not mess up indices)
|
|
4977
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
4978
|
+
|
|
4979
|
+
// Apply replacements
|
|
4980
|
+
for (const rep of replacements) {
|
|
4981
|
+
// Check for overlapping replacements and skip if needed
|
|
4982
|
+
const before = highlighted.substring(0, rep.start);
|
|
4983
|
+
const after = highlighted.substring(rep.end);
|
|
4984
|
+
|
|
4985
|
+
// Only apply if we're not inside another replacement
|
|
4986
|
+
if (!before.includes('<span') || before.lastIndexOf('</span>') > before.lastIndexOf('<span')) {
|
|
4987
|
+
highlighted = before + rep.replacement + after;
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4991
|
+
return highlighted;
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4994
|
+
/**
|
|
4995
|
+
* Toggle collapse/expand of a source section
|
|
4996
|
+
*/
|
|
4997
|
+
toggleSourceSection(lineElement) {
|
|
4998
|
+
const indicator = lineElement.querySelector('.collapse-indicator');
|
|
4999
|
+
const isExpanded = indicator.classList.contains('expanded');
|
|
5000
|
+
|
|
5001
|
+
if (isExpanded) {
|
|
5002
|
+
this.collapseSourceSection(lineElement);
|
|
5003
|
+
} else {
|
|
5004
|
+
this.expandSourceSection(lineElement);
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
|
|
5008
|
+
/**
|
|
5009
|
+
* Collapse a source section
|
|
5010
|
+
*/
|
|
5011
|
+
collapseSourceSection(lineElement) {
|
|
5012
|
+
const indicator = lineElement.querySelector('.collapse-indicator');
|
|
5013
|
+
indicator.classList.remove('expanded');
|
|
5014
|
+
indicator.classList.add('collapsed');
|
|
5015
|
+
|
|
5016
|
+
// Find and hide related lines (simple implementation)
|
|
5017
|
+
const startLine = parseInt(lineElement.dataset.lineNumber);
|
|
5018
|
+
const container = lineElement.parentElement;
|
|
5019
|
+
const lines = Array.from(container.children);
|
|
5020
|
+
|
|
5021
|
+
// Hide subsequent indented lines
|
|
5022
|
+
let currentIndex = lines.indexOf(lineElement) + 1;
|
|
5023
|
+
const baseIndent = this.getLineIndentation(lineElement.querySelector('.line-content').textContent);
|
|
5024
|
+
|
|
5025
|
+
while (currentIndex < lines.length) {
|
|
5026
|
+
const nextLine = lines[currentIndex];
|
|
5027
|
+
const nextContent = nextLine.querySelector('.line-content').textContent;
|
|
5028
|
+
const nextIndent = this.getLineIndentation(nextContent);
|
|
5029
|
+
|
|
5030
|
+
// Stop if we hit a line at the same or lower indentation level
|
|
5031
|
+
if (nextContent.trim() && nextIndent <= baseIndent) {
|
|
5032
|
+
break;
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
nextLine.classList.add('collapsed-content');
|
|
5036
|
+
currentIndex++;
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
// Add collapsed placeholder
|
|
5040
|
+
const placeholder = document.createElement('div');
|
|
5041
|
+
placeholder.className = 'source-line collapsed-placeholder';
|
|
5042
|
+
placeholder.innerHTML = `
|
|
5043
|
+
<span class="line-number"></span>
|
|
5044
|
+
<span class="collapse-indicator none"></span>
|
|
5045
|
+
<span class="line-content"> ... (collapsed)</span>
|
|
5046
|
+
`;
|
|
5047
|
+
lineElement.insertAdjacentElement('afterend', placeholder);
|
|
5048
|
+
}
|
|
5049
|
+
|
|
5050
|
+
/**
|
|
5051
|
+
* Expand a source section
|
|
5052
|
+
*/
|
|
5053
|
+
expandSourceSection(lineElement) {
|
|
5054
|
+
const indicator = lineElement.querySelector('.collapse-indicator');
|
|
5055
|
+
indicator.classList.remove('collapsed');
|
|
5056
|
+
indicator.classList.add('expanded');
|
|
5057
|
+
|
|
5058
|
+
// Show hidden lines
|
|
5059
|
+
const container = lineElement.parentElement;
|
|
5060
|
+
const lines = Array.from(container.children);
|
|
5061
|
+
|
|
5062
|
+
lines.forEach(line => {
|
|
5063
|
+
if (line.classList.contains('collapsed-content')) {
|
|
5064
|
+
line.classList.remove('collapsed-content');
|
|
5065
|
+
}
|
|
5066
|
+
});
|
|
5067
|
+
|
|
5068
|
+
// Remove placeholder
|
|
5069
|
+
const placeholder = lineElement.nextElementSibling;
|
|
5070
|
+
if (placeholder && placeholder.classList.contains('collapsed-placeholder')) {
|
|
5071
|
+
placeholder.remove();
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
/**
|
|
5076
|
+
* Get indentation level of a line
|
|
5077
|
+
*/
|
|
5078
|
+
getLineIndentation(content) {
|
|
5079
|
+
const match = content.match(/^(\s*)/);
|
|
5080
|
+
return match ? match[1].length : 0;
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
/**
|
|
5084
|
+
* Handle click on source line with AST elements
|
|
5085
|
+
*/
|
|
5086
|
+
onSourceLineClick(lineElement, astElements, node) {
|
|
5087
|
+
console.log('🎯 [SOURCE LINE CLICK] Line clicked:', {
|
|
5088
|
+
line: lineElement.dataset.lineNumber,
|
|
5089
|
+
astElements: astElements.length
|
|
5090
|
+
});
|
|
5091
|
+
|
|
5092
|
+
// Highlight the clicked line
|
|
5093
|
+
this.highlightSourceLine(lineElement);
|
|
5094
|
+
|
|
5095
|
+
// Show AST details for this line
|
|
5096
|
+
if (astElements.length > 0) {
|
|
5097
|
+
this.showASTElementDetails(astElements[0], node);
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
// If this is a collapsible line, also toggle it
|
|
5101
|
+
if (lineElement.classList.contains('collapsible')) {
|
|
5102
|
+
this.toggleSourceSection(lineElement);
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
|
|
5106
|
+
/**
|
|
5107
|
+
* Highlight a source line
|
|
5108
|
+
*/
|
|
5109
|
+
highlightSourceLine(lineElement) {
|
|
5110
|
+
// Remove previous highlights
|
|
5111
|
+
if (this.currentSourceContainer) {
|
|
5112
|
+
const lines = this.currentSourceContainer.querySelectorAll('.source-line');
|
|
5113
|
+
lines.forEach(line => line.classList.remove('highlighted'));
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
// Add highlight to clicked line
|
|
5117
|
+
lineElement.classList.add('highlighted');
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
/**
|
|
5121
|
+
* Show AST element details
|
|
5122
|
+
*/
|
|
5123
|
+
showASTElementDetails(astElement, node) {
|
|
5124
|
+
// This could open a detailed view or update another panel
|
|
5125
|
+
console.log('📋 [AST DETAILS] Showing details for:', astElement);
|
|
5126
|
+
|
|
5127
|
+
// For now, just log the details
|
|
5128
|
+
// In a full implementation, this might update a details panel
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
/**
|
|
5132
|
+
* Expand all collapsible sections in source viewer
|
|
5133
|
+
*/
|
|
5134
|
+
expandAllSource() {
|
|
5135
|
+
if (!this.currentSourceContainer) return;
|
|
5136
|
+
|
|
5137
|
+
const collapsibleLines = this.currentSourceContainer.querySelectorAll('.source-line.collapsible');
|
|
5138
|
+
collapsibleLines.forEach(line => {
|
|
5139
|
+
const indicator = line.querySelector('.collapse-indicator');
|
|
5140
|
+
if (indicator.classList.contains('collapsed')) {
|
|
5141
|
+
this.expandSourceSection(line);
|
|
5142
|
+
}
|
|
5143
|
+
});
|
|
5144
|
+
}
|
|
5145
|
+
|
|
5146
|
+
/**
|
|
5147
|
+
* Collapse all collapsible sections in source viewer
|
|
5148
|
+
*/
|
|
5149
|
+
collapseAllSource() {
|
|
5150
|
+
if (!this.currentSourceContainer) return;
|
|
5151
|
+
|
|
5152
|
+
const collapsibleLines = this.currentSourceContainer.querySelectorAll('.source-line.collapsible');
|
|
5153
|
+
collapsibleLines.forEach(line => {
|
|
5154
|
+
const indicator = line.querySelector('.collapse-indicator');
|
|
5155
|
+
if (indicator.classList.contains('expanded')) {
|
|
5156
|
+
this.collapseSourceSection(line);
|
|
5157
|
+
}
|
|
5158
|
+
});
|
|
5159
|
+
}
|
|
3444
5160
|
}
|
|
3445
5161
|
|
|
3446
5162
|
// Export for use in other modules
|
|
@@ -3451,7 +5167,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3451
5167
|
// Check if we're on a page with code tree container
|
|
3452
5168
|
if (document.getElementById('code-tree-container')) {
|
|
3453
5169
|
window.codeTree = new CodeTree();
|
|
3454
|
-
|
|
5170
|
+
|
|
5171
|
+
// Expose debug functions globally for troubleshooting
|
|
5172
|
+
window.debugCodeTree = {
|
|
5173
|
+
clearLoadingState: () => window.codeTree?.clearLoadingState(),
|
|
5174
|
+
showLoadingNodes: () => {
|
|
5175
|
+
console.log('Current loading nodes:', Array.from(window.codeTree?.loadingNodes || []));
|
|
5176
|
+
return Array.from(window.codeTree?.loadingNodes || []);
|
|
5177
|
+
},
|
|
5178
|
+
resetTree: () => {
|
|
5179
|
+
if (window.codeTree) {
|
|
5180
|
+
window.codeTree.clearLoadingState();
|
|
5181
|
+
window.codeTree.initializeTreeData();
|
|
5182
|
+
console.log('Tree reset complete');
|
|
5183
|
+
}
|
|
5184
|
+
},
|
|
5185
|
+
focusOnPath: (path) => {
|
|
5186
|
+
if (window.codeTree) {
|
|
5187
|
+
const node = window.codeTree.findD3NodeByPath(path);
|
|
5188
|
+
if (node) {
|
|
5189
|
+
window.codeTree.focusOnDirectory(node);
|
|
5190
|
+
console.log('Focused on:', path);
|
|
5191
|
+
} else {
|
|
5192
|
+
console.log('Node not found:', path);
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
},
|
|
5196
|
+
unfocus: () => window.codeTree?.unfocusDirectory()
|
|
5197
|
+
};
|
|
5198
|
+
|
|
3455
5199
|
// Listen for tab changes to initialize when code tab is selected
|
|
3456
5200
|
document.addEventListener('click', (e) => {
|
|
3457
5201
|
if (e.target.matches('[data-tab="code"]')) {
|