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