claude-mpm 4.1.10__py3-none-any.whl → 4.1.12__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/INSTRUCTIONS.md +8 -0
- claude_mpm/cli/__init__.py +11 -0
- claude_mpm/cli/commands/analyze.py +2 -1
- claude_mpm/cli/commands/configure.py +9 -8
- claude_mpm/cli/commands/configure_tui.py +3 -1
- claude_mpm/cli/commands/dashboard.py +288 -0
- claude_mpm/cli/commands/debug.py +0 -1
- claude_mpm/cli/commands/mpm_init.py +442 -0
- claude_mpm/cli/commands/mpm_init_handler.py +84 -0
- claude_mpm/cli/parsers/base_parser.py +15 -0
- claude_mpm/cli/parsers/dashboard_parser.py +113 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +128 -0
- claude_mpm/constants.py +10 -0
- claude_mpm/core/config.py +18 -0
- claude_mpm/core/instruction_reinforcement_hook.py +266 -0
- claude_mpm/core/pm_hook_interceptor.py +105 -8
- claude_mpm/dashboard/analysis_runner.py +52 -25
- claude_mpm/dashboard/static/built/components/activity-tree.js +1 -1
- claude_mpm/dashboard/static/built/components/code-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/code-viewer.js +2 -0
- claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/built/dashboard.js +1 -1
- claude_mpm/dashboard/static/built/socket-client.js +1 -1
- claude_mpm/dashboard/static/css/code-tree.css +330 -1
- claude_mpm/dashboard/static/dist/components/activity-tree.js +1 -1
- claude_mpm/dashboard/static/dist/components/code-tree.js +2593 -2
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/activity-tree.js +212 -13
- claude_mpm/dashboard/static/js/components/build-tracker.js +15 -13
- claude_mpm/dashboard/static/js/components/code-tree.js +2503 -917
- claude_mpm/dashboard/static/js/components/event-viewer.js +58 -19
- claude_mpm/dashboard/static/js/dashboard.js +46 -44
- claude_mpm/dashboard/static/js/socket-client.js +74 -32
- claude_mpm/dashboard/templates/index.html +25 -20
- claude_mpm/services/agents/deployment/agent_template_builder.py +11 -7
- claude_mpm/services/agents/memory/memory_format_service.py +3 -1
- claude_mpm/services/cli/agent_cleanup_service.py +1 -4
- claude_mpm/services/cli/socketio_manager.py +39 -8
- claude_mpm/services/cli/startup_checker.py +0 -1
- claude_mpm/services/core/cache_manager.py +0 -1
- claude_mpm/services/infrastructure/monitoring.py +1 -1
- claude_mpm/services/socketio/event_normalizer.py +64 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +449 -0
- claude_mpm/services/socketio/server/connection_manager.py +3 -1
- claude_mpm/tools/code_tree_analyzer.py +930 -24
- claude_mpm/tools/code_tree_builder.py +0 -1
- claude_mpm/tools/code_tree_events.py +113 -15
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/METADATA +2 -1
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/RECORD +56 -48
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.10.dist-info → claude_mpm-4.1.12.dist-info}/top_level.txt +0 -0
|
@@ -22,9 +22,12 @@ class CodeTree {
|
|
|
22
22
|
methods: 0,
|
|
23
23
|
lines: 0
|
|
24
24
|
};
|
|
25
|
-
|
|
25
|
+
// Radial layout settings
|
|
26
|
+
this.isRadialLayout = true; // Toggle for radial vs linear layout
|
|
27
|
+
this.margin = {top: 20, right: 20, bottom: 20, left: 20};
|
|
26
28
|
this.width = 960 - this.margin.left - this.margin.right;
|
|
27
29
|
this.height = 600 - this.margin.top - this.margin.bottom;
|
|
30
|
+
this.radius = Math.min(this.width, this.height) / 2;
|
|
28
31
|
this.nodeId = 0;
|
|
29
32
|
this.duration = 750;
|
|
30
33
|
this.languageFilter = 'all';
|
|
@@ -34,16 +37,17 @@ class CodeTree {
|
|
|
34
37
|
this.analyzing = false;
|
|
35
38
|
this.selectedNode = null;
|
|
36
39
|
this.socket = null;
|
|
40
|
+
this.autoDiscovered = false; // Track if auto-discovery has been done
|
|
41
|
+
this.zoom = null; // Store zoom behavior
|
|
42
|
+
this.activeNode = null; // Track currently active node
|
|
43
|
+
this.loadingNodes = new Set(); // Track nodes that are loading
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
47
|
* Initialize the code tree visualization
|
|
41
48
|
*/
|
|
42
49
|
initialize() {
|
|
43
|
-
console.log('CodeTree.initialize() called');
|
|
44
|
-
|
|
45
50
|
if (this.initialized) {
|
|
46
|
-
console.log('Code tree already initialized');
|
|
47
51
|
return;
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -53,8 +57,6 @@ class CodeTree {
|
|
|
53
57
|
return;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
console.log('Code tree container found:', this.container);
|
|
57
|
-
|
|
58
60
|
// Check if tab is visible
|
|
59
61
|
const tabPanel = document.getElementById('code-tab');
|
|
60
62
|
if (!tabPanel) {
|
|
@@ -62,303 +64,415 @@ class CodeTree {
|
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
// Check if working directory is set
|
|
68
|
+
const workingDir = this.getWorkingDirectory();
|
|
69
|
+
if (!workingDir || workingDir === 'Loading...' || workingDir === 'Not selected') {
|
|
70
|
+
this.showNoWorkingDirectoryMessage();
|
|
71
|
+
this.initialized = true;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
66
74
|
|
|
67
75
|
// Initialize always
|
|
68
76
|
this.setupControls();
|
|
69
77
|
this.initializeTreeData();
|
|
70
78
|
this.subscribeToEvents();
|
|
71
79
|
|
|
80
|
+
// Set initial status message
|
|
81
|
+
const breadcrumbContent = document.getElementById('breadcrumb-content');
|
|
82
|
+
if (breadcrumbContent && !this.analyzing) {
|
|
83
|
+
this.updateActivityTicker('Loading project structure...', 'info');
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
// Only create visualization if tab is visible
|
|
73
87
|
if (tabPanel.classList.contains('active')) {
|
|
74
|
-
console.log('Tab is active, creating visualization');
|
|
75
88
|
this.createVisualization();
|
|
76
89
|
if (this.root && this.svg) {
|
|
77
90
|
this.update(this.root);
|
|
78
91
|
}
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
// Auto-discover root level when tab is active
|
|
93
|
+
this.autoDiscoverRootLevel();
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
this.initialized = true;
|
|
84
|
-
console.log('Code tree initialization complete');
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
/**
|
|
88
100
|
* Render visualization when tab becomes visible
|
|
89
101
|
*/
|
|
90
102
|
renderWhenVisible() {
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
// Check if working directory is set
|
|
104
|
+
const workingDir = this.getWorkingDirectory();
|
|
105
|
+
if (!workingDir || workingDir === 'Loading...' || workingDir === 'Not selected') {
|
|
106
|
+
this.showNoWorkingDirectoryMessage();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If no directory message is shown, remove it
|
|
111
|
+
this.removeNoWorkingDirectoryMessage();
|
|
93
112
|
|
|
94
113
|
if (!this.initialized) {
|
|
95
|
-
console.log('Not initialized, calling initialize()');
|
|
96
114
|
this.initialize();
|
|
97
115
|
return;
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
if (!this.svg) {
|
|
101
|
-
console.log('No SVG found, creating visualization');
|
|
102
119
|
this.createVisualization();
|
|
103
120
|
if (this.svg && this.treeGroup) {
|
|
104
|
-
console.log('SVG created, updating tree');
|
|
105
121
|
this.update(this.root);
|
|
106
|
-
} else {
|
|
107
|
-
console.log('Failed to create SVG or treeGroup');
|
|
108
122
|
}
|
|
109
123
|
} else {
|
|
110
|
-
console.log('SVG exists, forcing update');
|
|
111
124
|
// Force update with current data
|
|
112
125
|
if (this.root && this.svg) {
|
|
113
126
|
this.update(this.root);
|
|
114
127
|
}
|
|
115
128
|
}
|
|
129
|
+
|
|
130
|
+
// Auto-discover root level if not done yet
|
|
131
|
+
if (!this.autoDiscovered) {
|
|
132
|
+
this.autoDiscoverRootLevel();
|
|
133
|
+
}
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
/**
|
|
119
|
-
*
|
|
137
|
+
* Set up control event handlers
|
|
120
138
|
*/
|
|
121
139
|
setupControls() {
|
|
122
|
-
//
|
|
123
|
-
const analyzeBtn = document.getElementById('analyze-code');
|
|
124
|
-
if (analyzeBtn) {
|
|
125
|
-
analyzeBtn.addEventListener('click', () => this.startAnalysis());
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Cancel button
|
|
129
|
-
const cancelBtn = document.getElementById('cancel-analysis');
|
|
130
|
-
if (cancelBtn) {
|
|
131
|
-
cancelBtn.addEventListener('click', () => this.cancelAnalysis());
|
|
132
|
-
}
|
|
140
|
+
// Remove analyze and cancel button handlers since they're no longer in the UI
|
|
133
141
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
const languageFilter = document.getElementById('language-filter');
|
|
143
|
+
if (languageFilter) {
|
|
144
|
+
languageFilter.addEventListener('change', (e) => {
|
|
145
|
+
this.languageFilter = e.target.value;
|
|
146
|
+
this.filterTree();
|
|
147
|
+
});
|
|
138
148
|
}
|
|
139
149
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
const searchBox = document.getElementById('code-search');
|
|
151
|
+
if (searchBox) {
|
|
152
|
+
searchBox.addEventListener('input', (e) => {
|
|
153
|
+
this.searchTerm = e.target.value.toLowerCase();
|
|
154
|
+
this.filterTree();
|
|
155
|
+
});
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
|
|
158
|
+
const expandBtn = document.getElementById('code-expand-all');
|
|
159
|
+
if (expandBtn) {
|
|
160
|
+
expandBtn.addEventListener('click', () => this.expandAll());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const collapseBtn = document.getElementById('code-collapse-all');
|
|
164
|
+
if (collapseBtn) {
|
|
165
|
+
collapseBtn.addEventListener('click', () => this.collapseAll());
|
|
166
|
+
}
|
|
167
|
+
|
|
147
168
|
const resetZoomBtn = document.getElementById('code-reset-zoom');
|
|
148
169
|
if (resetZoomBtn) {
|
|
149
170
|
resetZoomBtn.addEventListener('click', () => this.resetZoom());
|
|
150
171
|
}
|
|
151
172
|
|
|
152
|
-
// Toggle legend button
|
|
153
173
|
const toggleLegendBtn = document.getElementById('code-toggle-legend');
|
|
154
174
|
if (toggleLegendBtn) {
|
|
155
175
|
toggleLegendBtn.addEventListener('click', () => this.toggleLegend());
|
|
156
176
|
}
|
|
177
|
+
|
|
178
|
+
// Listen for working directory changes
|
|
179
|
+
document.addEventListener('workingDirectoryChanged', (e) => {
|
|
180
|
+
this.onWorkingDirectoryChanged(e.detail.directory);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handle working directory change
|
|
186
|
+
*/
|
|
187
|
+
onWorkingDirectoryChanged(newDirectory) {
|
|
188
|
+
if (!newDirectory || newDirectory === 'Loading...' || newDirectory === 'Not selected') {
|
|
189
|
+
// Show no directory message
|
|
190
|
+
this.showNoWorkingDirectoryMessage();
|
|
191
|
+
// Reset tree state
|
|
192
|
+
this.autoDiscovered = false;
|
|
193
|
+
this.analyzing = false;
|
|
194
|
+
this.nodes.clear();
|
|
195
|
+
this.stats = {
|
|
196
|
+
files: 0,
|
|
197
|
+
classes: 0,
|
|
198
|
+
functions: 0,
|
|
199
|
+
methods: 0,
|
|
200
|
+
lines: 0
|
|
201
|
+
};
|
|
202
|
+
this.updateStats();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Remove any no directory message
|
|
207
|
+
this.removeNoWorkingDirectoryMessage();
|
|
208
|
+
|
|
209
|
+
// Reset discovery state for new directory
|
|
210
|
+
this.autoDiscovered = false;
|
|
211
|
+
this.analyzing = false;
|
|
212
|
+
|
|
213
|
+
// Clear existing data
|
|
214
|
+
this.nodes.clear();
|
|
215
|
+
this.stats = {
|
|
216
|
+
files: 0,
|
|
217
|
+
classes: 0,
|
|
218
|
+
functions: 0,
|
|
219
|
+
methods: 0,
|
|
220
|
+
lines: 0
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Re-initialize with new directory
|
|
224
|
+
this.initializeTreeData();
|
|
225
|
+
if (this.svg) {
|
|
226
|
+
this.update(this.root);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check if Code tab is currently active
|
|
230
|
+
const tabPanel = document.getElementById('code-tab');
|
|
231
|
+
if (tabPanel && tabPanel.classList.contains('active')) {
|
|
232
|
+
// Auto-discover in the new directory
|
|
233
|
+
this.autoDiscoverRootLevel();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.updateStats();
|
|
237
|
+
}
|
|
157
238
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Show loading spinner
|
|
241
|
+
*/
|
|
242
|
+
showLoading() {
|
|
243
|
+
let loadingDiv = document.getElementById('code-tree-loading');
|
|
244
|
+
if (!loadingDiv) {
|
|
245
|
+
// Create loading element if it doesn't exist
|
|
246
|
+
const container = document.getElementById('code-tree-container');
|
|
247
|
+
if (container) {
|
|
248
|
+
loadingDiv = document.createElement('div');
|
|
249
|
+
loadingDiv.id = 'code-tree-loading';
|
|
250
|
+
loadingDiv.innerHTML = `
|
|
251
|
+
<div class="code-tree-spinner"></div>
|
|
252
|
+
<div class="code-tree-loading-text">Analyzing code structure...</div>
|
|
253
|
+
`;
|
|
254
|
+
container.appendChild(loadingDiv);
|
|
255
|
+
}
|
|
165
256
|
}
|
|
257
|
+
if (loadingDiv) {
|
|
258
|
+
loadingDiv.classList.remove('hidden');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
166
261
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Hide loading spinner
|
|
264
|
+
*/
|
|
265
|
+
hideLoading() {
|
|
266
|
+
const loadingDiv = document.getElementById('code-tree-loading');
|
|
267
|
+
if (loadingDiv) {
|
|
268
|
+
loadingDiv.classList.add('hidden');
|
|
174
269
|
}
|
|
175
270
|
}
|
|
176
271
|
|
|
177
272
|
/**
|
|
178
|
-
* Create D3 visualization
|
|
273
|
+
* Create the D3.js visualization
|
|
179
274
|
*/
|
|
180
275
|
createVisualization() {
|
|
181
|
-
console.log('Creating code tree visualization');
|
|
182
|
-
|
|
183
|
-
// Check if D3 is available
|
|
184
276
|
if (typeof d3 === 'undefined') {
|
|
185
|
-
console.error('D3.js is not loaded
|
|
277
|
+
console.error('D3.js is not loaded');
|
|
186
278
|
return;
|
|
187
279
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!container) {
|
|
193
|
-
console.error('Code tree
|
|
280
|
+
|
|
281
|
+
const container = d3.select('#code-tree-container');
|
|
282
|
+
container.selectAll('*').remove();
|
|
283
|
+
|
|
284
|
+
if (!container || !container.node()) {
|
|
285
|
+
console.error('Code tree container not found');
|
|
194
286
|
return;
|
|
195
287
|
}
|
|
196
|
-
|
|
197
|
-
console.log('Code tree div found:', container);
|
|
198
|
-
|
|
199
|
-
// Create root if it doesn't exist
|
|
200
|
-
if (!this.root && this.treeData) {
|
|
201
|
-
console.log('Creating D3 hierarchy from tree data');
|
|
202
|
-
this.root = d3.hierarchy(this.treeData);
|
|
203
|
-
this.root.x0 = this.height / 2;
|
|
204
|
-
this.root.y0 = 0;
|
|
205
|
-
}
|
|
206
288
|
|
|
207
|
-
//
|
|
208
|
-
|
|
289
|
+
// Calculate dimensions
|
|
290
|
+
const containerNode = container.node();
|
|
291
|
+
const containerWidth = containerNode.clientWidth || 960;
|
|
292
|
+
const containerHeight = containerNode.clientHeight || 600;
|
|
209
293
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.
|
|
213
|
-
this.height = Math.max(500, rect.height) - this.margin.top - this.margin.bottom;
|
|
294
|
+
this.width = containerWidth - this.margin.left - this.margin.right;
|
|
295
|
+
this.height = containerHeight - this.margin.top - this.margin.bottom;
|
|
296
|
+
this.radius = Math.min(this.width, this.height) / 2;
|
|
214
297
|
|
|
215
298
|
// Create SVG
|
|
216
|
-
this.svg =
|
|
217
|
-
.
|
|
218
|
-
.attr('
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
299
|
+
this.svg = container.append('svg')
|
|
300
|
+
.attr('width', containerWidth)
|
|
301
|
+
.attr('height', containerHeight);
|
|
302
|
+
|
|
303
|
+
// Create tree group with appropriate centering
|
|
304
|
+
const centerX = containerWidth / 2;
|
|
305
|
+
const centerY = containerHeight / 2;
|
|
306
|
+
|
|
307
|
+
// Different initial positioning for different layouts
|
|
308
|
+
if (this.isRadialLayout) {
|
|
309
|
+
// Radial: center in the middle of the canvas
|
|
310
|
+
this.treeGroup = this.svg.append('g')
|
|
311
|
+
.attr('transform', `translate(${centerX},${centerY})`);
|
|
312
|
+
} else {
|
|
313
|
+
// Linear: start from left with some margin
|
|
314
|
+
this.treeGroup = this.svg.append('g')
|
|
315
|
+
.attr('transform', `translate(${this.margin.left + 100},${centerY})`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Create tree layout with improved spacing
|
|
319
|
+
if (this.isRadialLayout) {
|
|
320
|
+
// Use d3.cluster for better radial distribution
|
|
321
|
+
this.treeLayout = d3.cluster()
|
|
322
|
+
.size([2 * Math.PI, this.radius - 100])
|
|
323
|
+
.separation((a, b) => {
|
|
324
|
+
// Enhanced separation for radial layout
|
|
325
|
+
if (a.parent == b.parent) {
|
|
326
|
+
// Base separation on tree depth for better spacing
|
|
327
|
+
const depthFactor = Math.max(1, 4 - a.depth);
|
|
328
|
+
// Increase spacing for nodes with many siblings
|
|
329
|
+
const siblingCount = a.parent ? (a.parent.children?.length || 1) : 1;
|
|
330
|
+
const siblingFactor = siblingCount > 5 ? 2 : (siblingCount > 3 ? 1.5 : 1);
|
|
331
|
+
// More spacing at outer levels where circumference is larger
|
|
332
|
+
const radiusFactor = 1 + (a.depth * 0.2);
|
|
333
|
+
return (depthFactor * siblingFactor) / (a.depth || 1) * radiusFactor;
|
|
334
|
+
} else {
|
|
335
|
+
// Different parents - ensure enough space
|
|
336
|
+
return 4 / (a.depth || 1);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
// Linear layout with dynamic sizing based on node count
|
|
341
|
+
// Use nodeSize for consistent spacing regardless of tree size
|
|
342
|
+
this.treeLayout = d3.tree()
|
|
343
|
+
.nodeSize([30, 200]) // Fixed spacing: 30px vertical, 200px horizontal
|
|
344
|
+
.separation((a, b) => {
|
|
345
|
+
// Consistent separation for linear layout
|
|
346
|
+
if (a.parent == b.parent) {
|
|
347
|
+
// Same parent - standard spacing
|
|
348
|
+
return 1;
|
|
349
|
+
} else {
|
|
350
|
+
// Different parents - slightly more space
|
|
351
|
+
return 1.5;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Add zoom behavior with proper transform handling
|
|
357
|
+
this.zoom = d3.zoom()
|
|
358
|
+
.scaleExtent([0.1, 10])
|
|
359
|
+
.on('zoom', (event) => {
|
|
360
|
+
if (this.isRadialLayout) {
|
|
361
|
+
// Radial: maintain center point
|
|
362
|
+
this.treeGroup.attr('transform',
|
|
363
|
+
`translate(${centerX + event.transform.x},${centerY + event.transform.y}) scale(${event.transform.k})`);
|
|
364
|
+
} else {
|
|
365
|
+
// Linear: maintain left margin
|
|
366
|
+
this.treeGroup.attr('transform',
|
|
367
|
+
`translate(${this.margin.left + 100 + event.transform.x},${centerY + event.transform.y}) scale(${event.transform.k})`);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
226
370
|
|
|
227
|
-
this.
|
|
228
|
-
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
|
|
371
|
+
this.svg.call(this.zoom);
|
|
229
372
|
|
|
230
|
-
//
|
|
231
|
-
this.
|
|
232
|
-
.size([this.height, this.width]);
|
|
373
|
+
// Add controls overlay
|
|
374
|
+
this.addVisualizationControls();
|
|
233
375
|
|
|
234
376
|
// Create tooltip
|
|
235
377
|
this.tooltip = d3.select('body').append('div')
|
|
236
|
-
.attr('class', 'code-tooltip')
|
|
237
|
-
.style('opacity', 0)
|
|
378
|
+
.attr('class', 'code-tree-tooltip')
|
|
379
|
+
.style('opacity', 0)
|
|
380
|
+
.style('position', 'absolute')
|
|
381
|
+
.style('background', 'rgba(0, 0, 0, 0.8)')
|
|
382
|
+
.style('color', 'white')
|
|
383
|
+
.style('padding', '8px')
|
|
384
|
+
.style('border-radius', '4px')
|
|
385
|
+
.style('font-size', '12px')
|
|
386
|
+
.style('pointer-events', 'none');
|
|
238
387
|
}
|
|
239
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Clear all D3 visualization elements
|
|
391
|
+
*/
|
|
392
|
+
clearD3Visualization() {
|
|
393
|
+
if (this.treeGroup) {
|
|
394
|
+
// Remove all existing nodes and links
|
|
395
|
+
this.treeGroup.selectAll('g.node').remove();
|
|
396
|
+
this.treeGroup.selectAll('path.link').remove();
|
|
397
|
+
}
|
|
398
|
+
// Reset node ID counter for proper tracking
|
|
399
|
+
this.nodeId = 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
240
402
|
/**
|
|
241
403
|
* Initialize tree data structure
|
|
242
404
|
*/
|
|
243
405
|
initializeTreeData() {
|
|
244
|
-
|
|
406
|
+
const workingDir = this.getWorkingDirectory();
|
|
407
|
+
const dirName = workingDir ? workingDir.split('/').pop() || 'Project Root' : 'Project Root';
|
|
245
408
|
|
|
409
|
+
// Use '.' as the root path for consistency with relative path handling
|
|
410
|
+
// The actual working directory is retrieved via getWorkingDirectory() when needed
|
|
246
411
|
this.treeData = {
|
|
247
|
-
name:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
412
|
+
name: dirName,
|
|
413
|
+
path: '.', // Always use '.' for root to simplify path handling
|
|
414
|
+
type: 'root',
|
|
415
|
+
children: [],
|
|
416
|
+
loaded: false,
|
|
417
|
+
expanded: true // Start expanded
|
|
252
418
|
};
|
|
253
419
|
|
|
254
|
-
// Only create D3 hierarchy if D3 is available
|
|
255
420
|
if (typeof d3 !== 'undefined') {
|
|
256
421
|
this.root = d3.hierarchy(this.treeData);
|
|
257
422
|
this.root.x0 = this.height / 2;
|
|
258
423
|
this.root.y0 = 0;
|
|
259
|
-
console.log('Tree root created:', this.root);
|
|
260
|
-
} else {
|
|
261
|
-
console.warn('D3 not available yet, deferring hierarchy creation');
|
|
262
|
-
this.root = null;
|
|
263
424
|
}
|
|
264
425
|
}
|
|
265
426
|
|
|
266
427
|
/**
|
|
267
|
-
* Subscribe to
|
|
428
|
+
* Subscribe to code analysis events
|
|
268
429
|
*/
|
|
269
430
|
subscribeToEvents() {
|
|
270
|
-
// Try multiple socket sources
|
|
271
|
-
this.getSocket();
|
|
272
|
-
|
|
273
|
-
if (this.socket) {
|
|
274
|
-
console.log('CodeTree: Socket available, subscribing to events');
|
|
275
|
-
|
|
276
|
-
// Code analysis events - match the server-side event names
|
|
277
|
-
this.socket.on('code:analysis:start', (data) => this.handleAnalysisStart(data));
|
|
278
|
-
this.socket.on('code:analysis:accepted', (data) => this.handleAnalysisAccepted(data));
|
|
279
|
-
this.socket.on('code:analysis:queued', (data) => this.handleAnalysisQueued(data));
|
|
280
|
-
this.socket.on('code:analysis:cancelled', (data) => this.handleAnalysisCancelled(data));
|
|
281
|
-
this.socket.on('code:analysis:progress', (data) => this.handleProgress(data));
|
|
282
|
-
this.socket.on('code:analysis:complete', (data) => this.handleAnalysisComplete(data));
|
|
283
|
-
this.socket.on('code:analysis:error', (data) => this.handleAnalysisError(data));
|
|
284
|
-
|
|
285
|
-
// File and node events
|
|
286
|
-
this.socket.on('code:file:start', (data) => this.handleFileStart(data));
|
|
287
|
-
this.socket.on('code:file:complete', (data) => this.handleFileComplete(data));
|
|
288
|
-
this.socket.on('code:node:found', (data) => this.handleNodeFound(data));
|
|
289
|
-
} else {
|
|
290
|
-
console.warn('CodeTree: Socket not available yet, will retry on analysis');
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Get socket connection from available sources
|
|
296
|
-
*/
|
|
297
|
-
getSocket() {
|
|
298
|
-
// Try multiple sources for the socket
|
|
299
431
|
if (!this.socket) {
|
|
300
|
-
// Try window.socket first (most common)
|
|
301
432
|
if (window.socket) {
|
|
302
433
|
this.socket = window.socket;
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
// Try from dashboard's socketClient
|
|
306
|
-
else if (window.dashboard?.socketClient?.socket) {
|
|
434
|
+
this.setupEventHandlers();
|
|
435
|
+
} else if (window.dashboard?.socketClient?.socket) {
|
|
307
436
|
this.socket = window.dashboard.socketClient.socket;
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
// Try from socketClient directly
|
|
311
|
-
else if (window.socketClient?.socket) {
|
|
437
|
+
this.setupEventHandlers();
|
|
438
|
+
} else if (window.socketClient?.socket) {
|
|
312
439
|
this.socket = window.socketClient.socket;
|
|
313
|
-
|
|
440
|
+
this.setupEventHandlers();
|
|
314
441
|
}
|
|
315
442
|
}
|
|
316
|
-
return this.socket;
|
|
317
443
|
}
|
|
318
444
|
|
|
319
445
|
/**
|
|
320
|
-
*
|
|
446
|
+
* Automatically discover root-level objects when tab opens
|
|
321
447
|
*/
|
|
322
|
-
|
|
323
|
-
if (this.analyzing) {
|
|
324
|
-
console.log('Analysis already in progress');
|
|
448
|
+
autoDiscoverRootLevel() {
|
|
449
|
+
if (this.autoDiscovered || this.analyzing) {
|
|
325
450
|
return;
|
|
326
451
|
}
|
|
327
|
-
|
|
328
|
-
console.log('Starting code analysis...');
|
|
329
452
|
|
|
330
|
-
//
|
|
331
|
-
this.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
453
|
+
// Update activity ticker
|
|
454
|
+
this.updateActivityTicker('🔍 Discovering project structure...', 'info');
|
|
455
|
+
|
|
456
|
+
// Get working directory
|
|
457
|
+
const workingDir = this.getWorkingDirectory();
|
|
458
|
+
if (!workingDir || workingDir === 'Loading...' || workingDir === 'Not selected') {
|
|
459
|
+
console.warn('Cannot auto-discover: no working directory set');
|
|
460
|
+
this.showNoWorkingDirectoryMessage();
|
|
335
461
|
return;
|
|
336
462
|
}
|
|
337
463
|
|
|
338
|
-
//
|
|
339
|
-
if (!
|
|
340
|
-
console.
|
|
341
|
-
this.
|
|
464
|
+
// Ensure we have an absolute path
|
|
465
|
+
if (!workingDir.startsWith('/') && !workingDir.match(/^[A-Z]:\\/)) {
|
|
466
|
+
console.error('Working directory is not absolute:', workingDir);
|
|
467
|
+
this.showNotification('Invalid working directory path', 'error');
|
|
468
|
+
return;
|
|
342
469
|
}
|
|
343
470
|
|
|
344
|
-
this.analyzing = true;
|
|
345
|
-
|
|
346
|
-
// Update button state - but keep it responsive
|
|
347
|
-
const analyzeBtn = document.getElementById('analyze-code');
|
|
348
|
-
const cancelBtn = document.getElementById('cancel-analysis');
|
|
349
|
-
if (analyzeBtn) {
|
|
350
|
-
analyzeBtn.textContent = 'Analyzing...';
|
|
351
|
-
analyzeBtn.classList.add('analyzing');
|
|
352
|
-
}
|
|
353
|
-
if (cancelBtn) {
|
|
354
|
-
cancelBtn.style.display = 'inline-block';
|
|
355
|
-
}
|
|
356
471
|
|
|
357
|
-
|
|
358
|
-
this.
|
|
472
|
+
this.autoDiscovered = true;
|
|
473
|
+
this.analyzing = true;
|
|
359
474
|
|
|
360
|
-
//
|
|
361
|
-
this.initializeTreeData();
|
|
475
|
+
// Clear any existing nodes
|
|
362
476
|
this.nodes.clear();
|
|
363
477
|
this.stats = {
|
|
364
478
|
files: 0,
|
|
@@ -367,825 +481,2039 @@ class CodeTree {
|
|
|
367
481
|
methods: 0,
|
|
368
482
|
lines: 0
|
|
369
483
|
};
|
|
370
|
-
this.updateStats();
|
|
371
484
|
|
|
372
|
-
//
|
|
373
|
-
if (!this.
|
|
374
|
-
this.
|
|
485
|
+
// Subscribe to events if not already done
|
|
486
|
+
if (this.socket && !this.socket.hasListeners('code:node:found')) {
|
|
487
|
+
this.setupEventHandlers();
|
|
375
488
|
}
|
|
376
489
|
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
490
|
+
// Update tree data with working directory as the root
|
|
491
|
+
const dirName = workingDir.split('/').pop() || 'Project Root';
|
|
492
|
+
this.treeData = {
|
|
493
|
+
name: dirName,
|
|
494
|
+
path: '.', // Use '.' for root to maintain consistency with relative path handling
|
|
495
|
+
type: 'root',
|
|
496
|
+
children: [],
|
|
497
|
+
loaded: false,
|
|
498
|
+
expanded: true // Start expanded to show discovered items
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
if (typeof d3 !== 'undefined') {
|
|
502
|
+
this.root = d3.hierarchy(this.treeData);
|
|
503
|
+
this.root.x0 = this.height / 2;
|
|
504
|
+
this.root.y0 = 0;
|
|
380
505
|
}
|
|
381
506
|
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
507
|
+
// Update UI
|
|
508
|
+
this.showLoading();
|
|
509
|
+
this.updateBreadcrumb(`Discovering structure in ${dirName}...`, 'info');
|
|
385
510
|
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
window.socketClient?.sessions?.values()?.next()?.value?.working_directory ||
|
|
392
|
-
'.';
|
|
393
|
-
|
|
394
|
-
// Update the input field with the detected path
|
|
395
|
-
if (pathInput && path !== '.') {
|
|
396
|
-
pathInput.value = path;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
511
|
+
// Get selected languages from checkboxes
|
|
512
|
+
const selectedLanguages = [];
|
|
513
|
+
document.querySelectorAll('.language-checkbox:checked').forEach(cb => {
|
|
514
|
+
selectedLanguages.push(cb.value);
|
|
515
|
+
});
|
|
399
516
|
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
const ignorePatterns = this.getIgnorePatterns();
|
|
517
|
+
// Get ignore patterns
|
|
518
|
+
const ignorePatterns = document.getElementById('ignore-patterns')?.value || '';
|
|
403
519
|
|
|
404
|
-
//
|
|
405
|
-
const requestId = this.generateRequestId();
|
|
406
|
-
this.currentRequestId = requestId;
|
|
520
|
+
// Enhanced debug logging
|
|
407
521
|
|
|
408
|
-
//
|
|
522
|
+
// Request top-level discovery with working directory
|
|
409
523
|
const requestPayload = {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
languages:
|
|
413
|
-
|
|
414
|
-
|
|
524
|
+
path: workingDir, // Use working directory instead of '.'
|
|
525
|
+
depth: 'top_level',
|
|
526
|
+
languages: selectedLanguages,
|
|
527
|
+
ignore_patterns: ignorePatterns,
|
|
528
|
+
request_id: `discover_${Date.now()}` // Add request ID for tracking
|
|
415
529
|
};
|
|
416
530
|
|
|
417
|
-
|
|
531
|
+
// Sending top-level discovery request
|
|
418
532
|
|
|
419
|
-
|
|
420
|
-
|
|
533
|
+
if (this.socket) {
|
|
534
|
+
this.socket.emit('code:discover:top_level', requestPayload);
|
|
535
|
+
}
|
|
421
536
|
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
this.requestTimeout = setTimeout(() => {
|
|
425
|
-
if (this.analyzing && this.currentRequestId === requestId) {
|
|
426
|
-
console.warn('Analysis appears stuck after 60 seconds');
|
|
427
|
-
this.showNotification('Analysis is taking longer than expected. You can cancel if needed.', 'warning');
|
|
428
|
-
// Don't auto-reset, let user decide to cancel
|
|
429
|
-
}
|
|
430
|
-
}, 60000); // 60 second safety timeout
|
|
537
|
+
// Update stats display
|
|
538
|
+
this.updateStats();
|
|
431
539
|
}
|
|
432
540
|
|
|
433
541
|
/**
|
|
434
|
-
*
|
|
542
|
+
* Legacy analyzeCode method - redirects to auto-discovery
|
|
435
543
|
*/
|
|
436
|
-
|
|
437
|
-
if (
|
|
544
|
+
analyzeCode() {
|
|
545
|
+
if (this.analyzing) {
|
|
438
546
|
return;
|
|
439
547
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (this.socket && this.currentRequestId) {
|
|
444
|
-
this.socket.emit('code:analyze:cancel', {
|
|
445
|
-
request_id: this.currentRequestId
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
this.resetAnalysisState();
|
|
548
|
+
|
|
549
|
+
// Redirect to auto-discovery
|
|
550
|
+
this.autoDiscoverRootLevel();
|
|
450
551
|
}
|
|
451
|
-
|
|
552
|
+
|
|
452
553
|
/**
|
|
453
|
-
*
|
|
554
|
+
* Cancel ongoing analysis - removed since we no longer have a cancel button
|
|
454
555
|
*/
|
|
455
|
-
|
|
556
|
+
cancelAnalysis() {
|
|
456
557
|
this.analyzing = false;
|
|
457
|
-
this.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
clearTimeout(this.requestTimeout);
|
|
462
|
-
this.requestTimeout = null;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Update button state
|
|
466
|
-
const analyzeBtn = document.getElementById('analyze-code');
|
|
467
|
-
const cancelBtn = document.getElementById('cancel-analysis');
|
|
468
|
-
if (analyzeBtn) {
|
|
469
|
-
analyzeBtn.disabled = false;
|
|
470
|
-
analyzeBtn.textContent = 'Analyze';
|
|
471
|
-
analyzeBtn.classList.remove('analyzing');
|
|
472
|
-
}
|
|
473
|
-
if (cancelBtn) {
|
|
474
|
-
cancelBtn.style.display = 'none';
|
|
558
|
+
this.hideLoading();
|
|
559
|
+
|
|
560
|
+
if (this.socket) {
|
|
561
|
+
this.socket.emit('code:analysis:cancel');
|
|
475
562
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
this.
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Generate unique request ID
|
|
483
|
-
*/
|
|
484
|
-
generateRequestId() {
|
|
485
|
-
return `analysis-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Get selected languages from UI
|
|
490
|
-
*/
|
|
491
|
-
getSelectedLanguages() {
|
|
492
|
-
const languages = [];
|
|
493
|
-
const checkboxes = document.querySelectorAll('.language-checkbox:checked');
|
|
494
|
-
checkboxes.forEach(cb => {
|
|
495
|
-
languages.push(cb.value);
|
|
496
|
-
});
|
|
497
|
-
return languages;
|
|
563
|
+
|
|
564
|
+
this.updateBreadcrumb('Analysis cancelled', 'warning');
|
|
565
|
+
this.showNotification('Analysis cancelled', 'warning');
|
|
566
|
+
this.addEventToDisplay('Analysis cancelled', 'warning');
|
|
498
567
|
}
|
|
499
|
-
|
|
568
|
+
|
|
500
569
|
/**
|
|
501
|
-
*
|
|
570
|
+
* Create the events display area
|
|
502
571
|
*/
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
572
|
+
createEventsDisplay() {
|
|
573
|
+
let eventsContainer = document.getElementById('analysis-events');
|
|
574
|
+
if (!eventsContainer) {
|
|
575
|
+
const treeContainer = document.getElementById('code-tree-container');
|
|
576
|
+
if (treeContainer) {
|
|
577
|
+
eventsContainer = document.createElement('div');
|
|
578
|
+
eventsContainer.id = 'analysis-events';
|
|
579
|
+
eventsContainer.className = 'analysis-events';
|
|
580
|
+
eventsContainer.style.display = 'none';
|
|
581
|
+
treeContainer.appendChild(eventsContainer);
|
|
582
|
+
}
|
|
508
583
|
}
|
|
509
|
-
return patterns;
|
|
510
584
|
}
|
|
511
585
|
|
|
512
586
|
/**
|
|
513
|
-
*
|
|
587
|
+
* Clear the events display
|
|
514
588
|
*/
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return;
|
|
589
|
+
clearEventsDisplay() {
|
|
590
|
+
const eventsContainer = document.getElementById('analysis-events');
|
|
591
|
+
if (eventsContainer) {
|
|
592
|
+
eventsContainer.innerHTML = '';
|
|
593
|
+
eventsContainer.style.display = 'block';
|
|
521
594
|
}
|
|
522
|
-
|
|
523
|
-
// Clear request timeout since we got a response
|
|
524
|
-
if (this.requestTimeout) {
|
|
525
|
-
clearTimeout(this.requestTimeout);
|
|
526
|
-
this.requestTimeout = null;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const message = `Analyzing ${data.total_files || 0} files...`;
|
|
530
|
-
this.updateProgress(0, message);
|
|
531
|
-
this.updateTicker(message, 'progress');
|
|
532
|
-
this.showNotification('Analysis started - building tree in real-time...', 'info');
|
|
533
595
|
}
|
|
534
596
|
|
|
535
597
|
/**
|
|
536
|
-
*
|
|
598
|
+
* Add an event to the display
|
|
537
599
|
*/
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
this.addNodeToTree(fileNode, data.path);
|
|
554
|
-
this.stats.files++;
|
|
555
|
-
this.updateStats();
|
|
556
|
-
|
|
557
|
-
// Incremental tree update - update visualization as files are discovered
|
|
558
|
-
if (this.svg && this.root) {
|
|
559
|
-
// Throttle updates to avoid performance issues
|
|
560
|
-
if (!this.updateThrottleTimer) {
|
|
561
|
-
this.updateThrottleTimer = setTimeout(() => {
|
|
562
|
-
this.update(this.root);
|
|
563
|
-
this.updateThrottleTimer = null;
|
|
564
|
-
}, 100); // Update every 100ms max
|
|
565
|
-
}
|
|
600
|
+
addEventToDisplay(message, type = 'info') {
|
|
601
|
+
const eventsContainer = document.getElementById('analysis-events');
|
|
602
|
+
if (eventsContainer) {
|
|
603
|
+
const eventEl = document.createElement('div');
|
|
604
|
+
eventEl.className = 'analysis-event';
|
|
605
|
+
eventEl.style.borderLeftColor = type === 'warning' ? '#f59e0b' :
|
|
606
|
+
type === 'error' ? '#ef4444' : '#3b82f6';
|
|
607
|
+
|
|
608
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
609
|
+
eventEl.innerHTML = `<span style="color: #718096;">[${timestamp}]</span> ${message}`;
|
|
610
|
+
|
|
611
|
+
eventsContainer.appendChild(eventEl);
|
|
612
|
+
// Auto-scroll to bottom
|
|
613
|
+
eventsContainer.scrollTop = eventsContainer.scrollHeight;
|
|
566
614
|
}
|
|
567
615
|
}
|
|
568
616
|
|
|
569
617
|
/**
|
|
570
|
-
*
|
|
618
|
+
* Setup Socket.IO event handlers
|
|
571
619
|
*/
|
|
572
|
-
|
|
573
|
-
|
|
620
|
+
setupEventHandlers() {
|
|
621
|
+
if (!this.socket) return;
|
|
622
|
+
|
|
623
|
+
// Analysis lifecycle events
|
|
624
|
+
this.socket.on('code:analysis:accepted', (data) => this.onAnalysisAccepted(data));
|
|
625
|
+
this.socket.on('code:analysis:queued', (data) => this.onAnalysisQueued(data));
|
|
626
|
+
this.socket.on('code:analysis:start', (data) => this.onAnalysisStart(data));
|
|
627
|
+
this.socket.on('code:analysis:complete', (data) => this.onAnalysisComplete(data));
|
|
628
|
+
this.socket.on('code:analysis:cancelled', (data) => this.onAnalysisCancelled(data));
|
|
629
|
+
this.socket.on('code:analysis:error', (data) => this.onAnalysisError(data));
|
|
630
|
+
|
|
631
|
+
// Node discovery events
|
|
632
|
+
this.socket.on('code:top_level:discovered', (data) => this.onTopLevelDiscovered(data));
|
|
633
|
+
this.socket.on('code:directory:discovered', (data) => this.onDirectoryDiscovered(data));
|
|
634
|
+
this.socket.on('code:file:discovered', (data) => this.onFileDiscovered(data));
|
|
635
|
+
this.socket.on('code:file:analyzed', (data) => this.onFileAnalyzed(data));
|
|
636
|
+
this.socket.on('code:node:found', (data) => this.onNodeFound(data));
|
|
637
|
+
|
|
638
|
+
// Progress updates
|
|
639
|
+
this.socket.on('code:analysis:progress', (data) => this.onProgressUpdate(data));
|
|
640
|
+
|
|
641
|
+
// Lazy loading responses
|
|
642
|
+
this.socket.on('code:directory:contents', (data) => {
|
|
643
|
+
// Update the requested directory with its contents
|
|
644
|
+
if (data.path) {
|
|
645
|
+
// Convert absolute path back to relative path to match tree nodes
|
|
646
|
+
let searchPath = data.path;
|
|
647
|
+
const workingDir = this.getWorkingDirectory();
|
|
648
|
+
if (workingDir && searchPath.startsWith(workingDir)) {
|
|
649
|
+
// Remove working directory prefix to get relative path
|
|
650
|
+
searchPath = searchPath.substring(workingDir.length).replace(/^\//, '');
|
|
651
|
+
// If empty after removing prefix, it's the root
|
|
652
|
+
if (!searchPath) {
|
|
653
|
+
searchPath = '.';
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const node = this.findNodeByPath(searchPath);
|
|
658
|
+
if (node && data.children) {
|
|
659
|
+
// Find D3 node and remove loading pulse (use searchPath, not data.path)
|
|
660
|
+
const d3Node = this.findD3NodeByPath(searchPath);
|
|
661
|
+
if (d3Node && this.loadingNodes.has(searchPath)) {
|
|
662
|
+
this.removeLoadingPulse(d3Node);
|
|
663
|
+
}
|
|
664
|
+
node.children = data.children.map(child => {
|
|
665
|
+
// Construct full path for child by combining parent path with child name
|
|
666
|
+
// The backend now returns just the item name, not the full path
|
|
667
|
+
let childPath;
|
|
668
|
+
if (searchPath === '.' || searchPath === '') {
|
|
669
|
+
// Root level - child path is just the name
|
|
670
|
+
childPath = child.name || child.path;
|
|
671
|
+
} else {
|
|
672
|
+
// Subdirectory - combine parent path with child name
|
|
673
|
+
// Use child.name (backend returns just the name) or fallback to child.path
|
|
674
|
+
const childName = child.name || child.path;
|
|
675
|
+
childPath = `${searchPath}/${childName}`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
...child,
|
|
680
|
+
path: childPath, // Override with constructed path
|
|
681
|
+
loaded: child.type === 'directory' ? false : undefined,
|
|
682
|
+
analyzed: child.type === 'file' ? false : undefined,
|
|
683
|
+
expanded: false,
|
|
684
|
+
children: []
|
|
685
|
+
};
|
|
686
|
+
});
|
|
687
|
+
node.loaded = true;
|
|
688
|
+
node.expanded = true; // Mark as expanded to show children
|
|
689
|
+
|
|
690
|
+
// Update D3 hierarchy and make sure the node is expanded
|
|
691
|
+
if (this.root && this.svg) {
|
|
692
|
+
// Store old root to preserve expansion state
|
|
693
|
+
const oldRoot = this.root;
|
|
694
|
+
|
|
695
|
+
// Recreate hierarchy with updated data
|
|
696
|
+
this.root = d3.hierarchy(this.treeData);
|
|
697
|
+
this.root.x0 = this.height / 2;
|
|
698
|
+
this.root.y0 = 0;
|
|
699
|
+
|
|
700
|
+
// Preserve expansion state from old tree
|
|
701
|
+
this.preserveExpansionState(oldRoot, this.root);
|
|
702
|
+
|
|
703
|
+
// Find the D3 node again after hierarchy recreation
|
|
704
|
+
const updatedD3Node = this.findD3NodeByPath(searchPath);
|
|
705
|
+
if (updatedD3Node) {
|
|
706
|
+
// Ensure children are visible (not collapsed)
|
|
707
|
+
updatedD3Node.children = updatedD3Node._children || updatedD3Node.children;
|
|
708
|
+
updatedD3Node._children = null;
|
|
709
|
+
updatedD3Node.data.expanded = true;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
this.update(this.root);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Update stats based on discovered contents
|
|
716
|
+
if (data.stats) {
|
|
717
|
+
this.stats.files += data.stats.files || 0;
|
|
718
|
+
this.stats.directories += data.stats.directories || 0;
|
|
719
|
+
this.updateStats();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
this.updateBreadcrumb(`Loaded ${data.path}`, 'success');
|
|
723
|
+
this.hideLoading();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
});
|
|
574
727
|
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
728
|
+
// Top level discovery response
|
|
729
|
+
this.socket.on('code:top_level:discovered', (data) => {
|
|
730
|
+
if (data.items && Array.isArray(data.items)) {
|
|
731
|
+
|
|
732
|
+
// Add discovered items to the root node
|
|
733
|
+
this.treeData.children = data.items.map(item => ({
|
|
734
|
+
name: item.name,
|
|
735
|
+
path: item.path,
|
|
736
|
+
type: item.type,
|
|
737
|
+
language: item.type === 'file' ? this.detectLanguage(item.path) : undefined,
|
|
738
|
+
size: item.size,
|
|
739
|
+
lines: item.lines,
|
|
740
|
+
loaded: item.type === 'directory' ? false : undefined,
|
|
741
|
+
analyzed: item.type === 'file' ? false : undefined,
|
|
742
|
+
expanded: false,
|
|
743
|
+
children: []
|
|
744
|
+
}));
|
|
745
|
+
|
|
746
|
+
this.treeData.loaded = true;
|
|
747
|
+
|
|
748
|
+
// Update stats
|
|
749
|
+
if (data.stats) {
|
|
750
|
+
this.stats = { ...this.stats, ...data.stats };
|
|
582
751
|
this.updateStats();
|
|
583
752
|
}
|
|
753
|
+
|
|
754
|
+
// Update D3 hierarchy
|
|
755
|
+
if (typeof d3 !== 'undefined') {
|
|
756
|
+
// Clear any existing nodes before creating new ones
|
|
757
|
+
this.clearD3Visualization();
|
|
758
|
+
|
|
759
|
+
// Create new hierarchy
|
|
760
|
+
this.root = d3.hierarchy(this.treeData);
|
|
761
|
+
this.root.x0 = this.height / 2;
|
|
762
|
+
this.root.y0 = 0;
|
|
763
|
+
|
|
764
|
+
if (this.svg) {
|
|
765
|
+
this.update(this.root);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
this.analyzing = false;
|
|
770
|
+
this.hideLoading();
|
|
771
|
+
this.updateBreadcrumb(`Discovered ${data.items.length} root items`, 'success');
|
|
772
|
+
this.showNotification(`Found ${data.items.length} items in project root`, 'success');
|
|
584
773
|
}
|
|
585
|
-
}
|
|
774
|
+
});
|
|
586
775
|
}
|
|
587
776
|
|
|
588
777
|
/**
|
|
589
|
-
* Handle
|
|
778
|
+
* Handle analysis start event
|
|
590
779
|
*/
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
// Update ticker with node discovery
|
|
595
|
-
const icons = {
|
|
596
|
-
'function': '⚡',
|
|
597
|
-
'class': '🏛️',
|
|
598
|
-
'method': '🔧',
|
|
599
|
-
'module': '📦'
|
|
600
|
-
};
|
|
601
|
-
const icon = icons[data.type] || '📌';
|
|
602
|
-
const nodeName = data.name || 'unnamed';
|
|
603
|
-
this.updateTicker(`${icon} ${nodeName}`, 'node');
|
|
604
|
-
|
|
605
|
-
// Create node object
|
|
606
|
-
const node = {
|
|
607
|
-
name: data.name,
|
|
608
|
-
type: data.type, // module, class, function, method
|
|
609
|
-
path: data.path,
|
|
610
|
-
line: data.line,
|
|
611
|
-
complexity: data.complexity || 0,
|
|
612
|
-
docstring: data.docstring,
|
|
613
|
-
params: data.params,
|
|
614
|
-
returns: data.returns,
|
|
615
|
-
children: []
|
|
616
|
-
};
|
|
780
|
+
onAnalysisStart(data) {
|
|
781
|
+
this.analyzing = true;
|
|
782
|
+
const message = data.message || 'Starting code analysis...';
|
|
617
783
|
|
|
618
|
-
//
|
|
619
|
-
this.
|
|
784
|
+
// Update activity ticker
|
|
785
|
+
this.updateActivityTicker('🚀 Starting analysis...', 'info');
|
|
620
786
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
case 'class':
|
|
624
|
-
this.stats.classes++;
|
|
625
|
-
break;
|
|
626
|
-
case 'function':
|
|
627
|
-
this.stats.functions++;
|
|
628
|
-
break;
|
|
629
|
-
case 'method':
|
|
630
|
-
this.stats.methods++;
|
|
631
|
-
break;
|
|
632
|
-
}
|
|
787
|
+
this.updateBreadcrumb(message, 'info');
|
|
788
|
+
this.addEventToDisplay(`🚀 ${message}`, 'info');
|
|
633
789
|
|
|
634
|
-
|
|
635
|
-
|
|
790
|
+
// Initialize or clear the tree
|
|
791
|
+
if (!this.treeData || this.treeData.children.length === 0) {
|
|
792
|
+
this.initializeTreeData();
|
|
636
793
|
}
|
|
637
794
|
|
|
795
|
+
// Reset stats
|
|
796
|
+
this.stats = {
|
|
797
|
+
files: 0,
|
|
798
|
+
classes: 0,
|
|
799
|
+
functions: 0,
|
|
800
|
+
methods: 0,
|
|
801
|
+
lines: 0
|
|
802
|
+
};
|
|
638
803
|
this.updateStats();
|
|
639
|
-
|
|
640
|
-
// Incremental tree update - batch updates for performance
|
|
641
|
-
if (this.svg && this.root) {
|
|
642
|
-
// Clear existing throttle timer
|
|
643
|
-
if (this.updateThrottleTimer) {
|
|
644
|
-
clearTimeout(this.updateThrottleTimer);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Set new throttle timer - batch multiple nodes together
|
|
648
|
-
this.updateThrottleTimer = setTimeout(() => {
|
|
649
|
-
this.update(this.root);
|
|
650
|
-
this.updateThrottleTimer = null;
|
|
651
|
-
}, 200); // Update every 200ms max for node additions
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Handle progress update
|
|
657
|
-
*/
|
|
658
|
-
handleProgress(data) {
|
|
659
|
-
this.updateProgress(data.percentage, data.message);
|
|
660
804
|
}
|
|
661
805
|
|
|
662
806
|
/**
|
|
663
|
-
* Handle
|
|
807
|
+
* Handle top-level discovery event (initial root directory scan)
|
|
664
808
|
*/
|
|
665
|
-
|
|
666
|
-
|
|
809
|
+
onTopLevelDiscovered(data) {
|
|
810
|
+
// Received top-level discovery response
|
|
667
811
|
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
812
|
+
// Update activity ticker
|
|
813
|
+
this.updateActivityTicker(`📁 Discovered ${(data.items || []).length} top-level items`, 'success');
|
|
672
814
|
|
|
673
|
-
|
|
815
|
+
// Add to events display
|
|
816
|
+
this.addEventToDisplay(`📁 Found ${(data.items || []).length} top-level items in project root`, 'info');
|
|
674
817
|
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
this.update(this.root);
|
|
678
|
-
}
|
|
818
|
+
// The root node (with path '.') should receive the children
|
|
819
|
+
const rootNode = this.findNodeByPath('.');
|
|
679
820
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
821
|
+
console.log('🔎 Looking for root node with path ".", found:', rootNode ? {
|
|
822
|
+
name: rootNode.name,
|
|
823
|
+
path: rootNode.path,
|
|
824
|
+
currentChildren: rootNode.children ? rootNode.children.length : 0
|
|
825
|
+
} : 'NOT FOUND');
|
|
685
826
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
827
|
+
if (rootNode && data.items) {
|
|
828
|
+
console.log('🌳 Populating root node with children');
|
|
829
|
+
|
|
830
|
+
// Update the root node with discovered children
|
|
831
|
+
rootNode.children = data.items.map(child => {
|
|
832
|
+
// Items at root level get their name as the path
|
|
833
|
+
const childPath = child.name;
|
|
834
|
+
|
|
835
|
+
console.log(` Adding child: ${child.name} with path: ${childPath}`);
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
name: child.name,
|
|
839
|
+
path: childPath, // Just the name for top-level items
|
|
840
|
+
type: child.type,
|
|
841
|
+
loaded: child.type === 'directory' ? false : undefined,
|
|
842
|
+
analyzed: child.type === 'file' ? false : undefined,
|
|
843
|
+
expanded: false,
|
|
844
|
+
children: child.type === 'directory' ? [] : undefined,
|
|
845
|
+
size: child.size,
|
|
846
|
+
has_code: child.has_code
|
|
847
|
+
};
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
rootNode.loaded = true;
|
|
851
|
+
rootNode.expanded = true;
|
|
852
|
+
|
|
853
|
+
// Update D3 hierarchy and render
|
|
854
|
+
if (this.root && this.svg) {
|
|
855
|
+
// Recreate hierarchy with new data
|
|
856
|
+
this.root = d3.hierarchy(this.treeData);
|
|
857
|
+
this.root.x0 = this.height / 2;
|
|
858
|
+
this.root.y0 = 0;
|
|
859
|
+
|
|
860
|
+
// Update the tree visualization
|
|
861
|
+
this.update(this.root);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Hide loading and show success
|
|
865
|
+
this.hideLoading();
|
|
866
|
+
this.updateBreadcrumb(`Discovered ${data.items.length} items`, 'success');
|
|
867
|
+
this.showNotification(`Found ${data.items.length} top-level items`, 'success');
|
|
868
|
+
} else {
|
|
869
|
+
console.error('❌ Could not find root node to populate');
|
|
870
|
+
this.showNotification('Failed to populate root directory', 'error');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Mark analysis as complete
|
|
874
|
+
this.analyzing = false;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Handle directory discovered event
|
|
694
879
|
*/
|
|
695
|
-
|
|
696
|
-
|
|
880
|
+
onDirectoryDiscovered(data) {
|
|
881
|
+
// Update activity ticker first
|
|
882
|
+
this.updateActivityTicker(`📁 Discovered: ${data.name || 'directory'}`);
|
|
697
883
|
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
884
|
+
// Add to events display
|
|
885
|
+
this.addEventToDisplay(`📁 Found ${(data.children || []).length} items in: ${data.name || data.path}`, 'info');
|
|
886
|
+
|
|
887
|
+
console.log('📥 Received directory discovery:', {
|
|
888
|
+
path: data.path,
|
|
889
|
+
name: data.name,
|
|
890
|
+
childrenCount: (data.children || []).length,
|
|
891
|
+
children: (data.children || []).map(c => ({ name: c.name, type: c.type })),
|
|
892
|
+
workingDir: this.getWorkingDirectory()
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// Convert absolute path back to relative path to match tree nodes
|
|
896
|
+
let searchPath = data.path;
|
|
897
|
+
const workingDir = this.getWorkingDirectory();
|
|
898
|
+
if (workingDir && searchPath.startsWith(workingDir)) {
|
|
899
|
+
// Remove working directory prefix to get relative path
|
|
900
|
+
searchPath = searchPath.substring(workingDir.length).replace(/^\//, '');
|
|
901
|
+
// If empty after removing prefix, it's the root
|
|
902
|
+
if (!searchPath) {
|
|
903
|
+
searchPath = '.';
|
|
904
|
+
}
|
|
701
905
|
}
|
|
702
906
|
|
|
703
|
-
|
|
907
|
+
console.log('🔎 Searching for node with path:', searchPath);
|
|
704
908
|
|
|
705
|
-
//
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
909
|
+
// Find the node that was clicked to trigger this discovery
|
|
910
|
+
const node = this.findNodeByPath(searchPath);
|
|
911
|
+
|
|
912
|
+
// Located target node for expansion
|
|
913
|
+
|
|
914
|
+
if (node && data.children) {
|
|
915
|
+
// Update the node with discovered children
|
|
916
|
+
node.children = data.children.map(child => {
|
|
917
|
+
// Construct full path for child by combining parent path with child name
|
|
918
|
+
// The backend now returns just the item name, not the full path
|
|
919
|
+
let childPath;
|
|
920
|
+
if (searchPath === '.' || searchPath === '') {
|
|
921
|
+
// Root level - child path is just the name
|
|
922
|
+
childPath = child.name || child.path;
|
|
923
|
+
} else {
|
|
924
|
+
// Subdirectory - combine parent path with child name
|
|
925
|
+
// Use child.name (backend returns just the name) or fallback to child.path
|
|
926
|
+
const childName = child.name || child.path;
|
|
927
|
+
childPath = `${searchPath}/${childName}`;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return {
|
|
931
|
+
name: child.name,
|
|
932
|
+
path: childPath, // Use constructed path instead of child.path
|
|
933
|
+
type: child.type,
|
|
934
|
+
loaded: child.type === 'directory' ? false : undefined,
|
|
935
|
+
analyzed: child.type === 'file' ? false : undefined,
|
|
936
|
+
expanded: false,
|
|
937
|
+
children: child.type === 'directory' ? [] : undefined,
|
|
938
|
+
size: child.size,
|
|
939
|
+
has_code: child.has_code
|
|
940
|
+
};
|
|
941
|
+
});
|
|
942
|
+
node.loaded = true;
|
|
943
|
+
node.expanded = true;
|
|
944
|
+
|
|
945
|
+
// Find D3 node and remove loading pulse (use searchPath, not data.path)
|
|
946
|
+
const d3Node = this.findD3NodeByPath(searchPath);
|
|
947
|
+
if (d3Node) {
|
|
948
|
+
// Remove loading animation
|
|
949
|
+
if (this.loadingNodes.has(searchPath)) {
|
|
950
|
+
this.removeLoadingPulse(d3Node);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Update D3 hierarchy and redraw with expanded node
|
|
955
|
+
if (this.root && this.svg) {
|
|
956
|
+
// Store old root to preserve expansion state
|
|
957
|
+
const oldRoot = this.root;
|
|
958
|
+
|
|
959
|
+
// Recreate hierarchy with updated data
|
|
960
|
+
this.root = d3.hierarchy(this.treeData);
|
|
961
|
+
|
|
962
|
+
// Restore positions for smooth animation
|
|
963
|
+
this.root.x0 = this.height / 2;
|
|
964
|
+
this.root.y0 = 0;
|
|
965
|
+
|
|
966
|
+
// Preserve expansion state from old tree
|
|
967
|
+
this.preserveExpansionState(oldRoot, this.root);
|
|
968
|
+
|
|
969
|
+
// Find the D3 node again after hierarchy recreation
|
|
970
|
+
const updatedD3Node = this.findD3NodeByPath(searchPath);
|
|
971
|
+
if (updatedD3Node && updatedD3Node.data.children && updatedD3Node.data.children.length > 0) {
|
|
972
|
+
// Ensure the node is expanded to show children
|
|
973
|
+
updatedD3Node.children = updatedD3Node._children || updatedD3Node.children;
|
|
974
|
+
updatedD3Node._children = null;
|
|
975
|
+
// Mark data as expanded
|
|
976
|
+
updatedD3Node.data.expanded = true;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
this.update(this.root);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
this.updateBreadcrumb(`Loaded ${node.children.length} items from ${node.name}`, 'success');
|
|
983
|
+
this.updateStats();
|
|
984
|
+
} else if (!node) {
|
|
985
|
+
this.logAllPaths(this.treeData);
|
|
986
|
+
} else if (node && !data.children) {
|
|
987
|
+
// This might be a top-level directory discovery
|
|
988
|
+
const pathParts = data.path ? data.path.split('/').filter(p => p) : [];
|
|
989
|
+
const isTopLevel = pathParts.length === 1;
|
|
990
|
+
|
|
991
|
+
if (isTopLevel || data.forceAdd) {
|
|
992
|
+
const dirNode = {
|
|
993
|
+
name: data.name || pathParts[pathParts.length - 1] || 'Unknown',
|
|
994
|
+
path: data.path,
|
|
995
|
+
type: 'directory',
|
|
996
|
+
children: [],
|
|
997
|
+
loaded: false,
|
|
998
|
+
expanded: false,
|
|
999
|
+
stats: data.stats || {}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
this.addNodeToTree(dirNode, data.parent || '');
|
|
1003
|
+
this.updateBreadcrumb(`Discovered: ${data.path}`, 'info');
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
709
1006
|
}
|
|
710
|
-
|
|
1007
|
+
|
|
711
1008
|
/**
|
|
712
|
-
* Handle
|
|
1009
|
+
* Handle file discovered event
|
|
713
1010
|
*/
|
|
714
|
-
|
|
715
|
-
|
|
1011
|
+
onFileDiscovered(data) {
|
|
1012
|
+
// Update activity ticker
|
|
1013
|
+
const fileName = data.name || (data.path ? data.path.split('/').pop() : 'file');
|
|
1014
|
+
this.updateActivityTicker(`📄 Found: ${fileName}`);
|
|
716
1015
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1016
|
+
// Add to events display
|
|
1017
|
+
this.addEventToDisplay(`📄 Discovered: ${data.path || 'Unknown file'}`, 'info');
|
|
1018
|
+
|
|
1019
|
+
const pathParts = data.path ? data.path.split('/').filter(p => p) : [];
|
|
1020
|
+
const parentPath = pathParts.slice(0, -1).join('/');
|
|
1021
|
+
|
|
1022
|
+
const fileNode = {
|
|
1023
|
+
name: data.name || pathParts[pathParts.length - 1] || 'Unknown',
|
|
1024
|
+
path: data.path,
|
|
1025
|
+
type: 'file',
|
|
1026
|
+
language: data.language || this.detectLanguage(data.path),
|
|
1027
|
+
size: data.size || 0,
|
|
1028
|
+
lines: data.lines || 0,
|
|
1029
|
+
children: [],
|
|
1030
|
+
analyzed: false
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
this.addNodeToTree(fileNode, parentPath);
|
|
1034
|
+
this.stats.files++;
|
|
1035
|
+
this.updateStats();
|
|
1036
|
+
this.updateBreadcrumb(`Found: ${data.path}`, 'info');
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Handle file analyzed event
|
|
1041
|
+
*/
|
|
1042
|
+
onFileAnalyzed(data) {
|
|
1043
|
+
// Remove loading pulse if this file was being analyzed
|
|
1044
|
+
const d3Node = this.findD3NodeByPath(data.path);
|
|
1045
|
+
if (d3Node && this.loadingNodes.has(data.path)) {
|
|
1046
|
+
this.removeLoadingPulse(d3Node);
|
|
1047
|
+
}
|
|
1048
|
+
// Update activity ticker
|
|
1049
|
+
if (data.path) {
|
|
1050
|
+
const fileName = data.path.split('/').pop();
|
|
1051
|
+
this.updateActivityTicker(`🔍 Analyzed: ${fileName}`);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const fileNode = this.findNodeByPath(data.path);
|
|
1055
|
+
if (fileNode) {
|
|
1056
|
+
fileNode.analyzed = true;
|
|
1057
|
+
fileNode.complexity = data.complexity || 0;
|
|
1058
|
+
fileNode.lines = data.lines || 0;
|
|
1059
|
+
|
|
1060
|
+
// Add code elements as children
|
|
1061
|
+
if (data.elements && Array.isArray(data.elements)) {
|
|
1062
|
+
fileNode.children = data.elements.map(elem => ({
|
|
1063
|
+
name: elem.name,
|
|
1064
|
+
type: elem.type.toLowerCase(),
|
|
1065
|
+
path: `${data.path}#${elem.name}`,
|
|
1066
|
+
line: elem.line,
|
|
1067
|
+
complexity: elem.complexity || 1,
|
|
1068
|
+
docstring: elem.docstring || '',
|
|
1069
|
+
children: elem.methods ? elem.methods.map(m => ({
|
|
1070
|
+
name: m.name,
|
|
1071
|
+
type: 'method',
|
|
1072
|
+
path: `${data.path}#${elem.name}.${m.name}`,
|
|
1073
|
+
line: m.line,
|
|
1074
|
+
complexity: m.complexity || 1,
|
|
1075
|
+
docstring: m.docstring || ''
|
|
1076
|
+
})) : []
|
|
1077
|
+
}));
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Update stats
|
|
1081
|
+
if (data.stats) {
|
|
1082
|
+
this.stats.classes += data.stats.classes || 0;
|
|
1083
|
+
this.stats.functions += data.stats.functions || 0;
|
|
1084
|
+
this.stats.methods += data.stats.methods || 0;
|
|
1085
|
+
this.stats.lines += data.stats.lines || 0;
|
|
722
1086
|
}
|
|
723
|
-
|
|
1087
|
+
|
|
1088
|
+
this.updateStats();
|
|
1089
|
+
if (this.root) {
|
|
1090
|
+
this.update(this.root);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
this.updateBreadcrumb(`Analyzed: ${data.path}`, 'success');
|
|
724
1094
|
}
|
|
725
1095
|
}
|
|
726
|
-
|
|
1096
|
+
|
|
727
1097
|
/**
|
|
728
|
-
* Handle
|
|
1098
|
+
* Handle node found event
|
|
729
1099
|
*/
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1100
|
+
onNodeFound(data) {
|
|
1101
|
+
// Add to events display with appropriate icon
|
|
1102
|
+
const typeIcon = data.type === 'class' ? '🏛️' :
|
|
1103
|
+
data.type === 'function' ? '⚡' :
|
|
1104
|
+
data.type === 'method' ? '🔧' : '📦';
|
|
1105
|
+
this.addEventToDisplay(`${typeIcon} Found ${data.type || 'node'}: ${data.name || 'Unknown'}`);
|
|
1106
|
+
|
|
1107
|
+
// Extract node info
|
|
1108
|
+
const nodeInfo = {
|
|
1109
|
+
name: data.name || 'Unknown',
|
|
1110
|
+
type: (data.type || 'unknown').toLowerCase(),
|
|
1111
|
+
path: data.path || '',
|
|
1112
|
+
line: data.line || 0,
|
|
1113
|
+
complexity: data.complexity || 1,
|
|
1114
|
+
docstring: data.docstring || ''
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// Map event types to our internal types
|
|
1118
|
+
const typeMapping = {
|
|
1119
|
+
'class': 'class',
|
|
1120
|
+
'function': 'function',
|
|
1121
|
+
'method': 'method',
|
|
1122
|
+
'module': 'module',
|
|
1123
|
+
'file': 'file',
|
|
1124
|
+
'directory': 'directory'
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
nodeInfo.type = typeMapping[nodeInfo.type] || nodeInfo.type;
|
|
1128
|
+
|
|
1129
|
+
// Determine parent path
|
|
1130
|
+
let parentPath = '';
|
|
1131
|
+
if (data.parent_path) {
|
|
1132
|
+
parentPath = data.parent_path;
|
|
1133
|
+
} else if (data.file_path) {
|
|
1134
|
+
parentPath = data.file_path;
|
|
1135
|
+
} else if (nodeInfo.path.includes('/')) {
|
|
1136
|
+
const parts = nodeInfo.path.split('/');
|
|
1137
|
+
parts.pop();
|
|
1138
|
+
parentPath = parts.join('/');
|
|
735
1139
|
}
|
|
1140
|
+
|
|
1141
|
+
// Update stats based on node type
|
|
1142
|
+
switch(nodeInfo.type) {
|
|
1143
|
+
case 'class':
|
|
1144
|
+
this.stats.classes++;
|
|
1145
|
+
break;
|
|
1146
|
+
case 'function':
|
|
1147
|
+
this.stats.functions++;
|
|
1148
|
+
break;
|
|
1149
|
+
case 'method':
|
|
1150
|
+
this.stats.methods++;
|
|
1151
|
+
break;
|
|
1152
|
+
case 'file':
|
|
1153
|
+
this.stats.files++;
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Add node to tree
|
|
1158
|
+
this.addNodeToTree(nodeInfo, parentPath);
|
|
1159
|
+
this.updateStats();
|
|
1160
|
+
|
|
1161
|
+
// Show progress in breadcrumb
|
|
1162
|
+
const elementType = nodeInfo.type.charAt(0).toUpperCase() + nodeInfo.type.slice(1);
|
|
1163
|
+
this.updateBreadcrumb(`Found ${elementType}: ${nodeInfo.name}`, 'info');
|
|
736
1164
|
}
|
|
737
|
-
|
|
1165
|
+
|
|
738
1166
|
/**
|
|
739
|
-
* Handle
|
|
1167
|
+
* Handle progress update
|
|
740
1168
|
*/
|
|
741
|
-
|
|
742
|
-
|
|
1169
|
+
onProgressUpdate(data) {
|
|
1170
|
+
const progress = data.progress || 0;
|
|
1171
|
+
const message = data.message || `Processing... ${progress}%`;
|
|
743
1172
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1173
|
+
this.updateBreadcrumb(message, 'info');
|
|
1174
|
+
|
|
1175
|
+
// Update progress bar if it exists
|
|
1176
|
+
const progressBar = document.querySelector('.code-tree-progress');
|
|
1177
|
+
if (progressBar) {
|
|
1178
|
+
progressBar.style.width = `${progress}%`;
|
|
747
1179
|
}
|
|
748
1180
|
}
|
|
749
|
-
|
|
1181
|
+
|
|
750
1182
|
/**
|
|
751
|
-
*
|
|
1183
|
+
* Handle analysis complete event
|
|
752
1184
|
*/
|
|
753
|
-
|
|
754
|
-
|
|
1185
|
+
onAnalysisComplete(data) {
|
|
1186
|
+
this.analyzing = false;
|
|
1187
|
+
this.hideLoading();
|
|
755
1188
|
|
|
756
|
-
//
|
|
757
|
-
|
|
1189
|
+
// Update activity ticker
|
|
1190
|
+
this.updateActivityTicker('✅ Ready', 'success');
|
|
758
1191
|
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1192
|
+
// Add completion event
|
|
1193
|
+
this.addEventToDisplay('✅ Analysis complete!', 'success');
|
|
1194
|
+
|
|
1195
|
+
// Update tree visualization
|
|
1196
|
+
if (this.root && this.svg) {
|
|
1197
|
+
this.update(this.root);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Update stats from completion data
|
|
1201
|
+
if (data.stats) {
|
|
1202
|
+
this.stats = { ...this.stats, ...data.stats };
|
|
1203
|
+
this.updateStats();
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const message = data.message || `Analysis complete: ${this.stats.files} files, ${this.stats.classes} classes, ${this.stats.functions} functions`;
|
|
1207
|
+
this.updateBreadcrumb(message, 'success');
|
|
1208
|
+
this.showNotification(message, 'success');
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Handle analysis error
|
|
1213
|
+
*/
|
|
1214
|
+
onAnalysisError(data) {
|
|
1215
|
+
this.analyzing = false;
|
|
1216
|
+
this.hideLoading();
|
|
1217
|
+
|
|
1218
|
+
const message = data.message || data.error || 'Analysis failed';
|
|
1219
|
+
this.updateBreadcrumb(message, 'error');
|
|
1220
|
+
this.showNotification(message, 'error');
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Handle analysis accepted
|
|
1225
|
+
*/
|
|
1226
|
+
onAnalysisAccepted(data) {
|
|
1227
|
+
const message = data.message || 'Analysis request accepted';
|
|
1228
|
+
this.updateBreadcrumb(message, 'info');
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Handle analysis queued
|
|
1233
|
+
*/
|
|
1234
|
+
onAnalysisQueued(data) {
|
|
1235
|
+
const position = data.position || 0;
|
|
1236
|
+
const message = `Analysis queued (position ${position})`;
|
|
1237
|
+
this.updateBreadcrumb(message, 'warning');
|
|
1238
|
+
this.showNotification(message, 'info');
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Handle INFO events for granular work tracking
|
|
1243
|
+
*/
|
|
1244
|
+
onInfoEvent(data) {
|
|
1245
|
+
// Log to console for debugging
|
|
1246
|
+
|
|
1247
|
+
// Update breadcrumb for certain events
|
|
1248
|
+
if (data.type && data.type.startsWith('discovery.')) {
|
|
1249
|
+
// Discovery events
|
|
1250
|
+
if (data.type === 'discovery.start') {
|
|
1251
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1252
|
+
} else if (data.type === 'discovery.complete') {
|
|
1253
|
+
this.updateBreadcrumb(data.message, 'success');
|
|
1254
|
+
// Show stats if available
|
|
1255
|
+
if (data.stats) {
|
|
1256
|
+
}
|
|
1257
|
+
} else if (data.type === 'discovery.directory' || data.type === 'discovery.file') {
|
|
1258
|
+
// Quick flash of discovery events
|
|
1259
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1260
|
+
}
|
|
1261
|
+
} else if (data.type && data.type.startsWith('analysis.')) {
|
|
1262
|
+
// Analysis events
|
|
1263
|
+
if (data.type === 'analysis.start') {
|
|
1264
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1265
|
+
} else if (data.type === 'analysis.complete') {
|
|
1266
|
+
this.updateBreadcrumb(data.message, 'success');
|
|
1267
|
+
// Show stats if available
|
|
1268
|
+
if (data.stats) {
|
|
1269
|
+
const statsMsg = `Found: ${data.stats.classes || 0} classes, ${data.stats.functions || 0} functions, ${data.stats.methods || 0} methods`;
|
|
1270
|
+
}
|
|
1271
|
+
} else if (data.type === 'analysis.class' || data.type === 'analysis.function' || data.type === 'analysis.method') {
|
|
1272
|
+
// Show found elements briefly
|
|
1273
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1274
|
+
} else if (data.type === 'analysis.parse') {
|
|
1275
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1276
|
+
}
|
|
1277
|
+
} else if (data.type && data.type.startsWith('filter.')) {
|
|
1278
|
+
// Filter events - optionally show in debug mode
|
|
1279
|
+
if (window.debugMode || this.showFilterEvents) {
|
|
1280
|
+
console.debug('[FILTER]', data.type, data.path, data.reason);
|
|
1281
|
+
if (this.showFilterEvents) {
|
|
1282
|
+
this.updateBreadcrumb(data.message, 'warning');
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
} else if (data.type && data.type.startsWith('cache.')) {
|
|
1286
|
+
// Cache events
|
|
1287
|
+
if (data.type === 'cache.hit') {
|
|
1288
|
+
console.debug('[CACHE HIT]', data.file);
|
|
1289
|
+
if (this.showCacheEvents) {
|
|
1290
|
+
this.updateBreadcrumb(data.message, 'info');
|
|
1291
|
+
}
|
|
1292
|
+
} else if (data.type === 'cache.miss') {
|
|
1293
|
+
console.debug('[CACHE MISS]', data.file);
|
|
765
1294
|
}
|
|
766
|
-
|
|
767
|
-
notification = document.createElement('div');
|
|
768
|
-
notification.className = 'notification-area';
|
|
769
|
-
notification.style.cssText = `
|
|
770
|
-
position: absolute;
|
|
771
|
-
top: 10px;
|
|
772
|
-
right: 10px;
|
|
773
|
-
max-width: 400px;
|
|
774
|
-
z-index: 1000;
|
|
775
|
-
padding: 12px 16px;
|
|
776
|
-
border-radius: 4px;
|
|
777
|
-
font-size: 14px;
|
|
778
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
779
|
-
transition: opacity 0.3s ease;
|
|
780
|
-
`;
|
|
781
|
-
codeTab.insertBefore(notification, codeTab.firstChild);
|
|
782
1295
|
}
|
|
783
1296
|
|
|
784
|
-
//
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1297
|
+
// Optionally add to an event log display if enabled
|
|
1298
|
+
if (this.eventLogEnabled && data.message) {
|
|
1299
|
+
this.addEventToDisplay(data);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Add event to display log (if we have one)
|
|
1305
|
+
*/
|
|
1306
|
+
addEventToDisplay(data) {
|
|
1307
|
+
// Could be implemented to show events in a dedicated log area
|
|
1308
|
+
// For now, just maintain a recent events list
|
|
1309
|
+
if (!this.recentEvents) {
|
|
1310
|
+
this.recentEvents = [];
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
this.recentEvents.unshift({
|
|
1314
|
+
timestamp: data.timestamp || new Date().toISOString(),
|
|
1315
|
+
type: data.type,
|
|
1316
|
+
message: data.message,
|
|
1317
|
+
data: data
|
|
1318
|
+
});
|
|
791
1319
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1320
|
+
// Keep only last 100 events
|
|
1321
|
+
if (this.recentEvents.length > 100) {
|
|
1322
|
+
this.recentEvents.pop();
|
|
1323
|
+
}
|
|
796
1324
|
|
|
797
|
-
//
|
|
1325
|
+
// Could update a UI element here if we had an event log display
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Handle analysis cancelled
|
|
1330
|
+
*/
|
|
1331
|
+
onAnalysisCancelled(data) {
|
|
1332
|
+
this.analyzing = false;
|
|
1333
|
+
this.hideLoading();
|
|
1334
|
+
const message = data.message || 'Analysis cancelled';
|
|
1335
|
+
this.updateBreadcrumb(message, 'warning');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Show notification toast
|
|
1340
|
+
*/
|
|
1341
|
+
showNotification(message, type = 'info') {
|
|
1342
|
+
const notification = document.createElement('div');
|
|
1343
|
+
notification.className = `code-tree-notification ${type}`;
|
|
798
1344
|
notification.textContent = message;
|
|
799
|
-
notification.style.display = 'block';
|
|
800
|
-
notification.style.opacity = '1';
|
|
801
1345
|
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
notification.style.
|
|
1346
|
+
// Change from appending to container to positioning absolutely within it
|
|
1347
|
+
const container = document.getElementById('code-tree-container');
|
|
1348
|
+
if (container) {
|
|
1349
|
+
// Position relative to the container
|
|
1350
|
+
notification.style.position = 'absolute';
|
|
1351
|
+
notification.style.top = '10px';
|
|
1352
|
+
notification.style.right = '10px';
|
|
1353
|
+
notification.style.zIndex = '1000';
|
|
1354
|
+
|
|
1355
|
+
// Ensure container is positioned
|
|
1356
|
+
if (!container.style.position || container.style.position === 'static') {
|
|
1357
|
+
container.style.position = 'relative';
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
container.appendChild(notification);
|
|
1361
|
+
|
|
1362
|
+
// Animate out after 3 seconds
|
|
810
1363
|
setTimeout(() => {
|
|
811
|
-
notification.style.
|
|
812
|
-
|
|
813
|
-
|
|
1364
|
+
notification.style.animation = 'slideOutRight 0.3s ease';
|
|
1365
|
+
setTimeout(() => notification.remove(), 300);
|
|
1366
|
+
}, 3000);
|
|
1367
|
+
}
|
|
814
1368
|
}
|
|
815
1369
|
|
|
816
1370
|
/**
|
|
817
1371
|
* Add node to tree structure
|
|
818
1372
|
*/
|
|
819
|
-
addNodeToTree(
|
|
820
|
-
//
|
|
821
|
-
|
|
822
|
-
if (
|
|
823
|
-
|
|
1373
|
+
addNodeToTree(nodeInfo, parentPath = '') {
|
|
1374
|
+
// CRITICAL: Validate that nodeInfo.path doesn't contain absolute paths
|
|
1375
|
+
// The backend should only send relative paths now
|
|
1376
|
+
if (nodeInfo.path && nodeInfo.path.startsWith('/')) {
|
|
1377
|
+
console.error('Absolute path detected in node, skipping:', nodeInfo.path);
|
|
1378
|
+
return;
|
|
824
1379
|
}
|
|
825
1380
|
|
|
826
|
-
//
|
|
827
|
-
if (
|
|
828
|
-
console.
|
|
1381
|
+
// Also validate parent path
|
|
1382
|
+
if (parentPath && parentPath.startsWith('/')) {
|
|
1383
|
+
console.error('Absolute path detected in parent, skipping:', parentPath);
|
|
829
1384
|
return;
|
|
830
1385
|
}
|
|
831
1386
|
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
|
|
1387
|
+
// Find parent node
|
|
1388
|
+
let parentNode = this.treeData;
|
|
1389
|
+
|
|
1390
|
+
if (parentPath) {
|
|
1391
|
+
parentNode = this.findNodeByPath(parentPath);
|
|
1392
|
+
if (!parentNode) {
|
|
1393
|
+
// CRITICAL: Do NOT create parent structure if it doesn't exist
|
|
1394
|
+
// This prevents creating nodes above the working directory
|
|
1395
|
+
console.warn('Parent node not found, skipping node creation:', parentPath);
|
|
1396
|
+
console.warn('Attempted to add node:', nodeInfo);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// Check if node already exists
|
|
1402
|
+
const existingNode = parentNode.children?.find(c =>
|
|
1403
|
+
c.path === nodeInfo.path ||
|
|
1404
|
+
(c.name === nodeInfo.name && c.type === nodeInfo.type)
|
|
1405
|
+
);
|
|
1406
|
+
|
|
1407
|
+
if (existingNode) {
|
|
1408
|
+
// Update existing node
|
|
1409
|
+
Object.assign(existingNode, nodeInfo);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// Add new node
|
|
1414
|
+
if (!parentNode.children) {
|
|
1415
|
+
parentNode.children = [];
|
|
835
1416
|
}
|
|
836
|
-
parent.children.push(node);
|
|
837
1417
|
|
|
838
|
-
//
|
|
839
|
-
|
|
1418
|
+
// Ensure the node has a children array
|
|
1419
|
+
if (!nodeInfo.children) {
|
|
1420
|
+
nodeInfo.children = [];
|
|
1421
|
+
}
|
|
840
1422
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Create new hierarchy
|
|
1423
|
+
parentNode.children.push(nodeInfo);
|
|
1424
|
+
|
|
1425
|
+
// Store node reference for quick access
|
|
1426
|
+
this.nodes.set(nodeInfo.path, nodeInfo);
|
|
1427
|
+
|
|
1428
|
+
// Update tree if initialized
|
|
1429
|
+
if (this.root && this.svg) {
|
|
1430
|
+
// Recreate hierarchy with new data
|
|
854
1431
|
this.root = d3.hierarchy(this.treeData);
|
|
855
1432
|
this.root.x0 = this.height / 2;
|
|
856
1433
|
this.root.y0 = 0;
|
|
857
1434
|
|
|
858
|
-
//
|
|
859
|
-
this.
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
1435
|
+
// Update only if we have a reasonable number of nodes to avoid performance issues
|
|
1436
|
+
if (this.nodes.size < 1000) {
|
|
1437
|
+
this.update(this.root);
|
|
1438
|
+
} else if (this.nodes.size % 100 === 0) {
|
|
1439
|
+
// Update every 100 nodes for large trees
|
|
1440
|
+
this.update(this.root);
|
|
1441
|
+
}
|
|
865
1442
|
}
|
|
866
1443
|
}
|
|
867
1444
|
|
|
868
1445
|
/**
|
|
869
|
-
* Find node by path
|
|
1446
|
+
* Find node by path in tree
|
|
1447
|
+
*/
|
|
1448
|
+
findNodeByPath(path, node = null) {
|
|
1449
|
+
if (!node) {
|
|
1450
|
+
node = this.treeData;
|
|
1451
|
+
// Searching for node by path
|
|
1452
|
+
// Root node identified
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (node.path === path) {
|
|
1456
|
+
return node;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (node.children) {
|
|
1460
|
+
for (const child of node.children) {
|
|
1461
|
+
const found = this.findNodeByPath(path, child);
|
|
1462
|
+
if (found) {
|
|
1463
|
+
return found;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (!node.parent) {
|
|
1469
|
+
// Node path not found in current tree structure
|
|
1470
|
+
}
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Helper to log all paths in tree for debugging
|
|
1476
|
+
*/
|
|
1477
|
+
logAllPaths(node, indent = '') {
|
|
1478
|
+
console.log(`${indent}${node.path} (${node.name})`);
|
|
1479
|
+
if (node.children) {
|
|
1480
|
+
for (const child of node.children) {
|
|
1481
|
+
this.logAllPaths(child, indent + ' ');
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Find D3 hierarchy node by path
|
|
870
1488
|
*/
|
|
871
|
-
|
|
872
|
-
|
|
1489
|
+
findD3NodeByPath(path) {
|
|
1490
|
+
if (!this.root) return null;
|
|
1491
|
+
return this.root.descendants().find(d => d.data.path === path);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Preserve expansion state when recreating hierarchy
|
|
1496
|
+
*/
|
|
1497
|
+
preserveExpansionState(oldRoot, newRoot) {
|
|
1498
|
+
if (!oldRoot || !newRoot) return;
|
|
1499
|
+
|
|
1500
|
+
// Create a map of expanded nodes from the old tree
|
|
1501
|
+
const expansionMap = new Map();
|
|
1502
|
+
oldRoot.descendants().forEach(node => {
|
|
1503
|
+
if (node.data.expanded || (node.children && !node._children)) {
|
|
1504
|
+
expansionMap.set(node.data.path, true);
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
// Apply expansion state to new tree
|
|
1509
|
+
newRoot.descendants().forEach(node => {
|
|
1510
|
+
if (expansionMap.has(node.data.path)) {
|
|
1511
|
+
node.children = node._children || node.children;
|
|
1512
|
+
node._children = null;
|
|
1513
|
+
node.data.expanded = true;
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
873
1516
|
}
|
|
874
1517
|
|
|
875
1518
|
/**
|
|
876
|
-
* Update
|
|
1519
|
+
* Update statistics display
|
|
877
1520
|
*/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1521
|
+
updateStats() {
|
|
1522
|
+
// Update stats display - use correct IDs from HTML
|
|
1523
|
+
const statsElements = {
|
|
1524
|
+
'file-count': this.stats.files,
|
|
1525
|
+
'class-count': this.stats.classes,
|
|
1526
|
+
'function-count': this.stats.functions,
|
|
1527
|
+
'line-count': this.stats.lines
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
for (const [id, value] of Object.entries(statsElements)) {
|
|
1531
|
+
const elem = document.getElementById(id);
|
|
1532
|
+
if (elem) {
|
|
1533
|
+
elem.textContent = value.toLocaleString();
|
|
885
1534
|
}
|
|
886
|
-
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Update progress text
|
|
1538
|
+
const progressText = document.getElementById('code-progress-text');
|
|
1539
|
+
if (progressText) {
|
|
1540
|
+
const statusText = this.analyzing ?
|
|
1541
|
+
`Analyzing... ${this.stats.files} files processed` :
|
|
1542
|
+
`Ready - ${this.stats.files} files in tree`;
|
|
1543
|
+
progressText.textContent = statusText;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Update breadcrumb trail
|
|
1549
|
+
*/
|
|
1550
|
+
updateBreadcrumb(message, type = 'info') {
|
|
1551
|
+
const breadcrumbContent = document.getElementById('breadcrumb-content');
|
|
1552
|
+
if (breadcrumbContent) {
|
|
1553
|
+
breadcrumbContent.textContent = message;
|
|
1554
|
+
breadcrumbContent.className = `breadcrumb-${type}`;
|
|
887
1555
|
}
|
|
888
1556
|
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* Detect language from file extension
|
|
1560
|
+
*/
|
|
1561
|
+
detectLanguage(filePath) {
|
|
1562
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
1563
|
+
const languageMap = {
|
|
1564
|
+
'py': 'python',
|
|
1565
|
+
'js': 'javascript',
|
|
1566
|
+
'ts': 'typescript',
|
|
1567
|
+
'jsx': 'javascript',
|
|
1568
|
+
'tsx': 'typescript',
|
|
1569
|
+
'java': 'java',
|
|
1570
|
+
'cpp': 'cpp',
|
|
1571
|
+
'c': 'c',
|
|
1572
|
+
'cs': 'csharp',
|
|
1573
|
+
'rb': 'ruby',
|
|
1574
|
+
'go': 'go',
|
|
1575
|
+
'rs': 'rust',
|
|
1576
|
+
'php': 'php',
|
|
1577
|
+
'swift': 'swift',
|
|
1578
|
+
'kt': 'kotlin',
|
|
1579
|
+
'scala': 'scala',
|
|
1580
|
+
'r': 'r',
|
|
1581
|
+
'sh': 'bash',
|
|
1582
|
+
'ps1': 'powershell'
|
|
1583
|
+
};
|
|
1584
|
+
return languageMap[ext] || 'unknown';
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Add visualization controls for layout toggle
|
|
1589
|
+
*/
|
|
1590
|
+
addVisualizationControls() {
|
|
1591
|
+
const controls = this.svg.append('g')
|
|
1592
|
+
.attr('class', 'viz-controls')
|
|
1593
|
+
.attr('transform', 'translate(10, 10)');
|
|
1594
|
+
|
|
1595
|
+
// Add layout toggle button
|
|
1596
|
+
const toggleButton = controls.append('g')
|
|
1597
|
+
.attr('class', 'layout-toggle')
|
|
1598
|
+
.style('cursor', 'pointer')
|
|
1599
|
+
.on('click', () => this.toggleLayout());
|
|
1600
|
+
|
|
1601
|
+
toggleButton.append('rect')
|
|
1602
|
+
.attr('width', 120)
|
|
1603
|
+
.attr('height', 30)
|
|
1604
|
+
.attr('rx', 5)
|
|
1605
|
+
.attr('fill', '#3b82f6')
|
|
1606
|
+
.attr('opacity', 0.8);
|
|
1607
|
+
|
|
1608
|
+
toggleButton.append('text')
|
|
1609
|
+
.attr('x', 60)
|
|
1610
|
+
.attr('y', 20)
|
|
1611
|
+
.attr('text-anchor', 'middle')
|
|
1612
|
+
.attr('fill', 'white')
|
|
1613
|
+
.style('font-size', '12px')
|
|
1614
|
+
.text(this.isRadialLayout ? 'Switch to Linear' : 'Switch to Radial');
|
|
1615
|
+
}
|
|
889
1616
|
|
|
890
1617
|
/**
|
|
891
|
-
*
|
|
1618
|
+
* Toggle between radial and linear layouts
|
|
892
1619
|
*/
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1620
|
+
toggleLayout() {
|
|
1621
|
+
this.isRadialLayout = !this.isRadialLayout;
|
|
1622
|
+
this.createVisualization();
|
|
1623
|
+
if (this.root) {
|
|
1624
|
+
this.update(this.root);
|
|
1625
|
+
}
|
|
1626
|
+
this.showNotification(
|
|
1627
|
+
this.isRadialLayout ? 'Switched to radial layout' : 'Switched to linear layout',
|
|
1628
|
+
'info'
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Convert radial coordinates to Cartesian
|
|
1634
|
+
*/
|
|
1635
|
+
radialPoint(x, y) {
|
|
1636
|
+
return [(y = +y) * Math.cos(x -= Math.PI / 2), y * Math.sin(x)];
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Update D3 tree visualization
|
|
1641
|
+
*/
|
|
1642
|
+
update(source) {
|
|
1643
|
+
if (!this.treeLayout || !this.treeGroup || !source) {
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Compute the new tree layout
|
|
1648
|
+
const treeData = this.treeLayout(this.root);
|
|
1649
|
+
const nodes = treeData.descendants();
|
|
1650
|
+
const links = treeData.descendants().slice(1);
|
|
1651
|
+
|
|
1652
|
+
if (this.isRadialLayout) {
|
|
1653
|
+
// Radial layout adjustments
|
|
1654
|
+
nodes.forEach(d => {
|
|
1655
|
+
// Store original x,y for transitions
|
|
1656
|
+
if (d.x0 === undefined) {
|
|
1657
|
+
d.x0 = d.x;
|
|
1658
|
+
d.y0 = d.y;
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
} else {
|
|
1662
|
+
// Linear layout with nodeSize doesn't need manual normalization
|
|
1663
|
+
// The tree layout handles spacing automatically
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// Update nodes
|
|
1667
|
+
const node = this.treeGroup.selectAll('g.node')
|
|
1668
|
+
.data(nodes, d => d.id || (d.id = ++this.nodeId));
|
|
1669
|
+
|
|
1670
|
+
// Enter new nodes
|
|
1671
|
+
const nodeEnter = node.enter().append('g')
|
|
1672
|
+
.attr('class', 'node')
|
|
1673
|
+
.attr('transform', d => {
|
|
1674
|
+
if (this.isRadialLayout) {
|
|
1675
|
+
const [x, y] = this.radialPoint(source.x0 || 0, source.y0 || 0);
|
|
1676
|
+
return `translate(${x},${y})`;
|
|
1677
|
+
} else {
|
|
1678
|
+
return `translate(${source.y0},${source.x0})`;
|
|
1679
|
+
}
|
|
1680
|
+
})
|
|
1681
|
+
.on('click', (event, d) => this.onNodeClick(event, d));
|
|
1682
|
+
|
|
1683
|
+
// Add circles for nodes
|
|
1684
|
+
nodeEnter.append('circle')
|
|
1685
|
+
.attr('class', 'node-circle')
|
|
1686
|
+
.attr('r', 1e-6)
|
|
1687
|
+
.style('fill', d => this.getNodeColor(d))
|
|
1688
|
+
.style('stroke', d => this.getNodeStrokeColor(d))
|
|
1689
|
+
.style('stroke-width', 2)
|
|
1690
|
+
.on('mouseover', (event, d) => this.showTooltip(event, d))
|
|
1691
|
+
.on('mouseout', () => this.hideTooltip());
|
|
1692
|
+
|
|
1693
|
+
// Add labels for nodes with smart positioning
|
|
1694
|
+
nodeEnter.append('text')
|
|
1695
|
+
.attr('class', 'node-label')
|
|
1696
|
+
.attr('dy', '.35em')
|
|
1697
|
+
.attr('x', d => {
|
|
1698
|
+
if (this.isRadialLayout) {
|
|
1699
|
+
// For radial layout, initial position
|
|
1700
|
+
return 0;
|
|
1701
|
+
} else {
|
|
1702
|
+
// Linear layout: standard positioning
|
|
1703
|
+
return d.children || d._children ? -13 : 13;
|
|
1704
|
+
}
|
|
1705
|
+
})
|
|
1706
|
+
.attr('text-anchor', d => {
|
|
1707
|
+
if (this.isRadialLayout) {
|
|
1708
|
+
return 'start'; // Will be adjusted in update
|
|
1709
|
+
} else {
|
|
1710
|
+
// Linear layout: standard anchoring
|
|
1711
|
+
return d.children || d._children ? 'end' : 'start';
|
|
1712
|
+
}
|
|
1713
|
+
})
|
|
1714
|
+
.text(d => {
|
|
1715
|
+
// Truncate long names
|
|
1716
|
+
const maxLength = 20;
|
|
1717
|
+
const name = d.data.name || '';
|
|
1718
|
+
return name.length > maxLength ?
|
|
1719
|
+
name.substring(0, maxLength - 3) + '...' : name;
|
|
1720
|
+
})
|
|
1721
|
+
.style('fill-opacity', 1e-6)
|
|
1722
|
+
.style('font-size', '12px')
|
|
1723
|
+
.style('font-family', '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif')
|
|
1724
|
+
.style('text-shadow', '1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8)');
|
|
1725
|
+
|
|
1726
|
+
// Add icons for node types
|
|
1727
|
+
nodeEnter.append('text')
|
|
1728
|
+
.attr('class', 'node-icon')
|
|
1729
|
+
.attr('dy', '.35em')
|
|
1730
|
+
.attr('x', 0)
|
|
1731
|
+
.attr('text-anchor', 'middle')
|
|
1732
|
+
.text(d => this.getNodeIcon(d))
|
|
1733
|
+
.style('font-size', '10px')
|
|
1734
|
+
.style('fill', 'white');
|
|
1735
|
+
|
|
1736
|
+
// Transition to new positions
|
|
1737
|
+
const nodeUpdate = nodeEnter.merge(node);
|
|
1738
|
+
|
|
1739
|
+
nodeUpdate.transition()
|
|
1740
|
+
.duration(this.duration)
|
|
1741
|
+
.attr('transform', d => {
|
|
1742
|
+
if (this.isRadialLayout) {
|
|
1743
|
+
const [x, y] = this.radialPoint(d.x, d.y);
|
|
1744
|
+
return `translate(${x},${y})`;
|
|
1745
|
+
} else {
|
|
1746
|
+
return `translate(${d.y},${d.x})`;
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
nodeUpdate.select('circle.node-circle')
|
|
1751
|
+
.attr('r', 8)
|
|
1752
|
+
.style('fill', d => this.getNodeColor(d))
|
|
1753
|
+
.style('stroke', d => this.getNodeStrokeColor(d))
|
|
1754
|
+
.attr('cursor', 'pointer');
|
|
1755
|
+
|
|
1756
|
+
// Update text labels with proper rotation for radial layout
|
|
1757
|
+
const isRadial = this.isRadialLayout; // Capture the layout type
|
|
1758
|
+
nodeUpdate.select('text.node-label')
|
|
1759
|
+
.style('fill-opacity', 1)
|
|
1760
|
+
.style('fill', '#333')
|
|
1761
|
+
.each(function(d) {
|
|
1762
|
+
const selection = d3.select(this);
|
|
1763
|
+
|
|
1764
|
+
if (isRadial) {
|
|
1765
|
+
// For radial layout, apply rotation and positioning
|
|
1766
|
+
const angle = (d.x * 180 / Math.PI) - 90; // Convert to degrees
|
|
1767
|
+
|
|
1768
|
+
// Determine if text should be flipped (left side of circle)
|
|
1769
|
+
const shouldFlip = angle > 90 || angle < -90;
|
|
1770
|
+
|
|
1771
|
+
// Calculate text position and rotation
|
|
1772
|
+
if (shouldFlip) {
|
|
1773
|
+
// Text on left side - rotate 180 degrees to read properly
|
|
1774
|
+
selection
|
|
1775
|
+
.attr('transform', `rotate(${angle + 180})`)
|
|
1776
|
+
.attr('x', -15) // Negative offset for flipped text
|
|
1777
|
+
.attr('text-anchor', 'end')
|
|
1778
|
+
.attr('dy', '.35em');
|
|
1779
|
+
} else {
|
|
1780
|
+
// Text on right side - normal orientation
|
|
1781
|
+
selection
|
|
1782
|
+
.attr('transform', `rotate(${angle})`)
|
|
1783
|
+
.attr('x', 15) // Positive offset for normal text
|
|
1784
|
+
.attr('text-anchor', 'start')
|
|
1785
|
+
.attr('dy', '.35em');
|
|
1786
|
+
}
|
|
1787
|
+
} else {
|
|
1788
|
+
// Linear layout - no rotation needed
|
|
1789
|
+
selection
|
|
1790
|
+
.attr('transform', null)
|
|
1791
|
+
.attr('x', d.children || d._children ? -13 : 13)
|
|
1792
|
+
.attr('text-anchor', d.children || d._children ? 'end' : 'start')
|
|
1793
|
+
.attr('dy', '.35em');
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
// Remove exiting nodes
|
|
1798
|
+
const nodeExit = node.exit().transition()
|
|
1799
|
+
.duration(this.duration)
|
|
1800
|
+
.attr('transform', d => {
|
|
1801
|
+
if (this.isRadialLayout) {
|
|
1802
|
+
const [x, y] = this.radialPoint(source.x, source.y);
|
|
1803
|
+
return `translate(${x},${y})`;
|
|
1804
|
+
} else {
|
|
1805
|
+
return `translate(${source.y},${source.x})`;
|
|
1806
|
+
}
|
|
1807
|
+
})
|
|
1808
|
+
.remove();
|
|
1809
|
+
|
|
1810
|
+
nodeExit.select('circle')
|
|
1811
|
+
.attr('r', 1e-6);
|
|
1812
|
+
|
|
1813
|
+
nodeExit.select('text.node-label')
|
|
1814
|
+
.style('fill-opacity', 1e-6);
|
|
896
1815
|
|
|
897
|
-
|
|
898
|
-
|
|
1816
|
+
nodeExit.select('text.node-icon')
|
|
1817
|
+
.style('fill-opacity', 1e-6);
|
|
1818
|
+
|
|
1819
|
+
// Update links
|
|
1820
|
+
const link = this.treeGroup.selectAll('path.link')
|
|
1821
|
+
.data(links, d => d.id);
|
|
1822
|
+
|
|
1823
|
+
// Enter new links
|
|
1824
|
+
const linkEnter = link.enter().insert('path', 'g')
|
|
1825
|
+
.attr('class', 'link')
|
|
1826
|
+
.attr('d', d => {
|
|
1827
|
+
const o = {x: source.x0, y: source.y0};
|
|
1828
|
+
return this.isRadialLayout ?
|
|
1829
|
+
this.radialDiagonal(o, o) :
|
|
1830
|
+
this.diagonal(o, o);
|
|
1831
|
+
})
|
|
1832
|
+
.style('fill', 'none')
|
|
1833
|
+
.style('stroke', '#ccc')
|
|
1834
|
+
.style('stroke-width', 2);
|
|
1835
|
+
|
|
1836
|
+
// Transition to new positions
|
|
1837
|
+
const linkUpdate = linkEnter.merge(link);
|
|
1838
|
+
|
|
1839
|
+
linkUpdate.transition()
|
|
1840
|
+
.duration(this.duration)
|
|
1841
|
+
.attr('d', d => this.isRadialLayout ?
|
|
1842
|
+
this.radialDiagonal(d, d.parent) :
|
|
1843
|
+
this.diagonal(d, d.parent));
|
|
1844
|
+
|
|
1845
|
+
// Remove exiting links
|
|
1846
|
+
link.exit().transition()
|
|
1847
|
+
.duration(this.duration)
|
|
1848
|
+
.attr('d', d => {
|
|
1849
|
+
const o = {x: source.x, y: source.y};
|
|
1850
|
+
return this.isRadialLayout ?
|
|
1851
|
+
this.radialDiagonal(o, o) :
|
|
1852
|
+
this.diagonal(o, o);
|
|
1853
|
+
})
|
|
1854
|
+
.remove();
|
|
1855
|
+
|
|
1856
|
+
// Store old positions for transition
|
|
1857
|
+
nodes.forEach(d => {
|
|
1858
|
+
d.x0 = d.x;
|
|
1859
|
+
d.y0 = d.y;
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/**
|
|
1864
|
+
* Center the view on a specific node (Linear layout)
|
|
1865
|
+
*/
|
|
1866
|
+
centerOnNode(d) {
|
|
1867
|
+
if (!this.svg || !this.zoom) return;
|
|
1868
|
+
|
|
1869
|
+
// Get current transform or use default zoom level
|
|
1870
|
+
const currentTransform = d3.zoomTransform(this.svg.node());
|
|
1871
|
+
// Zoom in to 2x for better focus on clicked node
|
|
1872
|
+
const targetScale = currentTransform.k < 2 ? 2 : currentTransform.k;
|
|
1873
|
+
|
|
1874
|
+
// Account for the initial tree group offset
|
|
1875
|
+
const initialOffsetX = this.margin.left + 100;
|
|
1876
|
+
const initialOffsetY = this.height / 2;
|
|
1877
|
+
|
|
1878
|
+
// Calculate position to center the node accounting for the tree group's transform
|
|
1879
|
+
const x = initialOffsetX - d.y * targetScale + this.width / 2;
|
|
1880
|
+
const y = initialOffsetY - d.x * targetScale + this.height / 2;
|
|
1881
|
+
|
|
1882
|
+
this.svg.transition()
|
|
1883
|
+
.duration(750)
|
|
1884
|
+
.call(
|
|
1885
|
+
this.zoom.transform,
|
|
1886
|
+
d3.zoomIdentity
|
|
1887
|
+
.translate(x, y)
|
|
1888
|
+
.scale(targetScale)
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* Center the view on a specific node (Radial layout)
|
|
1894
|
+
*/
|
|
1895
|
+
centerOnNodeRadial(d) {
|
|
1896
|
+
if (!this.svg || !this.zoom) return;
|
|
1897
|
+
|
|
1898
|
+
// Use the same radialPoint function for consistency
|
|
1899
|
+
const [x, y] = this.radialPoint(d.x, d.y);
|
|
1900
|
+
|
|
1901
|
+
// Get current transform or use default zoom level
|
|
1902
|
+
const currentTransform = d3.zoomTransform(this.svg.node());
|
|
1903
|
+
// Zoom in to 2x for better focus on clicked node
|
|
1904
|
+
const targetScale = currentTransform.k < 2 ? 2 : currentTransform.k;
|
|
1905
|
+
|
|
1906
|
+
// Account for the initial tree group centering
|
|
1907
|
+
const centerX = this.width / 2;
|
|
1908
|
+
const centerY = this.height / 2;
|
|
1909
|
+
|
|
1910
|
+
// Calculate translation to center the node
|
|
1911
|
+
// The tree group is centered at centerX, centerY
|
|
1912
|
+
// We need to offset by the node's position scaled
|
|
1913
|
+
const targetX = centerX - x * targetScale;
|
|
1914
|
+
const targetY = centerY - y * targetScale;
|
|
1915
|
+
|
|
1916
|
+
// Apply smooth transition to center the node with zoom
|
|
1917
|
+
this.svg.transition()
|
|
1918
|
+
.duration(750)
|
|
1919
|
+
.call(
|
|
1920
|
+
this.zoom.transform,
|
|
1921
|
+
d3.zoomIdentity
|
|
1922
|
+
.translate(targetX, targetY)
|
|
1923
|
+
.scale(targetScale)
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Highlight the active node with larger icon
|
|
1929
|
+
*/
|
|
1930
|
+
highlightActiveNode(d) {
|
|
1931
|
+
// Reset all nodes to normal size and clear parent context
|
|
1932
|
+
// First clear classes on the selection
|
|
1933
|
+
const allCircles = this.treeGroup.selectAll('circle.node-circle');
|
|
1934
|
+
allCircles
|
|
1935
|
+
.classed('active', false)
|
|
1936
|
+
.classed('parent-context', false);
|
|
1937
|
+
|
|
1938
|
+
// Then apply transition separately
|
|
1939
|
+
allCircles
|
|
1940
|
+
.transition()
|
|
1941
|
+
.duration(300)
|
|
1942
|
+
.attr('r', 8)
|
|
1943
|
+
.style('stroke', null)
|
|
1944
|
+
.style('stroke-width', null)
|
|
1945
|
+
.style('opacity', null);
|
|
1946
|
+
|
|
1947
|
+
// Reset all labels to normal
|
|
1948
|
+
this.treeGroup.selectAll('text.node-label')
|
|
1949
|
+
.style('font-weight', 'normal')
|
|
1950
|
+
.style('font-size', '12px');
|
|
1951
|
+
|
|
1952
|
+
// Find and increase size of clicked node - use data matching
|
|
1953
|
+
// Make the size increase MUCH more dramatic: 8 -> 20 (2.5x the size)
|
|
1954
|
+
const activeNodeCircle = this.treeGroup.selectAll('g.node')
|
|
1955
|
+
.filter(node => node === d)
|
|
1956
|
+
.select('circle.node-circle');
|
|
1957
|
+
|
|
1958
|
+
// First set the class (not part of transition)
|
|
1959
|
+
activeNodeCircle.classed('active', true);
|
|
1960
|
+
|
|
1961
|
+
// Then apply the transition with styles - MUCH LARGER
|
|
1962
|
+
activeNodeCircle
|
|
1963
|
+
.transition()
|
|
1964
|
+
.duration(300)
|
|
1965
|
+
.attr('r', 20) // Much larger radius (2.5x)
|
|
1966
|
+
.style('stroke', '#3b82f6')
|
|
1967
|
+
.style('stroke-width', 5) // Thicker border
|
|
1968
|
+
.style('filter', 'drop-shadow(0 0 15px rgba(59, 130, 246, 0.6))'); // Stronger glow effect
|
|
1969
|
+
|
|
1970
|
+
// Also make the label bold
|
|
1971
|
+
this.treeGroup.selectAll('g.node')
|
|
1972
|
+
.filter(node => node === d)
|
|
1973
|
+
.select('text.node-label')
|
|
1974
|
+
.style('font-weight', 'bold')
|
|
1975
|
+
.style('font-size', '14px'); // Slightly larger text
|
|
1976
|
+
|
|
1977
|
+
// Store active node
|
|
1978
|
+
this.activeNode = d;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* Add pulsing animation for loading state
|
|
1983
|
+
*/
|
|
1984
|
+
addLoadingPulse(d) {
|
|
1985
|
+
// Use consistent selection pattern
|
|
1986
|
+
const node = this.treeGroup.selectAll('g.node')
|
|
1987
|
+
.filter(node => node === d)
|
|
1988
|
+
.select('circle.node-circle');
|
|
1989
|
+
|
|
1990
|
+
// Add to loading set
|
|
1991
|
+
this.loadingNodes.add(d.data.path);
|
|
1992
|
+
|
|
1993
|
+
// Add pulsing class and orange color - separate operations
|
|
1994
|
+
node.classed('loading-pulse', true);
|
|
1995
|
+
node.style('fill', '#fb923c'); // Orange color for loading
|
|
1996
|
+
|
|
1997
|
+
// Create pulse animation
|
|
1998
|
+
const pulseAnimation = () => {
|
|
1999
|
+
if (!this.loadingNodes.has(d.data.path)) return;
|
|
2000
|
+
|
|
2001
|
+
node.transition()
|
|
2002
|
+
.duration(600)
|
|
2003
|
+
.attr('r', 14)
|
|
2004
|
+
.style('opacity', 0.6)
|
|
2005
|
+
.transition()
|
|
2006
|
+
.duration(600)
|
|
2007
|
+
.attr('r', 10)
|
|
2008
|
+
.style('opacity', 1)
|
|
2009
|
+
.on('end', () => {
|
|
2010
|
+
if (this.loadingNodes.has(d.data.path)) {
|
|
2011
|
+
pulseAnimation(); // Continue pulsing
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
pulseAnimation();
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Remove pulsing animation when loading complete
|
|
2021
|
+
*/
|
|
2022
|
+
removeLoadingPulse(d) {
|
|
2023
|
+
// Remove from loading set
|
|
2024
|
+
this.loadingNodes.delete(d.data.path);
|
|
2025
|
+
|
|
2026
|
+
// Use consistent selection pattern
|
|
2027
|
+
const node = this.treeGroup.selectAll('g.node')
|
|
2028
|
+
.filter(node => node === d)
|
|
2029
|
+
.select('circle.node-circle');
|
|
2030
|
+
|
|
2031
|
+
// Clear class first
|
|
2032
|
+
node.classed('loading-pulse', false);
|
|
2033
|
+
|
|
2034
|
+
// Then interrupt and transition
|
|
2035
|
+
node.interrupt() // Stop animation
|
|
2036
|
+
.transition()
|
|
2037
|
+
.duration(300)
|
|
2038
|
+
.attr('r', this.activeNode === d ? 20 : 8) // Use 20 for active node
|
|
2039
|
+
.style('opacity', 1)
|
|
2040
|
+
.style('fill', d => this.getNodeColor(d)); // Restore original color
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* Show parent node alongside for context
|
|
2045
|
+
*/
|
|
2046
|
+
showWithParent(d) {
|
|
2047
|
+
if (!d.parent) return;
|
|
2048
|
+
|
|
2049
|
+
// Make parent more visible
|
|
2050
|
+
const parentNode = this.treeGroup.selectAll('g.node')
|
|
2051
|
+
.filter(node => node === d.parent);
|
|
2052
|
+
|
|
2053
|
+
// Highlight parent with different style - separate class from styles
|
|
2054
|
+
const parentCircle = parentNode.select('circle.node-circle');
|
|
2055
|
+
parentCircle.classed('parent-context', true);
|
|
2056
|
+
parentCircle
|
|
2057
|
+
.style('stroke', '#10b981')
|
|
2058
|
+
.style('stroke-width', 3)
|
|
2059
|
+
.style('opacity', 0.8);
|
|
2060
|
+
|
|
2061
|
+
// For radial, adjust zoom to show both parent and clicked node
|
|
2062
|
+
if (this.isRadialLayout && d.parent) {
|
|
2063
|
+
// Calculate bounding box including parent and immediate children
|
|
2064
|
+
const nodes = [d, d.parent];
|
|
2065
|
+
if (d.children) nodes.push(...d.children);
|
|
2066
|
+
else if (d._children) nodes.push(...d._children);
|
|
2067
|
+
|
|
2068
|
+
const angles = nodes.map(n => n.x);
|
|
2069
|
+
const radii = nodes.map(n => n.y);
|
|
2070
|
+
|
|
2071
|
+
const minAngle = Math.min(...angles);
|
|
2072
|
+
const maxAngle = Math.max(...angles);
|
|
2073
|
+
const maxRadius = Math.max(...radii);
|
|
2074
|
+
|
|
2075
|
+
// Zoom to fit parent and children
|
|
2076
|
+
const angleSpan = maxAngle - minAngle;
|
|
2077
|
+
const scale = Math.min(
|
|
2078
|
+
angleSpan > 0 ? (Math.PI * 2) / (angleSpan * 2) : 2.5, // Fit angle span
|
|
2079
|
+
this.width / (2 * maxRadius), // Fit radius
|
|
2080
|
+
2.5 // Max zoom
|
|
2081
|
+
);
|
|
2082
|
+
|
|
2083
|
+
// Calculate center angle and radius
|
|
2084
|
+
const centerAngle = (minAngle + maxAngle) / 2;
|
|
2085
|
+
const centerRadius = maxRadius / 2;
|
|
2086
|
+
const centerX = centerRadius * Math.cos(centerAngle - Math.PI / 2);
|
|
2087
|
+
const centerY = centerRadius * Math.sin(centerAngle - Math.PI / 2);
|
|
2088
|
+
|
|
2089
|
+
this.svg.transition()
|
|
2090
|
+
.duration(750)
|
|
2091
|
+
.call(
|
|
2092
|
+
this.zoom.transform,
|
|
2093
|
+
d3.zoomIdentity
|
|
2094
|
+
.translate(this.width / 2 - centerX * scale, this.height / 2 - centerY * scale)
|
|
2095
|
+
.scale(scale)
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* Handle node click - implement lazy loading with enhanced visual feedback
|
|
2102
|
+
*/
|
|
2103
|
+
onNodeClick(event, d) {
|
|
2104
|
+
// Handle node click interaction
|
|
2105
|
+
|
|
2106
|
+
// Check event parameter
|
|
2107
|
+
if (event) {
|
|
2108
|
+
try {
|
|
2109
|
+
if (typeof event.stopPropagation === 'function') {
|
|
2110
|
+
event.stopPropagation();
|
|
2111
|
+
} else {
|
|
2112
|
+
}
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
console.error('[CodeTree] ERROR calling stopPropagation:', error);
|
|
2115
|
+
}
|
|
2116
|
+
} else {
|
|
899
2117
|
}
|
|
900
2118
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
2119
|
+
// Check d parameter structure
|
|
2120
|
+
if (!d) {
|
|
2121
|
+
console.error('[CodeTree] ERROR: d is null/undefined, cannot continue');
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
if (!d.data) {
|
|
2126
|
+
console.error('[CodeTree] ERROR: d.data is null/undefined, cannot continue');
|
|
2127
|
+
return;
|
|
905
2128
|
}
|
|
2129
|
+
|
|
2130
|
+
hasName: d.data.hasOwnProperty('name'),
|
|
2131
|
+
hasPath: d.data.hasOwnProperty('path'),
|
|
2132
|
+
hasType: d.data.hasOwnProperty('type'),
|
|
2133
|
+
hasLoaded: d.data.hasOwnProperty('loaded'),
|
|
2134
|
+
name: d.data.name,
|
|
2135
|
+
path: d.data.path,
|
|
2136
|
+
type: d.data.type,
|
|
2137
|
+
loaded: d.data.loaded
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
// Node interaction detected
|
|
2141
|
+
|
|
2142
|
+
// === PHASE 1: Immediate Visual Effects (Synchronous) ===
|
|
2143
|
+
// These execute immediately before any async operations
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
// Center on clicked node (immediate visual effect)
|
|
2147
|
+
try {
|
|
2148
|
+
if (this.isRadialLayout) {
|
|
2149
|
+
if (typeof this.centerOnNodeRadial === 'function') {
|
|
2150
|
+
this.centerOnNodeRadial(d);
|
|
2151
|
+
} else {
|
|
2152
|
+
console.error('[CodeTree] centerOnNodeRadial is not a function!');
|
|
2153
|
+
}
|
|
2154
|
+
} else {
|
|
2155
|
+
if (typeof this.centerOnNode === 'function') {
|
|
2156
|
+
this.centerOnNode(d);
|
|
2157
|
+
} else {
|
|
2158
|
+
console.error('[CodeTree] centerOnNode is not a function!');
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
console.error('[CodeTree] ERROR during centering:', error, error.stack);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
|
|
2166
|
+
// Highlight with larger icon (immediate visual effect)
|
|
2167
|
+
try {
|
|
2168
|
+
if (typeof this.highlightActiveNode === 'function') {
|
|
2169
|
+
this.highlightActiveNode(d);
|
|
2170
|
+
} else {
|
|
2171
|
+
console.error('[CodeTree] highlightActiveNode is not a function!');
|
|
2172
|
+
}
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
console.error('[CodeTree] ERROR during highlightActiveNode:', error, error.stack);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
// Show parent context (immediate visual effect)
|
|
2179
|
+
try {
|
|
2180
|
+
if (typeof this.showWithParent === 'function') {
|
|
2181
|
+
this.showWithParent(d);
|
|
2182
|
+
} else {
|
|
2183
|
+
console.error('[CodeTree] showWithParent is not a function!');
|
|
2184
|
+
}
|
|
2185
|
+
} catch (error) {
|
|
2186
|
+
console.error('[CodeTree] ERROR during showWithParent:', error, error.stack);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
|
|
2190
|
+
// Add pulsing animation immediately for directories
|
|
2191
|
+
|
|
2192
|
+
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
2193
|
+
try {
|
|
2194
|
+
if (typeof this.addLoadingPulse === 'function') {
|
|
2195
|
+
this.addLoadingPulse(d);
|
|
2196
|
+
} else {
|
|
2197
|
+
console.error('[CodeTree] addLoadingPulse is not a function!');
|
|
2198
|
+
}
|
|
2199
|
+
} catch (error) {
|
|
2200
|
+
console.error('[CodeTree] ERROR during addLoadingPulse:', error, error.stack);
|
|
2201
|
+
}
|
|
2202
|
+
} else {
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
|
|
2206
|
+
// === PHASE 2: Prepare Data (Synchronous) ===
|
|
2207
|
+
|
|
2208
|
+
|
|
2209
|
+
// Get selected languages from checkboxes
|
|
2210
|
+
const selectedLanguages = [];
|
|
2211
|
+
const checkboxes = document.querySelectorAll('.language-checkbox:checked');
|
|
2212
|
+
checkboxes.forEach(cb => {
|
|
2213
|
+
selectedLanguages.push(cb.value);
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
// Get ignore patterns
|
|
2217
|
+
const ignorePatternsElement = document.getElementById('ignore-patterns');
|
|
2218
|
+
const ignorePatterns = ignorePatternsElement?.value || '';
|
|
2219
|
+
|
|
2220
|
+
|
|
2221
|
+
// === PHASE 3: Async Operations (Delayed) ===
|
|
2222
|
+
// Add a small delay to ensure visual effects are rendered first
|
|
2223
|
+
|
|
2224
|
+
type: d.data.type,
|
|
2225
|
+
loaded: d.data.loaded,
|
|
2226
|
+
typeIsDirectory: d.data.type === 'directory',
|
|
2227
|
+
loadedIsFalsy: !d.data.loaded,
|
|
2228
|
+
willRequestDiscovery: (d.data.type === 'directory' && !d.data.loaded)
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
// For directories that haven't been loaded yet, request discovery
|
|
2232
|
+
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
2233
|
+
// Mark as loading immediately to prevent duplicate requests
|
|
2234
|
+
d.data.loaded = 'loading';
|
|
2235
|
+
|
|
2236
|
+
// Ensure path is absolute or relative to working directory
|
|
2237
|
+
const fullPath = this.ensureFullPath(d.data.path);
|
|
2238
|
+
|
|
2239
|
+
// Sending discovery request for child content
|
|
2240
|
+
|
|
2241
|
+
// Store reference to the D3 node for later expansion
|
|
2242
|
+
const clickedD3Node = d;
|
|
2243
|
+
|
|
2244
|
+
// Delay the socket request to ensure visual effects are rendered
|
|
2245
|
+
setTimeout(() => {
|
|
2246
|
+
|
|
2247
|
+
// Request directory contents via Socket.IO
|
|
2248
|
+
if (this.socket) {
|
|
2249
|
+
this.socket.emit('code:discover:directory', {
|
|
2250
|
+
path: fullPath,
|
|
2251
|
+
depth: 1, // Only get immediate children
|
|
2252
|
+
languages: selectedLanguages,
|
|
2253
|
+
ignore_patterns: ignorePatterns
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
this.updateBreadcrumb(`Loading ${d.data.name}...`, 'info');
|
|
2257
|
+
this.showNotification(`Loading directory: ${d.data.name}`, 'info');
|
|
2258
|
+
} else {
|
|
2259
|
+
}
|
|
2260
|
+
}, 100); // 100ms delay to ensure visual effects render first
|
|
2261
|
+
}
|
|
2262
|
+
// For files that haven't been analyzed, request analysis
|
|
2263
|
+
else if (d.data.type === 'file' && !d.data.analyzed) {
|
|
2264
|
+
// Only analyze files of selected languages
|
|
2265
|
+
const fileLanguage = this.detectLanguage(d.data.path);
|
|
2266
|
+
if (!selectedLanguages.includes(fileLanguage) && fileLanguage !== 'unknown') {
|
|
2267
|
+
this.showNotification(`Skipping ${d.data.name} - ${fileLanguage} not selected`, 'warning');
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// Add pulsing animation immediately
|
|
2272
|
+
this.addLoadingPulse(d);
|
|
2273
|
+
|
|
2274
|
+
// Mark as loading immediately
|
|
2275
|
+
d.data.analyzed = 'loading';
|
|
2276
|
+
|
|
2277
|
+
// Ensure path is absolute or relative to working directory
|
|
2278
|
+
const fullPath = this.ensureFullPath(d.data.path);
|
|
2279
|
+
|
|
2280
|
+
// Delay the socket request to ensure visual effects are rendered
|
|
2281
|
+
setTimeout(() => {
|
|
2282
|
+
|
|
2283
|
+
if (this.socket) {
|
|
2284
|
+
this.socket.emit('code:analyze:file', {
|
|
2285
|
+
path: fullPath
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
this.updateBreadcrumb(`Analyzing ${d.data.name}...`, 'info');
|
|
2289
|
+
this.showNotification(`Analyzing: ${d.data.name}`, 'info');
|
|
2290
|
+
}
|
|
2291
|
+
}, 100); // 100ms delay to ensure visual effects render first
|
|
2292
|
+
}
|
|
2293
|
+
// Toggle children visibility for already loaded nodes
|
|
2294
|
+
else if (d.data.type === 'directory' && d.data.loaded === true) {
|
|
2295
|
+
// Directory is loaded, toggle expansion
|
|
2296
|
+
if (d.children) {
|
|
2297
|
+
// Collapse - hide children
|
|
2298
|
+
d._children = d.children;
|
|
2299
|
+
d.children = null;
|
|
2300
|
+
d.data.expanded = false;
|
|
2301
|
+
} else if (d._children) {
|
|
2302
|
+
// Expand - show children
|
|
2303
|
+
d.children = d._children;
|
|
2304
|
+
d._children = null;
|
|
2305
|
+
d.data.expanded = true;
|
|
2306
|
+
} else if (d.data.children && d.data.children.length > 0) {
|
|
2307
|
+
// Children exist in data but not in D3 node, recreate hierarchy
|
|
2308
|
+
this.root = d3.hierarchy(this.treeData);
|
|
2309
|
+
const updatedD3Node = this.findD3NodeByPath(d.data.path);
|
|
2310
|
+
if (updatedD3Node) {
|
|
2311
|
+
updatedD3Node.children = updatedD3Node._children || updatedD3Node.children;
|
|
2312
|
+
updatedD3Node._children = null;
|
|
2313
|
+
updatedD3Node.data.expanded = true;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
this.update(this.root);
|
|
2317
|
+
}
|
|
2318
|
+
// Also handle other nodes that might have children
|
|
2319
|
+
else if (d.children || d._children) {
|
|
2320
|
+
if (d.children) {
|
|
2321
|
+
d._children = d.children;
|
|
2322
|
+
d.children = null;
|
|
2323
|
+
d.data.expanded = false;
|
|
2324
|
+
} else {
|
|
2325
|
+
d.children = d._children;
|
|
2326
|
+
d._children = null;
|
|
2327
|
+
d.data.expanded = true;
|
|
2328
|
+
}
|
|
2329
|
+
this.update(d);
|
|
2330
|
+
} else {
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// Update selection
|
|
2334
|
+
this.selectedNode = d;
|
|
2335
|
+
try {
|
|
2336
|
+
this.highlightNode(d);
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
console.error('[CodeTree] ERROR during highlightNode:', error);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
906
2341
|
}
|
|
907
2342
|
|
|
908
2343
|
/**
|
|
909
|
-
*
|
|
2344
|
+
* Ensure path is absolute or relative to working directory
|
|
910
2345
|
*/
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
2346
|
+
ensureFullPath(path) {
|
|
2347
|
+
console.log('🔗 ensureFullPath called with:', path);
|
|
2348
|
+
|
|
2349
|
+
if (!path) return path;
|
|
2350
|
+
|
|
2351
|
+
// If already absolute, return as is
|
|
2352
|
+
if (path.startsWith('/')) {
|
|
2353
|
+
console.log(' → Already absolute, returning:', path);
|
|
2354
|
+
return path;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// Get working directory
|
|
2358
|
+
const workingDir = this.getWorkingDirectory();
|
|
2359
|
+
console.log(' → Working directory:', workingDir);
|
|
2360
|
+
|
|
2361
|
+
if (!workingDir) {
|
|
2362
|
+
console.log(' → No working directory, returning original:', path);
|
|
2363
|
+
return path;
|
|
2364
|
+
}
|
|
914
2365
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
}, 2000);
|
|
2366
|
+
// Special handling for root path
|
|
2367
|
+
if (path === '.') {
|
|
2368
|
+
console.log(' → Root path detected, returning working dir:', workingDir);
|
|
2369
|
+
return workingDir;
|
|
920
2370
|
}
|
|
921
2371
|
|
|
922
|
-
|
|
923
|
-
|
|
2372
|
+
// If path equals working directory, return as is
|
|
2373
|
+
if (path === workingDir) {
|
|
2374
|
+
console.log(' → Path equals working directory, returning:', workingDir);
|
|
2375
|
+
return workingDir;
|
|
924
2376
|
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
/**
|
|
928
|
-
* Update statistics display
|
|
929
|
-
*/
|
|
930
|
-
updateStats() {
|
|
931
|
-
const fileCount = document.getElementById('file-count');
|
|
932
|
-
const classCount = document.getElementById('class-count');
|
|
933
|
-
const functionCount = document.getElementById('function-count');
|
|
934
|
-
const lineCount = document.getElementById('line-count');
|
|
935
2377
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
2378
|
+
// Combine working directory with relative path
|
|
2379
|
+
const result = `${workingDir}/${path}`.replace(/\/+/g, '/');
|
|
2380
|
+
console.log(' → Combining with working dir, result:', result);
|
|
2381
|
+
return result;
|
|
940
2382
|
}
|
|
941
2383
|
|
|
942
2384
|
/**
|
|
943
|
-
*
|
|
2385
|
+
* Highlight selected node
|
|
944
2386
|
*/
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
nodes.forEach(d => {
|
|
957
|
-
d.y = d.depth * 180;
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
// Update nodes
|
|
961
|
-
const node = this.treeGroup.selectAll('g.code-node')
|
|
962
|
-
.data(nodes, d => d.id || (d.id = ++this.nodeId));
|
|
963
|
-
|
|
964
|
-
// Enter new nodes
|
|
965
|
-
const nodeEnter = node.enter().append('g')
|
|
966
|
-
.attr('class', d => `code-node ${d.data.type} complexity-${this.getComplexityLevel(d.data.complexity)}`)
|
|
967
|
-
.attr('transform', d => `translate(${source.y0},${source.x0})`)
|
|
968
|
-
.on('click', (event, d) => this.toggleNode(event, d))
|
|
969
|
-
.on('mouseover', (event, d) => this.showTooltip(event, d))
|
|
970
|
-
.on('mouseout', () => this.hideTooltip());
|
|
971
|
-
|
|
972
|
-
// Add circles
|
|
973
|
-
nodeEnter.append('circle')
|
|
974
|
-
.attr('r', 1e-6)
|
|
975
|
-
.style('fill', d => d._children ? '#e2e8f0' : this.getNodeColor(d.data.type));
|
|
976
|
-
|
|
977
|
-
// Add text labels
|
|
978
|
-
nodeEnter.append('text')
|
|
979
|
-
.attr('dy', '.35em')
|
|
980
|
-
.attr('x', d => d.children || d._children ? -13 : 13)
|
|
981
|
-
.attr('text-anchor', d => d.children || d._children ? 'end' : 'start')
|
|
982
|
-
.text(d => d.data.name)
|
|
983
|
-
.style('fill-opacity', 1e-6);
|
|
984
|
-
|
|
985
|
-
// Add icons
|
|
986
|
-
nodeEnter.append('text')
|
|
987
|
-
.attr('class', 'node-icon')
|
|
988
|
-
.attr('dy', '.35em')
|
|
989
|
-
.attr('x', 0)
|
|
990
|
-
.attr('text-anchor', 'middle')
|
|
991
|
-
.text(d => this.getNodeIcon(d.data.type))
|
|
992
|
-
.style('font-size', '16px');
|
|
993
|
-
|
|
994
|
-
// Transition nodes to their new position
|
|
995
|
-
const nodeUpdate = nodeEnter.merge(node);
|
|
996
|
-
|
|
997
|
-
nodeUpdate.transition()
|
|
998
|
-
.duration(this.duration)
|
|
999
|
-
.attr('transform', d => `translate(${d.y},${d.x})`);
|
|
1000
|
-
|
|
1001
|
-
nodeUpdate.select('circle')
|
|
1002
|
-
.attr('r', 8)
|
|
1003
|
-
.style('fill', d => d._children ? '#e2e8f0' : this.getNodeColor(d.data.type));
|
|
1004
|
-
|
|
1005
|
-
nodeUpdate.select('text')
|
|
1006
|
-
.style('fill-opacity', 1);
|
|
1007
|
-
|
|
1008
|
-
// Remove exiting nodes
|
|
1009
|
-
const nodeExit = node.exit().transition()
|
|
1010
|
-
.duration(this.duration)
|
|
1011
|
-
.attr('transform', d => `translate(${source.y},${source.x})`)
|
|
1012
|
-
.remove();
|
|
1013
|
-
|
|
1014
|
-
nodeExit.select('circle')
|
|
1015
|
-
.attr('r', 1e-6);
|
|
1016
|
-
|
|
1017
|
-
nodeExit.select('text')
|
|
1018
|
-
.style('fill-opacity', 1e-6);
|
|
1019
|
-
|
|
1020
|
-
// Update links
|
|
1021
|
-
const link = this.treeGroup.selectAll('path.code-link')
|
|
1022
|
-
.data(links, d => d.target.id);
|
|
1023
|
-
|
|
1024
|
-
// Enter new links
|
|
1025
|
-
const linkEnter = link.enter().insert('path', 'g')
|
|
1026
|
-
.attr('class', 'code-link')
|
|
1027
|
-
.attr('d', d => {
|
|
1028
|
-
const o = {x: source.x0, y: source.y0};
|
|
1029
|
-
return this.diagonal(o, o);
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
// Transition links to their new position
|
|
1033
|
-
const linkUpdate = linkEnter.merge(link);
|
|
1034
|
-
|
|
1035
|
-
linkUpdate.transition()
|
|
1036
|
-
.duration(this.duration)
|
|
1037
|
-
.attr('d', d => this.diagonal(d.source, d.target));
|
|
1038
|
-
|
|
1039
|
-
// Remove exiting links
|
|
1040
|
-
link.exit().transition()
|
|
1041
|
-
.duration(this.duration)
|
|
1042
|
-
.attr('d', d => {
|
|
1043
|
-
const o = {x: source.x, y: source.y};
|
|
1044
|
-
return this.diagonal(o, o);
|
|
1045
|
-
})
|
|
1046
|
-
.remove();
|
|
1047
|
-
|
|
1048
|
-
// Store old positions for transition
|
|
1049
|
-
nodes.forEach(d => {
|
|
1050
|
-
d.x0 = d.x;
|
|
1051
|
-
d.y0 = d.y;
|
|
1052
|
-
});
|
|
2387
|
+
highlightNode(node) {
|
|
2388
|
+
// Remove previous highlights
|
|
2389
|
+
this.treeGroup.selectAll('circle.node-circle')
|
|
2390
|
+
.style('stroke-width', 2)
|
|
2391
|
+
.classed('selected', false);
|
|
2392
|
+
|
|
2393
|
+
// Highlight selected node
|
|
2394
|
+
this.treeGroup.selectAll('circle.node-circle')
|
|
2395
|
+
.filter(d => d === node)
|
|
2396
|
+
.style('stroke-width', 4)
|
|
2397
|
+
.classed('selected', true);
|
|
1053
2398
|
}
|
|
1054
2399
|
|
|
1055
2400
|
/**
|
|
1056
2401
|
* Create diagonal path for links
|
|
1057
2402
|
*/
|
|
1058
|
-
diagonal(
|
|
1059
|
-
return `M ${
|
|
1060
|
-
C ${(
|
|
1061
|
-
${(
|
|
1062
|
-
${
|
|
2403
|
+
diagonal(s, d) {
|
|
2404
|
+
return `M ${s.y} ${s.x}
|
|
2405
|
+
C ${(s.y + d.y) / 2} ${s.x},
|
|
2406
|
+
${(s.y + d.y) / 2} ${d.x},
|
|
2407
|
+
${d.y} ${d.x}`;
|
|
1063
2408
|
}
|
|
1064
|
-
|
|
2409
|
+
|
|
1065
2410
|
/**
|
|
1066
|
-
*
|
|
2411
|
+
* Create radial diagonal path for links
|
|
1067
2412
|
*/
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
d
|
|
1071
|
-
d
|
|
1072
|
-
|
|
1073
|
-
d.children = d._children;
|
|
1074
|
-
d._children = null;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
this.update(d);
|
|
1078
|
-
|
|
1079
|
-
// Update breadcrumb
|
|
1080
|
-
this.updateBreadcrumb(d);
|
|
1081
|
-
|
|
1082
|
-
// Mark as selected
|
|
1083
|
-
this.selectNode(d);
|
|
1084
|
-
|
|
1085
|
-
// Show code viewer if it's a code node
|
|
1086
|
-
if (d.data.type !== 'module' && d.data.type !== 'file') {
|
|
1087
|
-
this.showCodeViewer(d.data);
|
|
1088
|
-
}
|
|
2413
|
+
radialDiagonal(s, d) {
|
|
2414
|
+
const path = d3.linkRadial()
|
|
2415
|
+
.angle(d => d.x)
|
|
2416
|
+
.radius(d => d.y);
|
|
2417
|
+
return path({source: s, target: d});
|
|
1089
2418
|
}
|
|
1090
2419
|
|
|
1091
2420
|
/**
|
|
1092
|
-
*
|
|
2421
|
+
* Get node color based on type and complexity
|
|
1093
2422
|
*/
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
2423
|
+
getNodeColor(d) {
|
|
2424
|
+
const type = d.data.type;
|
|
2425
|
+
const complexity = d.data.complexity || 1;
|
|
2426
|
+
|
|
2427
|
+
// Base colors by type
|
|
2428
|
+
const baseColors = {
|
|
2429
|
+
'root': '#6B7280',
|
|
2430
|
+
'directory': '#3B82F6',
|
|
2431
|
+
'file': '#10B981',
|
|
2432
|
+
'module': '#8B5CF6',
|
|
2433
|
+
'class': '#F59E0B',
|
|
2434
|
+
'function': '#EF4444',
|
|
2435
|
+
'method': '#EC4899'
|
|
2436
|
+
};
|
|
2437
|
+
|
|
2438
|
+
const baseColor = baseColors[type] || '#6B7280';
|
|
2439
|
+
|
|
2440
|
+
// Adjust brightness based on complexity (higher complexity = darker)
|
|
2441
|
+
if (complexity > 10) {
|
|
2442
|
+
return d3.color(baseColor).darker(0.5);
|
|
2443
|
+
} else if (complexity > 5) {
|
|
2444
|
+
return d3.color(baseColor).darker(0.25);
|
|
1098
2445
|
}
|
|
1099
2446
|
|
|
1100
|
-
|
|
1101
|
-
this.selectedNode = node;
|
|
1102
|
-
if (node) {
|
|
1103
|
-
d3.select(node).classed('selected', true);
|
|
1104
|
-
}
|
|
2447
|
+
return baseColor;
|
|
1105
2448
|
}
|
|
1106
2449
|
|
|
1107
2450
|
/**
|
|
1108
|
-
*
|
|
2451
|
+
* Get node stroke color
|
|
1109
2452
|
*/
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
while (current) {
|
|
1115
|
-
path.unshift(current.data.name);
|
|
1116
|
-
current = current.parent;
|
|
2453
|
+
getNodeStrokeColor(d) {
|
|
2454
|
+
if (d.data.loaded === 'loading' || d.data.analyzed === 'loading') {
|
|
2455
|
+
return '#FCD34D'; // Yellow for loading
|
|
1117
2456
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (breadcrumbContent) {
|
|
1121
|
-
breadcrumbContent.textContent = path.join(' > ');
|
|
1122
|
-
breadcrumbContent.className = 'ticker-file';
|
|
2457
|
+
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
2458
|
+
return '#94A3B8'; // Gray for unloaded
|
|
1123
2459
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
/**
|
|
1127
|
-
* Update ticker with event
|
|
1128
|
-
*/
|
|
1129
|
-
updateTicker(message, type = 'info') {
|
|
1130
|
-
const breadcrumbContent = document.getElementById('breadcrumb-content');
|
|
1131
|
-
if (breadcrumbContent) {
|
|
1132
|
-
// Add class based on type
|
|
1133
|
-
let className = '';
|
|
1134
|
-
switch(type) {
|
|
1135
|
-
case 'file': className = 'ticker-file'; break;
|
|
1136
|
-
case 'node': className = 'ticker-node'; break;
|
|
1137
|
-
case 'progress': className = 'ticker-progress'; break;
|
|
1138
|
-
case 'error': className = 'ticker-error'; break;
|
|
1139
|
-
default: className = '';
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
breadcrumbContent.textContent = message;
|
|
1143
|
-
breadcrumbContent.className = className + ' ticker-event';
|
|
1144
|
-
|
|
1145
|
-
// Trigger animation
|
|
1146
|
-
breadcrumbContent.style.animation = 'none';
|
|
1147
|
-
setTimeout(() => {
|
|
1148
|
-
breadcrumbContent.style.animation = '';
|
|
1149
|
-
}, 10);
|
|
2460
|
+
if (d.data.type === 'file' && !d.data.analyzed) {
|
|
2461
|
+
return '#CBD5E1'; // Light gray for unanalyzed
|
|
1150
2462
|
}
|
|
2463
|
+
return this.getNodeColor(d);
|
|
1151
2464
|
}
|
|
1152
2465
|
|
|
1153
2466
|
/**
|
|
1154
|
-
*
|
|
2467
|
+
* Get icon for node type
|
|
1155
2468
|
*/
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
2469
|
+
getNodeIcon(d) {
|
|
2470
|
+
const icons = {
|
|
2471
|
+
'root': '📦',
|
|
2472
|
+
'directory': '📁',
|
|
2473
|
+
'file': '📄',
|
|
2474
|
+
'module': '📦',
|
|
2475
|
+
'class': 'C',
|
|
2476
|
+
'function': 'ƒ',
|
|
2477
|
+
'method': 'm'
|
|
2478
|
+
};
|
|
2479
|
+
return icons[d.data.type] || '•';
|
|
1161
2480
|
}
|
|
1162
2481
|
|
|
1163
2482
|
/**
|
|
1164
|
-
* Show tooltip
|
|
2483
|
+
* Show tooltip on hover
|
|
1165
2484
|
*/
|
|
1166
2485
|
showTooltip(event, d) {
|
|
1167
2486
|
if (!this.tooltip) return;
|
|
2487
|
+
|
|
2488
|
+
const info = [];
|
|
2489
|
+
info.push(`<strong>${d.data.name}</strong>`);
|
|
2490
|
+
info.push(`Type: ${d.data.type}`);
|
|
1168
2491
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
2492
|
+
if (d.data.language) {
|
|
2493
|
+
info.push(`Language: ${d.data.language}`);
|
|
2494
|
+
}
|
|
1172
2495
|
if (d.data.complexity) {
|
|
1173
|
-
|
|
2496
|
+
info.push(`Complexity: ${d.data.complexity}`);
|
|
1174
2497
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
content += `Line: ${d.data.line}<br/>`;
|
|
2498
|
+
if (d.data.lines) {
|
|
2499
|
+
info.push(`Lines: ${d.data.lines}`);
|
|
1178
2500
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
content += `<em>${d.data.docstring.substring(0, 100)}...</em>`;
|
|
2501
|
+
if (d.data.path) {
|
|
2502
|
+
info.push(`Path: ${d.data.path}`);
|
|
1182
2503
|
}
|
|
1183
2504
|
|
|
2505
|
+
// Special messages for lazy-loaded nodes
|
|
2506
|
+
if (d.data.type === 'directory' && !d.data.loaded) {
|
|
2507
|
+
info.push('<em>Click to explore contents</em>');
|
|
2508
|
+
} else if (d.data.type === 'file' && !d.data.analyzed) {
|
|
2509
|
+
info.push('<em>Click to analyze file</em>');
|
|
2510
|
+
}
|
|
2511
|
+
|
|
1184
2512
|
this.tooltip.transition()
|
|
1185
2513
|
.duration(200)
|
|
1186
2514
|
.style('opacity', .9);
|
|
1187
|
-
|
|
1188
|
-
this.tooltip.html(
|
|
2515
|
+
|
|
2516
|
+
this.tooltip.html(info.join('<br>'))
|
|
1189
2517
|
.style('left', (event.pageX + 10) + 'px')
|
|
1190
2518
|
.style('top', (event.pageY - 28) + 'px');
|
|
1191
2519
|
}
|
|
@@ -1194,101 +2522,214 @@ class CodeTree {
|
|
|
1194
2522
|
* Hide tooltip
|
|
1195
2523
|
*/
|
|
1196
2524
|
hideTooltip() {
|
|
1197
|
-
if (this.tooltip)
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
2525
|
+
if (!this.tooltip) return;
|
|
2526
|
+
|
|
2527
|
+
this.tooltip.transition()
|
|
2528
|
+
.duration(500)
|
|
2529
|
+
.style('opacity', 0);
|
|
1202
2530
|
}
|
|
1203
2531
|
|
|
1204
2532
|
/**
|
|
1205
|
-
*
|
|
2533
|
+
* Filter tree based on language and search
|
|
1206
2534
|
*/
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
module: '#8b5cf6',
|
|
1210
|
-
file: '#6366f1',
|
|
1211
|
-
class: '#3b82f6',
|
|
1212
|
-
function: '#f59e0b',
|
|
1213
|
-
method: '#10b981'
|
|
1214
|
-
};
|
|
1215
|
-
return colors[type] || '#718096';
|
|
1216
|
-
}
|
|
2535
|
+
filterTree() {
|
|
2536
|
+
if (!this.root) return;
|
|
1217
2537
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
getNodeIcon(type) {
|
|
1222
|
-
const icons = {
|
|
1223
|
-
module: '📦',
|
|
1224
|
-
file: '📄',
|
|
1225
|
-
class: '🏛️',
|
|
1226
|
-
function: '⚡',
|
|
1227
|
-
method: '🔧'
|
|
1228
|
-
};
|
|
1229
|
-
return icons[type] || '📌';
|
|
1230
|
-
}
|
|
2538
|
+
// Apply filters
|
|
2539
|
+
this.root.descendants().forEach(d => {
|
|
2540
|
+
d.data._hidden = false;
|
|
1231
2541
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
return 'high';
|
|
1239
|
-
}
|
|
2542
|
+
// Language filter
|
|
2543
|
+
if (this.languageFilter !== 'all') {
|
|
2544
|
+
if (d.data.type === 'file' && d.data.language !== this.languageFilter) {
|
|
2545
|
+
d.data._hidden = true;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
1240
2548
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
2549
|
+
// Search filter
|
|
2550
|
+
if (this.searchTerm) {
|
|
2551
|
+
if (!d.data.name.toLowerCase().includes(this.searchTerm)) {
|
|
2552
|
+
d.data._hidden = true;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
// Update display
|
|
1246
2558
|
this.update(this.root);
|
|
1247
2559
|
}
|
|
1248
2560
|
|
|
1249
2561
|
/**
|
|
1250
|
-
* Expand
|
|
2562
|
+
* Expand all nodes in the tree
|
|
1251
2563
|
*/
|
|
1252
|
-
|
|
1253
|
-
if (
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
2564
|
+
expandAll() {
|
|
2565
|
+
if (!this.root) return;
|
|
2566
|
+
|
|
2567
|
+
// Recursively expand all nodes
|
|
2568
|
+
const expandRecursive = (node) => {
|
|
2569
|
+
if (node._children) {
|
|
2570
|
+
node.children = node._children;
|
|
2571
|
+
node._children = null;
|
|
2572
|
+
}
|
|
2573
|
+
if (node.children) {
|
|
2574
|
+
node.children.forEach(expandRecursive);
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
|
|
2578
|
+
expandRecursive(this.root);
|
|
2579
|
+
this.update(this.root);
|
|
2580
|
+
this.showNotification('All nodes expanded', 'info');
|
|
1260
2581
|
}
|
|
1261
2582
|
|
|
1262
2583
|
/**
|
|
1263
|
-
* Collapse all nodes
|
|
2584
|
+
* Collapse all nodes in the tree
|
|
1264
2585
|
*/
|
|
1265
2586
|
collapseAll() {
|
|
1266
|
-
|
|
2587
|
+
if (!this.root) return;
|
|
2588
|
+
|
|
2589
|
+
// Recursively collapse all nodes except root
|
|
2590
|
+
const collapseRecursive = (node) => {
|
|
2591
|
+
if (node.children) {
|
|
2592
|
+
node._children = node.children;
|
|
2593
|
+
node.children = null;
|
|
2594
|
+
}
|
|
2595
|
+
if (node._children) {
|
|
2596
|
+
node._children.forEach(collapseRecursive);
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2599
|
+
|
|
2600
|
+
this.root.children?.forEach(collapseRecursive);
|
|
1267
2601
|
this.update(this.root);
|
|
2602
|
+
this.showNotification('All nodes collapsed', 'info');
|
|
1268
2603
|
}
|
|
1269
2604
|
|
|
1270
2605
|
/**
|
|
1271
|
-
*
|
|
2606
|
+
* Reset zoom to fit the tree
|
|
1272
2607
|
*/
|
|
1273
|
-
|
|
1274
|
-
if (
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
2608
|
+
resetZoom() {
|
|
2609
|
+
if (!this.svg || !this.zoom) return;
|
|
2610
|
+
|
|
2611
|
+
// Reset to identity transform for radial layout (centered)
|
|
2612
|
+
this.svg.transition()
|
|
2613
|
+
.duration(750)
|
|
2614
|
+
.call(
|
|
2615
|
+
this.zoom.transform,
|
|
2616
|
+
d3.zoomIdentity
|
|
2617
|
+
);
|
|
2618
|
+
|
|
2619
|
+
this.showNotification('Zoom reset', 'info');
|
|
1279
2620
|
}
|
|
1280
2621
|
|
|
1281
2622
|
/**
|
|
1282
|
-
*
|
|
2623
|
+
* Focus on a specific node and its subtree
|
|
1283
2624
|
*/
|
|
1284
|
-
|
|
1285
|
-
if (this.svg)
|
|
2625
|
+
focusOnNode(node) {
|
|
2626
|
+
if (!this.svg || !this.zoom || !node) return;
|
|
2627
|
+
|
|
2628
|
+
// Get all descendants of this node
|
|
2629
|
+
const descendants = node.descendants ? node.descendants() : [node];
|
|
2630
|
+
|
|
2631
|
+
if (this.isRadialLayout) {
|
|
2632
|
+
// For radial layout, calculate the bounding box in polar coordinates
|
|
2633
|
+
const angles = descendants.map(d => d.x);
|
|
2634
|
+
const radii = descendants.map(d => d.y);
|
|
2635
|
+
|
|
2636
|
+
const minAngle = Math.min(...angles);
|
|
2637
|
+
const maxAngle = Math.max(...angles);
|
|
2638
|
+
const minRadius = Math.min(...radii);
|
|
2639
|
+
const maxRadius = Math.max(...radii);
|
|
2640
|
+
|
|
2641
|
+
// Convert polar bounds to Cartesian for centering
|
|
2642
|
+
const centerAngle = (minAngle + maxAngle) / 2;
|
|
2643
|
+
const centerRadius = (minRadius + maxRadius) / 2;
|
|
2644
|
+
|
|
2645
|
+
// Convert to Cartesian coordinates
|
|
2646
|
+
const centerX = centerRadius * Math.cos(centerAngle - Math.PI / 2);
|
|
2647
|
+
const centerY = centerRadius * Math.sin(centerAngle - Math.PI / 2);
|
|
2648
|
+
|
|
2649
|
+
// Calculate the span for zoom scale
|
|
2650
|
+
const angleSpan = maxAngle - minAngle;
|
|
2651
|
+
const radiusSpan = maxRadius - minRadius;
|
|
2652
|
+
|
|
2653
|
+
// Calculate scale to fit the subtree
|
|
2654
|
+
// Use angle span to determine scale (radial layout specific)
|
|
2655
|
+
let scale = 1;
|
|
2656
|
+
if (angleSpan > 0 && radiusSpan > 0) {
|
|
2657
|
+
// Scale based on the larger dimension
|
|
2658
|
+
const angleFactor = Math.PI * 2 / angleSpan; // Full circle / angle span
|
|
2659
|
+
const radiusFactor = this.radius / radiusSpan;
|
|
2660
|
+
scale = Math.min(angleFactor, radiusFactor, 3); // Max zoom of 3x
|
|
2661
|
+
scale = Math.max(scale, 1); // Min zoom of 1x
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// Animate the zoom and center
|
|
2665
|
+
this.svg.transition()
|
|
2666
|
+
.duration(750)
|
|
2667
|
+
.call(
|
|
2668
|
+
this.zoom.transform,
|
|
2669
|
+
d3.zoomIdentity
|
|
2670
|
+
.translate(this.width/2 - centerX * scale, this.height/2 - centerY * scale)
|
|
2671
|
+
.scale(scale)
|
|
2672
|
+
);
|
|
2673
|
+
|
|
2674
|
+
} else {
|
|
2675
|
+
// For linear/tree layout
|
|
2676
|
+
const xValues = descendants.map(d => d.x);
|
|
2677
|
+
const yValues = descendants.map(d => d.y);
|
|
2678
|
+
|
|
2679
|
+
const minX = Math.min(...xValues);
|
|
2680
|
+
const maxX = Math.max(...xValues);
|
|
2681
|
+
const minY = Math.min(...yValues);
|
|
2682
|
+
const maxY = Math.max(...yValues);
|
|
2683
|
+
|
|
2684
|
+
// Calculate center
|
|
2685
|
+
const centerX = (minX + maxX) / 2;
|
|
2686
|
+
const centerY = (minY + maxY) / 2;
|
|
2687
|
+
|
|
2688
|
+
// Calculate bounds
|
|
2689
|
+
const width = maxX - minX;
|
|
2690
|
+
const height = maxY - minY;
|
|
2691
|
+
|
|
2692
|
+
// Calculate scale to fit
|
|
2693
|
+
const padding = 100;
|
|
2694
|
+
let scale = 1;
|
|
2695
|
+
if (width > 0 && height > 0) {
|
|
2696
|
+
const scaleX = (this.width - padding) / width;
|
|
2697
|
+
const scaleY = (this.height - padding) / height;
|
|
2698
|
+
scale = Math.min(scaleX, scaleY, 2.5); // Max zoom of 2.5x
|
|
2699
|
+
scale = Math.max(scale, 0.5); // Min zoom of 0.5x
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// Animate zoom to focus
|
|
1286
2703
|
this.svg.transition()
|
|
1287
2704
|
.duration(750)
|
|
1288
|
-
.call(
|
|
2705
|
+
.call(
|
|
2706
|
+
this.zoom.transform,
|
|
2707
|
+
d3.zoomIdentity
|
|
2708
|
+
.translate(this.width/2 - centerX * scale, this.height/2 - centerY * scale)
|
|
2709
|
+
.scale(scale)
|
|
2710
|
+
);
|
|
1289
2711
|
}
|
|
2712
|
+
|
|
2713
|
+
// Update breadcrumb with focused path
|
|
2714
|
+
const path = this.getNodePath(node);
|
|
2715
|
+
this.updateBreadcrumb(`Focused: ${path}`, 'info');
|
|
1290
2716
|
}
|
|
1291
2717
|
|
|
2718
|
+
/**
|
|
2719
|
+
* Get the full path of a node
|
|
2720
|
+
*/
|
|
2721
|
+
getNodePath(node) {
|
|
2722
|
+
const path = [];
|
|
2723
|
+
let current = node;
|
|
2724
|
+
while (current) {
|
|
2725
|
+
if (current.data && current.data.name) {
|
|
2726
|
+
path.unshift(current.data.name);
|
|
2727
|
+
}
|
|
2728
|
+
current = current.parent;
|
|
2729
|
+
}
|
|
2730
|
+
return path.join(' / ');
|
|
2731
|
+
}
|
|
2732
|
+
|
|
1292
2733
|
/**
|
|
1293
2734
|
* Toggle legend visibility
|
|
1294
2735
|
*/
|
|
@@ -1304,54 +2745,199 @@ class CodeTree {
|
|
|
1304
2745
|
}
|
|
1305
2746
|
|
|
1306
2747
|
/**
|
|
1307
|
-
*
|
|
2748
|
+
* Get the current working directory
|
|
1308
2749
|
*/
|
|
1309
|
-
|
|
1310
|
-
//
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
2750
|
+
getWorkingDirectory() {
|
|
2751
|
+
// Try to get from dashboard's working directory manager
|
|
2752
|
+
if (window.dashboard && window.dashboard.workingDirectoryManager) {
|
|
2753
|
+
return window.dashboard.workingDirectoryManager.getCurrentWorkingDir();
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// Fallback to checking the DOM element
|
|
2757
|
+
const workingDirPath = document.getElementById('working-dir-path');
|
|
2758
|
+
if (workingDirPath) {
|
|
2759
|
+
const pathText = workingDirPath.textContent.trim();
|
|
2760
|
+
if (pathText && pathText !== 'Loading...' && pathText !== 'Not selected') {
|
|
2761
|
+
return pathText;
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
return null;
|
|
1314
2766
|
}
|
|
1315
|
-
|
|
2767
|
+
|
|
1316
2768
|
/**
|
|
1317
|
-
*
|
|
2769
|
+
* Show a message when no working directory is selected
|
|
1318
2770
|
*/
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
2771
|
+
showNoWorkingDirectoryMessage() {
|
|
2772
|
+
const container = document.getElementById('code-tree-container');
|
|
2773
|
+
if (!container) return;
|
|
2774
|
+
|
|
2775
|
+
// Remove any existing message
|
|
2776
|
+
this.removeNoWorkingDirectoryMessage();
|
|
2777
|
+
|
|
2778
|
+
// Hide loading if shown
|
|
2779
|
+
this.hideLoading();
|
|
2780
|
+
|
|
2781
|
+
// Create message element
|
|
2782
|
+
const messageDiv = document.createElement('div');
|
|
2783
|
+
messageDiv.id = 'no-working-dir-message';
|
|
2784
|
+
messageDiv.className = 'no-working-dir-message';
|
|
2785
|
+
messageDiv.innerHTML = `
|
|
2786
|
+
<div class="message-icon">📁</div>
|
|
2787
|
+
<h3>No Working Directory Selected</h3>
|
|
2788
|
+
<p>Please select a working directory from the top menu to analyze code.</p>
|
|
2789
|
+
<button id="select-working-dir-btn" class="btn btn-primary">
|
|
2790
|
+
Select Working Directory
|
|
2791
|
+
</button>
|
|
2792
|
+
`;
|
|
2793
|
+
messageDiv.style.cssText = `
|
|
2794
|
+
text-align: center;
|
|
2795
|
+
padding: 40px;
|
|
2796
|
+
color: #666;
|
|
2797
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2798
|
+
`;
|
|
2799
|
+
|
|
2800
|
+
// Style the message elements
|
|
2801
|
+
const messageIcon = messageDiv.querySelector('.message-icon');
|
|
2802
|
+
if (messageIcon) {
|
|
2803
|
+
messageIcon.style.cssText = 'font-size: 48px; margin-bottom: 16px; opacity: 0.5;';
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
const h3 = messageDiv.querySelector('h3');
|
|
2807
|
+
if (h3) {
|
|
2808
|
+
h3.style.cssText = 'margin: 16px 0; color: #333; font-size: 20px;';
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
const p = messageDiv.querySelector('p');
|
|
2812
|
+
if (p) {
|
|
2813
|
+
p.style.cssText = 'margin: 16px 0; color: #666; font-size: 14px;';
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
const button = messageDiv.querySelector('button');
|
|
2817
|
+
if (button) {
|
|
2818
|
+
button.style.cssText = `
|
|
2819
|
+
margin-top: 20px;
|
|
2820
|
+
padding: 10px 20px;
|
|
2821
|
+
background: #3b82f6;
|
|
2822
|
+
color: white;
|
|
2823
|
+
border: none;
|
|
2824
|
+
border-radius: 6px;
|
|
2825
|
+
cursor: pointer;
|
|
2826
|
+
font-size: 14px;
|
|
2827
|
+
transition: background 0.2s;
|
|
2828
|
+
`;
|
|
2829
|
+
button.addEventListener('mouseenter', () => {
|
|
2830
|
+
button.style.background = '#2563eb';
|
|
2831
|
+
});
|
|
2832
|
+
button.addEventListener('mouseleave', () => {
|
|
2833
|
+
button.style.background = '#3b82f6';
|
|
2834
|
+
});
|
|
2835
|
+
button.addEventListener('click', () => {
|
|
2836
|
+
// Trigger working directory selection
|
|
2837
|
+
const changeDirBtn = document.getElementById('change-dir-btn');
|
|
2838
|
+
if (changeDirBtn) {
|
|
2839
|
+
changeDirBtn.click();
|
|
2840
|
+
} else if (window.dashboard && window.dashboard.workingDirectoryManager) {
|
|
2841
|
+
window.dashboard.workingDirectoryManager.showChangeDirDialog();
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
1324
2845
|
|
|
1325
|
-
|
|
2846
|
+
container.appendChild(messageDiv);
|
|
1326
2847
|
|
|
1327
|
-
//
|
|
1328
|
-
this.
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
2848
|
+
// Update breadcrumb
|
|
2849
|
+
this.updateBreadcrumb('Please select a working directory', 'warning');
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
/**
|
|
2853
|
+
* Remove the no working directory message
|
|
2854
|
+
*/
|
|
2855
|
+
removeNoWorkingDirectoryMessage() {
|
|
2856
|
+
const message = document.getElementById('no-working-dir-message');
|
|
2857
|
+
if (message) {
|
|
2858
|
+
message.remove();
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
/**
|
|
2863
|
+
* Export tree data
|
|
2864
|
+
*/
|
|
2865
|
+
exportTree() {
|
|
2866
|
+
const exportData = {
|
|
2867
|
+
timestamp: new Date().toISOString(),
|
|
2868
|
+
workingDirectory: this.getWorkingDirectory(),
|
|
2869
|
+
stats: this.stats,
|
|
2870
|
+
tree: this.treeData
|
|
2871
|
+
};
|
|
2872
|
+
|
|
2873
|
+
const blob = new Blob([JSON.stringify(exportData, null, 2)],
|
|
2874
|
+
{type: 'application/json'});
|
|
2875
|
+
const url = URL.createObjectURL(blob);
|
|
2876
|
+
const link = document.createElement('a');
|
|
2877
|
+
link.href = url;
|
|
2878
|
+
link.download = `code-tree-${Date.now()}.json`;
|
|
2879
|
+
link.click();
|
|
2880
|
+
URL.revokeObjectURL(url);
|
|
2881
|
+
|
|
2882
|
+
this.showNotification('Tree exported successfully', 'success');
|
|
1333
2883
|
}
|
|
1334
|
-
}
|
|
1335
2884
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
2885
|
+
/**
|
|
2886
|
+
* Update activity ticker with real-time messages
|
|
2887
|
+
*/
|
|
2888
|
+
updateActivityTicker(message, type = 'info') {
|
|
2889
|
+
const breadcrumb = document.getElementById('breadcrumb-content');
|
|
2890
|
+
if (breadcrumb) {
|
|
2891
|
+
// Add spinning icon for loading states
|
|
2892
|
+
const icon = type === 'info' && message.includes('...') ? '⟳ ' : '';
|
|
2893
|
+
breadcrumb.innerHTML = `${icon}${message}`;
|
|
2894
|
+
breadcrumb.className = `breadcrumb-${type}`;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
1339
2897
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
2898
|
+
/**
|
|
2899
|
+
* Update ticker message
|
|
2900
|
+
*/
|
|
2901
|
+
updateTicker(message, type = 'info') {
|
|
2902
|
+
const ticker = document.getElementById('code-tree-ticker');
|
|
2903
|
+
if (ticker) {
|
|
2904
|
+
ticker.textContent = message;
|
|
2905
|
+
ticker.className = `ticker ticker-${type}`;
|
|
2906
|
+
|
|
2907
|
+
// Auto-hide after 5 seconds for non-error messages
|
|
2908
|
+
if (type !== 'error') {
|
|
2909
|
+
setTimeout(() => {
|
|
2910
|
+
ticker.style.opacity = '0';
|
|
2911
|
+
setTimeout(() => {
|
|
2912
|
+
ticker.style.opacity = '1';
|
|
2913
|
+
ticker.textContent = '';
|
|
2914
|
+
}, 300);
|
|
2915
|
+
}, 5000);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
1355
2919
|
}
|
|
1356
2920
|
|
|
1357
|
-
|
|
2921
|
+
// Export for use in other modules
|
|
2922
|
+
window.CodeTree = CodeTree;
|
|
2923
|
+
|
|
2924
|
+
// Auto-initialize when DOM is ready
|
|
2925
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2926
|
+
// Check if we're on a page with code tree container
|
|
2927
|
+
if (document.getElementById('code-tree-container')) {
|
|
2928
|
+
window.codeTree = new CodeTree();
|
|
2929
|
+
|
|
2930
|
+
// Listen for tab changes to initialize when code tab is selected
|
|
2931
|
+
document.addEventListener('click', (e) => {
|
|
2932
|
+
if (e.target.matches('[data-tab="code"]')) {
|
|
2933
|
+
setTimeout(() => {
|
|
2934
|
+
if (window.codeTree && !window.codeTree.initialized) {
|
|
2935
|
+
window.codeTree.initialize();
|
|
2936
|
+
} else if (window.codeTree) {
|
|
2937
|
+
window.codeTree.renderWhenVisible();
|
|
2938
|
+
}
|
|
2939
|
+
}, 100);
|
|
2940
|
+
}
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
});/* Cache buster: 1756393851 */
|