opencode-fractal-memory 0.2.0
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/LICENSE +21 -0
- package/README.md +493 -0
- package/agent/memory-hints.md +98 -0
- package/agent/memory-researcher.md +56 -0
- package/commands/memory-auto-test.md +10 -0
- package/commands/memory-cache-status.md +13 -0
- package/commands/memory-check-context.md +4 -0
- package/commands/memory-compress.md +13 -0
- package/commands/memory-dashboard.md +23 -0
- package/commands/memory-delete.md +24 -0
- package/commands/memory-detect-topics.md +28 -0
- package/commands/memory-distill.md +35 -0
- package/commands/memory-drilldown-query.md +28 -0
- package/commands/memory-drilldown.md +11 -0
- package/commands/memory-extract-patterns.md +4 -0
- package/commands/memory-generate-embeddings.md +26 -0
- package/commands/memory-get.md +26 -0
- package/commands/memory-help.md +55 -0
- package/commands/memory-injection-feedback.md +26 -0
- package/commands/memory-injection-stats.md +11 -0
- package/commands/memory-list.md +4 -0
- package/commands/memory-llm-compress.md +34 -0
- package/commands/memory-mcp.md +20 -0
- package/commands/memory-prune.md +4 -0
- package/commands/memory-rate.md +48 -0
- package/commands/memory-reflect.md +37 -0
- package/commands/memory-replace.md +26 -0
- package/commands/memory-retrieve.md +34 -0
- package/commands/memory-search.md +28 -0
- package/commands/memory-session-stats.md +4 -0
- package/commands/memory-set.md +31 -0
- package/commands/memory-stats.md +11 -0
- package/commands/memory-summarize.md +29 -0
- package/commands/memory-tool-stats.md +4 -0
- package/commands/memory-total-tokens.md +10 -0
- package/commands/memory-verify.md +4 -0
- package/commands/memory-version.md +9 -0
- package/dist/cache.js +39 -0
- package/dist/config.js +120 -0
- package/dist/embeddings.js +125 -0
- package/dist/ensure-models.js +70 -0
- package/dist/file-summary.js +143 -0
- package/dist/frontmatter.js +28 -0
- package/dist/hnsw-index.js +138 -0
- package/dist/hooks/auto-discover.js +4 -0
- package/dist/hooks/auto-distill.js +120 -0
- package/dist/hooks/auto-retrieve/content.js +47 -0
- package/dist/hooks/auto-retrieve/detection.js +50 -0
- package/dist/hooks/auto-retrieve/formatting.js +19 -0
- package/dist/hooks/auto-retrieve/index.js +163 -0
- package/dist/hooks/auto-retrieve/scoring.js +56 -0
- package/dist/hooks/auto-retrieve.js +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/predictive-rating.js +87 -0
- package/dist/journal.js +279 -0
- package/dist/logging.js +147 -0
- package/dist/management/helpers.js +227 -0
- package/dist/management/router.js +48 -0
- package/dist/management/routes.js +197 -0
- package/dist/management-server.js +4 -0
- package/dist/management-standalone.js +31 -0
- package/dist/mcp/logging.js +57 -0
- package/dist/mcp/server.js +251 -0
- package/dist/mcp/transform.js +48 -0
- package/dist/mcp-server.js +18 -0
- package/dist/memory.js +2 -0
- package/dist/ollama.js +74 -0
- package/dist/plugin/hooks.js +168 -0
- package/dist/plugin/index.js +28 -0
- package/dist/plugin/init.js +109 -0
- package/dist/plugin/state.js +75 -0
- package/dist/plugin/tools.js +45 -0
- package/dist/plugin.js +2 -0
- package/dist/procedural/store.js +1 -0
- package/dist/procedural/types.js +1 -0
- package/dist/seed-nodes.js +804 -0
- package/dist/storage/compress-ops.js +129 -0
- package/dist/storage/compression/formatters.js +243 -0
- package/dist/storage/compression/index.js +107 -0
- package/dist/storage/compression/patterns.js +138 -0
- package/dist/storage/expiration.js +66 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/injection-events.js +82 -0
- package/dist/storage/lifecycle.js +65 -0
- package/dist/storage/maintenance.js +60 -0
- package/dist/storage/migrations/definitions.js +374 -0
- package/dist/storage/migrations/index.js +21 -0
- package/dist/storage/navigation.js +98 -0
- package/dist/storage/queries/base.js +44 -0
- package/dist/storage/queries/links.js +32 -0
- package/dist/storage/queries/nodes.js +189 -0
- package/dist/storage/queries/search-helpers.js +239 -0
- package/dist/storage/scoring.js +36 -0
- package/dist/storage/search.js +233 -0
- package/dist/storage/session-tracking.js +180 -0
- package/dist/storage/sqlite.js +329 -0
- package/dist/storage/tool-usage.js +56 -0
- package/dist/storage/types.js +1 -0
- package/dist/storage/utils.js +94 -0
- package/dist/tools/auto-test.js +24 -0
- package/dist/tools/cache-status.js +36 -0
- package/dist/tools/compress.js +186 -0
- package/dist/tools/core.js +307 -0
- package/dist/tools/dashboard.js +97 -0
- package/dist/tools/help.js +59 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/inject.js +91 -0
- package/dist/tools/injection-debug.js +48 -0
- package/dist/tools/journal.js +105 -0
- package/dist/tools/llm-compress.js +41 -0
- package/dist/tools/middle-term.js +68 -0
- package/dist/tools/playbook.js +64 -0
- package/dist/tools/reflect.js +291 -0
- package/dist/tools/search.js +188 -0
- package/dist/tools/session.js +189 -0
- package/dist/tools/shared.js +74 -0
- package/dist/tools/skill.js +37 -0
- package/dist/tools/stats.js +256 -0
- package/dist/tools/version.js +13 -0
- package/dist/tools.js +18 -0
- package/dist/utils/hybridScore.js +67 -0
- package/management/public/app.js +1529 -0
- package/management/public/index.html +486 -0
- package/management/public/three.min.js +6 -0
- package/package.json +65 -0
- package/scripts/download-models.ts +16 -0
- package/scripts/postinstall.cjs +30 -0
|
@@ -0,0 +1,1529 @@
|
|
|
1
|
+
const LEVEL_COLORS = {
|
|
2
|
+
0: 0x4a9eff,
|
|
3
|
+
1: 0x34d399,
|
|
4
|
+
2: 0xfb923c,
|
|
5
|
+
3: 0xa78bfa,
|
|
6
|
+
4: 0xf472b6,
|
|
7
|
+
5: 0xfbbf24,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const TYPE_SHAPES = {
|
|
11
|
+
note: "sphere",
|
|
12
|
+
event: "box",
|
|
13
|
+
episode: "box",
|
|
14
|
+
concept: "octahedron",
|
|
15
|
+
summary: "octahedron",
|
|
16
|
+
core: "dodecahedron",
|
|
17
|
+
improvement: "sphere",
|
|
18
|
+
howto: "sphere",
|
|
19
|
+
skill: "icosahedron",
|
|
20
|
+
unknown: "sphere",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const CUSTOM_TYPE_COLORS = {
|
|
24
|
+
'middle-term': 0xff6b6b,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CUSTOM_TYPE_SHAPES = {
|
|
28
|
+
'middle-term': "torus",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ==================== Filter Engine ====================
|
|
32
|
+
|
|
33
|
+
class NodeFilterEngine {
|
|
34
|
+
constructor() {
|
|
35
|
+
this.levels = new Set();
|
|
36
|
+
this.types = new Set();
|
|
37
|
+
this.customTypes = new Set();
|
|
38
|
+
this.shapes = new Set();
|
|
39
|
+
this.searchQuery = "";
|
|
40
|
+
this.searchMode = "text";
|
|
41
|
+
this.serverSearchIds = null;
|
|
42
|
+
this.onUpdate = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
initFromStats(stats) {
|
|
46
|
+
this.levels.clear();
|
|
47
|
+
this.types.clear();
|
|
48
|
+
this.customTypes.clear();
|
|
49
|
+
this.shapes.clear();
|
|
50
|
+
|
|
51
|
+
Object.keys(stats.nodesPerLevel || {}).map(Number).sort((a, b) => a - b).forEach(l => this.levels.add(l));
|
|
52
|
+
Object.keys(stats.nodesPerType || {}).sort().forEach(t => this.types.add(t));
|
|
53
|
+
Object.keys(stats.nodesPerCustomType || {}).sort().forEach(ct => this.customTypes.add(ct));
|
|
54
|
+
Object.keys(stats.nodesPerShape || {}).sort().forEach(s => this.shapes.add(s));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
toggleLevel(v) { this._toggle(this.levels, v); this.changed(); }
|
|
58
|
+
toggleType(v) { this._toggle(this.types, v); this.changed(); }
|
|
59
|
+
toggleCustomType(v) { this._toggle(this.customTypes, v); this.changed(); }
|
|
60
|
+
toggleShape(v) { this._toggle(this.shapes, v); this.changed(); }
|
|
61
|
+
|
|
62
|
+
setSearchQuery(q) { this.searchQuery = (q || "").toLowerCase(); }
|
|
63
|
+
setSearchMode(m) { this.searchMode = m; }
|
|
64
|
+
setServerSearchIds(ids) { this.serverSearchIds = ids; }
|
|
65
|
+
|
|
66
|
+
changed() {
|
|
67
|
+
if (this.onUpdate) this.onUpdate();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_toggle(set, value) {
|
|
71
|
+
if (set.has(value)) set.delete(value);
|
|
72
|
+
else set.add(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
matches(node) {
|
|
76
|
+
if (!node) return false;
|
|
77
|
+
if (!this.levels.has(node.level)) return false;
|
|
78
|
+
if (!this.types.has(node.type || "unknown")) return false;
|
|
79
|
+
|
|
80
|
+
if (this.customTypes.size > 0) {
|
|
81
|
+
const ct = node.metadata?.customType;
|
|
82
|
+
if (ct && !this.customTypes.has(ct)) return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.shapes.size > 0) {
|
|
86
|
+
if (!this.shapes.has(getNodeShape(node))) return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this.searchQuery) {
|
|
90
|
+
if (this.searchMode === "text") {
|
|
91
|
+
const q = this.searchQuery;
|
|
92
|
+
const lm = node.label && node.label.toLowerCase().includes(q);
|
|
93
|
+
const cm = node.content && node.content.toLowerCase().includes(q);
|
|
94
|
+
if (!lm && !cm) return false;
|
|
95
|
+
} else if (this.serverSearchIds) {
|
|
96
|
+
if (!this.serverSearchIds.has(node.id)) return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
apply(data) {
|
|
104
|
+
return data.filter(n => this.matches(n));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ==================== Scene Controller ====================
|
|
109
|
+
|
|
110
|
+
class SceneController {
|
|
111
|
+
constructor() {
|
|
112
|
+
this.scene = new THREE.Scene();
|
|
113
|
+
this.scene.background = new THREE.Color(0x0a0a0f);
|
|
114
|
+
this.scene.fog = new THREE.FogExp2(0x0a0a0f, 0.002);
|
|
115
|
+
|
|
116
|
+
this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
|
|
117
|
+
this.camera.position.set(0, 100, 300);
|
|
118
|
+
|
|
119
|
+
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
120
|
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
121
|
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
122
|
+
document.getElementById("canvas-container").appendChild(this.renderer.domElement);
|
|
123
|
+
|
|
124
|
+
this.nodeObjects = [];
|
|
125
|
+
this.edgeObjects = [];
|
|
126
|
+
this.nodePositions = new Map();
|
|
127
|
+
this.nodeVelocities = new Map();
|
|
128
|
+
this.hoveredNode = null;
|
|
129
|
+
this.selectedNode = null;
|
|
130
|
+
|
|
131
|
+
this.raycaster = new THREE.Raycaster();
|
|
132
|
+
this.mouse = new THREE.Vector2();
|
|
133
|
+
|
|
134
|
+
// Orbit state
|
|
135
|
+
this._isDragging = false;
|
|
136
|
+
this._prevMouse = { x: 0, y: 0 };
|
|
137
|
+
this._spherical = { theta: 0, phi: Math.PI / 3, radius: 350 };
|
|
138
|
+
this._target = new THREE.Vector3(0, 0, 0);
|
|
139
|
+
|
|
140
|
+
this._addLights();
|
|
141
|
+
this._updateCamera();
|
|
142
|
+
this._bindEvents();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_addLights() {
|
|
146
|
+
const a = new THREE.AmbientLight(0x404060, 0.6);
|
|
147
|
+
this.scene.add(a);
|
|
148
|
+
const l1 = new THREE.PointLight(0xffffff, 0.8, 1000);
|
|
149
|
+
l1.position.set(200, 200, 200);
|
|
150
|
+
this.scene.add(l1);
|
|
151
|
+
const l2 = new THREE.PointLight(0x4a9eff, 0.4, 800);
|
|
152
|
+
l2.position.set(-200, -100, -200);
|
|
153
|
+
this.scene.add(l2);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_updateCamera() {
|
|
157
|
+
const s = this._spherical;
|
|
158
|
+
const t = this._target;
|
|
159
|
+
this.camera.position.x = t.x + s.radius * Math.sin(s.phi) * Math.cos(s.theta);
|
|
160
|
+
this.camera.position.y = t.y + s.radius * Math.cos(s.phi);
|
|
161
|
+
this.camera.position.z = t.z + s.radius * Math.sin(s.phi) * Math.sin(s.theta);
|
|
162
|
+
this.camera.lookAt(t);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_bindEvents() {
|
|
166
|
+
const el = this.renderer.domElement;
|
|
167
|
+
|
|
168
|
+
el.addEventListener("mousedown", (e) => {
|
|
169
|
+
if (e.button === 0 && !this.hoveredNode) {
|
|
170
|
+
this._isDragging = true;
|
|
171
|
+
this._prevMouse = { x: e.clientX, y: e.clientY };
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
el.addEventListener("mousemove", (e) => {
|
|
176
|
+
if (this._isDragging) {
|
|
177
|
+
const dx = e.clientX - this._prevMouse.x;
|
|
178
|
+
const dy = e.clientY - this._prevMouse.y;
|
|
179
|
+
this._spherical.theta -= dx * 0.005;
|
|
180
|
+
this._spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, this._spherical.phi + dy * 0.005));
|
|
181
|
+
this._prevMouse = { x: e.clientX, y: e.clientY };
|
|
182
|
+
this._updateCamera();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
el.addEventListener("mouseup", () => { this._isDragging = false; });
|
|
187
|
+
el.addEventListener("mouseleave", () => { this._isDragging = false; });
|
|
188
|
+
|
|
189
|
+
el.addEventListener("wheel", (e) => {
|
|
190
|
+
this._spherical.radius *= e.deltaY > 0 ? 1.1 : 0.9;
|
|
191
|
+
this._spherical.radius = Math.max(10, Math.min(1000, this._spherical.radius));
|
|
192
|
+
this._updateCamera();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
el.addEventListener("mousemove", (e) => this._onMouseMove(e));
|
|
196
|
+
el.addEventListener("click", () => this._onClick());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_onMouseMove(event) {
|
|
200
|
+
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
201
|
+
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
202
|
+
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
203
|
+
const meshes = this.nodeObjects.filter(o => o.isMesh);
|
|
204
|
+
const hits = this.raycaster.intersectObjects(meshes);
|
|
205
|
+
const tooltip = document.getElementById("tooltip");
|
|
206
|
+
|
|
207
|
+
if (hits.length > 0) {
|
|
208
|
+
const obj = hits[0].object;
|
|
209
|
+
const nd = obj.userData.nodeData;
|
|
210
|
+
if (this.hoveredNode !== obj) {
|
|
211
|
+
if (this.hoveredNode && this.hoveredNode !== this.selectedNode) {
|
|
212
|
+
this.hoveredNode.material.emissiveIntensity = 0.2;
|
|
213
|
+
}
|
|
214
|
+
this.hoveredNode = obj;
|
|
215
|
+
if (obj !== this.selectedNode) {
|
|
216
|
+
obj.material.emissiveIntensity = 0.5;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
tooltip.style.display = "block";
|
|
220
|
+
tooltip.style.left = event.clientX + 15 + "px";
|
|
221
|
+
tooltip.style.top = event.clientY + 15 + "px";
|
|
222
|
+
tooltip.innerHTML = `<strong>${nd.label || "Unnamed"}</strong><br>Level: ${nd.level} | Importance: ${nd.importance}`;
|
|
223
|
+
} else {
|
|
224
|
+
if (this.hoveredNode && this.hoveredNode !== this.selectedNode) {
|
|
225
|
+
this.hoveredNode.material.emissiveIntensity = 0.2;
|
|
226
|
+
}
|
|
227
|
+
this.hoveredNode = null;
|
|
228
|
+
tooltip.style.display = "none";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_onClick() {
|
|
233
|
+
if (this.hoveredNode) {
|
|
234
|
+
this.selectedNode = this.hoveredNode;
|
|
235
|
+
this._updateHighlight();
|
|
236
|
+
showDetailPanel(this.hoveredNode.userData.nodeData);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_updateHighlight() {
|
|
241
|
+
this.nodeObjects.forEach(obj => {
|
|
242
|
+
if (obj.isMesh && obj.userData.nodeData) {
|
|
243
|
+
if (obj === this.selectedNode) {
|
|
244
|
+
obj.material.emissiveIntensity = 0.8;
|
|
245
|
+
} else if (obj !== this.hoveredNode) {
|
|
246
|
+
obj.material.emissiveIntensity = 0.2;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
clear() {
|
|
253
|
+
this.nodeObjects.forEach(obj => this.scene.remove(obj));
|
|
254
|
+
this.edgeObjects.forEach(obj => this.scene.remove(obj));
|
|
255
|
+
this.nodeObjects = [];
|
|
256
|
+
this.edgeObjects = [];
|
|
257
|
+
this.nodePositions.clear();
|
|
258
|
+
this.nodeVelocities.clear();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
buildFromData(data) {
|
|
262
|
+
this.clear();
|
|
263
|
+
|
|
264
|
+
if (data.length === 0) {
|
|
265
|
+
console.log("[scene] No data to build");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const levelGroups = {};
|
|
270
|
+
const levelCounters = {};
|
|
271
|
+
const levelCounts = {};
|
|
272
|
+
for (const node of data) {
|
|
273
|
+
if (!node) continue;
|
|
274
|
+
const lvl = node.level ?? 3;
|
|
275
|
+
if (!levelGroups[lvl]) { levelGroups[lvl] = []; levelCounters[lvl] = 0; levelCounts[lvl] = 0; }
|
|
276
|
+
levelGroups[lvl].push(node);
|
|
277
|
+
levelCounts[lvl]++;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const shellRadii = computeShellRadii(levelCounts);
|
|
281
|
+
this.shellRadii = shellRadii;
|
|
282
|
+
|
|
283
|
+
for (const node of data) {
|
|
284
|
+
if (!node) continue;
|
|
285
|
+
|
|
286
|
+
const lvl = node.level ?? 3;
|
|
287
|
+
const count = levelCounts[lvl];
|
|
288
|
+
const idx = levelCounters[lvl]++;
|
|
289
|
+
const radius = shellRadii[lvl] ?? 120;
|
|
290
|
+
|
|
291
|
+
const pos = fibonacciSphere(idx, count, radius);
|
|
292
|
+
// Add tiny random jitter to prevent exact overlaps
|
|
293
|
+
if (count > 1) {
|
|
294
|
+
pos.x += (Math.random() - 0.5) * 2;
|
|
295
|
+
pos.y += (Math.random() - 0.5) * 2;
|
|
296
|
+
pos.z += (Math.random() - 0.5) * 2;
|
|
297
|
+
}
|
|
298
|
+
this.nodePositions.set(node.id, pos);
|
|
299
|
+
this.nodeVelocities.set(node.id, new THREE.Vector3(0, 0, 0));
|
|
300
|
+
|
|
301
|
+
const size = getNodeSize(node);
|
|
302
|
+
const customType = node.metadata?.customType;
|
|
303
|
+
let color, shape;
|
|
304
|
+
|
|
305
|
+
if (customType && CUSTOM_TYPE_COLORS[customType]) {
|
|
306
|
+
color = CUSTOM_TYPE_COLORS[customType];
|
|
307
|
+
shape = CUSTOM_TYPE_SHAPES[customType] ?? "sphere";
|
|
308
|
+
} else {
|
|
309
|
+
color = LEVEL_COLORS[node.level] ?? 0x888888;
|
|
310
|
+
shape = TYPE_SHAPES[node.type] ?? "sphere";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const geometry = getGeometry(shape, size);
|
|
314
|
+
const material = new THREE.MeshPhongMaterial({
|
|
315
|
+
color, emissive: color, emissiveIntensity: 0.2, transparent: true, opacity: 0.9,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
319
|
+
mesh.position.copy(pos);
|
|
320
|
+
mesh.userData = { nodeId: node.id, nodeData: node };
|
|
321
|
+
this.scene.add(mesh);
|
|
322
|
+
this.nodeObjects.push(mesh);
|
|
323
|
+
|
|
324
|
+
const label = createTextSprite(node.label || node.id.slice(0, 8), color);
|
|
325
|
+
label.position.copy(pos);
|
|
326
|
+
label.position.y += size + 5;
|
|
327
|
+
label.userData = { nodeId: node.id, nodeData: node };
|
|
328
|
+
this.scene.add(label);
|
|
329
|
+
this.nodeObjects.push(label);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log(`[scene] Built ${data.length} nodes → ${this.nodeObjects.filter(o => o.isMesh).length} meshes`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
buildEdges(linkData) {
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
linkData.forEach(link => {
|
|
338
|
+
const key = `${link.source}-${link.target}`;
|
|
339
|
+
if (seen.has(key)) return;
|
|
340
|
+
seen.add(key);
|
|
341
|
+
|
|
342
|
+
const sp = this.nodePositions.get(link.source);
|
|
343
|
+
const tp = this.nodePositions.get(link.target);
|
|
344
|
+
if (!sp || !tp) return;
|
|
345
|
+
|
|
346
|
+
const isParent = link.type === "parent";
|
|
347
|
+
const color = isParent ? 0x4a9eff : 0x666666;
|
|
348
|
+
const opacity = isParent ? 0.4 : 0.2;
|
|
349
|
+
|
|
350
|
+
const g = new THREE.BufferGeometry().setFromPoints([sp, tp]);
|
|
351
|
+
const m = new THREE.LineBasicMaterial({ color, transparent: true, opacity });
|
|
352
|
+
const line = new THREE.Line(g, m);
|
|
353
|
+
line.userData = { source: link.source, target: link.target };
|
|
354
|
+
this.scene.add(line);
|
|
355
|
+
this.edgeObjects.push(line);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
runSimulation(iterations, linkData) {
|
|
360
|
+
const ids = Array.from(this.nodePositions.keys());
|
|
361
|
+
const radii = this.shellRadii || {};
|
|
362
|
+
|
|
363
|
+
const nodeLevels = new Map();
|
|
364
|
+
for (const id of ids) {
|
|
365
|
+
const mesh = this.nodeObjects.find(o => o.isMesh && o.userData.nodeId === id);
|
|
366
|
+
nodeLevels.set(id, mesh?.userData?.nodeData?.level ?? 3);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
370
|
+
const temp = 1 - iter / iterations;
|
|
371
|
+
|
|
372
|
+
for (const id of ids) {
|
|
373
|
+
const pos = this.nodePositions.get(id);
|
|
374
|
+
const vel = this.nodeVelocities.get(id);
|
|
375
|
+
const lvlA = nodeLevels.get(id) ?? 3;
|
|
376
|
+
const wA = Math.max(0.4, 1.0 - lvlA * 0.12);
|
|
377
|
+
const kA = 80 + lvlA * 10;
|
|
378
|
+
|
|
379
|
+
for (const oid of ids) {
|
|
380
|
+
if (oid === id) continue;
|
|
381
|
+
const op = this.nodePositions.get(oid);
|
|
382
|
+
const dx = pos.x - op.x, dy = pos.y - op.y, dz = pos.z - op.z;
|
|
383
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1;
|
|
384
|
+
const lvlB = nodeLevels.get(oid) ?? 3;
|
|
385
|
+
const wB = Math.max(0.4, 1.0 - lvlB * 0.12);
|
|
386
|
+
const kB = 80 + lvlB * 10;
|
|
387
|
+
const k = (kA + kB) / 2;
|
|
388
|
+
const f = (k * k) / dist * ((wA + wB) / 2);
|
|
389
|
+
vel.x += (dx / dist) * f * 0.008;
|
|
390
|
+
vel.y += (dy / dist) * f * 0.008;
|
|
391
|
+
vel.z += (dz / dist) * f * 0.008;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (linkData) {
|
|
396
|
+
linkData.forEach(link => {
|
|
397
|
+
const sp = this.nodePositions.get(link.source);
|
|
398
|
+
const tp = this.nodePositions.get(link.target);
|
|
399
|
+
if (!sp || !tp) return;
|
|
400
|
+
const dx = tp.x - sp.x, dy = tp.y - sp.y, dz = tp.z - sp.z;
|
|
401
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1;
|
|
402
|
+
const lvlA = nodeLevels.get(link.source) ?? 3;
|
|
403
|
+
const lvlB = nodeLevels.get(link.target) ?? 3;
|
|
404
|
+
const restLen = 40 + (lvlA + lvlB) * 8;
|
|
405
|
+
const f = (dist - restLen) * 0.06;
|
|
406
|
+
const sv = this.nodeVelocities.get(link.source);
|
|
407
|
+
const tv = this.nodeVelocities.get(link.target);
|
|
408
|
+
sv.x += dx * f * 0.01; sv.y += dy * f * 0.01; sv.z += dz * f * 0.01;
|
|
409
|
+
tv.x -= dx * f * 0.01; tv.y -= dy * f * 0.01; tv.z -= dz * f * 0.01;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (const id of ids) {
|
|
414
|
+
const pos = this.nodePositions.get(id);
|
|
415
|
+
const vel = this.nodeVelocities.get(id);
|
|
416
|
+
const lvl = nodeLevels.get(id) ?? 3;
|
|
417
|
+
const targetRadius = radii[lvl] ?? 120;
|
|
418
|
+
const curDist = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z) || 1;
|
|
419
|
+
|
|
420
|
+
const shellForce = (targetRadius - curDist) * 0.01;
|
|
421
|
+
vel.x += (pos.x / curDist) * shellForce;
|
|
422
|
+
vel.y += (pos.y / curDist) * shellForce;
|
|
423
|
+
vel.z += (pos.z / curDist) * shellForce;
|
|
424
|
+
|
|
425
|
+
vel.x *= 0.9; vel.y *= 0.9; vel.z *= 0.9;
|
|
426
|
+
const maxStep = 8 * temp + 1;
|
|
427
|
+
vel.x = Math.max(-maxStep, Math.min(maxStep, vel.x));
|
|
428
|
+
vel.y = Math.max(-maxStep, Math.min(maxStep, vel.y));
|
|
429
|
+
vel.z = Math.max(-maxStep, Math.min(maxStep, vel.z));
|
|
430
|
+
|
|
431
|
+
pos.add(vel);
|
|
432
|
+
|
|
433
|
+
const maxRadius = Math.max(targetRadius + 80, targetRadius * 1.3);
|
|
434
|
+
const newDist = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z) || 1;
|
|
435
|
+
if (newDist > maxRadius) {
|
|
436
|
+
const scale = maxRadius / newDist;
|
|
437
|
+
pos.x *= scale; pos.y *= scale; pos.z *= scale;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.nodeObjects.forEach(obj => {
|
|
443
|
+
if (obj.userData.nodeId) {
|
|
444
|
+
const pos = this.nodePositions.get(obj.userData.nodeId);
|
|
445
|
+
if (pos) {
|
|
446
|
+
obj.position.copy(pos);
|
|
447
|
+
if (obj.isSprite && obj.userData.nodeData) {
|
|
448
|
+
obj.position.y += getNodeSize(obj.userData.nodeData) + 5;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
this._syncEdges();
|
|
455
|
+
|
|
456
|
+
// Log final positions for debugging
|
|
457
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, minZ = Infinity, maxZ = -Infinity;
|
|
458
|
+
ids.forEach(id => {
|
|
459
|
+
const p = this.nodePositions.get(id);
|
|
460
|
+
if (!p) return;
|
|
461
|
+
if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x;
|
|
462
|
+
if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y;
|
|
463
|
+
if (p.z < minZ) minZ = p.z; if (p.z > maxZ) maxZ = p.z;
|
|
464
|
+
});
|
|
465
|
+
let maxDist = 0;
|
|
466
|
+
ids.forEach(id => {
|
|
467
|
+
const p = this.nodePositions.get(id);
|
|
468
|
+
if (!p) return;
|
|
469
|
+
const d = Math.sqrt(p.x*p.x + p.y*p.y + p.z*p.z);
|
|
470
|
+
if (d > maxDist) maxDist = d;
|
|
471
|
+
});
|
|
472
|
+
console.log(`[scene] Sim bounds: X[${minX.toFixed(1)}, ${maxX.toFixed(1)}] Y[${minY.toFixed(1)}, ${maxY.toFixed(1)}] Z[${minZ.toFixed(1)}, ${maxZ.toFixed(1)}] maxRadius=${maxDist.toFixed(1)}`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_syncEdges() {
|
|
476
|
+
this.edgeObjects.forEach(line => {
|
|
477
|
+
const s = this.nodePositions.get(line.userData.source);
|
|
478
|
+
const t = this.nodePositions.get(line.userData.target);
|
|
479
|
+
if (s && t) {
|
|
480
|
+
const arr = line.geometry.attributes.position.array;
|
|
481
|
+
arr[0] = s.x; arr[1] = s.y; arr[2] = s.z;
|
|
482
|
+
arr[3] = t.x; arr[4] = t.y; arr[5] = t.z;
|
|
483
|
+
line.geometry.attributes.position.needsUpdate = true;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
updateVisibility(filterEngine) {
|
|
489
|
+
const filtered = filterEngine.apply(nodeData);
|
|
490
|
+
const filteredIds = new Set(filtered.map(n => n.id));
|
|
491
|
+
|
|
492
|
+
let visibleCount = 0;
|
|
493
|
+
this.nodeObjects.forEach(obj => {
|
|
494
|
+
if (!obj.userData.nodeId) return;
|
|
495
|
+
const v = filteredIds.has(obj.userData.nodeId);
|
|
496
|
+
obj.visible = v;
|
|
497
|
+
if (v && obj.isMesh) visibleCount++;
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
this.edgeObjects.forEach(line => {
|
|
501
|
+
line.visible = filteredIds.has(line.userData.source) && filteredIds.has(line.userData.target);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
console.log(`[scene] Visibility: ${visibleCount}/${this.nodeObjects.filter(o => o.isMesh).length} meshes visible (${filtered.length} nodes in list)`);
|
|
505
|
+
buildNodeList();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
focusOnNode(nodeId) {
|
|
509
|
+
const mesh = this.nodeObjects.find(o => o.isMesh && o.userData.nodeId === nodeId);
|
|
510
|
+
if (!mesh) return;
|
|
511
|
+
|
|
512
|
+
this.selectedNode = mesh;
|
|
513
|
+
this._updateHighlight();
|
|
514
|
+
showDetailPanel(mesh.userData.nodeData);
|
|
515
|
+
buildNodeList();
|
|
516
|
+
|
|
517
|
+
const target = mesh.position.clone();
|
|
518
|
+
const size = getNodeSize(mesh.userData.nodeData);
|
|
519
|
+
const offset = size * 4 + 30;
|
|
520
|
+
this._animateCamera(target, offset);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
_animateCamera(target, offset) {
|
|
524
|
+
const start = this.camera.position.clone();
|
|
525
|
+
const end = new THREE.Vector3(target.x + offset * 0.5, target.y + offset * 0.3, target.z + offset);
|
|
526
|
+
const duration = 600;
|
|
527
|
+
const startTime = performance.now();
|
|
528
|
+
|
|
529
|
+
const step = (now) => {
|
|
530
|
+
const t = Math.min((now - startTime) / duration, 1);
|
|
531
|
+
const ease = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
532
|
+
this.camera.position.lerpVectors(start, end, ease);
|
|
533
|
+
this.camera.lookAt(target);
|
|
534
|
+
if (t < 1) {
|
|
535
|
+
requestAnimationFrame(step);
|
|
536
|
+
} else {
|
|
537
|
+
// Update orbit target to node position and recalc spherical state
|
|
538
|
+
this._target.copy(target);
|
|
539
|
+
const dx = end.x - this._target.x;
|
|
540
|
+
const dy = end.y - this._target.y;
|
|
541
|
+
const dz = end.z - this._target.z;
|
|
542
|
+
this._spherical.radius = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
543
|
+
this._spherical.theta = Math.atan2(dz, dx);
|
|
544
|
+
this._spherical.phi = Math.acos(Math.max(-1, Math.min(1, dy / (this._spherical.radius || 1))));
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
requestAnimationFrame(step);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
render() {
|
|
552
|
+
this.nodeObjects.forEach(obj => {
|
|
553
|
+
if (obj.isMesh && obj.userData.nodeData) {
|
|
554
|
+
obj.rotation.y += 0.002;
|
|
555
|
+
obj.rotation.x += 0.001;
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
this.renderer.render(this.scene, this.camera);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
resize() {
|
|
562
|
+
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
563
|
+
this.camera.updateProjectionMatrix();
|
|
564
|
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ==================== Globals ====================
|
|
569
|
+
|
|
570
|
+
let filterEngine;
|
|
571
|
+
let sceneCtrl;
|
|
572
|
+
let nodeData = [];
|
|
573
|
+
let linkData = [];
|
|
574
|
+
let statsData = null;
|
|
575
|
+
let editingNode = null;
|
|
576
|
+
let currentScope = "project";
|
|
577
|
+
let availableScopes = [];
|
|
578
|
+
|
|
579
|
+
// ==================== Init ====================
|
|
580
|
+
|
|
581
|
+
function init() {
|
|
582
|
+
filterEngine = new NodeFilterEngine();
|
|
583
|
+
sceneCtrl = new SceneController();
|
|
584
|
+
|
|
585
|
+
// Wire filter engine to trigger view updates
|
|
586
|
+
filterEngine.onUpdate = () => {
|
|
587
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
setupEventListeners();
|
|
591
|
+
loadData();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ==================== Event Setup ====================
|
|
595
|
+
|
|
596
|
+
function setupEventListeners() {
|
|
597
|
+
window.addEventListener("resize", () => sceneCtrl.resize());
|
|
598
|
+
|
|
599
|
+
document.getElementById("search-input").addEventListener("input", (e) => {
|
|
600
|
+
filterEngine.setSearchQuery(e.target.value);
|
|
601
|
+
filterEngine.setServerSearchIds(null);
|
|
602
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
document.getElementById("search-input").addEventListener("keydown", (e) => {
|
|
606
|
+
if (e.key === "Enter") {
|
|
607
|
+
if (filterEngine.searchMode === "text") {
|
|
608
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
609
|
+
} else {
|
|
610
|
+
performServerSearch(filterEngine.searchQuery);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
document.getElementById("close-detail").addEventListener("click", () => {
|
|
616
|
+
document.getElementById("detail-panel").classList.remove("open");
|
|
617
|
+
sceneCtrl.selectedNode = null;
|
|
618
|
+
editingNode = null;
|
|
619
|
+
sceneCtrl._updateHighlight();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
document.getElementById("edit-node").addEventListener("click", () => {
|
|
623
|
+
if (sceneCtrl.selectedNode && sceneCtrl.selectedNode.userData.nodeData) {
|
|
624
|
+
toggleEditMode(sceneCtrl.selectedNode.userData.nodeData);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
document.getElementById("delete-node").addEventListener("click", () => {
|
|
629
|
+
if (sceneCtrl.selectedNode && sceneCtrl.selectedNode.userData.nodeData) {
|
|
630
|
+
deleteNode(sceneCtrl.selectedNode.userData.nodeData);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
document.getElementById("inject-node").addEventListener("click", () => {
|
|
635
|
+
if (sceneCtrl.selectedNode && sceneCtrl.selectedNode.userData.nodeData) {
|
|
636
|
+
injectNode(sceneCtrl.selectedNode.userData.nodeData);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
document.getElementById("toggle-sidebar").addEventListener("click", () => {
|
|
641
|
+
const sidebar = document.getElementById("sidebar");
|
|
642
|
+
sidebar.style.display = sidebar.style.display === "none" ? "block" : "none";
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Consolidated filter button handler
|
|
646
|
+
document.getElementById("sidebar").addEventListener("click", (e) => {
|
|
647
|
+
const btn = e.target.closest(".filter-btn");
|
|
648
|
+
if (!btn) return;
|
|
649
|
+
|
|
650
|
+
if (btn.dataset.scope !== undefined) {
|
|
651
|
+
const scope = btn.dataset.scope;
|
|
652
|
+
if (scope === currentScope) return;
|
|
653
|
+
currentScope = scope;
|
|
654
|
+
document.querySelectorAll("#scope-filters .filter-btn").forEach(b => b.classList.remove("active"));
|
|
655
|
+
btn.classList.add("active");
|
|
656
|
+
filterEngine.setSearchQuery("");
|
|
657
|
+
filterEngine.setServerSearchIds(null);
|
|
658
|
+
document.getElementById("search-input").value = "";
|
|
659
|
+
document.getElementById("search-info").textContent = "";
|
|
660
|
+
loadData();
|
|
661
|
+
} else if (btn.dataset.level !== undefined) {
|
|
662
|
+
filterEngine.toggleLevel(parseInt(btn.dataset.level));
|
|
663
|
+
btn.classList.toggle("active");
|
|
664
|
+
} else if (btn.dataset.type !== undefined) {
|
|
665
|
+
filterEngine.toggleType(btn.dataset.type);
|
|
666
|
+
btn.classList.toggle("active");
|
|
667
|
+
} else if (btn.dataset.customType !== undefined) {
|
|
668
|
+
filterEngine.toggleCustomType(btn.dataset.customType);
|
|
669
|
+
btn.classList.toggle("active");
|
|
670
|
+
} else if (btn.dataset.shape !== undefined) {
|
|
671
|
+
filterEngine.toggleShape(btn.dataset.shape);
|
|
672
|
+
btn.classList.toggle("active");
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// Search mode toggles
|
|
677
|
+
document.querySelectorAll(".search-mode-btn").forEach(btn => {
|
|
678
|
+
btn.addEventListener("click", () => {
|
|
679
|
+
document.querySelectorAll(".search-mode-btn").forEach(b => b.classList.remove("active"));
|
|
680
|
+
btn.classList.add("active");
|
|
681
|
+
filterEngine.setSearchMode(btn.dataset.mode);
|
|
682
|
+
filterEngine.setServerSearchIds(null);
|
|
683
|
+
const info = document.getElementById("search-info");
|
|
684
|
+
if (filterEngine.searchMode === "text") {
|
|
685
|
+
info.textContent = "";
|
|
686
|
+
} else if (filterEngine.searchMode === "embedding") {
|
|
687
|
+
info.textContent = "Semantic search via embeddings \u2014 type query and press Search";
|
|
688
|
+
} else if (filterEngine.searchMode === "bm25") {
|
|
689
|
+
info.textContent = "Full-text search via BM25 index \u2014 type query and press Search";
|
|
690
|
+
}
|
|
691
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// Search button
|
|
696
|
+
document.getElementById("search-btn").addEventListener("click", () => {
|
|
697
|
+
if (filterEngine.searchMode === "text") {
|
|
698
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
699
|
+
} else {
|
|
700
|
+
performServerSearch(filterEngine.searchQuery);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Node list click
|
|
705
|
+
document.getElementById("node-list").addEventListener("click", (e) => {
|
|
706
|
+
const item = e.target.closest(".node-list-item");
|
|
707
|
+
if (!item) return;
|
|
708
|
+
sceneCtrl.focusOnNode(item.dataset.nodeId);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Tab buttons
|
|
712
|
+
document.querySelectorAll(".tab-btn").forEach(btn => {
|
|
713
|
+
btn.addEventListener("click", () => {
|
|
714
|
+
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
|
715
|
+
btn.classList.add("active");
|
|
716
|
+
const tab = btn.dataset.tab;
|
|
717
|
+
const visualizePanel = document.getElementById("visualize-panel");
|
|
718
|
+
const playbooksPanel = document.getElementById("playbooks-panel");
|
|
719
|
+
const settingsPanel = document.getElementById("settings-panel");
|
|
720
|
+
if (visualizePanel) visualizePanel.classList.toggle("active", tab === "visualize");
|
|
721
|
+
if (playbooksPanel) playbooksPanel.classList.toggle("active", tab === "playbooks");
|
|
722
|
+
if (settingsPanel) settingsPanel.classList.toggle("active", tab === "settings");
|
|
723
|
+
if (tab === "settings") loadSettings();
|
|
724
|
+
if (tab === "playbooks") loadPlaybooks();
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// Playbook search input
|
|
729
|
+
const pbSearch = document.getElementById("playbook-search-input");
|
|
730
|
+
if (pbSearch) {
|
|
731
|
+
pbSearch.addEventListener("input", () => renderPlaybooks(playbookData));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ==================== Data Loading ====================
|
|
736
|
+
|
|
737
|
+
async function loadData() {
|
|
738
|
+
try {
|
|
739
|
+
const [scopesRes, nodesRes, linksRes, statsRes] = await Promise.all([
|
|
740
|
+
fetch("/api/scopes"),
|
|
741
|
+
fetch(`/api/nodes?scope=${currentScope}`),
|
|
742
|
+
fetch(`/api/links?scope=${currentScope}`),
|
|
743
|
+
fetch(`/api/stats?scope=${currentScope}`),
|
|
744
|
+
]);
|
|
745
|
+
|
|
746
|
+
if (!nodesRes.ok || !linksRes.ok || !statsRes.ok) {
|
|
747
|
+
throw new Error(`API error: nodes=${nodesRes.status}, links=${linksRes.status}, stats=${statsRes.status}`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
availableScopes = await scopesRes.json();
|
|
751
|
+
nodeData = await nodesRes.json();
|
|
752
|
+
linkData = await linksRes.json();
|
|
753
|
+
statsData = await statsRes.json();
|
|
754
|
+
|
|
755
|
+
document.getElementById("loading").style.display = "none";
|
|
756
|
+
|
|
757
|
+
console.log(`[data] Scope=${currentScope} nodes=${nodeData.length} links=${linkData.length} stats=${JSON.stringify({ total: statsData.totalNodes, levels: Object.keys(statsData.nodesPerLevel || {}), types: Object.keys(statsData.nodesPerType || {}), shapes: Object.keys(statsData.nodesPerShape || {}), customTypes: Object.keys(statsData.nodesPerCustomType || {}) })}`);
|
|
758
|
+
|
|
759
|
+
if (nodeData.length > 0) {
|
|
760
|
+
console.log(`[data] Sample node:`, { id: nodeData[0].id, label: nodeData[0].label, level: nodeData[0].level, type: nodeData[0].type, importance: nodeData[0].importance, customType: nodeData[0].metadata?.customType });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
sceneCtrl.buildFromData(nodeData);
|
|
765
|
+
sceneCtrl.buildEdges(linkData);
|
|
766
|
+
sceneCtrl.runSimulation(100, linkData);
|
|
767
|
+
} catch (vizErr) {
|
|
768
|
+
console.error("[viewer] Visualization error:", vizErr);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
try {
|
|
772
|
+
buildUI();
|
|
773
|
+
} catch (uiErr) {
|
|
774
|
+
console.error("[viewer] UI build error:", uiErr);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
console.error("[viewer] Load error:", err);
|
|
780
|
+
document.getElementById("loading").textContent = `Error: ${err.message}. Is the server running?`;
|
|
781
|
+
document.getElementById("loading").style.display = "block";
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ==================== Layout Helpers ====================
|
|
786
|
+
|
|
787
|
+
function fibonacciSphere(index, count, radius) {
|
|
788
|
+
if (count <= 1) return new THREE.Vector3(0, 0, 0);
|
|
789
|
+
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
|
|
790
|
+
const y = 1 - (index / (count - 1)) * 2;
|
|
791
|
+
const r = Math.sqrt(1 - y * y);
|
|
792
|
+
const theta = goldenAngle * index;
|
|
793
|
+
return new THREE.Vector3(
|
|
794
|
+
r * Math.cos(theta) * radius,
|
|
795
|
+
y * radius,
|
|
796
|
+
r * Math.sin(theta) * radius
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function computeShellRadii(levelCounts) {
|
|
801
|
+
const sortedLevels = Object.keys(levelCounts).map(Number).sort((a, b) => a - b);
|
|
802
|
+
if (sortedLevels.length === 0) return {};
|
|
803
|
+
const maxCount = Math.max(...sortedLevels.map(l => levelCounts[l]));
|
|
804
|
+
const baseRadius = Math.max(40, Math.sqrt(maxCount) * 10);
|
|
805
|
+
const radii = {};
|
|
806
|
+
sortedLevels.forEach((lvl, i) => {
|
|
807
|
+
radii[lvl] = baseRadius + i * 60;
|
|
808
|
+
});
|
|
809
|
+
return radii;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ==================== Visualization Helpers ====================
|
|
813
|
+
|
|
814
|
+
function getNodeSize(node) {
|
|
815
|
+
const base = 3 + node.importance * 2;
|
|
816
|
+
const accessBoost = Math.min(node.accessCount * 0.5, 10);
|
|
817
|
+
return Math.max(3, Math.min(base + accessBoost, 25));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function getNodeShape(node) {
|
|
821
|
+
const customType = node.metadata?.customType;
|
|
822
|
+
if (customType && CUSTOM_TYPE_SHAPES[customType]) {
|
|
823
|
+
return CUSTOM_TYPE_SHAPES[customType];
|
|
824
|
+
}
|
|
825
|
+
return TYPE_SHAPES[node.type] ?? "sphere";
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function getGeometry(shape, size) {
|
|
829
|
+
switch (shape) {
|
|
830
|
+
case "box": return new THREE.BoxGeometry(size * 1.5, size * 1.5, size * 1.5);
|
|
831
|
+
case "octahedron": return new THREE.OctahedronGeometry(size);
|
|
832
|
+
case "dodecahedron": return new THREE.DodecahedronGeometry(size);
|
|
833
|
+
case "icosahedron": return new THREE.IcosahedronGeometry(size);
|
|
834
|
+
case "torus": return new THREE.TorusGeometry(size, size * 0.4, 16, 100);
|
|
835
|
+
default: return new THREE.SphereGeometry(size, 16, 16);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function createTextSprite(text, color) {
|
|
840
|
+
const canvas = document.createElement("canvas");
|
|
841
|
+
const ctx = canvas.getContext("2d");
|
|
842
|
+
canvas.width = 256;
|
|
843
|
+
canvas.height = 64;
|
|
844
|
+
|
|
845
|
+
ctx.fillStyle = "rgba(0,0,0,0)";
|
|
846
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
847
|
+
|
|
848
|
+
ctx.font = "bold 24px Inter, sans-serif";
|
|
849
|
+
ctx.textAlign = "center";
|
|
850
|
+
ctx.textBaseline = "middle";
|
|
851
|
+
|
|
852
|
+
const hex = "#" + color.toString(16).padStart(6, "0");
|
|
853
|
+
ctx.fillStyle = hex;
|
|
854
|
+
ctx.fillText(text.length > 25 ? text.slice(0, 25) + "..." : text, 128, 32);
|
|
855
|
+
|
|
856
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
857
|
+
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.8 });
|
|
858
|
+
const sprite = new THREE.Sprite(material);
|
|
859
|
+
sprite.scale.set(30, 7.5, 1);
|
|
860
|
+
return sprite;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ==================== UI Builders ====================
|
|
864
|
+
|
|
865
|
+
function buildUI() {
|
|
866
|
+
buildScopeButtons();
|
|
867
|
+
buildStats();
|
|
868
|
+
buildFilters();
|
|
869
|
+
buildLegend();
|
|
870
|
+
buildNodeList();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function buildScopeButtons() {
|
|
874
|
+
const container = document.getElementById("scope-filters");
|
|
875
|
+
container.innerHTML = availableScopes.map(s =>
|
|
876
|
+
`<button class="filter-btn ${s.scope === currentScope ? 'active' : ''}" data-scope="${s.scope}">${s.scope}</button>`
|
|
877
|
+
).join("");
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function buildStats() {
|
|
881
|
+
if (!statsData) return;
|
|
882
|
+
const container = document.getElementById("stats-container");
|
|
883
|
+
container.innerHTML = `
|
|
884
|
+
<div class="stat-row"><span class="stat-label">Total Nodes</span><span class="stat-value">${statsData.totalNodes}</span></div>
|
|
885
|
+
<div class="stat-row"><span class="stat-label">Avg Importance</span><span class="stat-value">${statsData.avgImportance}</span></div>
|
|
886
|
+
<div class="stat-row"><span class="stat-label">Avg Usefulness</span><span class="stat-value">${statsData.avgUsefulness}</span></div>
|
|
887
|
+
<div class="stat-row"><span class="stat-label">Total Accesses</span><span class="stat-value">${statsData.totalAccessCount}</span></div>
|
|
888
|
+
<div class="stat-row"><span class="stat-label">Sticky Nodes</span><span class="stat-value">${statsData.stickyCount}</span></div>
|
|
889
|
+
`;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function buildFilters() {
|
|
893
|
+
if (!statsData) return;
|
|
894
|
+
|
|
895
|
+
filterEngine.initFromStats(statsData);
|
|
896
|
+
|
|
897
|
+
// Level filters
|
|
898
|
+
const levels = Object.keys(statsData.nodesPerLevel || {}).map(Number).sort((a, b) => a - b);
|
|
899
|
+
const levelContainer = document.getElementById("level-filters");
|
|
900
|
+
levelContainer.innerHTML = levels.map(l =>
|
|
901
|
+
`<button class="filter-btn active" data-level="${l}">L${l} (${statsData.nodesPerLevel[l]})</button>`
|
|
902
|
+
).join("");
|
|
903
|
+
|
|
904
|
+
// Type filters
|
|
905
|
+
const types = Object.keys(statsData.nodesPerType || {}).sort();
|
|
906
|
+
const typeContainer = document.getElementById("type-filters");
|
|
907
|
+
typeContainer.innerHTML = types.map(t =>
|
|
908
|
+
`<button class="filter-btn active" data-type="${t}">${t} (${statsData.nodesPerType[t]})</button>`
|
|
909
|
+
).join("");
|
|
910
|
+
|
|
911
|
+
// Custom type filters
|
|
912
|
+
const customTypes = Object.keys(statsData.nodesPerCustomType || {}).sort();
|
|
913
|
+
const customTypeContainer = document.getElementById("custom-type-filters");
|
|
914
|
+
if (customTypeContainer) {
|
|
915
|
+
customTypeContainer.innerHTML = customTypes.map(ct =>
|
|
916
|
+
`<button class="filter-btn active" data-custom-type="${ct}">${ct} (${statsData.nodesPerCustomType[ct]})</button>`
|
|
917
|
+
).join("");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Shape filters
|
|
921
|
+
const shapes = Object.keys(statsData.nodesPerShape || {}).sort();
|
|
922
|
+
const shapeContainer = document.getElementById("shape-filters");
|
|
923
|
+
if (shapeContainer) {
|
|
924
|
+
const shapeLabels = {
|
|
925
|
+
sphere: "Sphere", box: "Box", octahedron: "Octahedron",
|
|
926
|
+
dodecahedron: "Dodecahedron", icosahedron: "Icosahedron", torus: "Torus",
|
|
927
|
+
};
|
|
928
|
+
shapeContainer.innerHTML = shapes.map(s =>
|
|
929
|
+
`<button class="filter-btn active" data-shape="${s}">${shapeLabels[s] || s} (${statsData.nodesPerShape[s]})</button>`
|
|
930
|
+
).join("");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function buildLegend() {
|
|
935
|
+
if (!statsData) return;
|
|
936
|
+
const legend = document.getElementById("legend");
|
|
937
|
+
let html = "";
|
|
938
|
+
for (const [level, color] of Object.entries(LEVEL_COLORS)) {
|
|
939
|
+
if (statsData.nodesPerLevel[level]) {
|
|
940
|
+
const hex = "#" + color.toString(16).padStart(6, "0");
|
|
941
|
+
html += `<div class="legend-item"><div class="legend-dot" style="background: ${hex}"></div><span class="legend-label">Level ${level}</span></div>`;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
html += `<div style="margin-top: 10px;">`;
|
|
945
|
+
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; border-radius: 50%;"></div><span class="legend-label">Sphere = Note</span></div>`;
|
|
946
|
+
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; border-radius: 2px;"></div><span class="legend-label">Box = Event/Episode</span></div>`;
|
|
947
|
+
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);"></div><span class="legend-label">Diamond = Concept/Summary</span></div>`;
|
|
948
|
+
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);"></div><span class="legend-label">Pentagon = Skill</span></div>`;
|
|
949
|
+
html += `<div class="legend-item"><div class="legend-dot" style="background: #ff6b6b; border-radius: 50%;"></div><span class="legend-label">Torus = Middle-Term</span></div>`;
|
|
950
|
+
html += `</div>`;
|
|
951
|
+
legend.innerHTML = html;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function buildNodeList() {
|
|
955
|
+
const container = document.getElementById("node-list");
|
|
956
|
+
const countEl = document.getElementById("node-list-count");
|
|
957
|
+
const filtered = filterEngine.apply(nodeData);
|
|
958
|
+
|
|
959
|
+
countEl.textContent = `(${filtered.length})`;
|
|
960
|
+
|
|
961
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
962
|
+
if (a.level !== b.level) return a.level - b.level;
|
|
963
|
+
return b.importance - a.importance;
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
container.innerHTML = sorted.map(node => {
|
|
967
|
+
const color = LEVEL_COLORS[node.level] ?? 0x888888;
|
|
968
|
+
const hex = "#" + color.toString(16).padStart(6, "0");
|
|
969
|
+
const isSelected = sceneCtrl.selectedNode && sceneCtrl.selectedNode.userData.nodeId === node.id;
|
|
970
|
+
|
|
971
|
+
let customIndicator = "";
|
|
972
|
+
if (node.metadata?.customType) {
|
|
973
|
+
const ct = node.metadata.customType;
|
|
974
|
+
if (ct === 'middle-term') {
|
|
975
|
+
customIndicator = ' <span style="color: #ff6b6b; font-size: 10px;">[MT]</span>';
|
|
976
|
+
} else {
|
|
977
|
+
customIndicator = ` <span style="color: #ff6b6b; font-size: 10px;">[${ct}]</span>`;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (node.type === 'skill') {
|
|
981
|
+
customIndicator += ' <span style="color: #fbbf24; font-size: 10px;">[SKILL]</span>';
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return `
|
|
985
|
+
<div class="node-list-item ${isSelected ? 'selected' : ''}" data-node-id="${node.id}">
|
|
986
|
+
<div class="node-label">${escapeHtml(node.label || "Unnamed")}${customIndicator}</div>
|
|
987
|
+
<div class="node-meta">L${node.level} · ${node.type || "unknown"} · imp: ${node.importance}</div>
|
|
988
|
+
</div>
|
|
989
|
+
`;
|
|
990
|
+
}).join("");
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ==================== Detail Panel ====================
|
|
994
|
+
|
|
995
|
+
function showDetailPanel(node) {
|
|
996
|
+
const panel = document.getElementById("detail-panel");
|
|
997
|
+
const title = document.getElementById("detail-title");
|
|
998
|
+
const content = document.getElementById("detail-content");
|
|
999
|
+
|
|
1000
|
+
title.textContent = node.label || "Unnamed Node";
|
|
1001
|
+
|
|
1002
|
+
const created = new Date(node.createdAt).toLocaleString();
|
|
1003
|
+
const updated = new Date(node.updatedAt).toLocaleString();
|
|
1004
|
+
|
|
1005
|
+
let metadataHtml = "";
|
|
1006
|
+
if (node.metadata && node.metadata.customType) {
|
|
1007
|
+
const meta = node.metadata;
|
|
1008
|
+
metadataHtml = `
|
|
1009
|
+
<div class="detail-section">
|
|
1010
|
+
<h4>Metadata (${escapeHtml(meta.customType)})</h4>
|
|
1011
|
+
<div class="stat-row"><span class="stat-label">Custom Type</span><span class="stat-value">${escapeHtml(meta.customType)}</span></div>
|
|
1012
|
+
${meta.sessionId ? `<div class="stat-row"><span class="stat-label">Session ID</span><span class="stat-value" style="font-family: monospace; font-size: 11px;">${escapeHtml(meta.sessionId)}</span></div>` : ""}
|
|
1013
|
+
${meta.contextTokens ? `<div class="stat-row"><span class="stat-label">Context Tokens</span><span class="stat-value">${meta.contextTokens}</span></div>` : ""}
|
|
1014
|
+
${meta.compactionPrompt ? `<div class="detail-section"><h4>Compaction Prompt</h4><div class="content-full" style="max-height: 200px;">${escapeHtml(meta.compactionPrompt)}</div></div>` : ""}
|
|
1015
|
+
</div>
|
|
1016
|
+
`;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
let skillHtml = "";
|
|
1020
|
+
if (node.type === 'skill') {
|
|
1021
|
+
const triggers = node.metadata?.triggers;
|
|
1022
|
+
const triggerHtml = triggers
|
|
1023
|
+
? `<div class="stat-row"><span class="stat-label">Triggers</span><span class="stat-value">${Array.isArray(triggers) ? triggers.map(escapeHtml).join(', ') : escapeHtml(String(triggers))}</span></div>`
|
|
1024
|
+
: '';
|
|
1025
|
+
skillHtml = `
|
|
1026
|
+
<div class="detail-section">
|
|
1027
|
+
<h4>Skill Info</h4>
|
|
1028
|
+
<div class="stat-row"><span class="stat-label">Type</span><span class="stat-value" style="color: #fbbf24;">Skill</span></div>
|
|
1029
|
+
${node.summary ? `<div class="stat-row"><span class="stat-label">Description</span><span class="stat-value">${escapeHtml(node.summary)}</span></div>` : ''}
|
|
1030
|
+
${triggerHtml}
|
|
1031
|
+
</div>
|
|
1032
|
+
`;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
content.innerHTML = `
|
|
1036
|
+
<div class="detail-section">
|
|
1037
|
+
<h4>ID</h4>
|
|
1038
|
+
<div class="detail-value" style="font-family: monospace; font-size: 11px;">${node.id}</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
<div class="detail-section">
|
|
1041
|
+
<h4>Metrics</h4>
|
|
1042
|
+
<div class="stat-row"><span class="stat-label">Level</span><span class="stat-value">${node.level}</span></div>
|
|
1043
|
+
<div class="stat-row"><span class="stat-label">Type</span><span class="stat-value">${node.type || "none"}${node.metadata?.customType ? ' <span style="color: #ff6b6b;">[' + escapeHtml(node.metadata.customType) + ']</span>' : ""}</span></div>
|
|
1044
|
+
<div class="stat-row"><span class="stat-label">Importance</span><span class="stat-value">${node.importance}</span></div>
|
|
1045
|
+
<div class="stat-row"><span class="stat-label">Usefulness Score</span><span class="stat-value">${node.usefulnessScore}</span></div>
|
|
1046
|
+
<div class="stat-row"><span class="stat-label">Access Count</span><span class="stat-value">${node.accessCount}</span></div>
|
|
1047
|
+
<div class="stat-row"><span class="stat-label">Times Used</span><span class="stat-value">${node.timesUsed}</span></div>
|
|
1048
|
+
<div class="stat-row"><span class="stat-label">Times Helpful</span><span class="stat-value">${node.timesHelpful}</span></div>
|
|
1049
|
+
<div class="stat-row"><span class="stat-label">Confidence</span><span class="stat-value">${node.confidence}</span></div>
|
|
1050
|
+
<div class="stat-row"><span class="stat-label">Sticky</span><span class="stat-value">${node.sticky ? "Yes" : "No"}</span></div>
|
|
1051
|
+
<div class="stat-row"><span class="stat-label">Content Length</span><span class="stat-value">${node.contentLength} chars</span></div>
|
|
1052
|
+
</div>
|
|
1053
|
+
${metadataHtml}
|
|
1054
|
+
${skillHtml}
|
|
1055
|
+
<div class="detail-section">
|
|
1056
|
+
<h4>Timestamps</h4>
|
|
1057
|
+
<div class="detail-value">Created: ${created}</div>
|
|
1058
|
+
<div class="detail-value">Updated: ${updated}</div>
|
|
1059
|
+
</div>
|
|
1060
|
+
<div class="detail-section">
|
|
1061
|
+
<h4>Content (${node.contentLength} chars)</h4>
|
|
1062
|
+
<div class="content-full">${escapeHtml(node.content)}</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
`;
|
|
1065
|
+
|
|
1066
|
+
panel.classList.add("open");
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function escapeHtml(text) {
|
|
1070
|
+
const div = document.createElement("div");
|
|
1071
|
+
div.textContent = text;
|
|
1072
|
+
return div.innerHTML;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ==================== Edit / Delete / Inject ====================
|
|
1076
|
+
|
|
1077
|
+
function toggleEditMode(node) {
|
|
1078
|
+
if (editingNode && editingNode.id === node.id) {
|
|
1079
|
+
cancelEdit();
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
editingNode = node;
|
|
1083
|
+
showEditForm(node);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function showEditForm(node) {
|
|
1087
|
+
const panel = document.getElementById("detail-panel");
|
|
1088
|
+
const title = document.getElementById("detail-title");
|
|
1089
|
+
const content = document.getElementById("detail-content");
|
|
1090
|
+
|
|
1091
|
+
title.textContent = "Edit: " + (node.label || "Unnamed Node");
|
|
1092
|
+
|
|
1093
|
+
const types = ["note", "event", "episode", "concept", "summary", "core", "improvement", "howto", "skill"];
|
|
1094
|
+
|
|
1095
|
+
content.innerHTML = `
|
|
1096
|
+
<div class="edit-field">
|
|
1097
|
+
<label>Label</label>
|
|
1098
|
+
<input type="text" id="edit-label" value="${escapeHtml(node.label || "")}">
|
|
1099
|
+
</div>
|
|
1100
|
+
<div class="edit-field">
|
|
1101
|
+
<label>Type</label>
|
|
1102
|
+
<select id="edit-type">
|
|
1103
|
+
${types.map(t => `<option value="${t}" ${node.type === t ? "selected" : ""}>${t}</option>`).join("")}
|
|
1104
|
+
</select>
|
|
1105
|
+
</div>
|
|
1106
|
+
<div class="edit-field">
|
|
1107
|
+
<label>Level</label>
|
|
1108
|
+
<input type="number" id="edit-level" value="${node.level}" min="0" max="10">
|
|
1109
|
+
</div>
|
|
1110
|
+
<div class="edit-field">
|
|
1111
|
+
<label>Importance</label>
|
|
1112
|
+
<input type="number" id="edit-importance" value="${node.importance}" min="0" max="10" step="0.1">
|
|
1113
|
+
</div>
|
|
1114
|
+
${node.summary !== undefined ? `
|
|
1115
|
+
<div class="edit-field">
|
|
1116
|
+
<label>Summary</label>
|
|
1117
|
+
<input type="text" id="edit-summary" value="${escapeHtml(node.summary || "")}">
|
|
1118
|
+
</div>
|
|
1119
|
+
` : ""}
|
|
1120
|
+
<div class="edit-field">
|
|
1121
|
+
<label>Content</label>
|
|
1122
|
+
<textarea id="edit-content" rows="12">${escapeHtml(node.content || "")}</textarea>
|
|
1123
|
+
</div>
|
|
1124
|
+
<div class="edit-field">
|
|
1125
|
+
<div class="checkbox-row">
|
|
1126
|
+
<input type="checkbox" id="edit-sticky" ${node.sticky ? "checked" : ""}>
|
|
1127
|
+
<label for="edit-sticky" style="margin: 0;">Sticky (prevent compression)</label>
|
|
1128
|
+
</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
<div class="edit-actions">
|
|
1131
|
+
<button class="btn-save" id="save-node-btn">Save</button>
|
|
1132
|
+
<button class="btn-cancel" id="cancel-edit-btn">Cancel</button>
|
|
1133
|
+
</div>
|
|
1134
|
+
<div id="edit-status" style="margin-top: 8px; font-size: 12px; text-align: center;"></div>
|
|
1135
|
+
`;
|
|
1136
|
+
|
|
1137
|
+
document.getElementById("save-node-btn").addEventListener("click", saveNode);
|
|
1138
|
+
document.getElementById("cancel-edit-btn").addEventListener("click", cancelEdit);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async function saveNode() {
|
|
1142
|
+
const statusEl = document.getElementById("edit-status");
|
|
1143
|
+
if (!editingNode) return;
|
|
1144
|
+
|
|
1145
|
+
statusEl.textContent = "Saving...";
|
|
1146
|
+
statusEl.style.color = "#888";
|
|
1147
|
+
|
|
1148
|
+
const body = {
|
|
1149
|
+
label: document.getElementById("edit-label").value,
|
|
1150
|
+
type: document.getElementById("edit-type").value,
|
|
1151
|
+
level: parseInt(document.getElementById("edit-level").value),
|
|
1152
|
+
importance: parseFloat(document.getElementById("edit-importance").value),
|
|
1153
|
+
content: document.getElementById("edit-content").value,
|
|
1154
|
+
sticky: document.getElementById("edit-sticky").checked,
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
const summaryEl = document.getElementById("edit-summary");
|
|
1158
|
+
if (summaryEl) body.summary = summaryEl.value;
|
|
1159
|
+
|
|
1160
|
+
try {
|
|
1161
|
+
const res = await fetch(`/api/nodes/${editingNode.id}?scope=${currentScope}`, {
|
|
1162
|
+
method: "PUT",
|
|
1163
|
+
headers: { "Content-Type": "application/json" },
|
|
1164
|
+
body: JSON.stringify(body),
|
|
1165
|
+
});
|
|
1166
|
+
const result = await res.json();
|
|
1167
|
+
|
|
1168
|
+
if (result.success) {
|
|
1169
|
+
statusEl.textContent = "Saved! Reloading...";
|
|
1170
|
+
statusEl.style.color = "#4f4";
|
|
1171
|
+
await loadData();
|
|
1172
|
+
editingNode = null;
|
|
1173
|
+
const updated = nodeData.find(n => n.id === editingNode?.id || n.id === body.label);
|
|
1174
|
+
if (updated) {
|
|
1175
|
+
const mesh = sceneCtrl.nodeObjects.find(o => o.isMesh && o.userData.nodeId === updated.id);
|
|
1176
|
+
if (mesh) {
|
|
1177
|
+
sceneCtrl.selectedNode = mesh;
|
|
1178
|
+
showDetailPanel(updated);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
} else {
|
|
1182
|
+
statusEl.textContent = "Error: " + (result.error || "Unknown");
|
|
1183
|
+
statusEl.style.color = "#f44";
|
|
1184
|
+
}
|
|
1185
|
+
} catch (e) {
|
|
1186
|
+
statusEl.textContent = "Error: " + e.message;
|
|
1187
|
+
statusEl.style.color = "#f44";
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function cancelEdit() {
|
|
1192
|
+
editingNode = null;
|
|
1193
|
+
if (sceneCtrl.selectedNode && sceneCtrl.selectedNode.userData.nodeData) {
|
|
1194
|
+
showDetailPanel(sceneCtrl.selectedNode.userData.nodeData);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async function deleteNode(node) {
|
|
1199
|
+
if (!confirm(`Delete node "${node.label || node.id}"? This cannot be undone.`)) return;
|
|
1200
|
+
|
|
1201
|
+
try {
|
|
1202
|
+
const res = await fetch(`/api/nodes/${node.id}?scope=${currentScope}`, {
|
|
1203
|
+
method: "DELETE",
|
|
1204
|
+
});
|
|
1205
|
+
const result = await res.json();
|
|
1206
|
+
|
|
1207
|
+
if (result.success) {
|
|
1208
|
+
document.getElementById("detail-panel").classList.remove("open");
|
|
1209
|
+
sceneCtrl.selectedNode = null;
|
|
1210
|
+
editingNode = null;
|
|
1211
|
+
await loadData();
|
|
1212
|
+
} else {
|
|
1213
|
+
alert("Delete failed: " + (result.error || "Unknown"));
|
|
1214
|
+
}
|
|
1215
|
+
} catch (e) {
|
|
1216
|
+
alert("Delete error: " + e.message);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
async function injectNode(node) {
|
|
1221
|
+
const statusEl = document.getElementById("inject-status");
|
|
1222
|
+
try {
|
|
1223
|
+
statusEl.textContent = "Injecting...";
|
|
1224
|
+
statusEl.style.display = "block";
|
|
1225
|
+
statusEl.style.color = "#888";
|
|
1226
|
+
|
|
1227
|
+
const res = await fetch("/api/inject", {
|
|
1228
|
+
method: "POST",
|
|
1229
|
+
headers: { "Content-Type": "application/json" },
|
|
1230
|
+
body: JSON.stringify({ nodeId: node.id, scope: currentScope }),
|
|
1231
|
+
});
|
|
1232
|
+
const result = await res.json();
|
|
1233
|
+
|
|
1234
|
+
if (result.success) {
|
|
1235
|
+
statusEl.textContent = "Injected!";
|
|
1236
|
+
statusEl.style.color = "#4f4";
|
|
1237
|
+
setTimeout(() => { statusEl.style.display = "none"; }, 2000);
|
|
1238
|
+
} else {
|
|
1239
|
+
statusEl.textContent = "Error: " + (result.error || "Unknown");
|
|
1240
|
+
statusEl.style.color = "#f44";
|
|
1241
|
+
}
|
|
1242
|
+
} catch (e) {
|
|
1243
|
+
statusEl.textContent = "Error: " + e.message;
|
|
1244
|
+
statusEl.style.color = "#f44";
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// ==================== Server Search ====================
|
|
1249
|
+
|
|
1250
|
+
async function performServerSearch(query) {
|
|
1251
|
+
const info = document.getElementById("search-info");
|
|
1252
|
+
if (!query || query.length < 2) {
|
|
1253
|
+
filterEngine.setServerSearchIds(null);
|
|
1254
|
+
info.textContent = "";
|
|
1255
|
+
info.classList.remove("loading");
|
|
1256
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
filterEngine.setServerSearchIds(null);
|
|
1261
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
1262
|
+
|
|
1263
|
+
info.classList.add("loading");
|
|
1264
|
+
info.textContent = filterEngine.searchMode === "embedding" ? "Searching embeddings..." : "Searching BM25 index...";
|
|
1265
|
+
info.style.color = "#888";
|
|
1266
|
+
|
|
1267
|
+
try {
|
|
1268
|
+
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&mode=${filterEngine.searchMode}&scope=${currentScope}`);
|
|
1269
|
+
if (!res.ok) {
|
|
1270
|
+
filterEngine.setServerSearchIds(null);
|
|
1271
|
+
info.textContent = "Search error: server returned " + res.status;
|
|
1272
|
+
info.style.color = "#f44";
|
|
1273
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const results = await res.json();
|
|
1277
|
+
|
|
1278
|
+
if (!Array.isArray(results)) {
|
|
1279
|
+
filterEngine.setServerSearchIds(null);
|
|
1280
|
+
info.textContent = "Search error: unexpected response format";
|
|
1281
|
+
info.style.color = "#f44";
|
|
1282
|
+
} else if (results.length === 0) {
|
|
1283
|
+
filterEngine.setServerSearchIds(new Set());
|
|
1284
|
+
info.textContent = "No results found";
|
|
1285
|
+
info.style.color = "#f44";
|
|
1286
|
+
} else {
|
|
1287
|
+
filterEngine.setServerSearchIds(new Set(results.map(r => r.id)));
|
|
1288
|
+
info.textContent = `${results.length} result(s) found`;
|
|
1289
|
+
info.style.color = "#4f4";
|
|
1290
|
+
}
|
|
1291
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
1292
|
+
} catch (e) {
|
|
1293
|
+
filterEngine.setServerSearchIds(null);
|
|
1294
|
+
info.textContent = "Search failed: " + e.message;
|
|
1295
|
+
info.style.color = "#f44";
|
|
1296
|
+
} finally {
|
|
1297
|
+
info.classList.remove("loading");
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// ==================== Playbooks ====================
|
|
1302
|
+
|
|
1303
|
+
let playbookData = [];
|
|
1304
|
+
|
|
1305
|
+
async function loadPlaybooks() {
|
|
1306
|
+
const container = document.getElementById('playbook-list');
|
|
1307
|
+
container.innerHTML = '<div style="color:#888;font-size:12px;text-align:center;padding:20px;">Loading playbooks...</div>';
|
|
1308
|
+
|
|
1309
|
+
try {
|
|
1310
|
+
const res = await fetch(`/api/playbooks?scope=${currentScope}`);
|
|
1311
|
+
if (!res.ok) {
|
|
1312
|
+
container.innerHTML = '<div style="color:#f44;font-size:12px;text-align:center;padding:20px;">Failed to load playbooks</div>';
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
playbookData = await res.json();
|
|
1316
|
+
renderPlaybooks(playbookData);
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
container.innerHTML = `<div style="color:#f44;font-size:12px;text-align:center;padding:20px;">Error: ${e.message}</div>`;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function renderPlaybooks(playbooks) {
|
|
1323
|
+
const container = document.getElementById('playbook-list');
|
|
1324
|
+
const searchQ = (document.getElementById('playbook-search-input').value || '').toLowerCase();
|
|
1325
|
+
|
|
1326
|
+
const filtered = searchQ
|
|
1327
|
+
? playbooks.filter(p =>
|
|
1328
|
+
p.name.toLowerCase().includes(searchQ) ||
|
|
1329
|
+
p.description.toLowerCase().includes(searchQ) ||
|
|
1330
|
+
(p.tags || []).some(t => t.toLowerCase().includes(searchQ))
|
|
1331
|
+
)
|
|
1332
|
+
: playbooks;
|
|
1333
|
+
|
|
1334
|
+
if (filtered.length === 0) {
|
|
1335
|
+
container.innerHTML = '<div style="color:#888;font-size:12px;text-align:center;padding:20px;">No playbooks found.</div>';
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
container.innerHTML = filtered.map(renderPlaybookCard).join('');
|
|
1340
|
+
|
|
1341
|
+
// Wire delete buttons after render
|
|
1342
|
+
container.querySelectorAll('.pb-delete-btn').forEach(btn => {
|
|
1343
|
+
btn.addEventListener('click', () => deletePlaybook(btn.dataset.pbId));
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function renderPlaybookCard(pb) {
|
|
1348
|
+
const tags = (pb.tags || []).map(t => `<span class="pb-tag">${t}</span>`).join('');
|
|
1349
|
+
const steps = (pb.steps || []).map(s =>
|
|
1350
|
+
`<div class="pb-step">
|
|
1351
|
+
<span class="step-tool">${s.toolName}</span>
|
|
1352
|
+
${s.critical ? '<span class="step-critical">\u26a0</span>' : ''}
|
|
1353
|
+
<span>${s.description}</span>
|
|
1354
|
+
</div>`
|
|
1355
|
+
).join('');
|
|
1356
|
+
const triggers = (pb.triggers || []).map(t => {
|
|
1357
|
+
if (t.type === 'task_keyword') return `<div class="pb-trigger">\ud83d\udd11 ${(t.keywords || []).join(', ')}</div>`;
|
|
1358
|
+
if (t.type === 'tool_sequence') return `<div class="pb-trigger">\ud83d\udd27 ${(t.pattern || []).join(' \u2192 ')}</div>`;
|
|
1359
|
+
return `<div class="pb-trigger">\ud83d\udccb ${t.type}</div>`;
|
|
1360
|
+
}).join('');
|
|
1361
|
+
|
|
1362
|
+
return `<div class="playbook-card">
|
|
1363
|
+
<div class="pb-header">
|
|
1364
|
+
<div>
|
|
1365
|
+
<div class="pb-name">${pb.name}</div>
|
|
1366
|
+
<div class="pb-desc">${pb.description}</div>
|
|
1367
|
+
</div>
|
|
1368
|
+
<button class="pb-delete-btn" data-pb-id="${pb.id}">Delete</button>
|
|
1369
|
+
</div>
|
|
1370
|
+
<div class="pb-meta">
|
|
1371
|
+
<span>\u26a1 ${pb.executionCount || 0} runs</span>
|
|
1372
|
+
${pb.avgDurationMs ? `<span>\u23f1 ${(pb.avgDurationMs / 1000).toFixed(1)}s avg</span>` : ''}
|
|
1373
|
+
${pb.lastExecutedAt ? `<span>\ud83d\udd50 ${new Date(pb.lastExecutedAt).toLocaleDateString()}</span>` : ''}
|
|
1374
|
+
</div>
|
|
1375
|
+
${triggers ? `<div class="pb-triggers">${triggers}</div>` : ''}
|
|
1376
|
+
<div class="pb-steps">${steps}</div>
|
|
1377
|
+
${tags ? `<div class="pb-tags">${tags}</div>` : ''}
|
|
1378
|
+
</div>`;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
async function deletePlaybook(id) {
|
|
1382
|
+
if (!confirm('Delete this playbook?')) return;
|
|
1383
|
+
try {
|
|
1384
|
+
const res = await fetch(`/api/playbooks/${id}?scope=${currentScope}`, { method: 'DELETE' });
|
|
1385
|
+
if (res.ok) {
|
|
1386
|
+
playbookData = playbookData.filter(p => p.id !== id);
|
|
1387
|
+
renderPlaybooks(playbookData);
|
|
1388
|
+
} else {
|
|
1389
|
+
alert('Failed to delete playbook');
|
|
1390
|
+
}
|
|
1391
|
+
} catch (e) {
|
|
1392
|
+
alert('Error: ' + e.message);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// Playbook search input
|
|
1397
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1398
|
+
const searchInput = document.getElementById('playbook-search-input');
|
|
1399
|
+
if (searchInput) {
|
|
1400
|
+
searchInput.addEventListener('input', () => {
|
|
1401
|
+
renderPlaybooks(playbookData);
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
// ==================== Animation Loop ====================
|
|
1407
|
+
|
|
1408
|
+
function animate() {
|
|
1409
|
+
requestAnimationFrame(animate);
|
|
1410
|
+
sceneCtrl.render();
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// ==================== Settings ====================
|
|
1414
|
+
|
|
1415
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1416
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
1417
|
+
btn.addEventListener('click', () => {
|
|
1418
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
1419
|
+
btn.classList.add('active');
|
|
1420
|
+
const tab = btn.dataset.tab;
|
|
1421
|
+
document.getElementById('visualize-panel').classList.toggle('active', tab === 'visualize');
|
|
1422
|
+
document.getElementById('playbooks-panel').classList.toggle('active', tab === 'playbooks');
|
|
1423
|
+
document.getElementById('settings-panel').classList.toggle('active', tab === 'settings');
|
|
1424
|
+
if (tab === 'settings') loadSettings();
|
|
1425
|
+
if (tab === 'playbooks') loadPlaybooks();
|
|
1426
|
+
});
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
document.getElementById('save-config').addEventListener('click', saveSettings);
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
async function loadSettings() {
|
|
1433
|
+
try {
|
|
1434
|
+
const res = await fetch('/api/config');
|
|
1435
|
+
const config = await res.json();
|
|
1436
|
+
document.getElementById('defaultTtlDays').value = config.defaultTtlDays ?? 0;
|
|
1437
|
+
document.getElementById('autoRetrieve-enabled').value = String(config.autoRetrieve?.enabled ?? false);
|
|
1438
|
+
document.getElementById('autoRetrieve-candidateCount').value = config.autoRetrieve?.candidateCount ?? 30;
|
|
1439
|
+
document.getElementById('autoRetrieve-maxInjectNodes').value = config.autoRetrieve?.maxInjectNodes ?? 5;
|
|
1440
|
+
document.getElementById('autoFileSummarization-enabled').value = String(config.autoFileSummarization?.enabled ?? false);
|
|
1441
|
+
document.getElementById('ollama-enabled').value = String(config.ollama?.enabled ?? false);
|
|
1442
|
+
document.getElementById('ollama-model').value = config.ollama?.model ?? 'qwen2.5-coder:1.5b';
|
|
1443
|
+
document.getElementById('ollama-baseUrl').value = config.ollama?.baseUrl ?? 'http://localhost:11434';
|
|
1444
|
+
document.getElementById('llmCompression-enabled').value = String(config.llmCompression?.enabled ?? false);
|
|
1445
|
+
document.getElementById('llmCompression-maxSummaryTokens').value = config.llmCompression?.maxSummaryTokens ?? 500;
|
|
1446
|
+
document.getElementById('llmCompression-model').value = config.llmCompression?.model ?? '';
|
|
1447
|
+
document.getElementById('autoDistill-enabled').value = String(config.autoDistill?.enabled ?? false);
|
|
1448
|
+
document.getElementById('autoDistill-minLessons').value = config.autoDistill?.minLessons ?? 3;
|
|
1449
|
+
document.getElementById('autoDistill-useLlm').value = String(config.autoDistill?.useLlm ?? false);
|
|
1450
|
+
document.getElementById('predictiveRating-enabled').value = String(config.predictiveRating?.enabled ?? false);
|
|
1451
|
+
document.getElementById('predictiveRating-decayDays').value = config.predictiveRating?.decayDays ?? 7;
|
|
1452
|
+
document.getElementById('predictiveRating-positiveBoost').value = config.predictiveRating?.positiveBoost ?? 0.1;
|
|
1453
|
+
document.getElementById('predictiveRating-negativePenalty').value = config.predictiveRating?.negativePenalty ?? 0.05;
|
|
1454
|
+
document.getElementById('autoDiscover-enabled').value = String(config.autoDiscover?.enabled ?? false);
|
|
1455
|
+
document.getElementById('autoDiscover-minSequenceLength').value = config.autoDiscover?.minSequenceLength ?? 3;
|
|
1456
|
+
document.getElementById('autoDiscover-minRepeatCount').value = config.autoDiscover?.minRepeatCount ?? 2;
|
|
1457
|
+
document.getElementById('autoDiscover-maxInjectPlaybooks').value = config.autoDiscover?.maxInjectPlaybooks ?? 3;
|
|
1458
|
+
} catch (e) {
|
|
1459
|
+
console.error('Failed to load config:', e);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
async function saveSettings() {
|
|
1464
|
+
const config = {
|
|
1465
|
+
defaultTtlDays: parseInt(document.getElementById('defaultTtlDays').value) || 0,
|
|
1466
|
+
autoRetrieve: {
|
|
1467
|
+
enabled: document.getElementById('autoRetrieve-enabled').value === 'true',
|
|
1468
|
+
candidateCount: parseInt(document.getElementById('autoRetrieve-candidateCount').value) || 30,
|
|
1469
|
+
maxInjectNodes: parseInt(document.getElementById('autoRetrieve-maxInjectNodes').value) || 5,
|
|
1470
|
+
},
|
|
1471
|
+
autoFileSummarization: {
|
|
1472
|
+
enabled: document.getElementById('autoFileSummarization-enabled').value === 'true',
|
|
1473
|
+
},
|
|
1474
|
+
ollama: {
|
|
1475
|
+
enabled: document.getElementById('ollama-enabled').value === 'true',
|
|
1476
|
+
model: document.getElementById('ollama-model').value || 'qwen2.5-coder:1.5b',
|
|
1477
|
+
baseUrl: document.getElementById('ollama-baseUrl').value || 'http://localhost:11434',
|
|
1478
|
+
},
|
|
1479
|
+
llmCompression: {
|
|
1480
|
+
enabled: document.getElementById('llmCompression-enabled').value === 'true',
|
|
1481
|
+
maxSummaryTokens: parseInt(document.getElementById('llmCompression-maxSummaryTokens').value) || 500,
|
|
1482
|
+
model: document.getElementById('llmCompression-model').value || undefined,
|
|
1483
|
+
},
|
|
1484
|
+
autoDistill: {
|
|
1485
|
+
enabled: document.getElementById('autoDistill-enabled').value === 'true',
|
|
1486
|
+
minLessons: parseInt(document.getElementById('autoDistill-minLessons').value) || 3,
|
|
1487
|
+
useLlm: document.getElementById('autoDistill-useLlm').value === 'true',
|
|
1488
|
+
},
|
|
1489
|
+
predictiveRating: {
|
|
1490
|
+
enabled: document.getElementById('predictiveRating-enabled').value === 'true',
|
|
1491
|
+
decayDays: parseFloat(document.getElementById('predictiveRating-decayDays').value) || 7,
|
|
1492
|
+
positiveBoost: parseFloat(document.getElementById('predictiveRating-positiveBoost').value) || 0.1,
|
|
1493
|
+
negativePenalty: parseFloat(document.getElementById('predictiveRating-negativePenalty').value) || 0.05,
|
|
1494
|
+
},
|
|
1495
|
+
autoDiscover: {
|
|
1496
|
+
enabled: document.getElementById('autoDiscover-enabled').value === 'true',
|
|
1497
|
+
minSequenceLength: parseInt(document.getElementById('autoDiscover-minSequenceLength').value) || 3,
|
|
1498
|
+
minRepeatCount: parseInt(document.getElementById('autoDiscover-minRepeatCount').value) || 2,
|
|
1499
|
+
maxInjectPlaybooks: parseInt(document.getElementById('autoDiscover-maxInjectPlaybooks').value) || 3,
|
|
1500
|
+
},
|
|
1501
|
+
};
|
|
1502
|
+
try {
|
|
1503
|
+
const res = await fetch('/api/config', {
|
|
1504
|
+
method: 'PUT',
|
|
1505
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1506
|
+
body: JSON.stringify(config),
|
|
1507
|
+
});
|
|
1508
|
+
if (res.ok) {
|
|
1509
|
+
const data = await res.json();
|
|
1510
|
+
if (data.success) {
|
|
1511
|
+
document.getElementById('save-message').textContent = 'Saved! Restart plugin to apply.';
|
|
1512
|
+
} else {
|
|
1513
|
+
document.getElementById('save-message').textContent = 'Error: ' + (data.error || 'Unknown');
|
|
1514
|
+
document.getElementById('save-message').style.color = '#f44';
|
|
1515
|
+
}
|
|
1516
|
+
setTimeout(() => { document.getElementById('save-message').textContent = ''; document.getElementById('save-message').style.color = '#4f4'; }, 5000);
|
|
1517
|
+
} else {
|
|
1518
|
+
document.getElementById('save-message').textContent = 'HTTP Error: ' + res.status;
|
|
1519
|
+
document.getElementById('save-message').style.color = '#f44';
|
|
1520
|
+
}
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
console.error('Failed to save config:', e);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// ==================== Start ====================
|
|
1527
|
+
|
|
1528
|
+
init();
|
|
1529
|
+
animate();
|