claude-mpm 4.2.1__py3-none-any.whl → 4.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/agent-manager.json +1 -1
- claude_mpm/agents/templates/agentic_coder_optimizer.json +1 -1
- claude_mpm/agents/templates/api_qa.json +1 -1
- claude_mpm/agents/templates/code_analyzer.json +1 -1
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +2 -2
- claude_mpm/agents/templates/gcp_ops_agent.json +14 -9
- claude_mpm/agents/templates/imagemagick.json +1 -1
- claude_mpm/agents/templates/memory_manager.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/project_organizer.json +1 -1
- claude_mpm/agents/templates/qa.json +2 -2
- claude_mpm/agents/templates/refactoring_engineer.json +1 -1
- claude_mpm/agents/templates/research.json +3 -3
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/test-non-mpm.json +20 -0
- claude_mpm/agents/templates/ticketing.json +1 -1
- claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +3 -8
- claude_mpm/agents/templates/web_ui.json +1 -1
- claude_mpm/cli/commands/agents.py +3 -0
- claude_mpm/cli/commands/dashboard.py +3 -3
- claude_mpm/cli/commands/monitor.py +227 -64
- claude_mpm/core/config.py +25 -0
- claude_mpm/core/unified_agent_registry.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/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/js/components/code-simple.js +507 -15
- claude_mpm/dashboard/static/js/components/code-tree.js +2044 -124
- claude_mpm/dashboard/static/js/socket-client.js +5 -2
- claude_mpm/dashboard/templates/code_simple.html +79 -0
- claude_mpm/dashboard/templates/index.html +42 -41
- claude_mpm/services/agents/deployment/agent_deployment.py +4 -1
- claude_mpm/services/agents/deployment/agent_discovery_service.py +101 -2
- claude_mpm/services/agents/deployment/agent_format_converter.py +53 -9
- claude_mpm/services/agents/deployment/agent_template_builder.py +355 -25
- claude_mpm/services/agents/deployment/agent_validator.py +11 -6
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +83 -15
- claude_mpm/services/agents/deployment/validation/template_validator.py +51 -40
- claude_mpm/services/cli/agent_listing_service.py +2 -2
- claude_mpm/services/dashboard/stable_server.py +389 -0
- claude_mpm/services/socketio/client_proxy.py +16 -0
- claude_mpm/services/socketio/dashboard_server.py +360 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +27 -5
- claude_mpm/services/socketio/monitor_client.py +366 -0
- claude_mpm/services/socketio/monitor_server.py +505 -0
- claude_mpm/tools/code_tree_analyzer.py +95 -17
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/RECORD +57 -52
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/top_level.txt +0 -0
|
@@ -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,
|
|
@@ -440,15 +448,43 @@ class CodeTree {
|
|
|
440
448
|
*/
|
|
441
449
|
subscribeToEvents() {
|
|
442
450
|
if (!this.socket) {
|
|
443
|
-
if
|
|
451
|
+
// CRITICAL FIX: Create our own socket connection if no shared socket exists
|
|
452
|
+
// This ensures the tree view has a working WebSocket connection
|
|
453
|
+
if (window.socket && window.socket.connected) {
|
|
454
|
+
console.log('[CodeTree] Using existing global socket');
|
|
444
455
|
this.socket = window.socket;
|
|
445
456
|
this.setupEventHandlers();
|
|
446
|
-
} else if (window.dashboard?.socketClient?.socket) {
|
|
457
|
+
} else if (window.dashboard?.socketClient?.socket && window.dashboard.socketClient.socket.connected) {
|
|
458
|
+
console.log('[CodeTree] Using dashboard socket');
|
|
447
459
|
this.socket = window.dashboard.socketClient.socket;
|
|
448
460
|
this.setupEventHandlers();
|
|
449
|
-
} else if (window.socketClient?.socket) {
|
|
461
|
+
} else if (window.socketClient?.socket && window.socketClient.socket.connected) {
|
|
462
|
+
console.log('[CodeTree] Using socketClient socket');
|
|
450
463
|
this.socket = window.socketClient.socket;
|
|
451
464
|
this.setupEventHandlers();
|
|
465
|
+
} else if (window.io) {
|
|
466
|
+
// Create our own socket connection like the simple view does
|
|
467
|
+
console.log('[CodeTree] Creating new socket connection');
|
|
468
|
+
try {
|
|
469
|
+
this.socket = io('/');
|
|
470
|
+
|
|
471
|
+
this.socket.on('connect', () => {
|
|
472
|
+
console.log('[CodeTree] Socket connected successfully');
|
|
473
|
+
this.setupEventHandlers();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
this.socket.on('disconnect', () => {
|
|
477
|
+
console.log('[CodeTree] Socket disconnected');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
this.socket.on('connect_error', (error) => {
|
|
481
|
+
console.error('[CodeTree] Socket connection error:', error);
|
|
482
|
+
});
|
|
483
|
+
} catch (error) {
|
|
484
|
+
console.error('[CodeTree] Failed to create socket connection:', error);
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
console.error('[CodeTree] Socket.IO not available - cannot subscribe to events');
|
|
452
488
|
}
|
|
453
489
|
}
|
|
454
490
|
}
|
|
@@ -503,7 +539,7 @@ class CodeTree {
|
|
|
503
539
|
const dirName = workingDir.split('/').pop() || 'Project Root';
|
|
504
540
|
this.treeData = {
|
|
505
541
|
name: dirName,
|
|
506
|
-
path:
|
|
542
|
+
path: workingDir, // Use absolute path for consistency with API expectations
|
|
507
543
|
type: 'root',
|
|
508
544
|
children: [],
|
|
509
545
|
loaded: false,
|
|
@@ -521,10 +557,7 @@ class CodeTree {
|
|
|
521
557
|
this.updateBreadcrumb(`Discovering structure in ${dirName}...`, 'info');
|
|
522
558
|
|
|
523
559
|
// Get selected languages from checkboxes
|
|
524
|
-
const selectedLanguages =
|
|
525
|
-
document.querySelectorAll('.language-checkbox:checked').forEach(cb => {
|
|
526
|
-
selectedLanguages.push(cb.value);
|
|
527
|
-
});
|
|
560
|
+
const selectedLanguages = this.getSelectedLanguages();
|
|
528
561
|
|
|
529
562
|
// Get ignore patterns
|
|
530
563
|
const ignorePatterns = document.getElementById('ignore-patterns')?.value || '';
|
|
@@ -615,7 +648,37 @@ class CodeTree {
|
|
|
615
648
|
.attr('title', 'Toggle between radial and linear layouts')
|
|
616
649
|
.text('◎')
|
|
617
650
|
.on('click', () => this.toggleLayout());
|
|
618
|
-
|
|
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
|
+
|
|
619
682
|
// Path Search
|
|
620
683
|
const searchInput = toolbar.append('input')
|
|
621
684
|
.attr('class', 'tree-control-btn')
|
|
@@ -920,11 +983,44 @@ class CodeTree {
|
|
|
920
983
|
this.socket.on('code:top_level:discovered', (data) => this.onTopLevelDiscovered(data));
|
|
921
984
|
this.socket.on('code:directory:discovered', (data) => this.onDirectoryDiscovered(data));
|
|
922
985
|
this.socket.on('code:file:discovered', (data) => this.onFileDiscovered(data));
|
|
923
|
-
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
|
+
});
|
|
924
990
|
this.socket.on('code:node:found', (data) => this.onNodeFound(data));
|
|
925
991
|
|
|
926
992
|
// Progress updates
|
|
927
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
|
+
});
|
|
928
1024
|
|
|
929
1025
|
// Lazy loading responses
|
|
930
1026
|
this.socket.on('code:directory:contents', (data) => {
|
|
@@ -993,13 +1089,16 @@ class CodeTree {
|
|
|
993
1089
|
// Find the D3 node again after hierarchy recreation
|
|
994
1090
|
const updatedD3Node = this.findD3NodeByPath(searchPath);
|
|
995
1091
|
if (updatedD3Node) {
|
|
996
|
-
//
|
|
997
|
-
updatedD3Node.children
|
|
998
|
-
|
|
999
|
-
|
|
1092
|
+
// D3.hierarchy already creates the children - just ensure visible
|
|
1093
|
+
if (updatedD3Node.children && updatedD3Node.children.length > 0) {
|
|
1094
|
+
updatedD3Node._children = null;
|
|
1095
|
+
updatedD3Node.data.expanded = true;
|
|
1096
|
+
console.log('✅ [D3 UPDATE] Node expanded after loading:', searchPath);
|
|
1097
|
+
}
|
|
1000
1098
|
}
|
|
1001
1099
|
|
|
1002
|
-
|
|
1100
|
+
// Update with the specific node for smooth animation
|
|
1101
|
+
this.update(updatedD3Node || this.root);
|
|
1003
1102
|
}
|
|
1004
1103
|
|
|
1005
1104
|
// Update stats based on discovered contents
|
|
@@ -1105,10 +1204,11 @@ class CodeTree {
|
|
|
1105
1204
|
// Add to events display
|
|
1106
1205
|
this.addEventToDisplay(`📁 Found ${(data.items || []).length} top-level items in project root`, 'info');
|
|
1107
1206
|
|
|
1108
|
-
// The root node
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
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 ? {
|
|
1112
1212
|
name: rootNode.name,
|
|
1113
1213
|
path: rootNode.path,
|
|
1114
1214
|
currentChildren: rootNode.children ? rootNode.children.length : 0
|
|
@@ -1119,16 +1219,18 @@ class CodeTree {
|
|
|
1119
1219
|
|
|
1120
1220
|
// Update the root node with discovered children
|
|
1121
1221
|
rootNode.children = data.items.map(child => {
|
|
1122
|
-
//
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
+
|
|
1125
1227
|
console.log(` Adding child: ${child.name} with path: ${childPath}`);
|
|
1126
|
-
|
|
1228
|
+
|
|
1127
1229
|
return {
|
|
1128
1230
|
name: child.name,
|
|
1129
|
-
path: childPath, //
|
|
1231
|
+
path: childPath, // Use absolute path for consistency
|
|
1130
1232
|
type: child.type,
|
|
1131
|
-
loaded: child.type === 'directory' ? false : undefined,
|
|
1233
|
+
loaded: child.type === 'directory' ? false : undefined, // Explicitly false for directories
|
|
1132
1234
|
analyzed: child.type === 'file' ? false : undefined,
|
|
1133
1235
|
expanded: false,
|
|
1134
1236
|
children: child.type === 'directory' ? [] : undefined,
|
|
@@ -1142,10 +1244,30 @@ class CodeTree {
|
|
|
1142
1244
|
|
|
1143
1245
|
// Update D3 hierarchy and render
|
|
1144
1246
|
if (this.root && this.svg) {
|
|
1145
|
-
//
|
|
1146
|
-
|
|
1147
|
-
this.root.
|
|
1148
|
-
|
|
1247
|
+
// CRITICAL FIX: Preserve existing D3 node structure when possible
|
|
1248
|
+
// Instead of recreating the entire hierarchy, update the existing root
|
|
1249
|
+
if (this.root.data === this.treeData) {
|
|
1250
|
+
// Same root data object - update children in place
|
|
1251
|
+
console.log('📊 Updating existing D3 tree structure');
|
|
1252
|
+
|
|
1253
|
+
// Create D3 hierarchy nodes for the new children
|
|
1254
|
+
this.root.children = rootNode.children.map(childData => {
|
|
1255
|
+
const childNode = d3.hierarchy(childData);
|
|
1256
|
+
childNode.parent = this.root;
|
|
1257
|
+
childNode.depth = 1;
|
|
1258
|
+
return childNode;
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// Ensure root is marked as expanded
|
|
1262
|
+
this.root._children = null;
|
|
1263
|
+
this.root.data.expanded = true;
|
|
1264
|
+
} else {
|
|
1265
|
+
// Different root - need to recreate
|
|
1266
|
+
console.log('🔄 Recreating D3 tree structure');
|
|
1267
|
+
this.root = d3.hierarchy(this.treeData);
|
|
1268
|
+
this.root.x0 = this.height / 2;
|
|
1269
|
+
this.root.y0 = 0;
|
|
1270
|
+
}
|
|
1149
1271
|
|
|
1150
1272
|
// Update the tree visualization
|
|
1151
1273
|
this.update(this.root);
|
|
@@ -1297,15 +1419,34 @@ class CodeTree {
|
|
|
1297
1419
|
|
|
1298
1420
|
// Find the D3 node again after hierarchy recreation
|
|
1299
1421
|
const updatedD3Node = this.findD3NodeByPath(searchPath);
|
|
1300
|
-
if (updatedD3Node
|
|
1301
|
-
//
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1422
|
+
if (updatedD3Node) {
|
|
1423
|
+
// CRITICAL FIX: D3.hierarchy() creates nodes with children already set
|
|
1424
|
+
// We just need to ensure they're not hidden in _children
|
|
1425
|
+
// When d3.hierarchy creates the tree, it puts all children in the 'children' array
|
|
1426
|
+
|
|
1427
|
+
// If the node has children from d3.hierarchy, make sure they're visible
|
|
1428
|
+
if (updatedD3Node.children && updatedD3Node.children.length > 0) {
|
|
1429
|
+
// Children are already there from d3.hierarchy - just ensure not hidden
|
|
1430
|
+
updatedD3Node._children = null;
|
|
1431
|
+
updatedD3Node.data.expanded = true;
|
|
1432
|
+
|
|
1433
|
+
console.log('✅ [D3 UPDATE] Node expanded with children:', {
|
|
1434
|
+
path: searchPath,
|
|
1435
|
+
d3ChildrenCount: updatedD3Node.children.length,
|
|
1436
|
+
dataChildrenCount: updatedD3Node.data.children ? updatedD3Node.data.children.length : 0,
|
|
1437
|
+
childPaths: updatedD3Node.children.map(c => c.data.path)
|
|
1438
|
+
});
|
|
1439
|
+
} else if (!updatedD3Node.children && updatedD3Node.data.children && updatedD3Node.data.children.length > 0) {
|
|
1440
|
+
// This shouldn't happen if d3.hierarchy is working correctly
|
|
1441
|
+
console.error('⚠️ [D3 UPDATE] Data has children but D3 node does not!', {
|
|
1442
|
+
path: searchPath,
|
|
1443
|
+
dataChildren: updatedD3Node.data.children
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1306
1446
|
}
|
|
1307
1447
|
|
|
1308
|
-
|
|
1448
|
+
// Force update with the source node for smooth animation
|
|
1449
|
+
this.update(updatedD3Node || this.root);
|
|
1309
1450
|
}
|
|
1310
1451
|
|
|
1311
1452
|
// Provide better feedback for empty vs populated directories
|
|
@@ -1400,6 +1541,45 @@ class CodeTree {
|
|
|
1400
1541
|
* Handle file analyzed event
|
|
1401
1542
|
*/
|
|
1402
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
|
+
|
|
1403
1583
|
// Remove loading pulse if this file was being analyzed
|
|
1404
1584
|
const d3Node = this.findD3NodeByPath(data.path);
|
|
1405
1585
|
if (d3Node && this.loadingNodes.has(data.path)) {
|
|
@@ -1414,13 +1594,14 @@ class CodeTree {
|
|
|
1414
1594
|
|
|
1415
1595
|
const fileNode = this.findNodeByPath(data.path);
|
|
1416
1596
|
if (fileNode) {
|
|
1597
|
+
console.log('🔍 [FILE NODE] Found file node for:', data.path);
|
|
1417
1598
|
fileNode.analyzed = true;
|
|
1418
1599
|
fileNode.complexity = data.complexity || 0;
|
|
1419
1600
|
fileNode.lines = data.lines || 0;
|
|
1420
|
-
|
|
1601
|
+
|
|
1421
1602
|
// Add code elements as children
|
|
1422
1603
|
if (data.elements && Array.isArray(data.elements)) {
|
|
1423
|
-
|
|
1604
|
+
const children = data.elements.map(elem => ({
|
|
1424
1605
|
name: elem.name,
|
|
1425
1606
|
type: elem.type.toLowerCase(),
|
|
1426
1607
|
path: `${data.path}#${elem.name}`,
|
|
@@ -1436,8 +1617,17 @@ class CodeTree {
|
|
|
1436
1617
|
docstring: m.docstring || ''
|
|
1437
1618
|
})) : []
|
|
1438
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');
|
|
1439
1629
|
}
|
|
1440
|
-
|
|
1630
|
+
|
|
1441
1631
|
// Update stats
|
|
1442
1632
|
if (data.stats) {
|
|
1443
1633
|
this.stats.classes += data.stats.classes || 0;
|
|
@@ -1445,13 +1635,48 @@ class CodeTree {
|
|
|
1445
1635
|
this.stats.methods += data.stats.methods || 0;
|
|
1446
1636
|
this.stats.lines += data.stats.lines || 0;
|
|
1447
1637
|
}
|
|
1448
|
-
|
|
1638
|
+
|
|
1449
1639
|
this.updateStats();
|
|
1450
|
-
|
|
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) {
|
|
1451
1674
|
this.update(this.root);
|
|
1452
1675
|
}
|
|
1453
|
-
|
|
1676
|
+
|
|
1454
1677
|
this.updateBreadcrumb(`Analyzed: ${data.path}`, 'success');
|
|
1678
|
+
} else {
|
|
1679
|
+
console.error('❌ [FILE NODE] Could not find file node for path:', data.path);
|
|
1455
1680
|
}
|
|
1456
1681
|
}
|
|
1457
1682
|
|
|
@@ -1895,12 +2120,12 @@ class CodeTree {
|
|
|
1895
2120
|
* Update statistics display
|
|
1896
2121
|
*/
|
|
1897
2122
|
updateStats() {
|
|
1898
|
-
// Update stats display - use correct IDs from
|
|
2123
|
+
// Update stats display - use correct IDs from corner controls
|
|
1899
2124
|
const statsElements = {
|
|
1900
|
-
'
|
|
1901
|
-
'
|
|
1902
|
-
'
|
|
1903
|
-
'
|
|
2125
|
+
'stats-files': this.stats.files,
|
|
2126
|
+
'stats-classes': this.stats.classes,
|
|
2127
|
+
'stats-functions': this.stats.functions,
|
|
2128
|
+
'stats-methods': this.stats.methods
|
|
1904
2129
|
};
|
|
1905
2130
|
|
|
1906
2131
|
for (const [id, value] of Object.entries(statsElements)) {
|
|
@@ -1931,6 +2156,145 @@ class CodeTree {
|
|
|
1931
2156
|
}
|
|
1932
2157
|
}
|
|
1933
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
|
+
|
|
1934
2298
|
/**
|
|
1935
2299
|
* Detect language from file extension
|
|
1936
2300
|
*/
|
|
@@ -2012,6 +2376,172 @@ class CodeTree {
|
|
|
2012
2376
|
return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)];
|
|
2013
2377
|
}
|
|
2014
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
|
+
|
|
2015
2545
|
/**
|
|
2016
2546
|
* Update D3 tree visualization
|
|
2017
2547
|
*/
|
|
@@ -2025,6 +2555,9 @@ class CodeTree {
|
|
|
2025
2555
|
const nodes = treeData.descendants();
|
|
2026
2556
|
const links = treeData.descendants().slice(1);
|
|
2027
2557
|
|
|
2558
|
+
// Apply horizontal layout for singleton chains
|
|
2559
|
+
this.applySingletonHorizontalLayout(nodes);
|
|
2560
|
+
|
|
2028
2561
|
if (this.isRadialLayout) {
|
|
2029
2562
|
// Radial layout adjustments
|
|
2030
2563
|
nodes.forEach(d => {
|
|
@@ -2103,20 +2636,42 @@ class CodeTree {
|
|
|
2103
2636
|
|
|
2104
2637
|
// Add labels for nodes with smart positioning
|
|
2105
2638
|
nodeEnter.append('text')
|
|
2106
|
-
.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
|
+
})
|
|
2107
2648
|
.attr('dy', '.35em')
|
|
2108
2649
|
.attr('x', d => {
|
|
2109
2650
|
if (this.isRadialLayout) {
|
|
2110
2651
|
// For radial layout, initial position
|
|
2111
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;
|
|
2112
2659
|
} else {
|
|
2113
2660
|
// Linear layout: standard positioning
|
|
2661
|
+
console.log(`📝 [TEXT] Positioning vertical text for: ${d.data.name} (depth: ${d.depth}, path: ${d.data.path})`);
|
|
2114
2662
|
return d.children || d._children ? -13 : 13;
|
|
2115
2663
|
}
|
|
2116
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
|
+
})
|
|
2117
2669
|
.attr('text-anchor', d => {
|
|
2118
2670
|
if (this.isRadialLayout) {
|
|
2119
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';
|
|
2120
2675
|
} else {
|
|
2121
2676
|
// Linear layout: standard anchoring
|
|
2122
2677
|
return d.children || d._children ? 'end' : 'start';
|
|
@@ -2133,6 +2688,22 @@ class CodeTree {
|
|
|
2133
2688
|
.style('font-size', '12px')
|
|
2134
2689
|
.style('font-family', '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif')
|
|
2135
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
|
+
})
|
|
2136
2707
|
.on('click', (event, d) => this.onNodeClick(event, d)) // CRITICAL FIX: Add click handler to labels
|
|
2137
2708
|
.style('cursor', 'pointer');
|
|
2138
2709
|
|
|
@@ -2171,6 +2742,10 @@ class CodeTree {
|
|
|
2171
2742
|
// CRITICAL FIX: Ensure ALL nodes (new and existing) have click handlers
|
|
2172
2743
|
// This fixes the issue where subdirectory clicks stop working after tree updates
|
|
2173
2744
|
nodeUpdate.on('click', (event, d) => this.onNodeClick(event, d));
|
|
2745
|
+
|
|
2746
|
+
// ADDITIONAL FIX: Also ensure click handlers on all child elements
|
|
2747
|
+
nodeUpdate.selectAll('circle').on('click', (event, d) => this.onNodeClick(event, d));
|
|
2748
|
+
nodeUpdate.selectAll('text').on('click', (event, d) => this.onNodeClick(event, d));
|
|
2174
2749
|
|
|
2175
2750
|
nodeUpdate.transition()
|
|
2176
2751
|
.duration(this.duration)
|
|
@@ -2227,6 +2802,7 @@ class CodeTree {
|
|
|
2227
2802
|
|
|
2228
2803
|
// Update text labels with proper rotation for radial layout
|
|
2229
2804
|
const isRadial = this.isRadialLayout; // Capture the layout type
|
|
2805
|
+
const horizontalNodes = this.horizontalNodes; // Capture horizontal nodes set
|
|
2230
2806
|
nodeUpdate.select('text.node-label')
|
|
2231
2807
|
.style('fill-opacity', 1)
|
|
2232
2808
|
.style('fill', '#333')
|
|
@@ -2257,12 +2833,26 @@ class CodeTree {
|
|
|
2257
2833
|
.attr('dy', '.35em');
|
|
2258
2834
|
}
|
|
2259
2835
|
} else {
|
|
2260
|
-
// Linear layout -
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
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
|
+
}
|
|
2266
2856
|
}
|
|
2267
2857
|
});
|
|
2268
2858
|
|
|
@@ -2330,6 +2920,14 @@ class CodeTree {
|
|
|
2330
2920
|
d.x0 = d.x;
|
|
2331
2921
|
d.y0 = d.y;
|
|
2332
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
|
+
}
|
|
2333
2931
|
}
|
|
2334
2932
|
|
|
2335
2933
|
/**
|
|
@@ -2448,10 +3046,11 @@ class CodeTree {
|
|
|
2448
3046
|
|
|
2449
3047
|
/**
|
|
2450
3048
|
* Remove pulsing animation when loading complete
|
|
3049
|
+
* Note: This function only handles visual animation removal.
|
|
3050
|
+
* The caller is responsible for managing the loadingNodes Set.
|
|
2451
3051
|
*/
|
|
2452
3052
|
removeLoadingPulse(d) {
|
|
2453
|
-
//
|
|
2454
|
-
this.loadingNodes.delete(d.data.path);
|
|
3053
|
+
// Note: loadingNodes.delete() is handled by the caller for explicit control
|
|
2455
3054
|
|
|
2456
3055
|
// Use consistent selection pattern
|
|
2457
3056
|
const node = this.treeGroup.selectAll('g.node')
|
|
@@ -2503,6 +3102,21 @@ class CodeTree {
|
|
|
2503
3102
|
* Handle node click - implement lazy loading with enhanced visual feedback
|
|
2504
3103
|
*/
|
|
2505
3104
|
onNodeClick(event, d) {
|
|
3105
|
+
const clickId = Date.now() + Math.random();
|
|
3106
|
+
// DEBUG: Log all clicks to verify handler is working
|
|
3107
|
+
console.log(`🖱️ [NODE CLICK] Clicked on node (ID: ${clickId}):`, {
|
|
3108
|
+
name: d?.data?.name,
|
|
3109
|
+
path: d?.data?.path,
|
|
3110
|
+
type: d?.data?.type,
|
|
3111
|
+
loaded: d?.data?.loaded,
|
|
3112
|
+
hasChildren: !!(d?.children || d?._children),
|
|
3113
|
+
dataChildren: d?.data?.children?.length || 0,
|
|
3114
|
+
loadingNodesSize: this.loadingNodes ? this.loadingNodes.size : 'undefined'
|
|
3115
|
+
});
|
|
3116
|
+
|
|
3117
|
+
// Update structured data with clicked node
|
|
3118
|
+
this.updateStructuredData(d);
|
|
3119
|
+
|
|
2506
3120
|
// Handle node click interaction
|
|
2507
3121
|
|
|
2508
3122
|
// Check event parameter
|
|
@@ -2601,11 +3215,8 @@ class CodeTree {
|
|
|
2601
3215
|
|
|
2602
3216
|
|
|
2603
3217
|
// Get selected languages from checkboxes
|
|
2604
|
-
const selectedLanguages =
|
|
2605
|
-
|
|
2606
|
-
checkboxes.forEach(cb => {
|
|
2607
|
-
selectedLanguages.push(cb.value);
|
|
2608
|
-
});
|
|
3218
|
+
const selectedLanguages = this.getSelectedLanguages();
|
|
3219
|
+
console.log('🔍 [LANGUAGE] Selected languages:', selectedLanguages);
|
|
2609
3220
|
|
|
2610
3221
|
// Get ignore patterns
|
|
2611
3222
|
const ignorePatternsElement = document.getElementById('ignore-patterns');
|
|
@@ -2616,13 +3227,54 @@ class CodeTree {
|
|
|
2616
3227
|
// Add a small delay to ensure visual effects are rendered first
|
|
2617
3228
|
|
|
2618
3229
|
// For directories that haven't been loaded yet, request discovery
|
|
3230
|
+
console.log('🔍 [LOAD CHECK]', {
|
|
3231
|
+
type: d.data.type,
|
|
3232
|
+
loaded: d.data.loaded,
|
|
3233
|
+
loadedType: typeof d.data.loaded,
|
|
3234
|
+
isDirectory: d.data.type === 'directory',
|
|
3235
|
+
notLoaded: !d.data.loaded,
|
|
3236
|
+
shouldLoad: d.data.type === 'directory' && !d.data.loaded
|
|
3237
|
+
});
|
|
2619
3238
|
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
this.
|
|
2623
|
-
|
|
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');
|
|
2624
3274
|
}
|
|
2625
|
-
|
|
3275
|
+
|
|
3276
|
+
console.log('✅ [SUBDIRECTORY LOADING] No duplicate request, proceeding to mark as loading');
|
|
3277
|
+
|
|
2626
3278
|
// Mark as loading immediately to prevent duplicate requests
|
|
2627
3279
|
d.data.loaded = 'loading';
|
|
2628
3280
|
this.loadingNodes.add(d.data.path);
|
|
@@ -2646,30 +3298,134 @@ class CodeTree {
|
|
|
2646
3298
|
const clickedD3Node = d;
|
|
2647
3299
|
|
|
2648
3300
|
// Delay the socket request to ensure visual effects are rendered
|
|
3301
|
+
// Use arrow function to preserve 'this' context
|
|
2649
3302
|
setTimeout(() => {
|
|
2650
3303
|
|
|
2651
|
-
//
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
3304
|
+
// CRITICAL FIX: Use REST API instead of WebSocket for reliability
|
|
3305
|
+
// The simple view works because it uses REST API, so let's do the same
|
|
3306
|
+
console.log('📡 [SUBDIRECTORY LOADING] Using REST API for directory:', {
|
|
3307
|
+
originalPath: d.data.path,
|
|
3308
|
+
fullPath: fullPath,
|
|
3309
|
+
apiUrl: `${window.location.origin}/api/directory/list?path=${encodeURIComponent(fullPath)}`,
|
|
3310
|
+
loadingNodesSize: this.loadingNodes.size,
|
|
3311
|
+
loadingNodesContent: Array.from(this.loadingNodes)
|
|
3312
|
+
});
|
|
3313
|
+
|
|
3314
|
+
const apiUrl = `${window.location.origin}/api/directory/list?path=${encodeURIComponent(fullPath)}`;
|
|
3315
|
+
|
|
3316
|
+
fetch(apiUrl)
|
|
3317
|
+
.then(response => {
|
|
3318
|
+
if (!response.ok) {
|
|
3319
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
3320
|
+
}
|
|
3321
|
+
return response.json();
|
|
3322
|
+
})
|
|
3323
|
+
.then(data => {
|
|
3324
|
+
console.log('✅ [SUBDIRECTORY LOADING] REST API response:', {
|
|
3325
|
+
data: data,
|
|
3326
|
+
pathToDelete: d.data.path,
|
|
3327
|
+
loadingNodesBefore: Array.from(this.loadingNodes)
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
// Remove from loading set
|
|
3331
|
+
const deleted = this.loadingNodes.delete(d.data.path);
|
|
3332
|
+
d.data.loaded = true;
|
|
3333
|
+
|
|
3334
|
+
console.log('🧹 [SUBDIRECTORY LOADING] Cleanup result:', {
|
|
3335
|
+
pathDeleted: d.data.path,
|
|
3336
|
+
wasDeleted: deleted,
|
|
3337
|
+
loadingNodesAfter: Array.from(this.loadingNodes)
|
|
3338
|
+
});
|
|
3339
|
+
|
|
3340
|
+
// Remove loading animation
|
|
3341
|
+
const d3Node = this.findD3NodeByPath(d.data.path);
|
|
3342
|
+
if (d3Node) {
|
|
3343
|
+
this.removeLoadingPulse(d3Node);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// Process the directory contents
|
|
3347
|
+
if (data.exists && data.is_directory && data.contents) {
|
|
3348
|
+
const node = this.findNodeByPath(d.data.path);
|
|
3349
|
+
if (node) {
|
|
3350
|
+
console.log('🔧 [SUBDIRECTORY LOADING] Creating children with paths:',
|
|
3351
|
+
data.contents.map(item => ({ name: item.name, path: item.path })));
|
|
3352
|
+
|
|
3353
|
+
// Add children to the node
|
|
3354
|
+
node.children = data.contents.map(item => ({
|
|
3355
|
+
name: item.name,
|
|
3356
|
+
path: item.path, // Use the full path from API response
|
|
3357
|
+
type: item.is_directory ? 'directory' : 'file',
|
|
3358
|
+
loaded: item.is_directory ? false : undefined,
|
|
3359
|
+
analyzed: !item.is_directory ? false : undefined,
|
|
3360
|
+
expanded: false,
|
|
3361
|
+
children: item.is_directory ? [] : undefined
|
|
3362
|
+
}));
|
|
3363
|
+
node.loaded = true;
|
|
3364
|
+
node.expanded = true;
|
|
3365
|
+
|
|
3366
|
+
// Update D3 hierarchy
|
|
3367
|
+
if (this.root && this.svg) {
|
|
3368
|
+
const oldRoot = this.root;
|
|
3369
|
+
this.root = d3.hierarchy(this.treeData);
|
|
3370
|
+
this.root.x0 = this.height / 2;
|
|
3371
|
+
this.root.y0 = 0;
|
|
3372
|
+
|
|
3373
|
+
this.preserveExpansionState(oldRoot, this.root);
|
|
3374
|
+
|
|
3375
|
+
const updatedD3Node = this.findD3NodeByPath(d.data.path);
|
|
3376
|
+
if (updatedD3Node && updatedD3Node.children && updatedD3Node.children.length > 0) {
|
|
3377
|
+
updatedD3Node._children = null;
|
|
3378
|
+
updatedD3Node.data.expanded = true;
|
|
3379
|
+
}
|
|
3380
|
+
|
|
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
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
this.updateBreadcrumb(`Loaded ${data.contents.length} items`, 'success');
|
|
3392
|
+
this.showNotification(`Loaded ${data.contents.length} items from ${d.data.name}`, 'success');
|
|
3393
|
+
}
|
|
3394
|
+
} else {
|
|
3395
|
+
this.showNotification(`Directory ${d.data.name} is empty or inaccessible`, 'warning');
|
|
3396
|
+
}
|
|
3397
|
+
})
|
|
3398
|
+
.catch(error => {
|
|
3399
|
+
console.error('❌ [SUBDIRECTORY LOADING] REST API error:', {
|
|
3400
|
+
error: error.message,
|
|
3401
|
+
stack: error.stack,
|
|
3402
|
+
pathToDelete: d.data.path,
|
|
3403
|
+
loadingNodesBefore: Array.from(this.loadingNodes)
|
|
3404
|
+
});
|
|
3405
|
+
|
|
3406
|
+
// Clean up loading state
|
|
3407
|
+
const deleted = this.loadingNodes.delete(d.data.path);
|
|
3408
|
+
d.data.loaded = false;
|
|
3409
|
+
|
|
3410
|
+
console.log('🧹 [SUBDIRECTORY LOADING] Error cleanup:', {
|
|
3411
|
+
pathDeleted: d.data.path,
|
|
3412
|
+
wasDeleted: deleted,
|
|
3413
|
+
loadingNodesAfter: Array.from(this.loadingNodes)
|
|
3414
|
+
});
|
|
3415
|
+
|
|
3416
|
+
const d3Node = this.findD3NodeByPath(d.data.path);
|
|
3417
|
+
if (d3Node) {
|
|
3418
|
+
this.removeLoadingPulse(d3Node);
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
this.showNotification(`Failed to load ${d.data.name}: ${error.message}`, 'error');
|
|
2668
3422
|
});
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
3423
|
+
|
|
3424
|
+
this.updateBreadcrumb(`Loading ${d.data.name}...`, 'info');
|
|
3425
|
+
this.showNotification(`Loading directory: ${d.data.name}`, 'info');
|
|
3426
|
+
|
|
3427
|
+
// Keep the original else clause for when fetch isn't available
|
|
3428
|
+
if (!window.fetch) {
|
|
2673
3429
|
console.error('❌ [SUBDIRECTORY LOADING] No WebSocket connection available!');
|
|
2674
3430
|
this.showNotification(`Cannot load directory: No connection`, 'error');
|
|
2675
3431
|
|
|
@@ -2683,12 +3439,37 @@ class CodeTree {
|
|
|
2683
3439
|
d.data.loaded = false;
|
|
2684
3440
|
}
|
|
2685
3441
|
}, 100); // 100ms delay to ensure visual effects render first
|
|
2686
|
-
|
|
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
|
+
}
|
|
2687
3453
|
// For files that haven't been analyzed, request analysis
|
|
2688
3454
|
else if (d.data.type === 'file' && !d.data.analyzed) {
|
|
2689
3455
|
// Only analyze files of selected languages
|
|
2690
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
|
+
|
|
2691
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
|
+
});
|
|
2692
3473
|
this.showNotification(`Skipping ${d.data.name} - ${fileLanguage} not selected`, 'warning');
|
|
2693
3474
|
return;
|
|
2694
3475
|
}
|
|
@@ -2704,14 +3485,43 @@ class CodeTree {
|
|
|
2704
3485
|
|
|
2705
3486
|
// Delay the socket request to ensure visual effects are rendered
|
|
2706
3487
|
setTimeout(() => {
|
|
2707
|
-
|
|
2708
|
-
|
|
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
|
+
|
|
2709
3504
|
this.socket.emit('code:analyze:file', {
|
|
2710
3505
|
path: fullPath
|
|
2711
3506
|
});
|
|
2712
|
-
|
|
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');
|
|
2713
3522
|
this.updateBreadcrumb(`Analyzing ${d.data.name}...`, 'info');
|
|
2714
3523
|
this.showNotification(`Analyzing: ${d.data.name}`, 'info');
|
|
3524
|
+
this.analyzeFileHTTP(fullPath, d.data.name, d3.select(event.target.closest('g')));
|
|
2715
3525
|
}
|
|
2716
3526
|
}, 100); // 100ms delay to ensure visual effects render first
|
|
2717
3527
|
}
|
|
@@ -3027,15 +3837,295 @@ class CodeTree {
|
|
|
3027
3837
|
this.showNotification('All nodes collapsed', 'info');
|
|
3028
3838
|
}
|
|
3029
3839
|
|
|
3840
|
+
/**
|
|
3841
|
+
* Focus on a specific directory, hiding parent directories and showing only its contents
|
|
3842
|
+
*/
|
|
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
|
+
|
|
3030
3934
|
/**
|
|
3031
3935
|
* Reset zoom to fit the tree
|
|
3032
3936
|
*/
|
|
3033
3937
|
resetZoom() {
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
this.
|
|
3038
|
-
|
|
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
|
+
}
|
|
3039
4129
|
}
|
|
3040
4130
|
|
|
3041
4131
|
/**
|
|
@@ -3197,6 +4287,35 @@ class CodeTree {
|
|
|
3197
4287
|
}
|
|
3198
4288
|
}
|
|
3199
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
|
+
|
|
3200
4319
|
/**
|
|
3201
4320
|
* Export tree data
|
|
3202
4321
|
*/
|
|
@@ -3241,7 +4360,7 @@ class CodeTree {
|
|
|
3241
4360
|
if (ticker) {
|
|
3242
4361
|
ticker.textContent = message;
|
|
3243
4362
|
ticker.className = `ticker ticker-${type}`;
|
|
3244
|
-
|
|
4363
|
+
|
|
3245
4364
|
// Auto-hide after 5 seconds for non-error messages
|
|
3246
4365
|
if (type !== 'error') {
|
|
3247
4366
|
setTimeout(() => {
|
|
@@ -3254,6 +4373,779 @@ class CodeTree {
|
|
|
3254
4373
|
}
|
|
3255
4374
|
}
|
|
3256
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
|
+
// For now, we'll use a placeholder since we don't have direct file access
|
|
4688
|
+
// In a real implementation, this would make an API call to read the file
|
|
4689
|
+
console.log('📖 [SOURCE READER] Would read file:', filePath);
|
|
4690
|
+
|
|
4691
|
+
// Return placeholder content for demonstration
|
|
4692
|
+
return this.generatePlaceholderSource(filePath);
|
|
4693
|
+
} catch (error) {
|
|
4694
|
+
console.error('Failed to read source file:', error);
|
|
4695
|
+
return null;
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
|
|
4699
|
+
/**
|
|
4700
|
+
* Generate placeholder source content for demonstration
|
|
4701
|
+
*/
|
|
4702
|
+
generatePlaceholderSource(filePath) {
|
|
4703
|
+
const fileName = filePath.split('/').pop();
|
|
4704
|
+
|
|
4705
|
+
if (fileName.endsWith('.py')) {
|
|
4706
|
+
return `"""
|
|
4707
|
+
${fileName}
|
|
4708
|
+
Generated placeholder content for demonstration
|
|
4709
|
+
"""
|
|
4710
|
+
|
|
4711
|
+
import os
|
|
4712
|
+
import sys
|
|
4713
|
+
from typing import List, Dict, Optional
|
|
4714
|
+
|
|
4715
|
+
class ExampleClass:
|
|
4716
|
+
"""Example class with methods."""
|
|
4717
|
+
|
|
4718
|
+
def __init__(self, name: str):
|
|
4719
|
+
"""Initialize the example class."""
|
|
4720
|
+
self.name = name
|
|
4721
|
+
self.data = {}
|
|
4722
|
+
|
|
4723
|
+
def process_data(self, items: List[str]) -> Dict[str, int]:
|
|
4724
|
+
"""Process a list of items and return counts."""
|
|
4725
|
+
result = {}
|
|
4726
|
+
for item in items:
|
|
4727
|
+
result[item] = result.get(item, 0) + 1
|
|
4728
|
+
return result
|
|
4729
|
+
|
|
4730
|
+
def get_summary(self) -> str:
|
|
4731
|
+
"""Get a summary of the processed data."""
|
|
4732
|
+
if not self.data:
|
|
4733
|
+
return "No data processed"
|
|
4734
|
+
return f"Processed {len(self.data)} items"
|
|
4735
|
+
|
|
4736
|
+
def main():
|
|
4737
|
+
"""Main function."""
|
|
4738
|
+
example = ExampleClass("demo")
|
|
4739
|
+
items = ["a", "b", "a", "c", "b", "a"]
|
|
4740
|
+
result = example.process_data(items)
|
|
4741
|
+
print(example.get_summary())
|
|
4742
|
+
return result
|
|
4743
|
+
|
|
4744
|
+
if __name__ == "__main__":
|
|
4745
|
+
main()
|
|
4746
|
+
`;
|
|
4747
|
+
} else {
|
|
4748
|
+
return `// ${fileName}
|
|
4749
|
+
// Generated placeholder content for demonstration
|
|
4750
|
+
|
|
4751
|
+
class ExampleClass {
|
|
4752
|
+
constructor(name) {
|
|
4753
|
+
this.name = name;
|
|
4754
|
+
this.data = {};
|
|
4755
|
+
}
|
|
4756
|
+
|
|
4757
|
+
processData(items) {
|
|
4758
|
+
const result = {};
|
|
4759
|
+
for (const item of items) {
|
|
4760
|
+
result[item] = (result[item] || 0) + 1;
|
|
4761
|
+
}
|
|
4762
|
+
return result;
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4765
|
+
getSummary() {
|
|
4766
|
+
if (Object.keys(this.data).length === 0) {
|
|
4767
|
+
return "No data processed";
|
|
4768
|
+
}
|
|
4769
|
+
return \`Processed \${Object.keys(this.data).length} items\`;
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
function main() {
|
|
4774
|
+
const example = new ExampleClass("demo");
|
|
4775
|
+
const items = ["a", "b", "a", "c", "b", "a"];
|
|
4776
|
+
const result = example.processData(items);
|
|
4777
|
+
console.log(example.getSummary());
|
|
4778
|
+
return result;
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
main();
|
|
4782
|
+
`;
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
/**
|
|
4787
|
+
* Render source code with AST integration and collapsible sections
|
|
4788
|
+
*/
|
|
4789
|
+
renderSourceWithAST(sourceContent, astElements, container, node) {
|
|
4790
|
+
const lines = sourceContent.split('\n');
|
|
4791
|
+
const astMap = this.createASTLineMap(astElements);
|
|
4792
|
+
|
|
4793
|
+
console.log('🎨 [SOURCE RENDERER] Rendering source with AST:', {
|
|
4794
|
+
lines: lines.length,
|
|
4795
|
+
astElements: astElements.length,
|
|
4796
|
+
astMap: Object.keys(astMap).length
|
|
4797
|
+
});
|
|
4798
|
+
|
|
4799
|
+
// Create line elements with AST integration
|
|
4800
|
+
lines.forEach((line, index) => {
|
|
4801
|
+
const lineNumber = index + 1;
|
|
4802
|
+
const lineElement = this.createSourceLine(line, lineNumber, astMap[lineNumber], node);
|
|
4803
|
+
container.appendChild(lineElement);
|
|
4804
|
+
});
|
|
4805
|
+
|
|
4806
|
+
// Store reference for expand/collapse operations
|
|
4807
|
+
this.currentSourceContainer = container;
|
|
4808
|
+
this.currentASTElements = astElements;
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
/**
|
|
4812
|
+
* Create AST line mapping for quick lookup
|
|
4813
|
+
*/
|
|
4814
|
+
createASTLineMap(astElements) {
|
|
4815
|
+
const lineMap = {};
|
|
4816
|
+
|
|
4817
|
+
astElements.forEach(element => {
|
|
4818
|
+
if (element.line) {
|
|
4819
|
+
if (!lineMap[element.line]) {
|
|
4820
|
+
lineMap[element.line] = [];
|
|
4821
|
+
}
|
|
4822
|
+
lineMap[element.line].push(element);
|
|
4823
|
+
}
|
|
4824
|
+
});
|
|
4825
|
+
|
|
4826
|
+
return lineMap;
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
/**
|
|
4830
|
+
* Create a source line element with AST integration
|
|
4831
|
+
*/
|
|
4832
|
+
createSourceLine(content, lineNumber, astElements, node) {
|
|
4833
|
+
const lineDiv = document.createElement('div');
|
|
4834
|
+
lineDiv.className = 'source-line';
|
|
4835
|
+
lineDiv.dataset.lineNumber = lineNumber;
|
|
4836
|
+
|
|
4837
|
+
// Check if this line has AST elements
|
|
4838
|
+
const hasAST = astElements && astElements.length > 0;
|
|
4839
|
+
if (hasAST) {
|
|
4840
|
+
lineDiv.classList.add('ast-element');
|
|
4841
|
+
lineDiv.dataset.astElements = JSON.stringify(astElements);
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4844
|
+
// Determine if this line should be collapsible
|
|
4845
|
+
const isCollapsible = this.isCollapsibleLine(content, astElements);
|
|
4846
|
+
if (isCollapsible) {
|
|
4847
|
+
lineDiv.classList.add('collapsible');
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4850
|
+
// Create line number
|
|
4851
|
+
const lineNumberSpan = document.createElement('span');
|
|
4852
|
+
lineNumberSpan.className = 'line-number';
|
|
4853
|
+
lineNumberSpan.textContent = lineNumber;
|
|
4854
|
+
|
|
4855
|
+
// Create collapse indicator
|
|
4856
|
+
const collapseIndicator = document.createElement('span');
|
|
4857
|
+
collapseIndicator.className = 'collapse-indicator';
|
|
4858
|
+
if (isCollapsible) {
|
|
4859
|
+
collapseIndicator.classList.add('expanded');
|
|
4860
|
+
collapseIndicator.addEventListener('click', (e) => {
|
|
4861
|
+
e.stopPropagation();
|
|
4862
|
+
this.toggleSourceSection(lineDiv);
|
|
4863
|
+
});
|
|
4864
|
+
} else {
|
|
4865
|
+
collapseIndicator.classList.add('none');
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
// Create line content with syntax highlighting
|
|
4869
|
+
const lineContentSpan = document.createElement('span');
|
|
4870
|
+
lineContentSpan.className = 'line-content';
|
|
4871
|
+
lineContentSpan.innerHTML = this.applySyntaxHighlighting(content);
|
|
4872
|
+
|
|
4873
|
+
// Add click handler for AST integration
|
|
4874
|
+
if (hasAST) {
|
|
4875
|
+
lineDiv.addEventListener('click', () => {
|
|
4876
|
+
this.onSourceLineClick(lineDiv, astElements, node);
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
|
|
4880
|
+
lineDiv.appendChild(lineNumberSpan);
|
|
4881
|
+
lineDiv.appendChild(collapseIndicator);
|
|
4882
|
+
lineDiv.appendChild(lineContentSpan);
|
|
4883
|
+
|
|
4884
|
+
return lineDiv;
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
/**
|
|
4888
|
+
* Check if a line should be collapsible (function/class definitions)
|
|
4889
|
+
*/
|
|
4890
|
+
isCollapsibleLine(content, astElements) {
|
|
4891
|
+
const trimmed = content.trim();
|
|
4892
|
+
|
|
4893
|
+
// Python patterns
|
|
4894
|
+
if (trimmed.startsWith('def ') || trimmed.startsWith('class ') ||
|
|
4895
|
+
trimmed.startsWith('async def ')) {
|
|
4896
|
+
return true;
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
// JavaScript patterns
|
|
4900
|
+
if (trimmed.includes('function ') || trimmed.includes('class ') ||
|
|
4901
|
+
trimmed.includes('=> {') || trimmed.match(/^\s*\w+\s*\([^)]*\)\s*{/)) {
|
|
4902
|
+
return true;
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
// Check AST elements for function/class definitions
|
|
4906
|
+
if (astElements) {
|
|
4907
|
+
return astElements.some(el =>
|
|
4908
|
+
el.type === 'function' || el.type === 'class' ||
|
|
4909
|
+
el.type === 'method' || el.type === 'FunctionDef' ||
|
|
4910
|
+
el.type === 'ClassDef'
|
|
4911
|
+
);
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
return false;
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
/**
|
|
4918
|
+
* Apply basic syntax highlighting
|
|
4919
|
+
*/
|
|
4920
|
+
applySyntaxHighlighting(content) {
|
|
4921
|
+
// First, properly escape HTML entities
|
|
4922
|
+
let highlighted = content
|
|
4923
|
+
.replace(/&/g, '&')
|
|
4924
|
+
.replace(/</g, '<')
|
|
4925
|
+
.replace(/>/g, '>');
|
|
4926
|
+
|
|
4927
|
+
// Store markers for where we'll insert spans
|
|
4928
|
+
const replacements = [];
|
|
4929
|
+
|
|
4930
|
+
// Python and JavaScript keywords (combined)
|
|
4931
|
+
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;
|
|
4932
|
+
|
|
4933
|
+
// Find all matches first without replacing
|
|
4934
|
+
let match;
|
|
4935
|
+
|
|
4936
|
+
// Keywords
|
|
4937
|
+
while ((match = keywords.exec(highlighted)) !== null) {
|
|
4938
|
+
replacements.push({
|
|
4939
|
+
start: match.index,
|
|
4940
|
+
end: match.index + match[0].length,
|
|
4941
|
+
replacement: `<span class="keyword">${match[0]}</span>`
|
|
4942
|
+
});
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4945
|
+
// Strings - simple pattern for now
|
|
4946
|
+
const stringPattern = /(["'`])([^"'`]*?)\1/g;
|
|
4947
|
+
while ((match = stringPattern.exec(highlighted)) !== null) {
|
|
4948
|
+
replacements.push({
|
|
4949
|
+
start: match.index,
|
|
4950
|
+
end: match.index + match[0].length,
|
|
4951
|
+
replacement: `<span class="string">${match[0]}</span>`
|
|
4952
|
+
});
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
// Comments
|
|
4956
|
+
const commentPattern = /(#.*$|\/\/.*$)/gm;
|
|
4957
|
+
while ((match = commentPattern.exec(highlighted)) !== null) {
|
|
4958
|
+
replacements.push({
|
|
4959
|
+
start: match.index,
|
|
4960
|
+
end: match.index + match[0].length,
|
|
4961
|
+
replacement: `<span class="comment">${match[0]}</span>`
|
|
4962
|
+
});
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4965
|
+
// Sort replacements by start position (reverse order to not mess up indices)
|
|
4966
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
4967
|
+
|
|
4968
|
+
// Apply replacements
|
|
4969
|
+
for (const rep of replacements) {
|
|
4970
|
+
// Check for overlapping replacements and skip if needed
|
|
4971
|
+
const before = highlighted.substring(0, rep.start);
|
|
4972
|
+
const after = highlighted.substring(rep.end);
|
|
4973
|
+
|
|
4974
|
+
// Only apply if we're not inside another replacement
|
|
4975
|
+
if (!before.includes('<span') || before.lastIndexOf('</span>') > before.lastIndexOf('<span')) {
|
|
4976
|
+
highlighted = before + rep.replacement + after;
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
return highlighted;
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
/**
|
|
4984
|
+
* Toggle collapse/expand of a source section
|
|
4985
|
+
*/
|
|
4986
|
+
toggleSourceSection(lineElement) {
|
|
4987
|
+
const indicator = lineElement.querySelector('.collapse-indicator');
|
|
4988
|
+
const isExpanded = indicator.classList.contains('expanded');
|
|
4989
|
+
|
|
4990
|
+
if (isExpanded) {
|
|
4991
|
+
this.collapseSourceSection(lineElement);
|
|
4992
|
+
} else {
|
|
4993
|
+
this.expandSourceSection(lineElement);
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
|
|
4997
|
+
/**
|
|
4998
|
+
* Collapse a source section
|
|
4999
|
+
*/
|
|
5000
|
+
collapseSourceSection(lineElement) {
|
|
5001
|
+
const indicator = lineElement.querySelector('.collapse-indicator');
|
|
5002
|
+
indicator.classList.remove('expanded');
|
|
5003
|
+
indicator.classList.add('collapsed');
|
|
5004
|
+
|
|
5005
|
+
// Find and hide related lines (simple implementation)
|
|
5006
|
+
const startLine = parseInt(lineElement.dataset.lineNumber);
|
|
5007
|
+
const container = lineElement.parentElement;
|
|
5008
|
+
const lines = Array.from(container.children);
|
|
5009
|
+
|
|
5010
|
+
// Hide subsequent indented lines
|
|
5011
|
+
let currentIndex = lines.indexOf(lineElement) + 1;
|
|
5012
|
+
const baseIndent = this.getLineIndentation(lineElement.querySelector('.line-content').textContent);
|
|
5013
|
+
|
|
5014
|
+
while (currentIndex < lines.length) {
|
|
5015
|
+
const nextLine = lines[currentIndex];
|
|
5016
|
+
const nextContent = nextLine.querySelector('.line-content').textContent;
|
|
5017
|
+
const nextIndent = this.getLineIndentation(nextContent);
|
|
5018
|
+
|
|
5019
|
+
// Stop if we hit a line at the same or lower indentation level
|
|
5020
|
+
if (nextContent.trim() && nextIndent <= baseIndent) {
|
|
5021
|
+
break;
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
nextLine.classList.add('collapsed-content');
|
|
5025
|
+
currentIndex++;
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
// Add collapsed placeholder
|
|
5029
|
+
const placeholder = document.createElement('div');
|
|
5030
|
+
placeholder.className = 'source-line collapsed-placeholder';
|
|
5031
|
+
placeholder.innerHTML = `
|
|
5032
|
+
<span class="line-number"></span>
|
|
5033
|
+
<span class="collapse-indicator none"></span>
|
|
5034
|
+
<span class="line-content"> ... (collapsed)</span>
|
|
5035
|
+
`;
|
|
5036
|
+
lineElement.insertAdjacentElement('afterend', placeholder);
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
/**
|
|
5040
|
+
* Expand a source section
|
|
5041
|
+
*/
|
|
5042
|
+
expandSourceSection(lineElement) {
|
|
5043
|
+
const indicator = lineElement.querySelector('.collapse-indicator');
|
|
5044
|
+
indicator.classList.remove('collapsed');
|
|
5045
|
+
indicator.classList.add('expanded');
|
|
5046
|
+
|
|
5047
|
+
// Show hidden lines
|
|
5048
|
+
const container = lineElement.parentElement;
|
|
5049
|
+
const lines = Array.from(container.children);
|
|
5050
|
+
|
|
5051
|
+
lines.forEach(line => {
|
|
5052
|
+
if (line.classList.contains('collapsed-content')) {
|
|
5053
|
+
line.classList.remove('collapsed-content');
|
|
5054
|
+
}
|
|
5055
|
+
});
|
|
5056
|
+
|
|
5057
|
+
// Remove placeholder
|
|
5058
|
+
const placeholder = lineElement.nextElementSibling;
|
|
5059
|
+
if (placeholder && placeholder.classList.contains('collapsed-placeholder')) {
|
|
5060
|
+
placeholder.remove();
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
/**
|
|
5065
|
+
* Get indentation level of a line
|
|
5066
|
+
*/
|
|
5067
|
+
getLineIndentation(content) {
|
|
5068
|
+
const match = content.match(/^(\s*)/);
|
|
5069
|
+
return match ? match[1].length : 0;
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
/**
|
|
5073
|
+
* Handle click on source line with AST elements
|
|
5074
|
+
*/
|
|
5075
|
+
onSourceLineClick(lineElement, astElements, node) {
|
|
5076
|
+
console.log('🎯 [SOURCE LINE CLICK] Line clicked:', {
|
|
5077
|
+
line: lineElement.dataset.lineNumber,
|
|
5078
|
+
astElements: astElements.length
|
|
5079
|
+
});
|
|
5080
|
+
|
|
5081
|
+
// Highlight the clicked line
|
|
5082
|
+
this.highlightSourceLine(lineElement);
|
|
5083
|
+
|
|
5084
|
+
// Show AST details for this line
|
|
5085
|
+
if (astElements.length > 0) {
|
|
5086
|
+
this.showASTElementDetails(astElements[0], node);
|
|
5087
|
+
}
|
|
5088
|
+
|
|
5089
|
+
// If this is a collapsible line, also toggle it
|
|
5090
|
+
if (lineElement.classList.contains('collapsible')) {
|
|
5091
|
+
this.toggleSourceSection(lineElement);
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
/**
|
|
5096
|
+
* Highlight a source line
|
|
5097
|
+
*/
|
|
5098
|
+
highlightSourceLine(lineElement) {
|
|
5099
|
+
// Remove previous highlights
|
|
5100
|
+
if (this.currentSourceContainer) {
|
|
5101
|
+
const lines = this.currentSourceContainer.querySelectorAll('.source-line');
|
|
5102
|
+
lines.forEach(line => line.classList.remove('highlighted'));
|
|
5103
|
+
}
|
|
5104
|
+
|
|
5105
|
+
// Add highlight to clicked line
|
|
5106
|
+
lineElement.classList.add('highlighted');
|
|
5107
|
+
}
|
|
5108
|
+
|
|
5109
|
+
/**
|
|
5110
|
+
* Show AST element details
|
|
5111
|
+
*/
|
|
5112
|
+
showASTElementDetails(astElement, node) {
|
|
5113
|
+
// This could open a detailed view or update another panel
|
|
5114
|
+
console.log('📋 [AST DETAILS] Showing details for:', astElement);
|
|
5115
|
+
|
|
5116
|
+
// For now, just log the details
|
|
5117
|
+
// In a full implementation, this might update a details panel
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
/**
|
|
5121
|
+
* Expand all collapsible sections in source viewer
|
|
5122
|
+
*/
|
|
5123
|
+
expandAllSource() {
|
|
5124
|
+
if (!this.currentSourceContainer) return;
|
|
5125
|
+
|
|
5126
|
+
const collapsibleLines = this.currentSourceContainer.querySelectorAll('.source-line.collapsible');
|
|
5127
|
+
collapsibleLines.forEach(line => {
|
|
5128
|
+
const indicator = line.querySelector('.collapse-indicator');
|
|
5129
|
+
if (indicator.classList.contains('collapsed')) {
|
|
5130
|
+
this.expandSourceSection(line);
|
|
5131
|
+
}
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
|
|
5135
|
+
/**
|
|
5136
|
+
* Collapse all collapsible sections in source viewer
|
|
5137
|
+
*/
|
|
5138
|
+
collapseAllSource() {
|
|
5139
|
+
if (!this.currentSourceContainer) return;
|
|
5140
|
+
|
|
5141
|
+
const collapsibleLines = this.currentSourceContainer.querySelectorAll('.source-line.collapsible');
|
|
5142
|
+
collapsibleLines.forEach(line => {
|
|
5143
|
+
const indicator = line.querySelector('.collapse-indicator');
|
|
5144
|
+
if (indicator.classList.contains('expanded')) {
|
|
5145
|
+
this.collapseSourceSection(line);
|
|
5146
|
+
}
|
|
5147
|
+
});
|
|
5148
|
+
}
|
|
3257
5149
|
}
|
|
3258
5150
|
|
|
3259
5151
|
// Export for use in other modules
|
|
@@ -3264,7 +5156,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3264
5156
|
// Check if we're on a page with code tree container
|
|
3265
5157
|
if (document.getElementById('code-tree-container')) {
|
|
3266
5158
|
window.codeTree = new CodeTree();
|
|
3267
|
-
|
|
5159
|
+
|
|
5160
|
+
// Expose debug functions globally for troubleshooting
|
|
5161
|
+
window.debugCodeTree = {
|
|
5162
|
+
clearLoadingState: () => window.codeTree?.clearLoadingState(),
|
|
5163
|
+
showLoadingNodes: () => {
|
|
5164
|
+
console.log('Current loading nodes:', Array.from(window.codeTree?.loadingNodes || []));
|
|
5165
|
+
return Array.from(window.codeTree?.loadingNodes || []);
|
|
5166
|
+
},
|
|
5167
|
+
resetTree: () => {
|
|
5168
|
+
if (window.codeTree) {
|
|
5169
|
+
window.codeTree.clearLoadingState();
|
|
5170
|
+
window.codeTree.initializeTreeData();
|
|
5171
|
+
console.log('Tree reset complete');
|
|
5172
|
+
}
|
|
5173
|
+
},
|
|
5174
|
+
focusOnPath: (path) => {
|
|
5175
|
+
if (window.codeTree) {
|
|
5176
|
+
const node = window.codeTree.findD3NodeByPath(path);
|
|
5177
|
+
if (node) {
|
|
5178
|
+
window.codeTree.focusOnDirectory(node);
|
|
5179
|
+
console.log('Focused on:', path);
|
|
5180
|
+
} else {
|
|
5181
|
+
console.log('Node not found:', path);
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
},
|
|
5185
|
+
unfocus: () => window.codeTree?.unfocusDirectory()
|
|
5186
|
+
};
|
|
5187
|
+
|
|
3268
5188
|
// Listen for tab changes to initialize when code tab is selected
|
|
3269
5189
|
document.addEventListener('click', (e) => {
|
|
3270
5190
|
if (e.target.matches('[data-tab="code"]')) {
|