@syke1/mcp-server 1.3.20 → 1.4.1
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.
- package/dist/index.js +3 -3
- package/dist/web/public/app.js +1064 -122
- package/dist/web/public/index.html +144 -4
- package/dist/web/public/style.css +812 -39
- package/dist/web/server.js +1 -0
- package/package.json +1 -1
package/dist/web/public/app.js
CHANGED
|
@@ -20,12 +20,424 @@ let heartbeatNodes = new Map(); // nodeId → { riskLevel, startTime, interval }
|
|
|
20
20
|
let diffScrollAnim = null; // animation for diff scroll
|
|
21
21
|
let knownNodeIds = new Set(); // track existing nodes for star-birth detection
|
|
22
22
|
let birthAnimations = new Map(); // nodeId → { startTime, spawnPos, targetPos }
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
let searchActive = false; // true when search input has text
|
|
24
|
+
let _searchRAF = null; // RAF loop for search glow animation
|
|
25
|
+
|
|
26
|
+
// ── File Tree state ──
|
|
27
|
+
let fileTreeData = null; // built tree structure
|
|
28
|
+
let fileTreeExpanded = new Set(); // open folder paths
|
|
29
|
+
let fileTreeFilter = ''; // search filter
|
|
30
|
+
let fileTreeSort = 'name'; // sort mode
|
|
31
|
+
let fileTreeModified = new Map(); // path → {type, timestamp}
|
|
32
|
+
let fileTreeVisible = true; // panel visibility
|
|
33
|
+
let _fileTreeRenderTimer = null; // debounce timer
|
|
34
|
+
let _treeScrollLock = false; // manual scroll lock
|
|
35
|
+
let _treeScrollLockTimer = null; // scroll lock timer
|
|
36
|
+
|
|
37
|
+
let LAYER_HEX = {
|
|
25
38
|
FE: "#00d4ff", BE: "#c084fc", DB: "#ff6b35",
|
|
26
39
|
API: "#00ffaa", CONFIG: "#ffd700", UTIL: "#ff69b4",
|
|
27
40
|
};
|
|
28
41
|
|
|
42
|
+
// ═══════════════════════════════════════════
|
|
43
|
+
// SETTINGS SYSTEM
|
|
44
|
+
// ═══════════════════════════════════════════
|
|
45
|
+
const SETTINGS_DEFAULTS = {
|
|
46
|
+
nodes: {
|
|
47
|
+
sizeMin: 30,
|
|
48
|
+
sizeMultiplier: 3,
|
|
49
|
+
opacity: 1.0,
|
|
50
|
+
resolution: 16,
|
|
51
|
+
selectedColor: "#ffffff",
|
|
52
|
+
},
|
|
53
|
+
links: {
|
|
54
|
+
normalWidth: 1.5,
|
|
55
|
+
highlightWidth: 4.0,
|
|
56
|
+
opacity: 0.9,
|
|
57
|
+
curvatureBase: 0.12,
|
|
58
|
+
normalAlpha: 0.25,
|
|
59
|
+
highlightColor: "#ff2d55",
|
|
60
|
+
},
|
|
61
|
+
particles: {
|
|
62
|
+
normalCount: 6,
|
|
63
|
+
highlightCount: 14,
|
|
64
|
+
normalWidth: 2.0,
|
|
65
|
+
highlightWidth: 4.5,
|
|
66
|
+
normalSpeed: 0.006,
|
|
67
|
+
highlightSpeed: 0.004,
|
|
68
|
+
highlightColor: "#ffffff",
|
|
69
|
+
},
|
|
70
|
+
colors: {
|
|
71
|
+
FE: "#00d4ff",
|
|
72
|
+
BE: "#c084fc",
|
|
73
|
+
DB: "#ff6b35",
|
|
74
|
+
API: "#00ffaa",
|
|
75
|
+
CONFIG: "#ffd700",
|
|
76
|
+
UTIL: "#ff69b4",
|
|
77
|
+
},
|
|
78
|
+
arrows: {
|
|
79
|
+
length: 4,
|
|
80
|
+
position: 1,
|
|
81
|
+
},
|
|
82
|
+
scene: {
|
|
83
|
+
background: "#050a18",
|
|
84
|
+
ambientIntensity: 8,
|
|
85
|
+
pointIntensity: 3,
|
|
86
|
+
pointDistance: 5000,
|
|
87
|
+
fogDensity: 0.00012,
|
|
88
|
+
scanlineOpacity: 1.0,
|
|
89
|
+
},
|
|
90
|
+
camera: {
|
|
91
|
+
initialZ: 3500,
|
|
92
|
+
autoRotateSpeed: 0.0005,
|
|
93
|
+
autoRotateRadius: 1600,
|
|
94
|
+
resetDistance: 1600,
|
|
95
|
+
},
|
|
96
|
+
animation: {
|
|
97
|
+
birthDuration: 2500,
|
|
98
|
+
birthScale: 3,
|
|
99
|
+
spawnX: 5000,
|
|
100
|
+
spawnY: 4000,
|
|
101
|
+
spawnZ: -2000,
|
|
102
|
+
},
|
|
103
|
+
physics: {
|
|
104
|
+
alphaDecay: 0.008,
|
|
105
|
+
velocityDecay: 0.3,
|
|
106
|
+
chargeStrength: -800,
|
|
107
|
+
sameLayerDistance: 250,
|
|
108
|
+
crossLayerDistance: 900,
|
|
109
|
+
clusterStrength: 0.015,
|
|
110
|
+
},
|
|
111
|
+
fileTree: {
|
|
112
|
+
panelWidth: 280,
|
|
113
|
+
fontSize: 11,
|
|
114
|
+
indentSize: 14,
|
|
115
|
+
pulseDuration: 5,
|
|
116
|
+
showLineCount: false,
|
|
117
|
+
showDeps: true,
|
|
118
|
+
showRisk: true,
|
|
119
|
+
autoScrollOnChange: true,
|
|
120
|
+
compactMode: false,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function deepMerge(defaults, override) {
|
|
125
|
+
const result = {};
|
|
126
|
+
for (const key of Object.keys(defaults)) {
|
|
127
|
+
if (typeof defaults[key] === "object" && defaults[key] !== null && !Array.isArray(defaults[key])) {
|
|
128
|
+
result[key] = deepMerge(defaults[key], override?.[key] || {});
|
|
129
|
+
} else {
|
|
130
|
+
result[key] = override?.[key] !== undefined ? override[key] : defaults[key];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function loadSettings() {
|
|
137
|
+
try {
|
|
138
|
+
const stored = JSON.parse(localStorage.getItem("syke-dashboard-settings") || "{}");
|
|
139
|
+
return deepMerge(SETTINGS_DEFAULTS, stored);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.warn("[SYKE] Failed to load settings, using defaults", e);
|
|
142
|
+
return deepMerge(SETTINGS_DEFAULTS, {});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const SETTINGS = loadSettings();
|
|
147
|
+
|
|
148
|
+
// Apply colors from settings on startup
|
|
149
|
+
Object.assign(LAYER_HEX, SETTINGS.colors);
|
|
150
|
+
|
|
151
|
+
function saveSettings() {
|
|
152
|
+
try {
|
|
153
|
+
localStorage.setItem("syke-dashboard-settings", JSON.stringify(SETTINGS));
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.warn("[SYKE] Failed to save settings", e);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resetSettingsGroup(group) {
|
|
160
|
+
if (SETTINGS_DEFAULTS[group]) {
|
|
161
|
+
SETTINGS[group] = deepMerge(SETTINGS_DEFAULTS[group], {});
|
|
162
|
+
saveSettings();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resetAllSettings() {
|
|
167
|
+
for (const group of Object.keys(SETTINGS_DEFAULTS)) {
|
|
168
|
+
SETTINGS[group] = deepMerge(SETTINGS_DEFAULTS[group], {});
|
|
169
|
+
}
|
|
170
|
+
Object.assign(LAYER_HEX, SETTINGS.colors);
|
|
171
|
+
saveSettings();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function applySettings(group) {
|
|
175
|
+
if (!Graph) return;
|
|
176
|
+
switch (group) {
|
|
177
|
+
case "nodes":
|
|
178
|
+
refreshGraph();
|
|
179
|
+
break;
|
|
180
|
+
case "links":
|
|
181
|
+
Graph.linkCurvature(link => SETTINGS.links.curvatureBase + (hc(getSrcId(link) + getTgtId(link)) % 20) * 0.01);
|
|
182
|
+
Graph.linkOpacity(SETTINGS.links.opacity);
|
|
183
|
+
refreshGraph();
|
|
184
|
+
break;
|
|
185
|
+
case "particles":
|
|
186
|
+
refreshGraph();
|
|
187
|
+
break;
|
|
188
|
+
case "colors":
|
|
189
|
+
Object.assign(LAYER_HEX, SETTINGS.colors);
|
|
190
|
+
if (graphData) {
|
|
191
|
+
const layerCounts = {};
|
|
192
|
+
graphData.nodes.forEach(n => { layerCounts[n.layer] = (layerCounts[n.layer] || 0) + 1; });
|
|
193
|
+
buildLegend(layerCounts);
|
|
194
|
+
}
|
|
195
|
+
refreshGraph();
|
|
196
|
+
break;
|
|
197
|
+
case "arrows":
|
|
198
|
+
Graph.linkDirectionalArrowLength(SETTINGS.arrows.length);
|
|
199
|
+
Graph.linkDirectionalArrowRelPos(SETTINGS.arrows.position);
|
|
200
|
+
break;
|
|
201
|
+
case "scene":
|
|
202
|
+
Graph.backgroundColor(SETTINGS.scene.background);
|
|
203
|
+
try {
|
|
204
|
+
const scene = Graph.scene();
|
|
205
|
+
if (scene) {
|
|
206
|
+
scene.children.forEach(c => {
|
|
207
|
+
if (c.isAmbientLight) c.intensity = SETTINGS.scene.ambientIntensity;
|
|
208
|
+
if (c.isPointLight) { c.intensity = SETTINGS.scene.pointIntensity; c.distance = SETTINGS.scene.pointDistance; }
|
|
209
|
+
});
|
|
210
|
+
if (scene.fog) scene.fog.density = SETTINGS.scene.fogDensity;
|
|
211
|
+
}
|
|
212
|
+
} catch(e) {}
|
|
213
|
+
const scanline = document.getElementById("scanline");
|
|
214
|
+
if (scanline) scanline.style.opacity = SETTINGS.scene.scanlineOpacity;
|
|
215
|
+
break;
|
|
216
|
+
case "physics":
|
|
217
|
+
Graph.d3AlphaDecay(SETTINGS.physics.alphaDecay);
|
|
218
|
+
Graph.d3VelocityDecay(SETTINGS.physics.velocityDecay);
|
|
219
|
+
try {
|
|
220
|
+
Graph.d3Force("charge").strength(SETTINGS.physics.chargeStrength);
|
|
221
|
+
Graph.d3Force("link")
|
|
222
|
+
.distance(l => srcLayer(l) === tgtLayer(l) ? SETTINGS.physics.sameLayerDistance : SETTINGS.physics.crossLayerDistance);
|
|
223
|
+
Graph.d3Force("cluster", clusterForce(SETTINGS.physics.clusterStrength));
|
|
224
|
+
} catch(e) {}
|
|
225
|
+
Graph.d3ReheatSimulation();
|
|
226
|
+
break;
|
|
227
|
+
case "camera":
|
|
228
|
+
// Camera values are read live in autoRotate loop, no action needed
|
|
229
|
+
break;
|
|
230
|
+
case "animation":
|
|
231
|
+
// Animation values are read when new nodes appear, no action needed
|
|
232
|
+
break;
|
|
233
|
+
case "fileTree":
|
|
234
|
+
applyFileTreeSettings();
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function setupSettings() {
|
|
240
|
+
const CTRL_MAP = [
|
|
241
|
+
// [group, key, selector, type]
|
|
242
|
+
["nodes", "sizeMin", "#set-node-sizeMin", "range"],
|
|
243
|
+
["nodes", "sizeMultiplier", "#set-node-sizeMultiplier", "range"],
|
|
244
|
+
["nodes", "opacity", "#set-node-opacity", "range"],
|
|
245
|
+
["nodes", "resolution", "#set-node-resolution", "range"],
|
|
246
|
+
["nodes", "selectedColor", "#set-node-selectedColor", "color"],
|
|
247
|
+
["links", "normalWidth", "#set-link-normalWidth", "range"],
|
|
248
|
+
["links", "highlightWidth", "#set-link-highlightWidth", "range"],
|
|
249
|
+
["links", "opacity", "#set-link-opacity", "range"],
|
|
250
|
+
["links", "curvatureBase", "#set-link-curvatureBase", "range"],
|
|
251
|
+
["links", "normalAlpha", "#set-link-normalAlpha", "range"],
|
|
252
|
+
["links", "highlightColor", "#set-link-highlightColor", "color"],
|
|
253
|
+
["particles", "normalCount", "#set-part-normalCount", "range"],
|
|
254
|
+
["particles", "highlightCount", "#set-part-highlightCount", "range"],
|
|
255
|
+
["particles", "normalWidth", "#set-part-normalWidth", "range"],
|
|
256
|
+
["particles", "highlightWidth", "#set-part-highlightWidth", "range"],
|
|
257
|
+
["particles", "normalSpeed", "#set-part-normalSpeed", "range"],
|
|
258
|
+
["particles", "highlightSpeed", "#set-part-highlightSpeed", "range"],
|
|
259
|
+
["particles", "highlightColor", "#set-part-highlightColor", "color"],
|
|
260
|
+
["colors", "FE", "#set-color-FE", "color"],
|
|
261
|
+
["colors", "BE", "#set-color-BE", "color"],
|
|
262
|
+
["colors", "DB", "#set-color-DB", "color"],
|
|
263
|
+
["colors", "API", "#set-color-API", "color"],
|
|
264
|
+
["colors", "CONFIG", "#set-color-CONFIG", "color"],
|
|
265
|
+
["colors", "UTIL", "#set-color-UTIL", "color"],
|
|
266
|
+
["arrows", "length", "#set-arrow-length", "range"],
|
|
267
|
+
["arrows", "position", "#set-arrow-position", "range"],
|
|
268
|
+
["scene", "background", "#set-scene-background", "color"],
|
|
269
|
+
["scene", "ambientIntensity", "#set-scene-ambientIntensity", "range"],
|
|
270
|
+
["scene", "pointIntensity", "#set-scene-pointIntensity", "range"],
|
|
271
|
+
["scene", "pointDistance", "#set-scene-pointDistance", "range"],
|
|
272
|
+
["scene", "fogDensity", "#set-scene-fogDensity", "range"],
|
|
273
|
+
["scene", "scanlineOpacity", "#set-scene-scanlineOpacity", "range"],
|
|
274
|
+
["camera", "initialZ", "#set-cam-initialZ", "range"],
|
|
275
|
+
["camera", "autoRotateSpeed", "#set-cam-autoRotateSpeed", "range"],
|
|
276
|
+
["camera", "autoRotateRadius", "#set-cam-autoRotateRadius", "range"],
|
|
277
|
+
["camera", "resetDistance", "#set-cam-resetDistance", "range"],
|
|
278
|
+
["physics", "alphaDecay", "#set-phys-alphaDecay", "range"],
|
|
279
|
+
["physics", "velocityDecay", "#set-phys-velocityDecay", "range"],
|
|
280
|
+
["physics", "chargeStrength", "#set-phys-chargeStrength", "range"],
|
|
281
|
+
["physics", "sameLayerDistance", "#set-phys-sameLayerDistance", "range"],
|
|
282
|
+
["physics", "crossLayerDistance", "#set-phys-crossLayerDistance", "range"],
|
|
283
|
+
["physics", "clusterStrength", "#set-phys-clusterStrength", "range"],
|
|
284
|
+
["animation", "birthDuration", "#set-anim-birthDuration", "range"],
|
|
285
|
+
["animation", "birthScale", "#set-anim-birthScale", "range"],
|
|
286
|
+
["animation", "spawnX", "#set-anim-spawnX", "range"],
|
|
287
|
+
["animation", "spawnY", "#set-anim-spawnY", "range"],
|
|
288
|
+
["animation", "spawnZ", "#set-anim-spawnZ", "range"],
|
|
289
|
+
["fileTree", "panelWidth", "#set-ft-panelWidth", "range"],
|
|
290
|
+
["fileTree", "fontSize", "#set-ft-fontSize", "range"],
|
|
291
|
+
["fileTree", "indentSize", "#set-ft-indentSize", "range"],
|
|
292
|
+
["fileTree", "pulseDuration", "#set-ft-pulseDuration", "range"],
|
|
293
|
+
["fileTree", "showLineCount", "#set-ft-showLineCount", "checkbox"],
|
|
294
|
+
["fileTree", "showDeps", "#set-ft-showDeps", "checkbox"],
|
|
295
|
+
["fileTree", "showRisk", "#set-ft-showRisk", "checkbox"],
|
|
296
|
+
["fileTree", "autoScrollOnChange", "#set-ft-autoScrollOnChange", "checkbox"],
|
|
297
|
+
["fileTree", "compactMode", "#set-ft-compactMode", "checkbox"],
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
// Init values and bind events
|
|
301
|
+
for (const [group, key, selector, type] of CTRL_MAP) {
|
|
302
|
+
const el = document.querySelector(selector);
|
|
303
|
+
if (!el) continue;
|
|
304
|
+
const valEl = el.parentElement?.querySelector(".set-val");
|
|
305
|
+
|
|
306
|
+
if (type === "checkbox") {
|
|
307
|
+
el.checked = !!SETTINGS[group][key];
|
|
308
|
+
el.addEventListener("change", () => {
|
|
309
|
+
SETTINGS[group][key] = el.checked;
|
|
310
|
+
applySettings(group);
|
|
311
|
+
saveSettings();
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
el.value = SETTINGS[group][key];
|
|
315
|
+
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
316
|
+
el.addEventListener("input", () => {
|
|
317
|
+
const v = type === "color" ? el.value : parseFloat(el.value);
|
|
318
|
+
SETTINGS[group][key] = v;
|
|
319
|
+
if (valEl) valEl.textContent = formatSetVal(v, type);
|
|
320
|
+
applySettings(group);
|
|
321
|
+
saveSettings();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Collapsible sections
|
|
327
|
+
document.querySelectorAll(".set-section-hdr").forEach(hdr => {
|
|
328
|
+
hdr.addEventListener("click", () => {
|
|
329
|
+
const body = hdr.nextElementSibling;
|
|
330
|
+
const arrow = hdr.querySelector(".set-arrow");
|
|
331
|
+
if (body) body.classList.toggle("collapsed");
|
|
332
|
+
if (arrow) arrow.classList.toggle("open");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Reset per group
|
|
337
|
+
document.querySelectorAll(".set-rst-btn").forEach(btn => {
|
|
338
|
+
btn.addEventListener("click", (e) => {
|
|
339
|
+
e.stopPropagation();
|
|
340
|
+
const group = btn.dataset.group;
|
|
341
|
+
resetSettingsGroup(group);
|
|
342
|
+
applySettings(group);
|
|
343
|
+
// Refresh UI inputs
|
|
344
|
+
for (const [g, key, selector, type] of CTRL_MAP) {
|
|
345
|
+
if (g !== group) continue;
|
|
346
|
+
const el = document.querySelector(selector);
|
|
347
|
+
if (!el) continue;
|
|
348
|
+
if (type === "checkbox") { el.checked = !!SETTINGS[group][key]; }
|
|
349
|
+
else { el.value = SETTINGS[group][key]; }
|
|
350
|
+
const valEl = el.parentElement?.querySelector(".set-val");
|
|
351
|
+
if (valEl && type !== "checkbox") valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Reset all
|
|
357
|
+
const resetAllBtn = document.getElementById("set-reset-all");
|
|
358
|
+
if (resetAllBtn) {
|
|
359
|
+
resetAllBtn.addEventListener("click", () => {
|
|
360
|
+
resetAllSettings();
|
|
361
|
+
for (const group of Object.keys(SETTINGS_DEFAULTS)) applySettings(group);
|
|
362
|
+
// Refresh all UI inputs
|
|
363
|
+
for (const [group, key, selector, type] of CTRL_MAP) {
|
|
364
|
+
const el = document.querySelector(selector);
|
|
365
|
+
if (!el) continue;
|
|
366
|
+
if (type === "checkbox") { el.checked = !!SETTINGS[group][key]; }
|
|
367
|
+
else { el.value = SETTINGS[group][key]; }
|
|
368
|
+
const valEl = el.parentElement?.querySelector(".set-val");
|
|
369
|
+
if (valEl && type !== "checkbox") valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Export
|
|
375
|
+
const exportBtn = document.getElementById("set-export");
|
|
376
|
+
if (exportBtn) {
|
|
377
|
+
exportBtn.addEventListener("click", () => {
|
|
378
|
+
const blob = new Blob([JSON.stringify(SETTINGS, null, 2)], { type: "application/json" });
|
|
379
|
+
const url = URL.createObjectURL(blob);
|
|
380
|
+
const a = document.createElement("a");
|
|
381
|
+
a.href = url;
|
|
382
|
+
a.download = "syke-settings.json";
|
|
383
|
+
a.click();
|
|
384
|
+
URL.revokeObjectURL(url);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Import
|
|
389
|
+
const importBtn = document.getElementById("set-import");
|
|
390
|
+
if (importBtn) {
|
|
391
|
+
importBtn.addEventListener("click", () => {
|
|
392
|
+
const input = document.createElement("input");
|
|
393
|
+
input.type = "file";
|
|
394
|
+
input.accept = ".json";
|
|
395
|
+
input.addEventListener("change", () => {
|
|
396
|
+
const file = input.files[0];
|
|
397
|
+
if (!file) return;
|
|
398
|
+
const reader = new FileReader();
|
|
399
|
+
reader.onload = () => {
|
|
400
|
+
try {
|
|
401
|
+
const imported = JSON.parse(reader.result);
|
|
402
|
+
const merged = deepMerge(SETTINGS_DEFAULTS, imported);
|
|
403
|
+
for (const group of Object.keys(merged)) {
|
|
404
|
+
SETTINGS[group] = merged[group];
|
|
405
|
+
}
|
|
406
|
+
Object.assign(LAYER_HEX, SETTINGS.colors);
|
|
407
|
+
saveSettings();
|
|
408
|
+
for (const group of Object.keys(SETTINGS_DEFAULTS)) applySettings(group);
|
|
409
|
+
// Refresh all UI inputs
|
|
410
|
+
for (const [group, key, selector, type] of CTRL_MAP) {
|
|
411
|
+
const el = document.querySelector(selector);
|
|
412
|
+
if (!el) continue;
|
|
413
|
+
if (type === "checkbox") { el.checked = !!SETTINGS[group][key]; }
|
|
414
|
+
else { el.value = SETTINGS[group][key]; }
|
|
415
|
+
const valEl = el.parentElement?.querySelector(".set-val");
|
|
416
|
+
if (valEl && type !== "checkbox") valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
417
|
+
}
|
|
418
|
+
} catch(e) {
|
|
419
|
+
console.error("[SYKE] Import failed:", e);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
reader.readAsText(file);
|
|
423
|
+
});
|
|
424
|
+
input.click();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Apply scanline opacity on startup
|
|
429
|
+
const scanline = document.getElementById("scanline");
|
|
430
|
+
if (scanline) scanline.style.opacity = SETTINGS.scene.scanlineOpacity;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function formatSetVal(v, type) {
|
|
434
|
+
if (type === "color") return v;
|
|
435
|
+
if (Number.isInteger(v)) return v.toString();
|
|
436
|
+
if (Math.abs(v) < 0.01) return v.toFixed(5);
|
|
437
|
+
if (Math.abs(v) < 1) return v.toFixed(3);
|
|
438
|
+
return v.toFixed(1);
|
|
439
|
+
}
|
|
440
|
+
|
|
29
441
|
const LAYER_KEYS = ["FE", "BE", "DB", "API", "CONFIG", "UTIL"];
|
|
30
442
|
|
|
31
443
|
const LAYER_CENTERS = {
|
|
@@ -49,7 +461,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
49
461
|
setupKeyboardShortcuts();
|
|
50
462
|
setupContextMenu();
|
|
51
463
|
setupTabs();
|
|
464
|
+
setupSettings();
|
|
52
465
|
setupProjectModal();
|
|
466
|
+
setupFileTree();
|
|
53
467
|
initSSE();
|
|
54
468
|
startHealthCheck();
|
|
55
469
|
});
|
|
@@ -113,7 +527,7 @@ async function loadGraph() {
|
|
|
113
527
|
const isReload = Graph !== null;
|
|
114
528
|
|
|
115
529
|
// Spawn point for new nodes (top-right corner of 3D space)
|
|
116
|
-
const SPAWN = { x:
|
|
530
|
+
const SPAWN = { x: SETTINGS.animation.spawnX, y: SETTINGS.animation.spawnY, z: SETTINGS.animation.spawnZ };
|
|
117
531
|
|
|
118
532
|
// Preserve existing node positions on reload
|
|
119
533
|
const currentPositions = {};
|
|
@@ -143,7 +557,7 @@ async function loadGraph() {
|
|
|
143
557
|
startTime: Date.now(),
|
|
144
558
|
spawnPos: { ...SPAWN },
|
|
145
559
|
targetPos: { ...targetPos },
|
|
146
|
-
duration:
|
|
560
|
+
duration: SETTINGS.animation.birthDuration,
|
|
147
561
|
});
|
|
148
562
|
}
|
|
149
563
|
|
|
@@ -181,6 +595,7 @@ async function loadGraph() {
|
|
|
181
595
|
if (isReload) {
|
|
182
596
|
Graph.graphData(graphData);
|
|
183
597
|
buildLegend(layerCounts);
|
|
598
|
+
renderFileTreeDebounced();
|
|
184
599
|
console.log("[SYKE] Graph updated (reload), birth animations:", birthAnimations.size);
|
|
185
600
|
|
|
186
601
|
// ── Star birth camera choreography ──
|
|
@@ -209,7 +624,7 @@ async function loadGraph() {
|
|
|
209
624
|
// Phase 3: Zoom out to overview (at 3.5s, 1.5s transition)
|
|
210
625
|
setTimeout(() => {
|
|
211
626
|
Graph.cameraPosition(
|
|
212
|
-
{ x: 0, y: 0, z:
|
|
627
|
+
{ x: 0, y: 0, z: SETTINGS.camera.initialZ },
|
|
213
628
|
{ x: 0, y: 0, z: 0 },
|
|
214
629
|
1500
|
|
215
630
|
);
|
|
@@ -228,16 +643,22 @@ async function loadGraph() {
|
|
|
228
643
|
const container = document.getElementById("3d-graph");
|
|
229
644
|
|
|
230
645
|
Graph = ForceGraph3D()(container)
|
|
231
|
-
.width(
|
|
646
|
+
.width(getGraphPanelWidth())
|
|
232
647
|
.height(window.innerHeight - 100)
|
|
233
648
|
.graphData(graphData)
|
|
234
|
-
.backgroundColor(
|
|
649
|
+
.backgroundColor(SETTINGS.scene.background)
|
|
235
650
|
.showNavInfo(false)
|
|
236
651
|
|
|
237
652
|
.nodeColor(node => getNodeColor(node))
|
|
238
653
|
.nodeVal(node => {
|
|
239
654
|
if (!isNodeVisible(node)) return 0.001;
|
|
240
|
-
const base = Math.max(
|
|
655
|
+
const base = Math.max(SETTINGS.nodes.sizeMin, Math.sqrt(node.lineCount) * SETTINGS.nodes.sizeMultiplier);
|
|
656
|
+
// Search mode: shrink non-matches, boost matches
|
|
657
|
+
if (searchActive) {
|
|
658
|
+
if (!highlightNodes.has(node.id)) return base * 0.3;
|
|
659
|
+
const pulse = 0.9 + 0.1 * Math.sin(Date.now() / 400);
|
|
660
|
+
return base * 1.4 * pulse;
|
|
661
|
+
}
|
|
241
662
|
const hb = heartbeatNodes.get(node.id);
|
|
242
663
|
if (hb) {
|
|
243
664
|
const elapsed = Date.now() - hb.startTime;
|
|
@@ -255,13 +676,13 @@ async function loadGraph() {
|
|
|
255
676
|
if (birth) {
|
|
256
677
|
const t = Math.min(1, (Date.now() - birth.startTime) / birth.duration);
|
|
257
678
|
if (t >= 1) birthAnimations.delete(node.id);
|
|
258
|
-
const scale = 1 + (
|
|
679
|
+
const scale = 1 + (SETTINGS.animation.birthScale - 1) * (1 - t) * (1 - t); // ease-out: Nx → 1x
|
|
259
680
|
return base * scale;
|
|
260
681
|
}
|
|
261
682
|
return base;
|
|
262
683
|
})
|
|
263
|
-
.nodeOpacity(
|
|
264
|
-
.nodeResolution(
|
|
684
|
+
.nodeOpacity(SETTINGS.nodes.opacity)
|
|
685
|
+
.nodeResolution(SETTINGS.nodes.resolution)
|
|
265
686
|
.nodeVisibility(node => isNodeVisible(node))
|
|
266
687
|
.nodeLabel(node => {
|
|
267
688
|
const c = LAYER_HEX[node.layer] || "#ccc";
|
|
@@ -272,34 +693,36 @@ async function loadGraph() {
|
|
|
272
693
|
})
|
|
273
694
|
|
|
274
695
|
.linkColor(link => getLinkColor(link))
|
|
275
|
-
.linkWidth(link => highlightLinks.has(link) ?
|
|
276
|
-
.linkOpacity(
|
|
696
|
+
.linkWidth(link => highlightLinks.has(link) ? SETTINGS.links.highlightWidth : SETTINGS.links.normalWidth)
|
|
697
|
+
.linkOpacity(SETTINGS.links.opacity)
|
|
277
698
|
.linkVisibility(link => isLinkVisible(link))
|
|
278
|
-
.linkCurvature(link =>
|
|
699
|
+
.linkCurvature(link => SETTINGS.links.curvatureBase + (hc(getSrcId(link) + getTgtId(link)) % 20) * 0.01)
|
|
279
700
|
.linkCurveRotation(link => (hc(getTgtId(link) + getSrcId(link)) % 628) / 100)
|
|
280
701
|
|
|
281
702
|
.linkDirectionalParticles(link => {
|
|
282
|
-
if (highlightLinks.has(link)
|
|
703
|
+
if (searchActive) return highlightLinks.has(link) ? SETTINGS.particles.highlightCount : 0;
|
|
704
|
+
if (highlightLinks.has(link)) return SETTINGS.particles.highlightCount;
|
|
283
705
|
const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
|
|
284
|
-
return isActive ?
|
|
706
|
+
return isActive ? Math.round(SETTINGS.particles.normalCount * 0.67) : SETTINGS.particles.normalCount;
|
|
285
707
|
})
|
|
286
708
|
.linkDirectionalParticleWidth(link => {
|
|
287
|
-
if (highlightLinks.has(link)) return
|
|
709
|
+
if (highlightLinks.has(link)) return SETTINGS.particles.highlightWidth;
|
|
288
710
|
const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
|
|
289
|
-
return isActive ?
|
|
711
|
+
return isActive ? SETTINGS.particles.normalWidth * 0.75 : SETTINGS.particles.normalWidth;
|
|
290
712
|
})
|
|
291
|
-
.linkDirectionalParticleSpeed(link => highlightLinks.has(link) ?
|
|
713
|
+
.linkDirectionalParticleSpeed(link => highlightLinks.has(link) ? SETTINGS.particles.highlightSpeed : SETTINGS.particles.normalSpeed)
|
|
292
714
|
.linkDirectionalParticleColor(link => {
|
|
293
|
-
if (highlightLinks.has(link)) return "#
|
|
715
|
+
if (searchActive && highlightLinks.has(link)) return LAYER_HEX[srcLayer(link)] || "#00d4ff";
|
|
716
|
+
if (highlightLinks.has(link)) return SETTINGS.particles.highlightColor;
|
|
294
717
|
const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
|
|
295
718
|
if (isActive) return "rgba(150,180,220,0.6)";
|
|
296
719
|
return LAYER_HEX[srcLayer(link)] || "#ff69b4";
|
|
297
720
|
})
|
|
298
721
|
|
|
299
|
-
.linkDirectionalArrowLength(
|
|
300
|
-
.linkDirectionalArrowRelPos(
|
|
722
|
+
.linkDirectionalArrowLength(SETTINGS.arrows.length)
|
|
723
|
+
.linkDirectionalArrowRelPos(SETTINGS.arrows.position)
|
|
301
724
|
.linkDirectionalArrowColor(link => {
|
|
302
|
-
if (highlightLinks.has(link)) return
|
|
725
|
+
if (highlightLinks.has(link)) return SETTINGS.links.highlightColor;
|
|
303
726
|
return rgba(LAYER_HEX[srcLayer(link)] || "#ff69b4", 0.4);
|
|
304
727
|
})
|
|
305
728
|
|
|
@@ -345,27 +768,28 @@ async function loadGraph() {
|
|
|
345
768
|
}
|
|
346
769
|
})
|
|
347
770
|
|
|
348
|
-
.d3AlphaDecay(
|
|
349
|
-
.d3VelocityDecay(
|
|
771
|
+
.d3AlphaDecay(SETTINGS.physics.alphaDecay)
|
|
772
|
+
.d3VelocityDecay(SETTINGS.physics.velocityDecay)
|
|
350
773
|
.warmupTicks(300)
|
|
351
774
|
.cooldownTicks(800)
|
|
352
775
|
.enablePointerInteraction(true);
|
|
353
776
|
|
|
354
|
-
Graph.d3Force("cluster", clusterForce(
|
|
355
|
-
Graph.d3Force("charge").strength(
|
|
777
|
+
Graph.d3Force("cluster", clusterForce(SETTINGS.physics.clusterStrength));
|
|
778
|
+
Graph.d3Force("charge").strength(SETTINGS.physics.chargeStrength);
|
|
356
779
|
Graph.d3Force("link")
|
|
357
|
-
.distance(l => srcLayer(l) === tgtLayer(l) ?
|
|
780
|
+
.distance(l => srcLayer(l) === tgtLayer(l) ? SETTINGS.physics.sameLayerDistance : SETTINGS.physics.crossLayerDistance)
|
|
358
781
|
.strength(l => srcLayer(l) === tgtLayer(l) ? 0.2 : 0.05);
|
|
359
782
|
|
|
360
|
-
Graph.cameraPosition({ x: 0, y: 0, z:
|
|
783
|
+
Graph.cameraPosition({ x: 0, y: 0, z: SETTINGS.camera.initialZ });
|
|
361
784
|
|
|
362
785
|
setTimeout(() => {
|
|
363
786
|
try {
|
|
364
787
|
const scene = Graph.scene();
|
|
365
788
|
if (!scene) return;
|
|
366
|
-
scene.add(new THREE.AmbientLight(0xffffff,
|
|
367
|
-
|
|
368
|
-
scene.
|
|
789
|
+
scene.add(new THREE.AmbientLight(0xffffff, SETTINGS.scene.ambientIntensity));
|
|
790
|
+
const ptLight = new THREE.PointLight(0xffffff, SETTINGS.scene.pointIntensity, SETTINGS.scene.pointDistance);
|
|
791
|
+
scene.add(ptLight);
|
|
792
|
+
scene.fog = new THREE.FogExp2(parseInt(SETTINGS.scene.background.replace("#",""), 16), SETTINGS.scene.fogDensity);
|
|
369
793
|
console.log("[SYKE] Scene ready");
|
|
370
794
|
} catch(e) { console.warn(e); }
|
|
371
795
|
}, 500);
|
|
@@ -381,10 +805,11 @@ async function loadGraph() {
|
|
|
381
805
|
container.addEventListener("touchstart", stopUser);
|
|
382
806
|
|
|
383
807
|
window.addEventListener("resize", () => {
|
|
384
|
-
if (Graph) Graph.width(
|
|
808
|
+
if (Graph) Graph.width(getGraphPanelWidth()).height(window.innerHeight - 100);
|
|
385
809
|
});
|
|
386
810
|
|
|
387
811
|
buildLegend(layerCounts);
|
|
812
|
+
renderFileTreeDebounced();
|
|
388
813
|
createNodeLabels();
|
|
389
814
|
updateLabelsLoop();
|
|
390
815
|
|
|
@@ -528,13 +953,24 @@ function getNodeColor(node) {
|
|
|
528
953
|
const pulse = 0.5 + 0.5 * Math.sin(t);
|
|
529
954
|
return `rgb(255,${Math.round(180 + pulse * 75)},${Math.round(50 + pulse * 50)})`;
|
|
530
955
|
}
|
|
531
|
-
if (node.id === selectedNodeId) return
|
|
956
|
+
if (node.id === selectedNodeId) return SETTINGS.nodes.selectedColor;
|
|
532
957
|
const base = LAYER_HEX[node.layer] || "#ff69b4";
|
|
533
958
|
if (highlightNodes.size > 0 && !highlightNodes.has(node.id)) {
|
|
534
|
-
|
|
959
|
+
if (searchActive) {
|
|
960
|
+
// Search mode: near-invisible ghost nodes
|
|
961
|
+
return dimHex(base, 0.06);
|
|
962
|
+
}
|
|
535
963
|
const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
|
|
536
964
|
return dimHex(base, isActive ? 0.12 : 0.35);
|
|
537
965
|
}
|
|
966
|
+
// Search match: pulsing bright glow
|
|
967
|
+
if (searchActive && highlightNodes.has(node.id)) {
|
|
968
|
+
const pulse = 0.85 + 0.15 * Math.sin(Date.now() / 300);
|
|
969
|
+
const r = Math.min(255, Math.round(parseInt(base.slice(1,3),16) * pulse + 40));
|
|
970
|
+
const g = Math.min(255, Math.round(parseInt(base.slice(3,5),16) * pulse + 40));
|
|
971
|
+
const b = Math.min(255, Math.round(parseInt(base.slice(5,7),16) * pulse + 40));
|
|
972
|
+
return `rgb(${r},${g},${b})`;
|
|
973
|
+
}
|
|
538
974
|
return base;
|
|
539
975
|
}
|
|
540
976
|
|
|
@@ -599,12 +1035,17 @@ function dimHex(hex, factor) {
|
|
|
599
1035
|
}
|
|
600
1036
|
|
|
601
1037
|
function getLinkColor(link) {
|
|
602
|
-
if (highlightLinks.has(link))
|
|
1038
|
+
if (searchActive && highlightLinks.has(link)) {
|
|
1039
|
+
// Search mode: matched links glow with source layer color
|
|
1040
|
+
return LAYER_HEX[srcLayer(link)] || "#00d4ff";
|
|
1041
|
+
}
|
|
1042
|
+
if (highlightLinks.has(link)) return SETTINGS.links.highlightColor;
|
|
603
1043
|
if (highlightNodes.size > 0 && !highlightLinks.has(link)) {
|
|
1044
|
+
if (searchActive) return "rgba(30,40,60,0.02)"; // near-invisible
|
|
604
1045
|
const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
|
|
605
1046
|
return isActive ? "rgba(60,70,90,0.03)" : "rgba(100,120,150,0.06)";
|
|
606
1047
|
}
|
|
607
|
-
return rgba(LAYER_HEX[srcLayer(link)] || "#ff69b4",
|
|
1048
|
+
return rgba(LAYER_HEX[srcLayer(link)] || "#ff69b4", SETTINGS.links.normalAlpha);
|
|
608
1049
|
}
|
|
609
1050
|
|
|
610
1051
|
// ═══════════════════════════════════════════
|
|
@@ -689,11 +1130,11 @@ function buildLegend(counts) {
|
|
|
689
1130
|
let rotAngle = 0, rotRAF = null;
|
|
690
1131
|
function startAutoRotate() {
|
|
691
1132
|
if (!autoRotate || !Graph) return;
|
|
692
|
-
rotAngle +=
|
|
1133
|
+
rotAngle += SETTINGS.camera.autoRotateSpeed;
|
|
693
1134
|
Graph.cameraPosition({
|
|
694
|
-
x:
|
|
1135
|
+
x: SETTINGS.camera.autoRotateRadius * Math.sin(rotAngle),
|
|
695
1136
|
y: 200 + Math.sin(rotAngle * 0.3) * 100,
|
|
696
|
-
z:
|
|
1137
|
+
z: SETTINGS.camera.autoRotateRadius * Math.cos(rotAngle),
|
|
697
1138
|
});
|
|
698
1139
|
rotRAF = requestAnimationFrame(startAutoRotate);
|
|
699
1140
|
}
|
|
@@ -749,6 +1190,8 @@ async function handleNodeClick(node) {
|
|
|
749
1190
|
loadSimulation(node.id);
|
|
750
1191
|
// Start Star Wars code crawl
|
|
751
1192
|
startCodeCrawl(node.id);
|
|
1193
|
+
// Sync file tree selection
|
|
1194
|
+
treeScrollToFile(node.id);
|
|
752
1195
|
}
|
|
753
1196
|
|
|
754
1197
|
function handleBackgroundClick() {
|
|
@@ -783,7 +1226,12 @@ async function showImpact(fileId, nd) {
|
|
|
783
1226
|
document.getElementById("impact-content").innerHTML = '<div class="loading"><div class="spinner"></div>TRACING...</div>';
|
|
784
1227
|
|
|
785
1228
|
try {
|
|
786
|
-
const res = await fetch("/api/impact/" + fileId);
|
|
1229
|
+
const res = await fetch("/api/impact/" + fileId.split("/").map(encodeURIComponent).join("/"));
|
|
1230
|
+
const ct = res.headers.get("content-type") || "";
|
|
1231
|
+
if (!ct.includes("application/json")) {
|
|
1232
|
+
document.getElementById("impact-content").innerHTML = `<p class="placeholder">ERROR: Server returned non-JSON (${res.status})</p>`;
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
787
1235
|
const impact = await res.json();
|
|
788
1236
|
if (!res.ok) { document.getElementById("impact-content").innerHTML = `<p class="placeholder">ERROR: ${impact.error}</p>`; return; }
|
|
789
1237
|
|
|
@@ -876,7 +1324,12 @@ async function loadCodePreview(fileId) {
|
|
|
876
1324
|
const el = document.getElementById("code-content");
|
|
877
1325
|
el.innerHTML = '<div class="loading"><div class="spinner"></div>LOADING...</div>';
|
|
878
1326
|
try {
|
|
879
|
-
const res = await fetch("/api/file-content/" + fileId);
|
|
1327
|
+
const res = await fetch("/api/file-content/" + fileId.split("/").map(encodeURIComponent).join("/"));
|
|
1328
|
+
const ct = res.headers.get("content-type") || "";
|
|
1329
|
+
if (!ct.includes("application/json")) {
|
|
1330
|
+
el.innerHTML = `<p class="placeholder">ERROR: Server returned non-JSON (${res.status}). File: ${fileId}</p>`;
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
880
1333
|
const data = await res.json();
|
|
881
1334
|
if (!res.ok) { el.innerHTML = `<p class="placeholder">ERROR: ${data.error}</p>`; return; }
|
|
882
1335
|
|
|
@@ -901,7 +1354,12 @@ async function loadSimulation(fileId) {
|
|
|
901
1354
|
const el = document.getElementById("sim-content");
|
|
902
1355
|
el.innerHTML = '<div class="loading"><div class="spinner"></div>SIMULATING...</div>';
|
|
903
1356
|
try {
|
|
904
|
-
const res = await fetch("/api/simulate-delete/" + fileId);
|
|
1357
|
+
const res = await fetch("/api/simulate-delete/" + fileId.split("/").map(encodeURIComponent).join("/"));
|
|
1358
|
+
const ct = res.headers.get("content-type") || "";
|
|
1359
|
+
if (!ct.includes("application/json")) {
|
|
1360
|
+
el.innerHTML = `<p class="placeholder">ERROR: Server returned non-JSON (${res.status})</p>`;
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
905
1363
|
const data = await res.json();
|
|
906
1364
|
if (!res.ok) { el.innerHTML = `<p class="placeholder">ERROR: ${data.error}</p>`; return; }
|
|
907
1365
|
|
|
@@ -1437,6 +1895,9 @@ function setupKeyboardShortcuts() {
|
|
|
1437
1895
|
case "s":
|
|
1438
1896
|
if (selectedFile) { loadSimulation(selectedFile); switchTab("simulate"); }
|
|
1439
1897
|
break;
|
|
1898
|
+
case "t":
|
|
1899
|
+
toggleFileTreePanel();
|
|
1900
|
+
break;
|
|
1440
1901
|
case "d":
|
|
1441
1902
|
detectCycles();
|
|
1442
1903
|
break;
|
|
@@ -1495,7 +1956,7 @@ function resetView() {
|
|
|
1495
1956
|
if (pathMode) exitPathMode();
|
|
1496
1957
|
stopCodeCrawl();
|
|
1497
1958
|
refreshGraph();
|
|
1498
|
-
Graph.cameraPosition({ x:0,y:0,z:
|
|
1959
|
+
Graph.cameraPosition({ x:0,y:0,z:SETTINGS.camera.resetDistance }, { x:0,y:0,z:0 }, 1000);
|
|
1499
1960
|
}
|
|
1500
1961
|
|
|
1501
1962
|
function toggleAutoRotate() {
|
|
@@ -1548,22 +2009,67 @@ function setupEventListeners() {
|
|
|
1548
2009
|
document.getElementById("btn-cancel-path").addEventListener("click", exitPathMode);
|
|
1549
2010
|
|
|
1550
2011
|
document.getElementById("search-input").addEventListener("input", e => {
|
|
1551
|
-
const q = e.target.value.toLowerCase();
|
|
2012
|
+
const q = e.target.value.toLowerCase().trim();
|
|
1552
2013
|
highlightNodes.clear(); highlightLinks.clear(); selectedNodeId = null;
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
2014
|
+
|
|
2015
|
+
if (!q) {
|
|
2016
|
+
// ── Clear search: restore full view ──
|
|
2017
|
+
searchActive = false;
|
|
2018
|
+
if (_searchRAF) { cancelAnimationFrame(_searchRAF); _searchRAF = null; }
|
|
2019
|
+
refreshGraph();
|
|
2020
|
+
// Smooth zoom-out to overview
|
|
2021
|
+
if (Graph) {
|
|
2022
|
+
Graph.cameraPosition(
|
|
2023
|
+
{ x: 0, y: 0, z: SETTINGS.camera.resetDistance },
|
|
2024
|
+
{ x: 0, y: 0, z: 0 }, 800
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// ── Active search: find matching nodes ──
|
|
2031
|
+
searchActive = true;
|
|
2032
|
+
graphData.nodes.forEach(n => {
|
|
2033
|
+
if (n.fullPath.toLowerCase().includes(q) || n.layer.toLowerCase() === q || n.label.toLowerCase().includes(q)) {
|
|
2034
|
+
highlightNodes.add(n.id);
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
// Also highlight links between matched nodes
|
|
2039
|
+
if (highlightNodes.size > 0) {
|
|
2040
|
+
graphData.links.forEach(l => {
|
|
2041
|
+
const sid = getSrcId(l), tid = getTgtId(l);
|
|
2042
|
+
if (highlightNodes.has(sid) || highlightNodes.has(tid)) highlightLinks.add(l);
|
|
1556
2043
|
});
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
2044
|
+
|
|
2045
|
+
// Camera: frame all matched nodes (centroid)
|
|
2046
|
+
let cx = 0, cy = 0, cz = 0, cnt = 0;
|
|
2047
|
+
graphData.nodes.forEach(n => {
|
|
2048
|
+
if (highlightNodes.has(n.id) && n.x != null) {
|
|
2049
|
+
cx += n.x; cy += (n.y || 0); cz += (n.z || 0); cnt++;
|
|
1563
2050
|
}
|
|
2051
|
+
});
|
|
2052
|
+
if (cnt > 0) {
|
|
2053
|
+
cx /= cnt; cy /= cnt; cz /= cnt;
|
|
2054
|
+
stopUser();
|
|
2055
|
+
const dist = cnt === 1 ? 600 : Math.min(2500, 800 + cnt * 80);
|
|
2056
|
+
Graph.cameraPosition(
|
|
2057
|
+
{ x: cx + dist * 0.3, y: cy + dist * 0.2, z: cz + dist },
|
|
2058
|
+
{ x: cx, y: cy, z: cz }, 600
|
|
2059
|
+
);
|
|
1564
2060
|
}
|
|
1565
2061
|
}
|
|
2062
|
+
|
|
1566
2063
|
refreshGraph();
|
|
2064
|
+
// Start glow animation loop for search matches
|
|
2065
|
+
if (!_searchRAF && highlightNodes.size > 0) {
|
|
2066
|
+
function searchGlow() {
|
|
2067
|
+
if (!searchActive) { _searchRAF = null; return; }
|
|
2068
|
+
if (Graph) Graph.nodeColor(Graph.nodeColor());
|
|
2069
|
+
_searchRAF = requestAnimationFrame(searchGlow);
|
|
2070
|
+
}
|
|
2071
|
+
_searchRAF = requestAnimationFrame(searchGlow);
|
|
2072
|
+
}
|
|
1567
2073
|
});
|
|
1568
2074
|
|
|
1569
2075
|
document.getElementById("btn-toggle-hub").addEventListener("click", () => {
|
|
@@ -1607,8 +2113,9 @@ function setupEventListeners() {
|
|
|
1607
2113
|
});
|
|
1608
2114
|
});
|
|
1609
2115
|
|
|
1610
|
-
// ── Resizable
|
|
2116
|
+
// ── Resizable Panels ──
|
|
1611
2117
|
setupResizeHandle();
|
|
2118
|
+
setupTreeResizeHandle();
|
|
1612
2119
|
}
|
|
1613
2120
|
|
|
1614
2121
|
// ═══════════════════════════════════════════
|
|
@@ -2255,6 +2762,16 @@ async function initSSE() {
|
|
|
2255
2762
|
const data = JSON.parse(e.data);
|
|
2256
2763
|
console.log("[SYKE:SSE] File change:", data.file, data.type, data.diffCount, "diffs");
|
|
2257
2764
|
|
|
2765
|
+
// ── 0. Update file tree modification tracking ──
|
|
2766
|
+
fileTreeModified.set(data.file, { type: data.type, timestamp: Date.now() });
|
|
2767
|
+
renderFileTreeDebounced();
|
|
2768
|
+
// Auto-clear pulse after pulseDuration
|
|
2769
|
+
const pulseDur = (SETTINGS.fileTree.pulseDuration || 5) * 1000;
|
|
2770
|
+
setTimeout(() => {
|
|
2771
|
+
fileTreeModified.delete(data.file);
|
|
2772
|
+
renderFileTreeDebounced();
|
|
2773
|
+
}, pulseDur);
|
|
2774
|
+
|
|
2258
2775
|
// ── 1. Auto-select the modified node in 3D graph ──
|
|
2259
2776
|
selectedFile = data.file;
|
|
2260
2777
|
selectedNodeId = data.file;
|
|
@@ -2409,6 +2926,7 @@ async function initSSE() {
|
|
|
2409
2926
|
setTimeout(async () => {
|
|
2410
2927
|
await loadGraph();
|
|
2411
2928
|
await loadHubFiles();
|
|
2929
|
+
renderFileTreeDebounced();
|
|
2412
2930
|
}, 1000);
|
|
2413
2931
|
});
|
|
2414
2932
|
|
|
@@ -2528,6 +3046,489 @@ function renderRealtimePanel() {
|
|
|
2528
3046
|
// ═══════════════════════════════════════════
|
|
2529
3047
|
// RESIZABLE RIGHT PANEL
|
|
2530
3048
|
// ═══════════════════════════════════════════
|
|
3049
|
+
// ═══════════════════════════════════════════
|
|
3050
|
+
// FILE TREE PANEL
|
|
3051
|
+
// ═══════════════════════════════════════════
|
|
3052
|
+
|
|
3053
|
+
function getGraphPanelWidth() {
|
|
3054
|
+
const rightPanel = document.getElementById("right-panel");
|
|
3055
|
+
const treePanel = document.getElementById("file-tree-panel");
|
|
3056
|
+
const rw = rightPanel ? rightPanel.offsetWidth : 380;
|
|
3057
|
+
const tw = (treePanel && !treePanel.classList.contains("hidden")) ? treePanel.offsetWidth : 0;
|
|
3058
|
+
// 8px per resize handle (tree + right)
|
|
3059
|
+
return window.innerWidth - rw - tw - 16;
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function updateDynamicRightOffsets() {
|
|
3063
|
+
const rightPanel = document.getElementById("right-panel");
|
|
3064
|
+
const treePanel = document.getElementById("file-tree-panel");
|
|
3065
|
+
const rw = rightPanel ? rightPanel.offsetWidth : 380;
|
|
3066
|
+
const tw = (treePanel && !treePanel.classList.contains("hidden")) ? treePanel.offsetWidth : 0;
|
|
3067
|
+
const total = rw + tw + 16; // 2 resize handles × 8px
|
|
3068
|
+
document.documentElement.style.setProperty("--right-offset", total + "px");
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
function toggleFileTreePanel() {
|
|
3072
|
+
const panel = document.getElementById("file-tree-panel");
|
|
3073
|
+
const treeResize = document.getElementById("tree-resize-handle");
|
|
3074
|
+
if (!panel) return;
|
|
3075
|
+
fileTreeVisible = !fileTreeVisible;
|
|
3076
|
+
panel.classList.toggle("hidden", !fileTreeVisible);
|
|
3077
|
+
if (treeResize) treeResize.style.display = fileTreeVisible ? "" : "none";
|
|
3078
|
+
updateDynamicRightOffsets();
|
|
3079
|
+
if (Graph) {
|
|
3080
|
+
const gp = document.getElementById("graph-panel");
|
|
3081
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
function buildFileTree(nodes) {
|
|
3086
|
+
if (!nodes || !nodes.length) { fileTreeData = null; return; }
|
|
3087
|
+
const root = { name: "", children: {}, files: [], path: "" };
|
|
3088
|
+
|
|
3089
|
+
nodes.forEach(n => {
|
|
3090
|
+
const parts = (n.fullPath || n.id).split("/").filter(Boolean);
|
|
3091
|
+
let current = root;
|
|
3092
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
3093
|
+
const folderName = parts[i];
|
|
3094
|
+
const folderPath = parts.slice(0, i + 1).join("/");
|
|
3095
|
+
if (!current.children[folderName]) {
|
|
3096
|
+
current.children[folderName] = { name: folderName, children: {}, files: [], path: folderPath };
|
|
3097
|
+
}
|
|
3098
|
+
current = current.children[folderName];
|
|
3099
|
+
}
|
|
3100
|
+
current.files.push({
|
|
3101
|
+
name: parts[parts.length - 1] || n.label,
|
|
3102
|
+
node: n,
|
|
3103
|
+
path: n.fullPath || n.id,
|
|
3104
|
+
});
|
|
3105
|
+
});
|
|
3106
|
+
|
|
3107
|
+
fileTreeData = root;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
function countFilesRecursive(folder) {
|
|
3111
|
+
let count = folder.files.length;
|
|
3112
|
+
for (const child of Object.values(folder.children)) {
|
|
3113
|
+
count += countFilesRecursive(child);
|
|
3114
|
+
}
|
|
3115
|
+
return count;
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
function getAggregateRisk(folder) {
|
|
3119
|
+
const order = ["HIGH", "MEDIUM", "LOW", "NONE"];
|
|
3120
|
+
let highest = 3; // NONE
|
|
3121
|
+
for (const f of folder.files) {
|
|
3122
|
+
const idx = order.indexOf(f.node.riskLevel);
|
|
3123
|
+
if (idx >= 0 && idx < highest) highest = idx;
|
|
3124
|
+
}
|
|
3125
|
+
for (const child of Object.values(folder.children)) {
|
|
3126
|
+
const childRisk = getAggregateRisk(child);
|
|
3127
|
+
const idx = order.indexOf(childRisk);
|
|
3128
|
+
if (idx >= 0 && idx < highest) highest = idx;
|
|
3129
|
+
}
|
|
3130
|
+
return order[highest];
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
function folderHasModifiedChild(folder) {
|
|
3134
|
+
for (const f of folder.files) {
|
|
3135
|
+
if (fileTreeModified.has(f.path)) return true;
|
|
3136
|
+
}
|
|
3137
|
+
for (const child of Object.values(folder.children)) {
|
|
3138
|
+
if (folderHasModifiedChild(child)) return true;
|
|
3139
|
+
}
|
|
3140
|
+
return false;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function folderMatchesFilter(folder, filter) {
|
|
3144
|
+
if (!filter) return true;
|
|
3145
|
+
const fl = filter.toLowerCase();
|
|
3146
|
+
for (const f of folder.files) {
|
|
3147
|
+
if (f.name.toLowerCase().includes(fl) || f.path.toLowerCase().includes(fl)) return true;
|
|
3148
|
+
}
|
|
3149
|
+
for (const child of Object.values(folder.children)) {
|
|
3150
|
+
if (child.name.toLowerCase().includes(fl) || folderMatchesFilter(child, filter)) return true;
|
|
3151
|
+
}
|
|
3152
|
+
return false;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
function sortTreeItems(items, sortType) {
|
|
3156
|
+
return [...items].sort((a, b) => {
|
|
3157
|
+
switch (sortType) {
|
|
3158
|
+
case "layer": {
|
|
3159
|
+
const la = a.node ? (a.node.layer || "UTIL") : "";
|
|
3160
|
+
const lb = b.node ? (b.node.layer || "UTIL") : "";
|
|
3161
|
+
return la.localeCompare(lb) || (a.name || "").localeCompare(b.name || "");
|
|
3162
|
+
}
|
|
3163
|
+
case "risk": {
|
|
3164
|
+
const order = { HIGH: 0, MEDIUM: 1, LOW: 2, NONE: 3 };
|
|
3165
|
+
const ra = a.node ? (order[a.node.riskLevel] ?? 3) : 3;
|
|
3166
|
+
const rb = b.node ? (order[b.node.riskLevel] ?? 3) : 3;
|
|
3167
|
+
return ra - rb || (a.name || "").localeCompare(b.name || "");
|
|
3168
|
+
}
|
|
3169
|
+
case "deps": {
|
|
3170
|
+
const da = a.node ? (a.node.dependentCount || 0) : 0;
|
|
3171
|
+
const db = b.node ? (b.node.dependentCount || 0) : 0;
|
|
3172
|
+
return db - da || (a.name || "").localeCompare(b.name || "");
|
|
3173
|
+
}
|
|
3174
|
+
case "modified": {
|
|
3175
|
+
const ma = a.path && fileTreeModified.has(a.path) ? fileTreeModified.get(a.path).timestamp : 0;
|
|
3176
|
+
const mb = b.path && fileTreeModified.has(b.path) ? fileTreeModified.get(b.path).timestamp : 0;
|
|
3177
|
+
return mb - ma || (a.name || "").localeCompare(b.name || "");
|
|
3178
|
+
}
|
|
3179
|
+
default: // name
|
|
3180
|
+
return (a.name || "").localeCompare(b.name || "");
|
|
3181
|
+
}
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
function renderFileTree() {
|
|
3186
|
+
if (!graphData || !graphData.nodes) return;
|
|
3187
|
+
buildFileTree(graphData.nodes);
|
|
3188
|
+
if (!fileTreeData) return;
|
|
3189
|
+
|
|
3190
|
+
const container = document.getElementById("tree-content");
|
|
3191
|
+
const countEl = document.getElementById("tree-file-count");
|
|
3192
|
+
if (!container) return;
|
|
3193
|
+
|
|
3194
|
+
const s = SETTINGS.fileTree;
|
|
3195
|
+
const filter = fileTreeFilter.toLowerCase();
|
|
3196
|
+
let totalFiles = 0;
|
|
3197
|
+
|
|
3198
|
+
// Nested rendering: each folder level produces a <div class="tree-group">
|
|
3199
|
+
function renderGroup(folder) {
|
|
3200
|
+
// Collect visible items (folders first, then files)
|
|
3201
|
+
const visibleFolders = Object.values(folder.children)
|
|
3202
|
+
.filter(c => !filter || folderMatchesFilter(c, filter))
|
|
3203
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
3204
|
+
|
|
3205
|
+
const visibleFiles = sortTreeItems(
|
|
3206
|
+
folder.files.filter(f => !filter || f.name.toLowerCase().includes(filter) || f.path.toLowerCase().includes(filter)),
|
|
3207
|
+
fileTreeSort
|
|
3208
|
+
);
|
|
3209
|
+
|
|
3210
|
+
const itemCount = visibleFolders.length + visibleFiles.length;
|
|
3211
|
+
if (itemCount === 0) return "";
|
|
3212
|
+
|
|
3213
|
+
const hasModified = folderHasModifiedChild(folder);
|
|
3214
|
+
const singleClass = itemCount === 1 ? " single-child" : "";
|
|
3215
|
+
const modGroupClass = hasModified ? " has-modified" : "";
|
|
3216
|
+
let html = `<div class="tree-group${singleClass}${modGroupClass}">`;
|
|
3217
|
+
|
|
3218
|
+
// ── Folders ──
|
|
3219
|
+
for (const child of visibleFolders) {
|
|
3220
|
+
const isOpen = fileTreeExpanded.has(child.path);
|
|
3221
|
+
const fileCount = countFilesRecursive(child);
|
|
3222
|
+
const hasModChild = folderHasModifiedChild(child);
|
|
3223
|
+
const compactClass = s.compactMode ? " compact" : "";
|
|
3224
|
+
const glowClass = hasModChild ? " folder-glow" : "";
|
|
3225
|
+
const col = "var(--accent)";
|
|
3226
|
+
|
|
3227
|
+
html += `<div class="tree-node${glowClass}${compactClass}" data-path="${child.path}" data-type="folder">`;
|
|
3228
|
+
html += `<div class="tree-dot" style="border-color:${col};box-shadow:0 0 8px rgba(0,212,255,0.4)"></div>`;
|
|
3229
|
+
html += `<span class="tree-toggle${isOpen ? " open" : ""}" data-path="${child.path}">▶</span>`;
|
|
3230
|
+
html += `<span class="tree-name folder-name">${escHtml(child.name)}</span>`;
|
|
3231
|
+
html += `<span class="tree-folder-count">${fileCount}</span>`;
|
|
3232
|
+
html += `</div>`;
|
|
3233
|
+
|
|
3234
|
+
// Render expanded children as nested tree-group
|
|
3235
|
+
if (isOpen) {
|
|
3236
|
+
html += renderGroup(child);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// ── Files ──
|
|
3241
|
+
for (const f of visibleFiles) {
|
|
3242
|
+
totalFiles++;
|
|
3243
|
+
const isSelected = selectedNodeId === f.node.id;
|
|
3244
|
+
const isModified = fileTreeModified.has(f.path);
|
|
3245
|
+
const modData = isModified ? fileTreeModified.get(f.path) : null;
|
|
3246
|
+
const col = LAYER_HEX[f.node.layer] || "#999";
|
|
3247
|
+
const compactClass = s.compactMode ? " compact" : "";
|
|
3248
|
+
const selClass = isSelected ? " selected" : "";
|
|
3249
|
+
const modClass = isModified ? " modified" : "";
|
|
3250
|
+
|
|
3251
|
+
html += `<div class="tree-node${selClass}${modClass}${compactClass}" style="font-size:${s.fontSize}px" data-path="${f.path}" data-node-id="${f.node.id}" data-type="file">`;
|
|
3252
|
+
html += `<div class="tree-dot" style="background:${col};border-color:${col};box-shadow:0 0 8px ${col}"></div>`;
|
|
3253
|
+
html += `<span class="tree-name">${escHtml(f.name)}</span>`;
|
|
3254
|
+
|
|
3255
|
+
// Badges
|
|
3256
|
+
if (isModified && modData) {
|
|
3257
|
+
const modLabel = modData.type === "added" ? "A" : modData.type === "deleted" ? "D" : "M";
|
|
3258
|
+
const modCls = modData.type === "added" ? "mod-A" : modData.type === "deleted" ? "mod-D" : "mod-M";
|
|
3259
|
+
html += `<span class="tree-mod-badge ${modCls}">${modLabel}</span>`;
|
|
3260
|
+
}
|
|
3261
|
+
if (s.showRisk && f.node.riskLevel && f.node.riskLevel !== "NONE") {
|
|
3262
|
+
html += `<span class="tree-badge badge-risk-${f.node.riskLevel}">${f.node.riskLevel[0]}</span>`;
|
|
3263
|
+
}
|
|
3264
|
+
if (s.showDeps && f.node.dependentCount > 0) {
|
|
3265
|
+
html += `<span class="tree-badge badge-deps">D${f.node.dependentCount}</span>`;
|
|
3266
|
+
}
|
|
3267
|
+
if (s.showLineCount && f.node.lineCount > 0) {
|
|
3268
|
+
html += `<span class="tree-badge badge-lines">L${f.node.lineCount}</span>`;
|
|
3269
|
+
}
|
|
3270
|
+
html += `</div>`;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
html += `</div>`; // close tree-group
|
|
3274
|
+
return html;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
container.innerHTML = renderGroup(fileTreeData);
|
|
3278
|
+
if (countEl) countEl.textContent = totalFiles;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
function escHtml(s) {
|
|
3282
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
function renderFileTreeDebounced() {
|
|
3286
|
+
if (_fileTreeRenderTimer) clearTimeout(_fileTreeRenderTimer);
|
|
3287
|
+
_fileTreeRenderTimer = setTimeout(() => {
|
|
3288
|
+
renderFileTree();
|
|
3289
|
+
_fileTreeRenderTimer = null;
|
|
3290
|
+
}, 100);
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
function toggleFolder(path) {
|
|
3294
|
+
if (fileTreeExpanded.has(path)) fileTreeExpanded.delete(path);
|
|
3295
|
+
else fileTreeExpanded.add(path);
|
|
3296
|
+
renderFileTree();
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
function expandAllFolders() {
|
|
3300
|
+
if (!fileTreeData) return;
|
|
3301
|
+
function collectPaths(folder) {
|
|
3302
|
+
for (const child of Object.values(folder.children)) {
|
|
3303
|
+
fileTreeExpanded.add(child.path);
|
|
3304
|
+
collectPaths(child);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
collectPaths(fileTreeData);
|
|
3308
|
+
renderFileTree();
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
function collapseAllFolders() {
|
|
3312
|
+
fileTreeExpanded.clear();
|
|
3313
|
+
renderFileTree();
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function treeScrollToFile(nodeId) {
|
|
3317
|
+
if (!fileTreeData || !nodeId || _treeScrollLock) return;
|
|
3318
|
+
// Find file path and expand parent folders
|
|
3319
|
+
const node = graphData?.nodes.find(n => n.id === nodeId);
|
|
3320
|
+
if (!node) return;
|
|
3321
|
+
const fullPath = node.fullPath || nodeId;
|
|
3322
|
+
const parts = fullPath.split("/").filter(Boolean);
|
|
3323
|
+
|
|
3324
|
+
// Expand all parent folders
|
|
3325
|
+
for (let i = 1; i < parts.length; i++) {
|
|
3326
|
+
const folderPath = parts.slice(0, i).join("/");
|
|
3327
|
+
fileTreeExpanded.add(folderPath);
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
renderFileTree();
|
|
3331
|
+
|
|
3332
|
+
// Scroll to the file element
|
|
3333
|
+
requestAnimationFrame(() => {
|
|
3334
|
+
const container = document.getElementById("tree-scroll-container");
|
|
3335
|
+
const el = document.querySelector(`[data-node-id="${CSS.escape(nodeId)}"]`);
|
|
3336
|
+
if (el && container && SETTINGS.fileTree.autoScrollOnChange) {
|
|
3337
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
3338
|
+
}
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
function applyFileTreeSettings() {
|
|
3343
|
+
const panel = document.getElementById("file-tree-panel");
|
|
3344
|
+
if (panel) {
|
|
3345
|
+
const w = SETTINGS.fileTree.panelWidth;
|
|
3346
|
+
panel.style.flex = `0 0 ${w}px`;
|
|
3347
|
+
panel.style.width = `${w}px`;
|
|
3348
|
+
}
|
|
3349
|
+
updateDynamicRightOffsets();
|
|
3350
|
+
renderFileTree();
|
|
3351
|
+
if (Graph) {
|
|
3352
|
+
const gp = document.getElementById("graph-panel");
|
|
3353
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
function setupFileTree() {
|
|
3358
|
+
const container = document.getElementById("tree-content");
|
|
3359
|
+
const searchInput = document.getElementById("tree-search-input");
|
|
3360
|
+
const sortBtn = document.getElementById("tree-sort-btn");
|
|
3361
|
+
const sortMenu = document.getElementById("tree-sort-menu");
|
|
3362
|
+
const collapseBtn = document.getElementById("tree-collapse-all");
|
|
3363
|
+
const scrollContainer = document.getElementById("tree-scroll-container");
|
|
3364
|
+
|
|
3365
|
+
if (!container) return;
|
|
3366
|
+
|
|
3367
|
+
// Click delegation for tree nodes
|
|
3368
|
+
container.addEventListener("click", (e) => {
|
|
3369
|
+
const node = e.target.closest(".tree-node");
|
|
3370
|
+
if (!node) return;
|
|
3371
|
+
|
|
3372
|
+
if (node.dataset.type === "folder") {
|
|
3373
|
+
toggleFolder(node.dataset.path);
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// File click → select in 3D graph
|
|
3378
|
+
const nodeId = node.dataset.nodeId;
|
|
3379
|
+
if (nodeId && graphData) {
|
|
3380
|
+
const gNode = graphData.nodes.find(n => n.id === nodeId);
|
|
3381
|
+
if (gNode) {
|
|
3382
|
+
handleNodeClick(gNode);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
3386
|
+
|
|
3387
|
+
// Toggle arrow click
|
|
3388
|
+
container.addEventListener("click", (e) => {
|
|
3389
|
+
const toggle = e.target.closest(".tree-toggle");
|
|
3390
|
+
if (toggle) {
|
|
3391
|
+
e.stopPropagation();
|
|
3392
|
+
toggleFolder(toggle.dataset.path);
|
|
3393
|
+
}
|
|
3394
|
+
});
|
|
3395
|
+
|
|
3396
|
+
// Hover → highlight 3D node
|
|
3397
|
+
container.addEventListener("mouseenter", (e) => {
|
|
3398
|
+
const node = e.target.closest(".tree-node[data-type='file']");
|
|
3399
|
+
if (!node || !Graph) return;
|
|
3400
|
+
const nodeId = node.dataset.nodeId;
|
|
3401
|
+
if (nodeId) {
|
|
3402
|
+
highlightNodes.add(nodeId);
|
|
3403
|
+
refreshGraph();
|
|
3404
|
+
}
|
|
3405
|
+
}, true);
|
|
3406
|
+
|
|
3407
|
+
container.addEventListener("mouseleave", (e) => {
|
|
3408
|
+
const node = e.target.closest(".tree-node[data-type='file']");
|
|
3409
|
+
if (!node || !Graph) return;
|
|
3410
|
+
const nodeId = node.dataset.nodeId;
|
|
3411
|
+
if (nodeId && nodeId !== selectedNodeId) {
|
|
3412
|
+
highlightNodes.delete(nodeId);
|
|
3413
|
+
refreshGraph();
|
|
3414
|
+
}
|
|
3415
|
+
}, true);
|
|
3416
|
+
|
|
3417
|
+
// Search filter
|
|
3418
|
+
if (searchInput) {
|
|
3419
|
+
searchInput.addEventListener("input", () => {
|
|
3420
|
+
fileTreeFilter = searchInput.value.trim();
|
|
3421
|
+
// Auto-expand when filtering
|
|
3422
|
+
if (fileTreeFilter && fileTreeData) expandAllFolders();
|
|
3423
|
+
else renderFileTree();
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
// Sort button + menu
|
|
3428
|
+
if (sortBtn && sortMenu) {
|
|
3429
|
+
sortBtn.addEventListener("click", (e) => {
|
|
3430
|
+
e.stopPropagation();
|
|
3431
|
+
sortMenu.classList.toggle("hidden");
|
|
3432
|
+
});
|
|
3433
|
+
|
|
3434
|
+
sortMenu.querySelectorAll(".tree-sort-option").forEach(opt => {
|
|
3435
|
+
opt.addEventListener("click", (e) => {
|
|
3436
|
+
e.stopPropagation();
|
|
3437
|
+
fileTreeSort = opt.dataset.sort;
|
|
3438
|
+
sortMenu.querySelectorAll(".tree-sort-option").forEach(o => o.classList.remove("active"));
|
|
3439
|
+
opt.classList.add("active");
|
|
3440
|
+
sortMenu.classList.add("hidden");
|
|
3441
|
+
renderFileTree();
|
|
3442
|
+
});
|
|
3443
|
+
});
|
|
3444
|
+
|
|
3445
|
+
// Close sort menu on outside click
|
|
3446
|
+
document.addEventListener("click", () => {
|
|
3447
|
+
if (sortMenu) sortMenu.classList.add("hidden");
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// Collapse/expand all toggle
|
|
3452
|
+
let allExpanded = false;
|
|
3453
|
+
if (collapseBtn) {
|
|
3454
|
+
collapseBtn.addEventListener("click", () => {
|
|
3455
|
+
if (allExpanded) { collapseAllFolders(); collapseBtn.innerHTML = "▼"; }
|
|
3456
|
+
else { expandAllFolders(); collapseBtn.innerHTML = "▲"; }
|
|
3457
|
+
allExpanded = !allExpanded;
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// Scroll lock: disable auto-scroll when user manually scrolls
|
|
3462
|
+
if (scrollContainer) {
|
|
3463
|
+
scrollContainer.addEventListener("scroll", () => {
|
|
3464
|
+
_treeScrollLock = true;
|
|
3465
|
+
if (_treeScrollLockTimer) clearTimeout(_treeScrollLockTimer);
|
|
3466
|
+
_treeScrollLockTimer = setTimeout(() => { _treeScrollLock = false; }, 3000);
|
|
3467
|
+
}, { passive: true });
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// Apply initial settings
|
|
3471
|
+
applyFileTreeSettings();
|
|
3472
|
+
updateDynamicRightOffsets();
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
function setupTreeResizeHandle() {
|
|
3476
|
+
const handle = document.getElementById("tree-resize-handle");
|
|
3477
|
+
const panel = document.getElementById("file-tree-panel");
|
|
3478
|
+
if (!handle || !panel) return;
|
|
3479
|
+
|
|
3480
|
+
let startX = 0;
|
|
3481
|
+
let startW = 0;
|
|
3482
|
+
|
|
3483
|
+
function onMove(e) {
|
|
3484
|
+
e.preventDefault();
|
|
3485
|
+
const dx = e.clientX - startX;
|
|
3486
|
+
const newW = Math.max(200, Math.min(500, startW + dx));
|
|
3487
|
+
panel.style.flex = "0 0 " + newW + "px";
|
|
3488
|
+
panel.style.width = newW + "px";
|
|
3489
|
+
SETTINGS.fileTree.panelWidth = newW;
|
|
3490
|
+
updateDynamicRightOffsets();
|
|
3491
|
+
if (Graph) {
|
|
3492
|
+
const gp = document.getElementById("graph-panel");
|
|
3493
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
function onUp() {
|
|
3498
|
+
window.removeEventListener("mousemove", onMove, true);
|
|
3499
|
+
window.removeEventListener("mouseup", onUp, true);
|
|
3500
|
+
handle.classList.remove("dragging");
|
|
3501
|
+
document.body.style.cursor = "";
|
|
3502
|
+
document.body.style.userSelect = "";
|
|
3503
|
+
const overlay = document.getElementById("tree-resize-overlay");
|
|
3504
|
+
if (overlay) overlay.remove();
|
|
3505
|
+
saveSettings();
|
|
3506
|
+
if (Graph) {
|
|
3507
|
+
const gp = document.getElementById("graph-panel");
|
|
3508
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
handle.addEventListener("mousedown", (e) => {
|
|
3513
|
+
e.preventDefault();
|
|
3514
|
+
e.stopPropagation();
|
|
3515
|
+
startX = e.clientX;
|
|
3516
|
+
startW = panel.offsetWidth;
|
|
3517
|
+
handle.classList.add("dragging");
|
|
3518
|
+
document.body.style.cursor = "col-resize";
|
|
3519
|
+
document.body.style.userSelect = "none";
|
|
3520
|
+
const overlay = document.createElement("div");
|
|
3521
|
+
overlay.id = "tree-resize-overlay";
|
|
3522
|
+
overlay.style.cssText = "position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:9999;cursor:col-resize;background:transparent;";
|
|
3523
|
+
document.body.appendChild(overlay);
|
|
3524
|
+
window.addEventListener("mousemove", onMove, true);
|
|
3525
|
+
window.addEventListener("mouseup", onUp, true);
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
// ═══════════════════════════════════════════
|
|
3530
|
+
// RESIZE HANDLE (Right Panel)
|
|
3531
|
+
// ═══════════════════════════════════════════
|
|
2531
3532
|
function setupResizeHandle() {
|
|
2532
3533
|
const handle = document.getElementById("resize-handle");
|
|
2533
3534
|
const panel = document.getElementById("right-panel");
|
|
@@ -2543,7 +3544,7 @@ function setupResizeHandle() {
|
|
|
2543
3544
|
const newW = Math.max(250, Math.min(900, startW + dx));
|
|
2544
3545
|
panel.style.flex = "0 0 " + newW + "px";
|
|
2545
3546
|
panel.style.width = newW + "px";
|
|
2546
|
-
|
|
3547
|
+
updateDynamicRightOffsets();
|
|
2547
3548
|
if (Graph) {
|
|
2548
3549
|
const gp = document.getElementById("graph-panel");
|
|
2549
3550
|
if (gp) Graph.width(gp.clientWidth);
|
|
@@ -2587,82 +3588,23 @@ function setupResizeHandle() {
|
|
|
2587
3588
|
// ═══════════════════════════════════════════
|
|
2588
3589
|
// PROJECT SELECTOR
|
|
2589
3590
|
// ═══════════════════════════════════════════
|
|
2590
|
-
let licenseTimerInterval = null;
|
|
2591
|
-
|
|
2592
3591
|
function updateLicenseBadge(plan, expiresAt) {
|
|
2593
3592
|
const badge = document.getElementById("license-badge");
|
|
2594
3593
|
if (!badge) return;
|
|
2595
3594
|
|
|
2596
|
-
const planEl = badge.querySelector(".license-plan");
|
|
2597
|
-
const timerEl = badge.querySelector(".license-timer");
|
|
2598
|
-
|
|
2599
|
-
// Clear previous timer
|
|
2600
|
-
if (licenseTimerInterval) {
|
|
2601
|
-
clearInterval(licenseTimerInterval);
|
|
2602
|
-
licenseTimerInterval = null;
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
3595
|
badge.className = "license-badge";
|
|
2606
3596
|
|
|
2607
|
-
if (plan === "pro"
|
|
2608
|
-
|
|
2609
|
-
const now = new Date();
|
|
2610
|
-
const diffMs = expiry - now;
|
|
2611
|
-
|
|
2612
|
-
if (diffMs <= 0) {
|
|
2613
|
-
// Expired
|
|
3597
|
+
if (plan === "pro") {
|
|
3598
|
+
if (expiresAt && new Date(expiresAt) < new Date()) {
|
|
2614
3599
|
badge.classList.add("expired");
|
|
2615
|
-
|
|
2616
|
-
timerEl.textContent = "";
|
|
3600
|
+
badge.textContent = "EXPIRED";
|
|
2617
3601
|
} else {
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
// Trial or near-expiry
|
|
2622
|
-
badge.classList.add("trial-urgent");
|
|
2623
|
-
planEl.textContent = "TRIAL";
|
|
2624
|
-
} else if (totalDays <= 14) {
|
|
2625
|
-
badge.classList.add("trial");
|
|
2626
|
-
planEl.textContent = "TRIAL";
|
|
2627
|
-
} else {
|
|
2628
|
-
badge.classList.add("pro");
|
|
2629
|
-
planEl.textContent = "PRO";
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
// Live countdown
|
|
2633
|
-
function tick() {
|
|
2634
|
-
const remaining = new Date(expiresAt) - new Date();
|
|
2635
|
-
if (remaining <= 0) {
|
|
2636
|
-
timerEl.textContent = "EXPIRED";
|
|
2637
|
-
badge.className = "license-badge expired";
|
|
2638
|
-
planEl.textContent = "EXPIRED";
|
|
2639
|
-
clearInterval(licenseTimerInterval);
|
|
2640
|
-
return;
|
|
2641
|
-
}
|
|
2642
|
-
const d = Math.floor(remaining / (1000 * 60 * 60 * 24));
|
|
2643
|
-
const h = Math.floor((remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
2644
|
-
const m = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
|
|
2645
|
-
const s = Math.floor((remaining % (1000 * 60)) / 1000);
|
|
2646
|
-
|
|
2647
|
-
if (d > 0) {
|
|
2648
|
-
timerEl.textContent = `D-${d} ${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`;
|
|
2649
|
-
} else {
|
|
2650
|
-
timerEl.textContent = `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`;
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
tick();
|
|
2654
|
-
licenseTimerInterval = setInterval(tick, 1000);
|
|
2655
|
-
}
|
|
2656
|
-
} else if (plan === "pro") {
|
|
2657
|
-
// Pro without expiry (permanent or subscription)
|
|
2658
|
-
badge.classList.add("pro");
|
|
2659
|
-
planEl.textContent = "PRO";
|
|
2660
|
-
timerEl.textContent = "";
|
|
3602
|
+
badge.classList.add("pro");
|
|
3603
|
+
badge.textContent = "PRO";
|
|
3604
|
+
}
|
|
2661
3605
|
} else {
|
|
2662
|
-
// Free
|
|
2663
3606
|
badge.classList.add("free");
|
|
2664
|
-
|
|
2665
|
-
timerEl.textContent = "";
|
|
3607
|
+
badge.textContent = "FREE";
|
|
2666
3608
|
}
|
|
2667
3609
|
}
|
|
2668
3610
|
|