@0xmaxma/claude-gateway 1.6.8 → 1.7.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/README.md +149 -20
- package/config.template.json +59 -1
- package/dist/agent/dreaming/accept.d.ts +43 -0
- package/dist/agent/dreaming/accept.d.ts.map +1 -0
- package/dist/agent/dreaming/accept.js +230 -0
- package/dist/agent/dreaming/accept.js.map +1 -0
- package/dist/agent/dreaming/applier.d.ts +77 -0
- package/dist/agent/dreaming/applier.d.ts.map +1 -0
- package/dist/agent/dreaming/applier.js +406 -0
- package/dist/agent/dreaming/applier.js.map +1 -0
- package/dist/agent/dreaming/audit.d.ts +27 -0
- package/dist/agent/dreaming/audit.d.ts.map +1 -0
- package/dist/agent/dreaming/audit.js +100 -0
- package/dist/agent/dreaming/audit.js.map +1 -0
- package/dist/agent/dreaming/compactor.d.ts +66 -0
- package/dist/agent/dreaming/compactor.d.ts.map +1 -0
- package/dist/agent/dreaming/compactor.js +306 -0
- package/dist/agent/dreaming/compactor.js.map +1 -0
- package/dist/agent/dreaming/config.d.ts +19 -0
- package/dist/agent/dreaming/config.d.ts.map +1 -0
- package/dist/agent/dreaming/config.js +107 -0
- package/dist/agent/dreaming/config.js.map +1 -0
- package/dist/agent/dreaming/episodic.d.ts +42 -0
- package/dist/agent/dreaming/episodic.d.ts.map +1 -0
- package/dist/agent/dreaming/episodic.js +116 -0
- package/dist/agent/dreaming/episodic.js.map +1 -0
- package/dist/agent/dreaming/gather.d.ts +31 -0
- package/dist/agent/dreaming/gather.d.ts.map +1 -0
- package/dist/agent/dreaming/gather.js +55 -0
- package/dist/agent/dreaming/gather.js.map +1 -0
- package/dist/agent/dreaming/index.d.ts +87 -0
- package/dist/agent/dreaming/index.d.ts.map +1 -0
- package/dist/agent/dreaming/index.js +377 -0
- package/dist/agent/dreaming/index.js.map +1 -0
- package/dist/agent/dreaming/migrate-cli.d.ts +17 -0
- package/dist/agent/dreaming/migrate-cli.d.ts.map +1 -0
- package/dist/agent/dreaming/migrate-cli.js +42 -0
- package/dist/agent/dreaming/migrate-cli.js.map +1 -0
- package/dist/agent/dreaming/migrate.d.ts +72 -0
- package/dist/agent/dreaming/migrate.d.ts.map +1 -0
- package/dist/agent/dreaming/migrate.js +374 -0
- package/dist/agent/dreaming/migrate.js.map +1 -0
- package/dist/agent/dreaming/report.d.ts +39 -0
- package/dist/agent/dreaming/report.d.ts.map +1 -0
- package/dist/agent/dreaming/report.js +121 -0
- package/dist/agent/dreaming/report.js.map +1 -0
- package/dist/agent/dreaming/reviewer.d.ts +43 -0
- package/dist/agent/dreaming/reviewer.d.ts.map +1 -0
- package/dist/agent/dreaming/reviewer.js +249 -0
- package/dist/agent/dreaming/reviewer.js.map +1 -0
- package/dist/agent/dreaming/staleness.d.ts +68 -0
- package/dist/agent/dreaming/staleness.d.ts.map +1 -0
- package/dist/agent/dreaming/staleness.js +281 -0
- package/dist/agent/dreaming/staleness.js.map +1 -0
- package/dist/agent/dreaming/types.d.ts +130 -0
- package/dist/agent/dreaming/types.d.ts.map +1 -0
- package/dist/agent/dreaming/types.js +10 -0
- package/dist/agent/dreaming/types.js.map +1 -0
- package/dist/agent/knowledge/archive-db.d.ts +114 -0
- package/dist/agent/knowledge/archive-db.d.ts.map +1 -0
- package/dist/agent/knowledge/archive-db.js +474 -0
- package/dist/agent/knowledge/archive-db.js.map +1 -0
- package/dist/agent/knowledge/chunk.d.ts +21 -0
- package/dist/agent/knowledge/chunk.d.ts.map +1 -0
- package/dist/agent/knowledge/chunk.js +67 -0
- package/dist/agent/knowledge/chunk.js.map +1 -0
- package/dist/agent/knowledge/config.d.ts +20 -0
- package/dist/agent/knowledge/config.d.ts.map +1 -0
- package/dist/agent/knowledge/config.js +143 -0
- package/dist/agent/knowledge/config.js.map +1 -0
- package/dist/agent/knowledge/graph-demo.d.ts +25 -0
- package/dist/agent/knowledge/graph-demo.d.ts.map +1 -0
- package/dist/agent/knowledge/graph-demo.js +132 -0
- package/dist/agent/knowledge/graph-demo.js.map +1 -0
- package/dist/agent/knowledge/index.d.ts +24 -0
- package/dist/agent/knowledge/index.d.ts.map +1 -0
- package/dist/agent/knowledge/index.js +50 -0
- package/dist/agent/knowledge/index.js.map +1 -0
- package/dist/agent/knowledge/indexer.d.ts +34 -0
- package/dist/agent/knowledge/indexer.d.ts.map +1 -0
- package/dist/agent/knowledge/indexer.js +271 -0
- package/dist/agent/knowledge/indexer.js.map +1 -0
- package/dist/agent/knowledge/lifecycle.d.ts +91 -0
- package/dist/agent/knowledge/lifecycle.d.ts.map +1 -0
- package/dist/agent/knowledge/lifecycle.js +205 -0
- package/dist/agent/knowledge/lifecycle.js.map +1 -0
- package/dist/agent/knowledge/provenance.d.ts +15 -0
- package/dist/agent/knowledge/provenance.d.ts.map +1 -0
- package/dist/agent/knowledge/provenance.js +37 -0
- package/dist/agent/knowledge/provenance.js.map +1 -0
- package/dist/agent/knowledge/reindex-cli.d.ts +13 -0
- package/dist/agent/knowledge/reindex-cli.d.ts.map +1 -0
- package/dist/agent/knowledge/reindex-cli.js +53 -0
- package/dist/agent/knowledge/reindex-cli.js.map +1 -0
- package/dist/agent/knowledge/reindex-spawn.d.ts +30 -0
- package/dist/agent/knowledge/reindex-spawn.d.ts.map +1 -0
- package/dist/agent/knowledge/reindex-spawn.js +97 -0
- package/dist/agent/knowledge/reindex-spawn.js.map +1 -0
- package/dist/agent/knowledge/shared-promote.d.ts +18 -0
- package/dist/agent/knowledge/shared-promote.d.ts.map +1 -0
- package/dist/agent/knowledge/shared-promote.js +29 -0
- package/dist/agent/knowledge/shared-promote.js.map +1 -0
- package/dist/agent/knowledge/shared-writer.d.ts +24 -0
- package/dist/agent/knowledge/shared-writer.d.ts.map +1 -0
- package/dist/agent/knowledge/shared-writer.js +97 -0
- package/dist/agent/knowledge/shared-writer.js.map +1 -0
- package/dist/agent/knowledge/types.d.ts +83 -0
- package/dist/agent/knowledge/types.d.ts.map +1 -0
- package/dist/agent/knowledge/types.js +12 -0
- package/dist/agent/knowledge/types.js.map +1 -0
- package/dist/agent/knowledge/wiki.d.ts +107 -0
- package/dist/agent/knowledge/wiki.d.ts.map +1 -0
- package/dist/agent/knowledge/wiki.js +415 -0
- package/dist/agent/knowledge/wiki.js.map +1 -0
- package/dist/agent/memory-metrics.d.ts +48 -0
- package/dist/agent/memory-metrics.d.ts.map +1 -0
- package/dist/agent/memory-metrics.js +156 -0
- package/dist/agent/memory-metrics.js.map +1 -0
- package/dist/agent/runner.d.ts +24 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +82 -6
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/skill-learning/config.d.ts +18 -0
- package/dist/agent/skill-learning/config.d.ts.map +1 -0
- package/dist/agent/skill-learning/config.js +71 -0
- package/dist/agent/skill-learning/config.js.map +1 -0
- package/dist/agent/skill-learning/curator.d.ts +45 -0
- package/dist/agent/skill-learning/curator.d.ts.map +1 -0
- package/dist/agent/skill-learning/curator.js +171 -0
- package/dist/agent/skill-learning/curator.js.map +1 -0
- package/dist/agent/skill-learning/index.d.ts +79 -0
- package/dist/agent/skill-learning/index.d.ts.map +1 -0
- package/dist/agent/skill-learning/index.js +308 -0
- package/dist/agent/skill-learning/index.js.map +1 -0
- package/dist/agent/skill-learning/metrics.d.ts +13 -0
- package/dist/agent/skill-learning/metrics.d.ts.map +1 -0
- package/dist/agent/skill-learning/metrics.js +109 -0
- package/dist/agent/skill-learning/metrics.js.map +1 -0
- package/dist/agent/skill-learning/notifier.d.ts +111 -0
- package/dist/agent/skill-learning/notifier.d.ts.map +1 -0
- package/dist/agent/skill-learning/notifier.js +398 -0
- package/dist/agent/skill-learning/notifier.js.map +1 -0
- package/dist/agent/skill-learning/reviewer.d.ts +61 -0
- package/dist/agent/skill-learning/reviewer.d.ts.map +1 -0
- package/dist/agent/skill-learning/reviewer.js +271 -0
- package/dist/agent/skill-learning/reviewer.js.map +1 -0
- package/dist/agent/skill-learning/telemetry.d.ts +22 -0
- package/dist/agent/skill-learning/telemetry.d.ts.map +1 -0
- package/dist/agent/skill-learning/telemetry.js +90 -0
- package/dist/agent/skill-learning/telemetry.js.map +1 -0
- package/dist/agent/skill-learning/trigger.d.ts +23 -0
- package/dist/agent/skill-learning/trigger.d.ts.map +1 -0
- package/dist/agent/skill-learning/trigger.js +44 -0
- package/dist/agent/skill-learning/trigger.js.map +1 -0
- package/dist/agent/skill-learning/types.d.ts +165 -0
- package/dist/agent/skill-learning/types.d.ts.map +1 -0
- package/dist/agent/skill-learning/types.js +11 -0
- package/dist/agent/skill-learning/types.js.map +1 -0
- package/dist/agent/skill-learning/writer.d.ts +37 -0
- package/dist/agent/skill-learning/writer.d.ts.map +1 -0
- package/dist/agent/skill-learning/writer.js +223 -0
- package/dist/agent/skill-learning/writer.js.map +1 -0
- package/dist/agent/workspace-loader.d.ts +83 -6
- package/dist/agent/workspace-loader.d.ts.map +1 -1
- package/dist/agent/workspace-loader.js +207 -14
- package/dist/agent/workspace-loader.js.map +1 -1
- package/dist/api/gateway-router.d.ts +2 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +293 -6
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/share-router.d.ts.map +1 -1
- package/dist/api/share-router.js +27 -1
- package/dist/api/share-router.js.map +1 -1
- package/dist/api/skills-router.d.ts +2 -2
- package/dist/api/skills-router.d.ts.map +1 -1
- package/dist/api/skills-router.js +61 -1
- package/dist/api/skills-router.js.map +1 -1
- package/dist/apps/agent-manager.d.ts +7 -0
- package/dist/apps/agent-manager.d.ts.map +1 -1
- package/dist/apps/agent-manager.js +13 -1
- package/dist/apps/agent-manager.js.map +1 -1
- package/dist/config/migrator.d.ts +2 -0
- package/dist/config/migrator.d.ts.map +1 -1
- package/dist/config/migrator.js +72 -1
- package/dist/config/migrator.js.map +1 -1
- package/dist/history/cleanup.d.ts +7 -0
- package/dist/history/cleanup.d.ts.map +1 -1
- package/dist/history/cleanup.js +27 -0
- package/dist/history/cleanup.js.map +1 -1
- package/dist/history/db.d.ts +65 -0
- package/dist/history/db.d.ts.map +1 -1
- package/dist/history/db.js +261 -0
- package/dist/history/db.js.map +1 -1
- package/dist/index.js +122 -20
- package/dist/index.js.map +1 -1
- package/dist/session/process.d.ts +10 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +86 -1
- package/dist/session/process.js.map +1 -1
- package/dist/share/share-store.d.ts +11 -0
- package/dist/share/share-store.d.ts.map +1 -1
- package/dist/share/share-store.js +3 -1
- package/dist/share/share-store.js.map +1 -1
- package/dist/skills/loader.d.ts +8 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +29 -1
- package/dist/skills/loader.js.map +1 -1
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/web-ui.d.ts.map +1 -1
- package/dist/ui/web-ui.js +1126 -3
- package/dist/ui/web-ui.js.map +1 -1
- package/mcp/package.json +2 -1
- package/mcp/server.ts +2 -0
- package/mcp/tools/image/module.ts +114 -9
- package/mcp/tools/memory/archive-reader.test.ts +212 -0
- package/mcp/tools/memory/archive-reader.ts +253 -0
- package/mcp/tools/memory/module.ts +152 -0
- package/mcp/tools/memory/retrieval-recorder.ts +66 -0
- package/mcp/tools/shared/share-client.ts +20 -1
- package/mcp/tools/skills/module.ts +28 -0
- package/package.json +1 -1
- package/resource/claude_gateway.svg +1038 -112
package/dist/ui/web-ui.js
CHANGED
|
@@ -78,7 +78,14 @@ function generateDashboardHtml() {
|
|
|
78
78
|
.badge-fable { background: #553052; color: #f687b3; }
|
|
79
79
|
.badge-model { background: #2d3748; color: #cbd5e0; }
|
|
80
80
|
.ts { color: #718096; font-size: 0.8rem; }
|
|
81
|
-
|
|
81
|
+
/* Top-right cluster: the Logout button sits beside the auto-refresh status. */
|
|
82
|
+
#top-right { float: right; display: inline-flex; align-items: center; gap: 10px; }
|
|
83
|
+
#refresh-indicator { font-size: 0.75rem; color: #4a5568; }
|
|
84
|
+
#logout-btn {
|
|
85
|
+
font-size: 0.7rem; background: #2d3748; color: #a0aec0; border: 1px solid #4a5568;
|
|
86
|
+
border-radius: 4px; padding: 2px 8px; cursor: pointer;
|
|
87
|
+
}
|
|
88
|
+
#logout-btn:hover { background: #374151; }
|
|
82
89
|
.error { color: #fc8181; font-size: 0.85rem; margin-top: 8px; }
|
|
83
90
|
.btn-stream {
|
|
84
91
|
background: #44337a;
|
|
@@ -217,20 +224,202 @@ function generateDashboardHtml() {
|
|
|
217
224
|
h1 { font-size: 1.2rem; }
|
|
218
225
|
h2 { font-size: 1rem; }
|
|
219
226
|
.meta { font-size: 0.78rem; }
|
|
220
|
-
#
|
|
227
|
+
#top-right { float: none; display: flex; margin-top: 4px; }
|
|
221
228
|
.proc-tree { font-size: 0.72rem; padding: 10px 12px; }
|
|
222
229
|
/* On phones allow horizontal pan only — no vertical clipping. */
|
|
223
230
|
}
|
|
231
|
+
|
|
232
|
+
/* --- Tabs (Sessions | Knowledge base) --- */
|
|
233
|
+
.tabs { display: flex; gap: 4px; margin: 14px 0 18px; border-bottom: 1px solid #2d3748; }
|
|
234
|
+
.tab {
|
|
235
|
+
background: none; border: none; border-bottom: 2px solid transparent;
|
|
236
|
+
color: #a0aec0; font-size: 0.9rem; font-weight: 600; padding: 8px 16px;
|
|
237
|
+
cursor: pointer; margin-bottom: -1px;
|
|
238
|
+
}
|
|
239
|
+
.tab:hover { color: #e2e8f0; }
|
|
240
|
+
.tab.active { color: #63b3ed; border-bottom-color: #63b3ed; }
|
|
241
|
+
|
|
242
|
+
/* --- Knowledge Base graph view --- */
|
|
243
|
+
.kb-toolbar {
|
|
244
|
+
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
245
|
+
margin-bottom: 10px; font-size: 0.8rem; color: #a0aec0;
|
|
246
|
+
}
|
|
247
|
+
.kb-toolbar button {
|
|
248
|
+
background: #2d3748; color: #e2e8f0; border: 1px solid #4a5568;
|
|
249
|
+
border-radius: 4px; padding: 4px 10px; cursor: pointer; font-size: 0.8rem;
|
|
250
|
+
}
|
|
251
|
+
.kb-toolbar button:hover { background: #374151; }
|
|
252
|
+
.kb-toolbar label { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
|
|
253
|
+
#kb-demo-size, #kb-source, #dreams-agent {
|
|
254
|
+
background: #1a2333; color: #cbd5e0; border: 1px solid #2d3748;
|
|
255
|
+
border-radius: 4px; padding: 2px 4px; font-size: 0.78rem; cursor: pointer;
|
|
256
|
+
}
|
|
257
|
+
.kb-demo-badge {
|
|
258
|
+
background: #744210; color: #fbd38d; border: 1px solid #975a16;
|
|
259
|
+
border-radius: 4px; padding: 2px 8px; font-size: 0.72rem; font-weight: 600;
|
|
260
|
+
}
|
|
261
|
+
.kb-legend { display: inline-flex; gap: 10px; flex-wrap: wrap; }
|
|
262
|
+
.kb-legend span { display: inline-flex; align-items: center; gap: 4px; }
|
|
263
|
+
.kb-legend i { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
|
264
|
+
.kb-stage {
|
|
265
|
+
position: relative; width: 100%; height: 620px; overflow: hidden;
|
|
266
|
+
border: 1px solid #2d3748; border-radius: 8px;
|
|
267
|
+
/* Deep-space radial backdrop makes the node glow read as light. */
|
|
268
|
+
background: radial-gradient(ellipse 70% 62% at 50% 42%, #16203a 0%, #0d1322 55%, #070a12 100%);
|
|
269
|
+
}
|
|
270
|
+
/* The graph is a 3D sphere rendered on <canvas> (auto-spins; drag rotates the view;
|
|
271
|
+
always centred by projection). Node/edge/label styling lives in the canvas paint
|
|
272
|
+
code, not CSS. */
|
|
273
|
+
#kb-canvas { width: 100%; height: 100%; display: block; cursor: grab; touch-action: none; }
|
|
274
|
+
#kb-canvas.drag { cursor: grabbing; }
|
|
275
|
+
/* Zoom controls, pinned bottom-right of the graph stage. */
|
|
276
|
+
.kb-zoom {
|
|
277
|
+
position: absolute; right: 12px; bottom: 12px; display: flex; flex-direction: column;
|
|
278
|
+
gap: 6px; z-index: 3;
|
|
279
|
+
}
|
|
280
|
+
.kb-zoom button {
|
|
281
|
+
width: 32px; height: 32px; padding: 0; font-size: 1.15rem; line-height: 1;
|
|
282
|
+
display: flex; align-items: center; justify-content: center;
|
|
283
|
+
background: rgba(26, 35, 51, 0.82); color: #e2e8f0; border: 1px solid #2d3748;
|
|
284
|
+
border-radius: 6px; cursor: pointer; backdrop-filter: blur(3px); user-select: none;
|
|
285
|
+
}
|
|
286
|
+
.kb-zoom button:hover { background: rgba(45, 55, 72, 0.92); border-color: #4a5568; }
|
|
287
|
+
.kb-zoom button:active { background: #374151; }
|
|
288
|
+
.kb-search-wrap { position: relative; display: inline-flex; align-items: center; gap: 6px; }
|
|
289
|
+
#kb-search {
|
|
290
|
+
background: #0d1117; color: #e2e8f0; border: 1px solid #4a5568; border-radius: 4px;
|
|
291
|
+
padding: 3px 8px; font-size: 0.8rem; width: 160px; outline: none;
|
|
292
|
+
}
|
|
293
|
+
#kb-search:focus { border-color: #63b3ed; }
|
|
294
|
+
.kb-search-count { color: #718096; font-size: 0.75rem; min-width: 42px; }
|
|
295
|
+
.kb-empty {
|
|
296
|
+
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
297
|
+
color: #718096; font-size: 0.9rem; text-align: center; padding: 20px; pointer-events: none;
|
|
298
|
+
}
|
|
299
|
+
/* --- Nightly dreaming report --- */
|
|
300
|
+
.dreams-list { display: flex; flex-direction: column; gap: 12px; padding: 4px 2px 20px; }
|
|
301
|
+
.dreams-empty { color: #718096; font-size: 0.9rem; text-align: center; padding: 40px 20px; }
|
|
302
|
+
.dream-run {
|
|
303
|
+
background: #131a2b; border: 1px solid #222c40; border-radius: 8px; padding: 12px 14px;
|
|
304
|
+
}
|
|
305
|
+
.dream-run-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
|
|
306
|
+
.dream-run-head .agent { color: #63b3ed; font-weight: 600; }
|
|
307
|
+
.dream-run-head .when { color: #718096; font-size: 0.78rem; font-family: monospace; }
|
|
308
|
+
.dream-badge { border-radius: 4px; padding: 1px 7px; font-size: 0.7rem; font-weight: 600; }
|
|
309
|
+
.dream-badge.auto { background: #22543d; color: #9ae6b4; border: 1px solid #2f855a; }
|
|
310
|
+
.dream-badge.propose { background: #2a4365; color: #90cdf4; border: 1px solid #2b6cb0; }
|
|
311
|
+
.dream-badge.outcome { background: #2d3748; color: #cbd5e0; border: 1px solid #4a5568; }
|
|
312
|
+
.dream-summary { color: #cbd5e0; font-size: 0.86rem; margin: 4px 0 8px; }
|
|
313
|
+
.dream-meta { color: #718096; font-size: 0.75rem; margin-top: 8px; }
|
|
314
|
+
.dream-props { display: flex; flex-direction: column; gap: 6px; }
|
|
315
|
+
.dream-prop {
|
|
316
|
+
background: #0e1420; border: 1px solid #1e2740; border-radius: 6px; padding: 7px 9px; font-size: 0.82rem;
|
|
317
|
+
}
|
|
318
|
+
.dream-prop .op { font-weight: 600; text-transform: uppercase; font-size: 0.7rem; margin-right: 6px; }
|
|
319
|
+
.dream-prop .op.add { color: #68d391; } .dream-prop .op.replace { color: #f6ad55; } .dream-prop .op.remove { color: #fc8181; }
|
|
320
|
+
.dream-prop .file { color: #90cdf4; font-family: monospace; font-size: 0.76rem; }
|
|
321
|
+
.dream-prop .score { color: #718096; font-size: 0.72rem; float: right; }
|
|
322
|
+
.dream-prop .reason { color: #a0aec0; margin-top: 3px; }
|
|
323
|
+
.dream-prop .content { color: #cbd5e0; margin-top: 5px; white-space: pre-wrap; word-break: break-word;
|
|
324
|
+
background: #070a12; border-radius: 4px; padding: 6px 8px; font-size: 0.78rem; max-height: 140px; overflow: auto; }
|
|
325
|
+
.dream-prop .anchor { color: #f6ad55; font-family: monospace; font-size: 0.72rem; margin-top: 3px; }
|
|
326
|
+
.dream-prop.accepted { border-color: #2f855a; }
|
|
327
|
+
.dream-prop-actions { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
|
|
328
|
+
.dream-accept-btn { background: #22543d; color: #9ae6b4; border: 1px solid #2f855a; border-radius: 4px;
|
|
329
|
+
padding: 2px 10px; font-size: 0.74rem; font-weight: 600; cursor: pointer; }
|
|
330
|
+
.dream-accept-btn:hover { background: #276749; }
|
|
331
|
+
.dream-accept-btn:disabled { opacity: 0.55; cursor: default; }
|
|
332
|
+
.prop-status { font-size: 0.72rem; font-weight: 600; }
|
|
333
|
+
.prop-status.applied { color: #9ae6b4; }
|
|
334
|
+
.prop-status.applied-auto { color: #68d391; }
|
|
335
|
+
.prop-status.pending { color: #90cdf4; }
|
|
336
|
+
.prop-status.failed { color: #fc8181; }
|
|
337
|
+
.dream-accept-all { background: #22543d; color: #9ae6b4; border: 1px solid #2f855a; border-radius: 4px;
|
|
338
|
+
padding: 1px 9px; font-size: 0.7rem; font-weight: 600; cursor: pointer; }
|
|
339
|
+
.dream-accept-all:hover { background: #276749; }
|
|
340
|
+
.dream-accept-all:disabled { opacity: 0.55; cursor: default; }
|
|
341
|
+
/* Note detail — full-width card below the graph. Shows the whole file. */
|
|
342
|
+
.kb-note {
|
|
343
|
+
margin-top: 12px; background: #131a2b; border: 1px solid #2d3748; border-radius: 8px;
|
|
344
|
+
padding: 16px 18px; font-size: 0.85rem; color: #cbd5e0;
|
|
345
|
+
}
|
|
346
|
+
.kb-note-head { display: flex; align-items: flex-start; gap: 10px; }
|
|
347
|
+
.kb-note-head h3 { margin: 0; font-size: 1.02rem; color: #e2e8f0; flex: 1; word-break: break-word; }
|
|
348
|
+
.kb-note-close {
|
|
349
|
+
cursor: pointer; color: #718096; background: #1a202c; border: 1px solid #2d3748;
|
|
350
|
+
border-radius: 4px; padding: 1px 8px; font-size: 0.9rem; line-height: 1.4;
|
|
351
|
+
}
|
|
352
|
+
.kb-note-close:hover { color: #e2e8f0; background: #2d3748; }
|
|
353
|
+
/* Metadata chips row. */
|
|
354
|
+
.kb-note-meta { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 4px; }
|
|
355
|
+
.kb-note-chip {
|
|
356
|
+
display: inline-flex; align-items: center; gap: 5px; background: #0e1420;
|
|
357
|
+
border: 1px solid #1e2740; border-radius: 999px; padding: 2px 10px; font-size: 0.72rem; color: #a0aec0;
|
|
358
|
+
}
|
|
359
|
+
.kb-note-chip b { color: #cbd5e0; font-weight: 600; }
|
|
360
|
+
.kb-note-chip i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
361
|
+
/* File path line under the chips — full width, monospace, wraps on long paths. */
|
|
362
|
+
.kb-note-path {
|
|
363
|
+
margin: 6px 0 2px; font-size: 0.72rem; font-family: ui-monospace, monospace;
|
|
364
|
+
color: #718096; word-break: break-all;
|
|
365
|
+
}
|
|
366
|
+
.kb-note-path b { color: #a0aec0; font-weight: 600; }
|
|
367
|
+
/* Rendered Markdown body. */
|
|
368
|
+
.kb-md {
|
|
369
|
+
margin-top: 12px; padding-top: 12px; border-top: 1px solid #222c40;
|
|
370
|
+
line-height: 1.62; color: #cbd5e0; word-break: break-word;
|
|
371
|
+
}
|
|
372
|
+
.kb-md > :first-child { margin-top: 0; }
|
|
373
|
+
.kb-md h1, .kb-md h2, .kb-md h3, .kb-md h4 { color: #e2e8f0; line-height: 1.3; margin: 18px 0 8px; }
|
|
374
|
+
.kb-md h1 { font-size: 1.25rem; border-bottom: 1px solid #222c40; padding-bottom: 5px; }
|
|
375
|
+
.kb-md h2 { font-size: 1.1rem; border-bottom: 1px solid #1e2740; padding-bottom: 4px; }
|
|
376
|
+
.kb-md h3 { font-size: 1rem; } .kb-md h4 { font-size: 0.9rem; color: #a0aec0; }
|
|
377
|
+
.kb-md p { margin: 8px 0; }
|
|
378
|
+
.kb-md ul, .kb-md ol { margin: 8px 0; padding-left: 22px; }
|
|
379
|
+
.kb-md li { margin: 3px 0; }
|
|
380
|
+
.kb-md a { color: #63b3ed; text-decoration: none; } .kb-md a:hover { text-decoration: underline; }
|
|
381
|
+
.kb-md code {
|
|
382
|
+
background: #0d1117; border: 1px solid #1e2740; border-radius: 4px;
|
|
383
|
+
padding: 1px 5px; font-size: 0.82em; font-family: ui-monospace, monospace; color: #e2e8f0;
|
|
384
|
+
}
|
|
385
|
+
.kb-md pre {
|
|
386
|
+
background: #0a0e17; border: 1px solid #1e2740; border-radius: 6px; padding: 10px 12px;
|
|
387
|
+
overflow-x: auto; margin: 10px 0;
|
|
388
|
+
}
|
|
389
|
+
.kb-md pre code { background: none; border: none; padding: 0; font-size: 0.8rem; line-height: 1.5; }
|
|
390
|
+
.kb-md blockquote {
|
|
391
|
+
margin: 10px 0; padding: 2px 14px; border-left: 3px solid #2d3748; color: #a0aec0;
|
|
392
|
+
}
|
|
393
|
+
.kb-md hr { border: none; border-top: 1px solid #222c40; margin: 14px 0; }
|
|
394
|
+
.kb-md strong { color: #e2e8f0; } .kb-md table { border-collapse: collapse; margin: 0; width: 100%; font-size: 0.82rem; }
|
|
395
|
+
.kb-md th, .kb-md td { border: 1px solid #222c40; padding: 5px 10px; text-align: left; }
|
|
396
|
+
.kb-md th { background: #0e1420; color: #e2e8f0; font-weight: 600; }
|
|
397
|
+
.kb-md tbody tr:nth-child(even) { background: #0e1420; }
|
|
398
|
+
/* Wide tables scroll horizontally instead of breaking the panel. */
|
|
399
|
+
.kb-table-wrap { overflow-x: auto; margin: 10px 0; border-radius: 6px; }
|
|
400
|
+
.kb-md del { color: #718096; }
|
|
401
|
+
/* Task-list items: bullet replaced by a real (disabled) checkbox. */
|
|
402
|
+
.kb-md li.kb-task { list-style: none; margin-left: -18px; }
|
|
403
|
+
.kb-md li.kb-task input { margin-right: 7px; vertical-align: middle; accent-color: #63b3ed; }
|
|
404
|
+
.kb-note-loading { color: #718096; font-size: 0.82rem; margin-top: 12px; }
|
|
224
405
|
</style>
|
|
225
406
|
</head>
|
|
226
407
|
<body>
|
|
227
|
-
<h1><span class="rainbow">Claude Gateway</span> <span id="gateway-version" style="font-size:0.75rem;color:#718096;"></span> <span id="
|
|
408
|
+
<h1><span class="rainbow">Claude Gateway</span> <span id="gateway-version" style="font-size:0.75rem;color:#718096;"></span> <span id="top-right"><button id="logout-btn">Logout</button><span id="refresh-indicator">refreshing...</span></span></h1>
|
|
228
409
|
<div class="meta">
|
|
229
410
|
Uptime: <span id="uptime">—</span> |
|
|
230
411
|
Started: <span id="started-at">—</span> |
|
|
231
412
|
Last updated: <span id="last-updated">—</span>
|
|
232
413
|
</div>
|
|
233
414
|
|
|
415
|
+
<!-- Tab switcher: Sessions (live PTY + table) | Knowledge base (shared-KB graph) -->
|
|
416
|
+
<div class="tabs">
|
|
417
|
+
<button class="tab active" id="tab-sessions" data-view="view-sessions">Sessions</button>
|
|
418
|
+
<button class="tab" id="tab-kb" data-view="view-kb">Knowledge base</button>
|
|
419
|
+
<button class="tab" id="tab-dreams" data-view="view-dreams">Nightly dreaming</button>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div id="view-sessions" class="view">
|
|
234
423
|
<!-- Row: Processes 70% | Agent badges 30% (collapses on narrow screens) -->
|
|
235
424
|
<div class="top-grid">
|
|
236
425
|
<div>
|
|
@@ -284,6 +473,61 @@ function generateDashboardHtml() {
|
|
|
284
473
|
</tbody>
|
|
285
474
|
</table>
|
|
286
475
|
</div>
|
|
476
|
+
</div><!-- /view-sessions -->
|
|
477
|
+
|
|
478
|
+
<!-- Knowledge base — a 3D force-directed graph over the shared vault / an agent's memory.
|
|
479
|
+
Zero external deps: the spherical layout + canvas render + rotation are hand-rolled. -->
|
|
480
|
+
<div id="view-kb" class="view" style="display:none;">
|
|
481
|
+
<div class="kb-toolbar">
|
|
482
|
+
<label id="kb-source-wrap" title="Which memory graph to view: the cross-agent Shared KB, or one agent's own memory">Source
|
|
483
|
+
<select id="kb-source"><option value="shared">Shared Knowledge Base</option></select>
|
|
484
|
+
</label>
|
|
485
|
+
<button id="kb-refresh" title="Recompute the graph from the selected source">↻ Refresh</button>
|
|
486
|
+
<span class="kb-search-wrap">
|
|
487
|
+
<input id="kb-search" type="search" placeholder="Search notes…" autocomplete="off" spellcheck="false">
|
|
488
|
+
<span id="kb-search-count" class="kb-search-count"></span>
|
|
489
|
+
</span>
|
|
490
|
+
<label><input type="checkbox" id="kb-demo-toggle" checked> Show demo data when vault is empty</label>
|
|
491
|
+
<label id="kb-demo-size-wrap" title="Synthetic node count for the demo (stress-test the viewer at scale)">Demo size
|
|
492
|
+
<select id="kb-demo-size">
|
|
493
|
+
<option value="0">8 (sample)</option>
|
|
494
|
+
<option value="100">100</option>
|
|
495
|
+
<option value="300" selected>300</option>
|
|
496
|
+
<option value="600">600</option>
|
|
497
|
+
</select>
|
|
498
|
+
</label>
|
|
499
|
+
<span id="kb-demo-badge" class="kb-demo-badge" style="display:none;">DEMO DATA</span>
|
|
500
|
+
<span id="kb-stats"></span>
|
|
501
|
+
<span class="kb-legend" id="kb-legend"></span>
|
|
502
|
+
</div>
|
|
503
|
+
<div class="kb-stage">
|
|
504
|
+
<!-- 3D knowledge graph: a sphere of notes rendered on canvas. Auto-spins; drag
|
|
505
|
+
empty space to rotate the view; wheel to zoom. Centred by projection, so there
|
|
506
|
+
is no pan and no "center" button — the graph can never drift off-screen. -->
|
|
507
|
+
<canvas id="kb-canvas"></canvas>
|
|
508
|
+
<div id="kb-empty" class="kb-empty" style="display:none;"></div>
|
|
509
|
+
<div class="kb-zoom">
|
|
510
|
+
<button id="kb-zoom-in" type="button" title="Zoom in" aria-label="Zoom in">+</button>
|
|
511
|
+
<button id="kb-zoom-out" type="button" title="Zoom out" aria-label="Zoom out">−</button>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
<!-- Note detail — full width BELOW the graph so the whole file shows (no
|
|
515
|
+
truncation), rendered as formatted Markdown. Populated on node click. -->
|
|
516
|
+
<div id="kb-note" class="kb-note" style="display:none;"></div>
|
|
517
|
+
</div><!-- /view-kb -->
|
|
518
|
+
|
|
519
|
+
<!-- Nightly dreaming — the memory-consolidation audit trail (.dreaming/) per agent. -->
|
|
520
|
+
<div id="view-dreams" class="view" style="display:none;">
|
|
521
|
+
<div class="kb-toolbar">
|
|
522
|
+
<button id="dreams-refresh" title="Reload the dreaming audit trail">↻ Refresh</button>
|
|
523
|
+
<label id="dreams-agent-wrap">Agent
|
|
524
|
+
<select id="dreams-agent"><option value="">All agents</option></select>
|
|
525
|
+
</label>
|
|
526
|
+
<span id="dreams-stats"></span>
|
|
527
|
+
</div>
|
|
528
|
+
<div id="dreams-list" class="dreams-list"></div>
|
|
529
|
+
<div id="dreams-empty" class="dreams-empty" style="display:none;"></div>
|
|
530
|
+
</div><!-- /view-dreams -->
|
|
287
531
|
|
|
288
532
|
<div id="error-msg" class="error" style="display:none;"></div>
|
|
289
533
|
|
|
@@ -905,6 +1149,885 @@ function generateDashboardHtml() {
|
|
|
905
1149
|
// Process tree (with CPU/mem) is heavier (spawns ps) — refresh a bit slower.
|
|
906
1150
|
setInterval(refreshProcesses, 6000);
|
|
907
1151
|
</script>
|
|
1152
|
+
|
|
1153
|
+
<!-- Knowledge base graph: tab switching + hand-rolled force-directed renderer.
|
|
1154
|
+
No template literals here (this file is itself a template literal). -->
|
|
1155
|
+
<script id="kb-graph">
|
|
1156
|
+
(function(){
|
|
1157
|
+
// ---------- Tab switching ----------
|
|
1158
|
+
var tabs = document.querySelectorAll('.tab');
|
|
1159
|
+
var kbLoaded = false;
|
|
1160
|
+
tabs.forEach(function(tab){
|
|
1161
|
+
tab.addEventListener('click', function(){
|
|
1162
|
+
tabs.forEach(function(t){ t.classList.remove('active'); });
|
|
1163
|
+
tab.classList.add('active');
|
|
1164
|
+
document.querySelectorAll('.view').forEach(function(v){ v.style.display = 'none'; });
|
|
1165
|
+
var viewEl = document.getElementById(tab.getAttribute('data-view'));
|
|
1166
|
+
if (viewEl) viewEl.style.display = '';
|
|
1167
|
+
if (tab.getAttribute('data-view') === 'view-kb' && !kbLoaded) { kbLoaded = true; loadSources(); }
|
|
1168
|
+
if (tab.getAttribute('data-view') === 'view-dreams' && window.__loadDreams) { window.__loadDreams(); }
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// ---------- Graph elements + refs ----------
|
|
1173
|
+
var canvas = document.getElementById('kb-canvas');
|
|
1174
|
+
var elEmpty = document.getElementById('kb-empty');
|
|
1175
|
+
var elNote = document.getElementById('kb-note');
|
|
1176
|
+
var elStats = document.getElementById('kb-stats');
|
|
1177
|
+
var elLegend = document.getElementById('kb-legend');
|
|
1178
|
+
var elDemoBadge = document.getElementById('kb-demo-badge');
|
|
1179
|
+
var sourceSel = document.getElementById('kb-source');
|
|
1180
|
+
var demoToggle = document.getElementById('kb-demo-toggle');
|
|
1181
|
+
var demoToggleWrap = demoToggle ? demoToggle.closest('label') : null;
|
|
1182
|
+
var demoSize = document.getElementById('kb-demo-size');
|
|
1183
|
+
var demoSizeWrap = document.getElementById('kb-demo-size-wrap');
|
|
1184
|
+
var elSearch = document.getElementById('kb-search');
|
|
1185
|
+
var elSearchCount = document.getElementById('kb-search-count');
|
|
1186
|
+
|
|
1187
|
+
// Curated colours for the memory type: vocabulary that ACTUALLY appears in
|
|
1188
|
+
// notes (user / feedback / project / reference / …) — the old map keyed on a
|
|
1189
|
+
// KB ontology (decision/evidence/claim…) that never matched, so every node
|
|
1190
|
+
// fell to one grey. Any type not listed gets a stable, distinct hue from a
|
|
1191
|
+
// hash so tags never collapse to a single colour again.
|
|
1192
|
+
var TYPE_COLORS = {
|
|
1193
|
+
user: '#63b3ed', feedback: '#f6ad55', project: '#68d391',
|
|
1194
|
+
reference: '#b794f4', session: '#4fd1c5', fact: '#f687b3',
|
|
1195
|
+
decision: '#63b3ed', evidence: '#68d391', claim: '#f6ad55',
|
|
1196
|
+
policy: '#b794f4', infra: '#4fd1c5'
|
|
1197
|
+
};
|
|
1198
|
+
var DEFAULT_COLOR = '#7d8fb3';
|
|
1199
|
+
function hashHue(s){ var h = 0; for (var i = 0; i < s.length; i++){ h = (h * 31 + s.charCodeAt(i)) & 0xffffffff; } return Math.abs(h) % 360; }
|
|
1200
|
+
function colorFor(type){
|
|
1201
|
+
if (!type || type === 'other') return DEFAULT_COLOR;
|
|
1202
|
+
if (TYPE_COLORS[type]) return TYPE_COLORS[type];
|
|
1203
|
+
// Deterministic distinct colour for any unlisted type (mid tone, readable on the dark stage).
|
|
1204
|
+
return 'hsl(' + hashHue(type) + ', 62%, 62%)';
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// ---------- 3D graph engine (canvas) ----------
|
|
1208
|
+
// Notes are laid out on the surface of a unit sphere (Fibonacci seed + a short 3D
|
|
1209
|
+
// force relax) centred at the origin. Every frame the whole sphere is rotated by
|
|
1210
|
+
// (rotY, rotX) and perspective-projected to the canvas centre — so the graph is
|
|
1211
|
+
// ALWAYS centred by construction: no pan, no "center" button, it cannot drift off
|
|
1212
|
+
// screen. It auto-spins; dragging empty space rotates the view. This 3D rotation is
|
|
1213
|
+
// the "หมุน" a flat 2D SVG (which could only pan) was never able to do.
|
|
1214
|
+
var LABEL_MIN_DEG = 3; // hubs keep their label; others reveal on hover/search
|
|
1215
|
+
var G = null; // { nodes:[{nd,x,y,z,deg,i,hit}], edges:[{a,b}] }
|
|
1216
|
+
var rotY = 0.6, rotX = -0.25, zoom = 1;
|
|
1217
|
+
var dragging = false, moved = false, downX = 0, downY = 0, lastX = 0, lastY = 0;
|
|
1218
|
+
// Angular momentum: the last drag speed carries the sphere on after release so it
|
|
1219
|
+
// keeps orbiting (decays toward the gentle baseline auto-spin) instead of freezing.
|
|
1220
|
+
var velY = 0, velX = 0;
|
|
1221
|
+
var MIN_ZOOM = 0.25, MAX_ZOOM = 8, SPIN = 0.0025; // zoom clamp + baseline orbit speed
|
|
1222
|
+
var hover = null, selected = null, auto = true;
|
|
1223
|
+
var cvW = 0, cvH = 0, cvDpr = 1, needRender = true, lastFrame = 0;
|
|
1224
|
+
var P = []; // last projected screen coords, for hit-testing
|
|
1225
|
+
|
|
1226
|
+
function invalidate(){ needRender = true; }
|
|
1227
|
+
function sizeCanvas(){
|
|
1228
|
+
if (!canvas) return;
|
|
1229
|
+
cvDpr = window.devicePixelRatio || 1;
|
|
1230
|
+
cvW = canvas.clientWidth; cvH = canvas.clientHeight;
|
|
1231
|
+
canvas.width = cvW * cvDpr; canvas.height = cvH * cvDpr;
|
|
1232
|
+
invalidate();
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ---------- Build model from API payload ----------
|
|
1236
|
+
// Seed nodes on a Fibonacci sphere, then relax with a 3D repulsion + spring pass so
|
|
1237
|
+
// linked notes pull together into clusters while the whole set stays on the sphere.
|
|
1238
|
+
function buildModel(data){
|
|
1239
|
+
hideNote(); hover = null; selected = null;
|
|
1240
|
+
var raw = (data && data.nodes) || [];
|
|
1241
|
+
var rawEdges = (data && data.edges) || [];
|
|
1242
|
+
var N = raw.length || 1;
|
|
1243
|
+
var byId = {};
|
|
1244
|
+
var nodes = raw.map(function(nd, i){
|
|
1245
|
+
var t = Math.acos(1 - 2 * (i + 0.5) / N), p = Math.PI * (1 + Math.sqrt(5)) * i;
|
|
1246
|
+
return {
|
|
1247
|
+
nd: {
|
|
1248
|
+
id: nd.id, title: nd.title || nd.id, type: nd.type || null,
|
|
1249
|
+
degree: nd.degree || 0,
|
|
1250
|
+
confidence: (nd.confidence === undefined ? null : nd.confidence),
|
|
1251
|
+
updatedAt: nd.updatedAt || null, stale: !!nd.stale,
|
|
1252
|
+
contradiction: !!nd.contradiction, excerpt: nd.excerpt || null
|
|
1253
|
+
},
|
|
1254
|
+
x: Math.sin(t) * Math.cos(p), y: Math.sin(t) * Math.sin(p), z: Math.cos(t),
|
|
1255
|
+
deg: 0, i: i, hit: true
|
|
1256
|
+
};
|
|
1257
|
+
});
|
|
1258
|
+
nodes.forEach(function(o){ byId[o.nd.id] = o; });
|
|
1259
|
+
var edges = [];
|
|
1260
|
+
rawEdges.forEach(function(e){
|
|
1261
|
+
var a = byId[e.source], b = byId[e.target];
|
|
1262
|
+
if (a && b){ edges.push({ a: a, b: b }); a.deg++; b.deg++; }
|
|
1263
|
+
});
|
|
1264
|
+
relax(nodes, edges);
|
|
1265
|
+
G = { nodes: nodes, edges: edges };
|
|
1266
|
+
rotY = 0.6; rotX = -0.25; zoom = 1; auto = true; velY = 0; velX = 0;
|
|
1267
|
+
sizeCanvas(); invalidate();
|
|
1268
|
+
// Re-apply an active search to the freshly-built set (refresh / source switch).
|
|
1269
|
+
if (elSearch && elSearch.value.trim()) runSearch(); else clearSearch();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Short 3D force relax: all-pairs repulsion keeps nodes apart, edge springs pull
|
|
1273
|
+
// linked notes together, then recentre at the origin and renormalise to the unit
|
|
1274
|
+
// sphere so projection stays centred. Iterations scale down for large demo sets.
|
|
1275
|
+
function relax(nodes, edges){
|
|
1276
|
+
var N = nodes.length || 1;
|
|
1277
|
+
var iters = N > 400 ? 60 : 110;
|
|
1278
|
+
for (var it = 0; it < iters; it++){
|
|
1279
|
+
for (var i = 0; i < nodes.length; i++){
|
|
1280
|
+
for (var k = i + 1; k < nodes.length; k++){
|
|
1281
|
+
var dx = nodes[i].x - nodes[k].x, dy = nodes[i].y - nodes[k].y, dz = nodes[i].z - nodes[k].z;
|
|
1282
|
+
var d2 = dx*dx + dy*dy + dz*dz + 0.01, f = 0.018 / d2, inv = 1 / Math.sqrt(d2);
|
|
1283
|
+
dx *= inv; dy *= inv; dz *= inv;
|
|
1284
|
+
nodes[i].x += dx*f; nodes[i].y += dy*f; nodes[i].z += dz*f;
|
|
1285
|
+
nodes[k].x -= dx*f; nodes[k].y -= dy*f; nodes[k].z -= dz*f;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
for (var e = 0; e < edges.length; e++){
|
|
1289
|
+
var A = edges[e].a, B = edges[e].b;
|
|
1290
|
+
var ex = B.x - A.x, ey = B.y - A.y, ez = B.z - A.z, sf = 0.012;
|
|
1291
|
+
A.x += ex*sf; A.y += ey*sf; A.z += ez*sf; B.x -= ex*sf; B.y -= ey*sf; B.z -= ez*sf;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
var cx = 0, cy = 0, cz = 0;
|
|
1295
|
+
nodes.forEach(function(n){ cx += n.x; cy += n.y; cz += n.z; });
|
|
1296
|
+
cx /= N; cy /= N; cz /= N;
|
|
1297
|
+
var R = 0;
|
|
1298
|
+
nodes.forEach(function(n){ n.x -= cx; n.y -= cy; n.z -= cz; R = Math.max(R, Math.hypot(n.x, n.y, n.z)); });
|
|
1299
|
+
R = R || 1;
|
|
1300
|
+
nodes.forEach(function(n){ n.x /= R; n.y /= R; n.z /= R; });
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// ---------- Render ----------
|
|
1304
|
+
// Rotate each node by (rotY around the vertical axis, rotX around the horizontal),
|
|
1305
|
+
// perspective-divide, and paint. Depth (post-rotation z) drives alpha and paint
|
|
1306
|
+
// order so the near hemisphere reads brighter and overpaints the far one.
|
|
1307
|
+
function render(){
|
|
1308
|
+
if (!canvas || !G || !cvW) return;
|
|
1309
|
+
var ctx = canvas.getContext('2d');
|
|
1310
|
+
ctx.setTransform(cvDpr, 0, 0, cvDpr, 0, 0);
|
|
1311
|
+
ctx.clearRect(0, 0, cvW, cvH);
|
|
1312
|
+
var nodes = G.nodes;
|
|
1313
|
+
if (!nodes.length){ P = []; return; }
|
|
1314
|
+
var cyw = Math.cos(rotY), syw = Math.sin(rotY), cxp = Math.cos(rotX), sxp = Math.sin(rotX);
|
|
1315
|
+
var scale = Math.min(cvW, cvH) * 0.34 * zoom, ox = cvW / 2, oy = cvH / 2, cam = 3;
|
|
1316
|
+
var searching = !!(elSearch && elSearch.value.trim());
|
|
1317
|
+
P = nodes.map(function(o){
|
|
1318
|
+
var x = o.x * cyw - o.z * syw, z = o.x * syw + o.z * cyw;
|
|
1319
|
+
var y = o.y * cxp - z * sxp; z = o.y * sxp + z * cxp;
|
|
1320
|
+
var q = cam / (cam - z);
|
|
1321
|
+
return { sx: ox + x * scale * q, sy: oy + y * scale * q, z: z,
|
|
1322
|
+
r: Math.max(2.5, (4 + Math.sqrt(o.deg) * 2.2) * q), o: o };
|
|
1323
|
+
});
|
|
1324
|
+
// Edges first, depth-shaded (far side dimmer); dimmed hard when searching unless
|
|
1325
|
+
// both ends match the query, so the hit set reads as a connected subgraph.
|
|
1326
|
+
ctx.lineWidth = 1;
|
|
1327
|
+
G.edges.forEach(function(ed){
|
|
1328
|
+
var a = P[ed.a.i], b = P[ed.b.i];
|
|
1329
|
+
var depth = ((a.z + b.z) / 2 + 1) / 2;
|
|
1330
|
+
var lit = !searching || (ed.a.hit && ed.b.hit);
|
|
1331
|
+
ctx.strokeStyle = 'rgba(120,150,210,' + (lit ? (0.10 + 0.32 * depth) : 0.03) + ')';
|
|
1332
|
+
ctx.beginPath(); ctx.moveTo(a.sx, a.sy); ctx.lineTo(b.sx, b.sy); ctx.stroke();
|
|
1333
|
+
});
|
|
1334
|
+
// Nodes back-to-front so near nodes overpaint far ones.
|
|
1335
|
+
var order = P.map(function(_, i){ return i; }).sort(function(i, k){ return P[i].z - P[k].z; });
|
|
1336
|
+
order.forEach(function(i){
|
|
1337
|
+
var p = P[i], o = p.o, nd = o.nd, near = (p.z + 1) / 2;
|
|
1338
|
+
var isHov = hover === i, isSel = selected === i;
|
|
1339
|
+
var dim = searching && !o.hit;
|
|
1340
|
+
var alpha = dim ? 0.08 : (0.4 + 0.6 * near);
|
|
1341
|
+
var col = colorFor(nd.type); // keep the node's own type colour, even when hovered/selected
|
|
1342
|
+
// Soft additive bloom so each node reads as a point of LIGHT (fuzzy halo),
|
|
1343
|
+
// not a flat disc with a hard edge. Radial gradient fading to transparent,
|
|
1344
|
+
// painted with 'lighter' so overlapping glows accumulate like real light.
|
|
1345
|
+
if (!dim){
|
|
1346
|
+
var glowR = p.r * ((isHov || isSel) ? 6.5 : 4.2);
|
|
1347
|
+
var gi = ((isHov || isSel) ? 0.55 : 0.26) * (0.45 + 0.55 * near);
|
|
1348
|
+
var grad = ctx.createRadialGradient(p.sx, p.sy, 0, p.sx, p.sy, glowR);
|
|
1349
|
+
grad.addColorStop(0, col);
|
|
1350
|
+
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
|
1351
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
1352
|
+
ctx.globalAlpha = gi;
|
|
1353
|
+
ctx.fillStyle = grad;
|
|
1354
|
+
ctx.beginPath(); ctx.arc(p.sx, p.sy, glowR, 0, 7); ctx.fill();
|
|
1355
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1356
|
+
ctx.globalAlpha = 1;
|
|
1357
|
+
}
|
|
1358
|
+
// Bright solid core on top of its glow.
|
|
1359
|
+
ctx.beginPath(); ctx.arc(p.sx, p.sy, p.r, 0, 7);
|
|
1360
|
+
ctx.globalAlpha = alpha;
|
|
1361
|
+
ctx.fillStyle = col;
|
|
1362
|
+
ctx.fill();
|
|
1363
|
+
ctx.globalAlpha = 1;
|
|
1364
|
+
if (searching && o.hit){ ctx.strokeStyle = '#f6e05e'; ctx.lineWidth = 2; ctx.stroke(); ctx.lineWidth = 1; }
|
|
1365
|
+
else if (nd.contradiction){ ctx.strokeStyle = '#fc8181'; ctx.lineWidth = 2; ctx.stroke(); ctx.lineWidth = 1; }
|
|
1366
|
+
else if (nd.stale){ ctx.strokeStyle = '#f0883e'; ctx.lineWidth = 1.2; ctx.stroke(); ctx.lineWidth = 1; }
|
|
1367
|
+
// Hover / selection ring: a white halo OUTSIDE the node so the node keeps
|
|
1368
|
+
// its own type colour. Selected = bright + thick, hover = softer + thinner.
|
|
1369
|
+
if ((isSel || isHov) && !dim){
|
|
1370
|
+
ctx.beginPath(); ctx.arc(p.sx, p.sy, p.r + 3, 0, 7);
|
|
1371
|
+
ctx.strokeStyle = isSel ? 'rgba(255,255,255,0.95)' : 'rgba(255,255,255,0.5)';
|
|
1372
|
+
ctx.lineWidth = isSel ? 2 : 1.4; ctx.stroke(); ctx.lineWidth = 1;
|
|
1373
|
+
}
|
|
1374
|
+
// Labels: hubs always; hovered / selected / search-hit reveal on demand.
|
|
1375
|
+
var showLabel = (o.deg >= LABEL_MIN_DEG) || isHov || isSel || (searching && o.hit);
|
|
1376
|
+
if (showLabel && !dim){
|
|
1377
|
+
ctx.fillStyle = (isHov || isSel) ? '#e6edf3' : 'rgba(213,222,236,' + (0.25 + 0.6 * near) + ')';
|
|
1378
|
+
ctx.font = (isHov || isSel ? '12px ' : '11px ') + 'ui-sans-serif, system-ui, sans-serif';
|
|
1379
|
+
ctx.fillText(nd.title, p.sx + p.r + 4, p.sy + 3);
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// ---------- Hit-testing + interactions ----------
|
|
1385
|
+
function hitTest(mx, my){
|
|
1386
|
+
var best = null, bd = 16;
|
|
1387
|
+
for (var i = 0; i < P.length; i++){
|
|
1388
|
+
var p = P[i]; var d = Math.hypot(p.sx - mx, p.sy - my);
|
|
1389
|
+
if (d < bd && d < p.r + 8){ bd = d; best = i; }
|
|
1390
|
+
}
|
|
1391
|
+
return best;
|
|
1392
|
+
}
|
|
1393
|
+
function stopSpin(){ auto = false; }
|
|
1394
|
+
function initInteractions(){
|
|
1395
|
+
if (!canvas) return;
|
|
1396
|
+
sizeCanvas();
|
|
1397
|
+
window.addEventListener('resize', sizeCanvas);
|
|
1398
|
+
canvas.addEventListener('mousedown', function(e){
|
|
1399
|
+
dragging = true; moved = false; downX = lastX = e.clientX; downY = lastY = e.clientY;
|
|
1400
|
+
velY = 0; velX = 0; // grabbing halts any residual momentum
|
|
1401
|
+
canvas.classList.add('drag');
|
|
1402
|
+
});
|
|
1403
|
+
// A press that never crossed the drag threshold is a tap: on a node open its
|
|
1404
|
+
// panel, on empty space dismiss it. Bound on window so a drag that leaves the
|
|
1405
|
+
// canvas still ends cleanly.
|
|
1406
|
+
window.addEventListener('mouseup', function(e){
|
|
1407
|
+
if (dragging && !moved && G){
|
|
1408
|
+
var r = canvas.getBoundingClientRect();
|
|
1409
|
+
var hit = hitTest(e.clientX - r.left, e.clientY - r.top);
|
|
1410
|
+
if (hit != null){ selected = hit; showNote(G.nodes[hit].nd); }
|
|
1411
|
+
else { selected = null; hideNote(); }
|
|
1412
|
+
invalidate();
|
|
1413
|
+
}
|
|
1414
|
+
// Releasing after a rotate resumes the orbit — the sphere keeps spinning,
|
|
1415
|
+
// carried on by the momentum captured during the drag (see the frame loop).
|
|
1416
|
+
if (dragging) auto = true;
|
|
1417
|
+
dragging = false; canvas.classList.remove('drag');
|
|
1418
|
+
});
|
|
1419
|
+
canvas.addEventListener('mousemove', function(e){
|
|
1420
|
+
var r = canvas.getBoundingClientRect();
|
|
1421
|
+
if (dragging){
|
|
1422
|
+
// Empty-space drag ROTATES the sphere (X drag → spin, Y drag → tilt); this is
|
|
1423
|
+
// the interaction the old flat-SVG pan could never provide.
|
|
1424
|
+
if (Math.abs(e.clientX - downX) + Math.abs(e.clientY - downY) > 4){ moved = true; }
|
|
1425
|
+
var dY = (e.clientX - lastX) * 0.01, dX = (e.clientY - lastY) * 0.01;
|
|
1426
|
+
rotY += dY; rotX += dX;
|
|
1427
|
+
velY = dY; velX = dX; // remember the latest speed → release momentum
|
|
1428
|
+
if (rotX > 1.5) rotX = 1.5; else if (rotX < -1.5) rotX = -1.5;
|
|
1429
|
+
lastX = e.clientX; lastY = e.clientY; invalidate();
|
|
1430
|
+
} else {
|
|
1431
|
+
var hit = hitTest(e.clientX - r.left, e.clientY - r.top);
|
|
1432
|
+
if (hit !== hover){ hover = hit; canvas.style.cursor = (hit != null) ? 'pointer' : 'grab'; invalidate(); }
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
// Touch: one-finger drag rotates.
|
|
1436
|
+
canvas.addEventListener('touchstart', function(e){
|
|
1437
|
+
if (e.touches.length === 1){ dragging = true; moved = false; velY = 0; velX = 0;
|
|
1438
|
+
var t = e.touches[0]; downX = lastX = t.clientX; downY = lastY = t.clientY; }
|
|
1439
|
+
}, { passive: true });
|
|
1440
|
+
canvas.addEventListener('touchmove', function(e){
|
|
1441
|
+
if (dragging && e.touches.length === 1){
|
|
1442
|
+
var t = e.touches[0];
|
|
1443
|
+
var dY = (t.clientX - lastX) * 0.01, dX = (t.clientY - lastY) * 0.01;
|
|
1444
|
+
rotY += dY; rotX += dX; velY = dY; velX = dX;
|
|
1445
|
+
if (rotX > 1.5) rotX = 1.5; else if (rotX < -1.5) rotX = -1.5;
|
|
1446
|
+
lastX = t.clientX; lastY = t.clientY; moved = true;
|
|
1447
|
+
invalidate(); e.preventDefault();
|
|
1448
|
+
}
|
|
1449
|
+
}, { passive: false });
|
|
1450
|
+
canvas.addEventListener('touchend', function(){ if (dragging) auto = true; dragging = false; });
|
|
1451
|
+
// Zoom: wheel + the on-screen +/- buttons share one clamp. Range widened so the
|
|
1452
|
+
// sphere can be pushed much closer / further than before.
|
|
1453
|
+
function zoomBy(f){
|
|
1454
|
+
zoom *= f;
|
|
1455
|
+
if (zoom < MIN_ZOOM) zoom = MIN_ZOOM; else if (zoom > MAX_ZOOM) zoom = MAX_ZOOM;
|
|
1456
|
+
invalidate();
|
|
1457
|
+
}
|
|
1458
|
+
canvas.addEventListener('wheel', function(e){
|
|
1459
|
+
e.preventDefault();
|
|
1460
|
+
zoomBy(e.deltaY < 0 ? 1.1 : 0.9);
|
|
1461
|
+
}, { passive: false });
|
|
1462
|
+
var zin = document.getElementById('kb-zoom-in'), zout = document.getElementById('kb-zoom-out');
|
|
1463
|
+
if (zin) zin.addEventListener('click', function(){ zoomBy(1.25); });
|
|
1464
|
+
if (zout) zout.addEventListener('click', function(){ zoomBy(0.8); });
|
|
1465
|
+
// Single throttled rAF loop: ~30fps, pauses when hidden or nothing changed. Also
|
|
1466
|
+
// recovers the canvas size the first time the tab becomes visible (clientWidth is
|
|
1467
|
+
// 0 while the view is display:none, so the initial sizeCanvas() reads nothing).
|
|
1468
|
+
function frame(t){
|
|
1469
|
+
requestAnimationFrame(frame);
|
|
1470
|
+
if (document.hidden || !G) return;
|
|
1471
|
+
if (!cvW){ sizeCanvas(); if (!cvW) return; }
|
|
1472
|
+
if (!dragging && G.nodes.length){
|
|
1473
|
+
// Post-release momentum decays smoothly; the baseline auto-spin keeps the
|
|
1474
|
+
// sphere orbiting forever once the fling has bled off.
|
|
1475
|
+
if (Math.abs(velY) > 0.00008 || Math.abs(velX) > 0.00008){
|
|
1476
|
+
rotY += velY; rotX += velX;
|
|
1477
|
+
if (rotX > 1.5) rotX = 1.5; else if (rotX < -1.5) rotX = -1.5;
|
|
1478
|
+
velY *= 0.95; velX *= 0.95; needRender = true;
|
|
1479
|
+
} else { velY = 0; velX = 0; }
|
|
1480
|
+
if (auto){ rotY += SPIN; needRender = true; }
|
|
1481
|
+
}
|
|
1482
|
+
if (!needRender || t - lastFrame < 28) return;
|
|
1483
|
+
lastFrame = t; needRender = false; render();
|
|
1484
|
+
}
|
|
1485
|
+
requestAnimationFrame(frame);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// ---------- Search ----------
|
|
1489
|
+
// Mark matching notes (title / type / excerpt). render() dims the rest and rings the
|
|
1490
|
+
// hits; a hit's label is revealed even under the declutter threshold.
|
|
1491
|
+
function clearSearch(){
|
|
1492
|
+
if (G) G.nodes.forEach(function(o){ o.hit = true; });
|
|
1493
|
+
if (elSearchCount) elSearchCount.textContent = '';
|
|
1494
|
+
invalidate();
|
|
1495
|
+
}
|
|
1496
|
+
function runSearch(){
|
|
1497
|
+
if (!elSearch || !G) return [];
|
|
1498
|
+
var q = elSearch.value.trim().toLowerCase();
|
|
1499
|
+
if (!q){ clearSearch(); return []; }
|
|
1500
|
+
var hits = [];
|
|
1501
|
+
G.nodes.forEach(function(o){
|
|
1502
|
+
var nd = o.nd;
|
|
1503
|
+
var hay = (nd.title + ' ' + (nd.type || '') + ' ' + (nd.excerpt || '')).toLowerCase();
|
|
1504
|
+
o.hit = hay.indexOf(q) >= 0;
|
|
1505
|
+
if (o.hit) hits.push(o);
|
|
1506
|
+
});
|
|
1507
|
+
if (elSearchCount) elSearchCount.textContent = hits.length + ' / ' + G.nodes.length;
|
|
1508
|
+
invalidate();
|
|
1509
|
+
return hits;
|
|
1510
|
+
}
|
|
1511
|
+
// Enter → rotate the sphere so the first match faces the camera (front & centre) and
|
|
1512
|
+
// open its panel — the 3D analogue of "recentre on the first hit".
|
|
1513
|
+
function focusFirstMatch(){
|
|
1514
|
+
var hits = runSearch();
|
|
1515
|
+
if (!hits.length) return;
|
|
1516
|
+
var n0 = hits[0];
|
|
1517
|
+
stopSpin(); velY = 0; velX = 0;
|
|
1518
|
+
rotY = Math.atan2(n0.x, n0.z);
|
|
1519
|
+
var z1 = Math.hypot(n0.x, n0.z);
|
|
1520
|
+
rotX = Math.atan2(n0.y, z1);
|
|
1521
|
+
if (rotX > 1.5) rotX = 1.5; else if (rotX < -1.5) rotX = -1.5;
|
|
1522
|
+
selected = n0.i; showNote(n0.nd);
|
|
1523
|
+
invalidate();
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// ---------- Detail note (full-width, below the graph) ----------
|
|
1527
|
+
function el(tag, txt){ var e = document.createElement(tag); if (txt !== undefined) e.textContent = txt; return e; }
|
|
1528
|
+
function chip(label, value, dotColor){
|
|
1529
|
+
var c = el('span'); c.setAttribute('class', 'kb-note-chip');
|
|
1530
|
+
if (dotColor){ var i = el('i'); i.style.background = dotColor; c.appendChild(i); }
|
|
1531
|
+
c.appendChild(el('span', label + ' ')); c.appendChild(el('b', value));
|
|
1532
|
+
return c;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// ---------- Minimal, XSS-safe Markdown renderer ----------
|
|
1536
|
+
// Builds DOM nodes only (textContent everywhere — never assigns raw HTML), so
|
|
1537
|
+
// an untrusted note body cannot inject markup. Handles headings, fenced/inline
|
|
1538
|
+
// code, lists, blockquotes, hr, and inline bold/italic/code/links.
|
|
1539
|
+
// NOTE: this whole script lives inside a TS template literal, so every regex
|
|
1540
|
+
// backslash MUST be doubled (\\s not \s) — a single backslash is stripped at
|
|
1541
|
+
// build time. Literal backticks are written as \` (kept single).
|
|
1542
|
+
function sanitizeHref(h){
|
|
1543
|
+
var s = String(h || '').trim();
|
|
1544
|
+
// Allow only http(s), mailto, and in-vault relative links; block javascript:, data:, etc.
|
|
1545
|
+
if (/^https?:\\/\\//i.test(s) || /^mailto:/i.test(s)) return s;
|
|
1546
|
+
if (/^[\\w./#?=&%-]+$/.test(s) && !/^[a-z]+:/i.test(s)) return s;
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
function renderInline(container, text){
|
|
1550
|
+
// Tokenise on: inline code, bold, strikethrough, italic, and [label](href) links.
|
|
1551
|
+
var re = /(\`[^\`]+\`)|(\\*\\*[^*]+\\*\\*)|(~~[^~]+~~)|(\\*[^*]+\\*|_[^_]+_)|(\\[[^\\]]+\\]\\([^)]+\\))/;
|
|
1552
|
+
var rest = String(text);
|
|
1553
|
+
while (rest.length){
|
|
1554
|
+
var m = re.exec(rest);
|
|
1555
|
+
if (!m){ container.appendChild(document.createTextNode(rest)); break; }
|
|
1556
|
+
if (m.index > 0) container.appendChild(document.createTextNode(rest.slice(0, m.index)));
|
|
1557
|
+
var tok = m[0];
|
|
1558
|
+
if (tok.charAt(0) === '\`'){
|
|
1559
|
+
container.appendChild(el('code', tok.slice(1, -1)));
|
|
1560
|
+
} else if (tok.slice(0, 2) === '**'){
|
|
1561
|
+
container.appendChild(el('strong', tok.slice(2, -2)));
|
|
1562
|
+
} else if (tok.slice(0, 2) === '~~'){
|
|
1563
|
+
container.appendChild(el('del', tok.slice(2, -2)));
|
|
1564
|
+
} else if (tok.charAt(0) === '*' || tok.charAt(0) === '_'){
|
|
1565
|
+
container.appendChild(el('em', tok.slice(1, -1)));
|
|
1566
|
+
} else {
|
|
1567
|
+
var lm = /^\\[([^\\]]+)\\]\\(([^)]+)\\)$/.exec(tok);
|
|
1568
|
+
var href = lm ? sanitizeHref(lm[2]) : null;
|
|
1569
|
+
if (lm && href){
|
|
1570
|
+
var a = el('a', lm[1]); a.setAttribute('href', href);
|
|
1571
|
+
a.setAttribute('target', '_blank'); a.setAttribute('rel', 'noopener noreferrer');
|
|
1572
|
+
container.appendChild(a);
|
|
1573
|
+
} else {
|
|
1574
|
+
container.appendChild(document.createTextNode(tok));
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
rest = rest.slice(m.index + tok.length);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
function renderMarkdown(root, md){
|
|
1581
|
+
root.textContent = '';
|
|
1582
|
+
var lines = String(md).replace(/\\r\\n?/g, '\\n').split('\\n');
|
|
1583
|
+
var i = 0;
|
|
1584
|
+
// Nested-list stack: each entry = { list:<ul|ol>, ordered, indent }.
|
|
1585
|
+
var stack = [];
|
|
1586
|
+
function endList(){ stack.length = 0; }
|
|
1587
|
+
function listTarget(indent, ordered){
|
|
1588
|
+
while (stack.length && stack[stack.length - 1].indent > indent) stack.pop();
|
|
1589
|
+
var top = stack.length ? stack[stack.length - 1] : null;
|
|
1590
|
+
if (top && top.indent === indent){
|
|
1591
|
+
if (top.ordered === ordered) return top.list;
|
|
1592
|
+
stack.pop(); top = stack.length ? stack[stack.length - 1] : null;
|
|
1593
|
+
}
|
|
1594
|
+
var listEl = el(ordered ? 'ol' : 'ul');
|
|
1595
|
+
if (top && top.indent < indent && top.list.lastChild) top.list.lastChild.appendChild(listEl);
|
|
1596
|
+
else root.appendChild(listEl);
|
|
1597
|
+
stack.push({ list: listEl, ordered: ordered, indent: indent });
|
|
1598
|
+
return listEl;
|
|
1599
|
+
}
|
|
1600
|
+
// Split a table row into trimmed cells (drop the optional edge pipes).
|
|
1601
|
+
function tableCells(row){
|
|
1602
|
+
return row.trim().replace(/^\\|/, '').replace(/\\|$/, '').split('|').map(function(s){ return s.trim(); });
|
|
1603
|
+
}
|
|
1604
|
+
while (i < lines.length){
|
|
1605
|
+
var line = lines[i];
|
|
1606
|
+
// Fenced code block.
|
|
1607
|
+
var fence = /^\\s*\`\`\`(.*)$/.exec(line);
|
|
1608
|
+
if (fence){
|
|
1609
|
+
endList(); i++;
|
|
1610
|
+
var buf = [];
|
|
1611
|
+
while (i < lines.length && !/^\\s*\`\`\`\\s*$/.test(lines[i])){ buf.push(lines[i]); i++; }
|
|
1612
|
+
i++; // closing fence
|
|
1613
|
+
var pre = el('pre'); pre.appendChild(el('code', buf.join('\\n')));
|
|
1614
|
+
root.appendChild(pre); continue;
|
|
1615
|
+
}
|
|
1616
|
+
// Horizontal rule.
|
|
1617
|
+
if (/^\\s*([-*_])(\\s*\\1){2,}\\s*$/.test(line)){ endList(); root.appendChild(el('hr')); i++; continue; }
|
|
1618
|
+
// Heading.
|
|
1619
|
+
var h = /^(#{1,6})\\s+(.*)$/.exec(line);
|
|
1620
|
+
if (h){ endList(); var hEl = el('h' + h[1].length); renderInline(hEl, h[2].trim()); root.appendChild(hEl); i++; continue; }
|
|
1621
|
+
// Blockquote (collapse consecutive > lines).
|
|
1622
|
+
if (/^\\s*>\\s?/.test(line)){
|
|
1623
|
+
endList(); var q = el('blockquote'); var qbuf = [];
|
|
1624
|
+
while (i < lines.length && /^\\s*>\\s?/.test(lines[i])){ qbuf.push(lines[i].replace(/^\\s*>\\s?/, '')); i++; }
|
|
1625
|
+
renderInline(q, qbuf.join(' ')); root.appendChild(q); continue;
|
|
1626
|
+
}
|
|
1627
|
+
// GFM pipe table: a header row immediately followed by a |---|:--:| separator.
|
|
1628
|
+
if (/\\|/.test(line) && i + 1 < lines.length &&
|
|
1629
|
+
/^\\s*\\|?\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)*\\|?\\s*$/.test(lines[i + 1])){
|
|
1630
|
+
endList();
|
|
1631
|
+
var aligns = tableCells(lines[i + 1]).map(function(s){
|
|
1632
|
+
var lft = s.charAt(0) === ':', rgt = s.charAt(s.length - 1) === ':';
|
|
1633
|
+
return (lft && rgt) ? 'center' : rgt ? 'right' : lft ? 'left' : '';
|
|
1634
|
+
});
|
|
1635
|
+
var table = el('table'); var thead = el('thead'); var htr = el('tr');
|
|
1636
|
+
tableCells(line).forEach(function(c, idx){
|
|
1637
|
+
var th = el('th'); if (aligns[idx]) th.style.textAlign = aligns[idx];
|
|
1638
|
+
renderInline(th, c); htr.appendChild(th);
|
|
1639
|
+
});
|
|
1640
|
+
thead.appendChild(htr); table.appendChild(thead);
|
|
1641
|
+
var tbody = el('tbody'); i += 2;
|
|
1642
|
+
while (i < lines.length && /\\|/.test(lines[i]) && lines[i].trim() !== ''){
|
|
1643
|
+
var tr = el('tr');
|
|
1644
|
+
tableCells(lines[i]).forEach(function(c, idx){
|
|
1645
|
+
var td = el('td'); if (aligns[idx]) td.style.textAlign = aligns[idx];
|
|
1646
|
+
renderInline(td, c); tr.appendChild(td);
|
|
1647
|
+
});
|
|
1648
|
+
tbody.appendChild(tr); i++;
|
|
1649
|
+
}
|
|
1650
|
+
table.appendChild(tbody);
|
|
1651
|
+
var wrap = el('div'); wrap.setAttribute('class', 'kb-table-wrap'); wrap.appendChild(table);
|
|
1652
|
+
root.appendChild(wrap); continue;
|
|
1653
|
+
}
|
|
1654
|
+
// List item (ordered/unordered), indent-nested, with optional task checkbox.
|
|
1655
|
+
var li = /^(\\s*)(?:([-*+])|(\\d+)[.)])\\s+(.*)$/.exec(line);
|
|
1656
|
+
if (li){
|
|
1657
|
+
var indent = li[1].length;
|
|
1658
|
+
var ordered = li[3] !== undefined;
|
|
1659
|
+
var content = li[4];
|
|
1660
|
+
var target = listTarget(indent, ordered);
|
|
1661
|
+
var item = el('li');
|
|
1662
|
+
var task = /^\\[([ xX])\\]\\s+(.*)$/.exec(content);
|
|
1663
|
+
if (task){
|
|
1664
|
+
item.setAttribute('class', 'kb-task');
|
|
1665
|
+
var box = el('input'); box.setAttribute('type', 'checkbox'); box.setAttribute('disabled', '');
|
|
1666
|
+
if (task[1] !== ' ') box.setAttribute('checked', '');
|
|
1667
|
+
item.appendChild(box);
|
|
1668
|
+
renderInline(item, ' ' + task[2]);
|
|
1669
|
+
} else {
|
|
1670
|
+
renderInline(item, content);
|
|
1671
|
+
}
|
|
1672
|
+
target.appendChild(item); i++; continue;
|
|
1673
|
+
}
|
|
1674
|
+
// Blank line ends a list / paragraph.
|
|
1675
|
+
if (/^\\s*$/.test(line)){ endList(); i++; continue; }
|
|
1676
|
+
// Paragraph: gather consecutive plain lines.
|
|
1677
|
+
endList(); var pbuf = [line];
|
|
1678
|
+
i++;
|
|
1679
|
+
while (i < lines.length && !/^\\s*$/.test(lines[i]) &&
|
|
1680
|
+
!/^(#{1,6})\\s/.test(lines[i]) && !/^\\s*>\\s?/.test(lines[i]) &&
|
|
1681
|
+
!/^\\s*(?:[-*+]|\\d+[.)])\\s+/.test(lines[i]) && !/^\\s*\`\`\`/.test(lines[i])){
|
|
1682
|
+
pbuf.push(lines[i]); i++;
|
|
1683
|
+
}
|
|
1684
|
+
var p = el('p'); renderInline(p, pbuf.join('\\n')); root.appendChild(p);
|
|
1685
|
+
}
|
|
1686
|
+
if (!root.childNodes.length) root.appendChild(el('p', '(empty note)'));
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
var noteReq = 0; // guards against out-of-order fetch responses
|
|
1690
|
+
function showNote(nd){
|
|
1691
|
+
noteReq++;
|
|
1692
|
+
var myReq = noteReq;
|
|
1693
|
+
elNote.textContent = '';
|
|
1694
|
+
// Head: title + close.
|
|
1695
|
+
var head = el('div'); head.setAttribute('class', 'kb-note-head');
|
|
1696
|
+
head.appendChild(el('h3', nd.title));
|
|
1697
|
+
var close = el('button', '✕'); close.setAttribute('class', 'kb-note-close'); close.setAttribute('title', 'Close');
|
|
1698
|
+
close.addEventListener('click', function(){ selected = null; hideNote(); invalidate(); });
|
|
1699
|
+
head.appendChild(close);
|
|
1700
|
+
elNote.appendChild(head);
|
|
1701
|
+
// Metadata chips.
|
|
1702
|
+
var meta = el('div'); meta.setAttribute('class', 'kb-note-meta');
|
|
1703
|
+
meta.appendChild(chip('type', nd.type || '—', colorFor(nd.type)));
|
|
1704
|
+
meta.appendChild(chip('links', String(nd.degree)));
|
|
1705
|
+
if (nd.confidence !== null && nd.confidence !== undefined) meta.appendChild(chip('confidence', String(nd.confidence)));
|
|
1706
|
+
if (nd.updatedAt) meta.appendChild(chip('updated', nd.updatedAt));
|
|
1707
|
+
if (nd.stale) meta.appendChild(chip('status', 'stale ≥ 90d'));
|
|
1708
|
+
if (nd.contradiction) meta.appendChild(chip('flag', 'contradiction'));
|
|
1709
|
+
meta.appendChild(chip('id', nd.id));
|
|
1710
|
+
elNote.appendChild(meta);
|
|
1711
|
+
// File path line (full location) — filled once the note fetch returns.
|
|
1712
|
+
var pathLine = el('div'); pathLine.setAttribute('class', 'kb-note-path');
|
|
1713
|
+
pathLine.style.display = 'none'; elNote.appendChild(pathLine);
|
|
1714
|
+
// Body — start from the excerpt, then fetch and render the FULL file.
|
|
1715
|
+
var body = el('div'); body.setAttribute('class', 'kb-md');
|
|
1716
|
+
if (nd.excerpt) renderMarkdown(body, nd.excerpt);
|
|
1717
|
+
var loading = el('div', 'Loading full note…'); loading.setAttribute('class', 'kb-note-loading');
|
|
1718
|
+
elNote.appendChild(body); elNote.appendChild(loading);
|
|
1719
|
+
elNote.style.display = '';
|
|
1720
|
+
elNote.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
1721
|
+
// Fetch the whole markdown body for the current source scope.
|
|
1722
|
+
var url = apiUrl('/knowledge/note') + '?scope=' + encodeURIComponent(currentScope()) +
|
|
1723
|
+
'&id=' + encodeURIComponent(nd.id);
|
|
1724
|
+
fetch(url, { headers: { 'Accept': 'application/json' } }).then(function(res){
|
|
1725
|
+
if (res.status === 401){ onUnauthorized(); return null; }
|
|
1726
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1727
|
+
return res.json();
|
|
1728
|
+
}).then(function(data){
|
|
1729
|
+
if (myReq !== noteReq) return; // a newer note was opened — ignore
|
|
1730
|
+
if (loading.parentNode) loading.parentNode.removeChild(loading);
|
|
1731
|
+
if (!data) return;
|
|
1732
|
+
// Header enrichments from the backend: last-modified date + full path.
|
|
1733
|
+
if (typeof data.updated === 'string' && data.updated){
|
|
1734
|
+
meta.appendChild(chip('updated', String(data.updated).slice(0, 10)));
|
|
1735
|
+
}
|
|
1736
|
+
if (typeof data.path === 'string' && data.path){
|
|
1737
|
+
pathLine.textContent = '';
|
|
1738
|
+
pathLine.appendChild(el('span', '📄 ')); // page icon
|
|
1739
|
+
pathLine.appendChild(el('b', data.path));
|
|
1740
|
+
pathLine.style.display = '';
|
|
1741
|
+
}
|
|
1742
|
+
if (typeof data.body === 'string' && data.body.trim()) renderMarkdown(body, data.body);
|
|
1743
|
+
}).catch(function(){
|
|
1744
|
+
if (myReq !== noteReq) return;
|
|
1745
|
+
// Demo/synthetic ids have no file — keep the excerpt, just drop the spinner.
|
|
1746
|
+
if (loading.parentNode) loading.parentNode.removeChild(loading);
|
|
1747
|
+
if (!nd.excerpt) renderMarkdown(body, '(full note unavailable)');
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
function hideNote(){ noteReq++; elNote.style.display = 'none'; elNote.textContent = ''; }
|
|
1751
|
+
|
|
1752
|
+
// ---------- Legend + stats ----------
|
|
1753
|
+
function renderLegend(){
|
|
1754
|
+
elLegend.textContent = '';
|
|
1755
|
+
var present = {};
|
|
1756
|
+
G.nodes.forEach(function(o){ present[o.nd.type || 'other'] = true; });
|
|
1757
|
+
Object.keys(present).forEach(function(type){
|
|
1758
|
+
var s = el('span');
|
|
1759
|
+
var dot = el('i'); dot.style.background = colorFor(type === 'other' ? null : type);
|
|
1760
|
+
s.appendChild(dot); s.appendChild(el('span', type));
|
|
1761
|
+
elLegend.appendChild(s);
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// ---------- Load (data layer — unchanged) ----------
|
|
1766
|
+
function currentScope(){ return (sourceSel && sourceSel.value) || 'shared'; }
|
|
1767
|
+
// Demo controls only make sense for the Shared KB — an agent's own memory has no
|
|
1768
|
+
// synthetic demo. Hide them when an agent is picked.
|
|
1769
|
+
function syncSourceControls(){
|
|
1770
|
+
var isShared = currentScope() === 'shared';
|
|
1771
|
+
if (demoToggleWrap) demoToggleWrap.style.display = isShared ? '' : 'none';
|
|
1772
|
+
if (demoSizeWrap) demoSizeWrap.style.display = (isShared && demoToggle.checked) ? '' : 'none';
|
|
1773
|
+
if (!isShared) elDemoBadge.style.display = 'none';
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Populate the source selector from /knowledge/sources (Shared KB + agents that
|
|
1777
|
+
// have Lane-2 memory notes), preserving the current selection, then load.
|
|
1778
|
+
function loadSources(){
|
|
1779
|
+
fetch(apiUrl('/knowledge/sources'), { headers: { 'Accept': 'application/json' } }).then(function(res){
|
|
1780
|
+
if (res.status === 401) { onUnauthorized(); return null; }
|
|
1781
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1782
|
+
return res.json();
|
|
1783
|
+
}).then(function(data){
|
|
1784
|
+
if (!data || !sourceSel) { loadGraph(); return; }
|
|
1785
|
+
var prev = sourceSel.value || 'shared';
|
|
1786
|
+
sourceSel.textContent = '';
|
|
1787
|
+
(data.sources || [{ id: 'shared', label: 'Shared Knowledge Base', count: 0 }]).forEach(function(s){
|
|
1788
|
+
var o = document.createElement('option');
|
|
1789
|
+
o.value = s.id; o.textContent = s.label + (typeof s.count === 'number' ? ' (' + s.count + ')' : '');
|
|
1790
|
+
sourceSel.appendChild(o);
|
|
1791
|
+
});
|
|
1792
|
+
// Restore prior selection if it still exists, else fall back to Shared KB.
|
|
1793
|
+
sourceSel.value = prev;
|
|
1794
|
+
if (sourceSel.value !== prev) sourceSel.value = 'shared';
|
|
1795
|
+
loadGraph();
|
|
1796
|
+
}).catch(function(){ loadGraph(); }); // selector best-effort — the graph still loads
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function loadGraph(){
|
|
1800
|
+
syncSourceControls();
|
|
1801
|
+
var scope = currentScope();
|
|
1802
|
+
var q = '';
|
|
1803
|
+
if (scope !== 'shared') {
|
|
1804
|
+
q = '?scope=' + encodeURIComponent(scope);
|
|
1805
|
+
} else if (!demoToggle.checked) {
|
|
1806
|
+
q = '?demo=off';
|
|
1807
|
+
} else if (demoSize && demoSize.value !== '0') {
|
|
1808
|
+
q = '?demo=' + encodeURIComponent(demoSize.value);
|
|
1809
|
+
}
|
|
1810
|
+
var url = apiUrl('/knowledge/graph') + q;
|
|
1811
|
+
elStats.textContent = 'Loading…'; elEmpty.style.display = 'none';
|
|
1812
|
+
fetch(url, { headers: { 'Accept': 'application/json' } }).then(function(res){
|
|
1813
|
+
if (res.status === 401) { onUnauthorized(); return null; }
|
|
1814
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1815
|
+
return res.json();
|
|
1816
|
+
}).then(function(data){
|
|
1817
|
+
if (!data) return;
|
|
1818
|
+
var isDemo = !!data.demo;
|
|
1819
|
+
elDemoBadge.style.display = isDemo ? '' : 'none';
|
|
1820
|
+
if (!data.nodes || data.nodes.length === 0){
|
|
1821
|
+
buildModel({ nodes: [], edges: [] });
|
|
1822
|
+
elStats.textContent = '';
|
|
1823
|
+
elEmpty.textContent = scope === 'shared'
|
|
1824
|
+
? 'No notes in the shared Knowledge Base yet. Notes appear here once agents promote memories to the shared vault (dreaming auto mode), or enable the demo toggle above.'
|
|
1825
|
+
: 'This agent has no memory notes yet.';
|
|
1826
|
+
elEmpty.style.display = '';
|
|
1827
|
+
elLegend.textContent = '';
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
buildModel(data);
|
|
1831
|
+
renderLegend();
|
|
1832
|
+
elStats.textContent = data.nodes.length + ' notes, ' + data.edges.length + ' links';
|
|
1833
|
+
}).catch(function(err){
|
|
1834
|
+
elStats.textContent = ''; elEmpty.textContent = 'Failed to load graph: ' + err.message; elEmpty.style.display = '';
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
document.getElementById('kb-refresh').addEventListener('click', loadGraph);
|
|
1839
|
+
if (sourceSel) sourceSel.addEventListener('change', loadGraph);
|
|
1840
|
+
demoToggle.addEventListener('change', loadGraph);
|
|
1841
|
+
if (demoSize) demoSize.addEventListener('change', loadGraph);
|
|
1842
|
+
if (elSearch){
|
|
1843
|
+
elSearch.addEventListener('input', runSearch);
|
|
1844
|
+
elSearch.addEventListener('keydown', function(ev){
|
|
1845
|
+
if (ev.key === 'Enter') focusFirstMatch();
|
|
1846
|
+
else if (ev.key === 'Escape') { elSearch.value = ''; clearSearch(); }
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
initInteractions();
|
|
1850
|
+
})();
|
|
1851
|
+
</script>
|
|
1852
|
+
|
|
1853
|
+
<script id="kb-dreams">
|
|
1854
|
+
// Nightly dreaming report renderer. Fetches /knowledge/dreams (parsed audit
|
|
1855
|
+
// trail across all agents), renders a newest-first timeline with per-run
|
|
1856
|
+
// proposals, and an agent filter. All text goes through textContent (no HTML
|
|
1857
|
+
// injection). Uses the global apiUrl/onUnauthorized helpers.
|
|
1858
|
+
(function(){
|
|
1859
|
+
var listEl = document.getElementById('dreams-list');
|
|
1860
|
+
var emptyEl = document.getElementById('dreams-empty');
|
|
1861
|
+
var statsEl = document.getElementById('dreams-stats');
|
|
1862
|
+
var agentSel = document.getElementById('dreams-agent');
|
|
1863
|
+
var refreshBtn = document.getElementById('dreams-refresh');
|
|
1864
|
+
var allRuns = [];
|
|
1865
|
+
var loaded = false;
|
|
1866
|
+
// Notice to show once, after the next render (a reload overwrites statsEl,
|
|
1867
|
+
// so an accept result set directly on statsEl would be lost immediately).
|
|
1868
|
+
var pendingNotice = '';
|
|
1869
|
+
|
|
1870
|
+
function el(tag, cls, text){
|
|
1871
|
+
var e = document.createElement(tag);
|
|
1872
|
+
if (cls) e.className = cls;
|
|
1873
|
+
if (text != null) e.textContent = text;
|
|
1874
|
+
return e;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Apply one or more proposals of a run to the agent's memory via the K4
|
|
1878
|
+
// applier. indexes === null ⇒ accept every pending proposal in the run.
|
|
1879
|
+
function acceptDreams(agent, ts, indexes, btn){
|
|
1880
|
+
var origText = btn ? btn.textContent : '';
|
|
1881
|
+
if (btn){ btn.disabled = true; btn.textContent = 'Applying…'; }
|
|
1882
|
+
var payload = { agentId: agent, ts: ts };
|
|
1883
|
+
if (indexes) payload.indexes = indexes;
|
|
1884
|
+
fetch(apiUrl('/knowledge/dreams/apply'), {
|
|
1885
|
+
method: 'POST',
|
|
1886
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1887
|
+
body: JSON.stringify(payload)
|
|
1888
|
+
}).then(function(res){
|
|
1889
|
+
if (res.status === 401){ onUnauthorized(); return null; }
|
|
1890
|
+
return res.json().then(function(data){ return { ok: res.ok, data: data || {} }; });
|
|
1891
|
+
}).then(function(r){
|
|
1892
|
+
if (!r) return;
|
|
1893
|
+
if (!r.ok){
|
|
1894
|
+
if (btn){ btn.disabled = false; btn.textContent = origText || 'Accept'; }
|
|
1895
|
+
statsEl.textContent = 'Apply failed: ' + (r.data.error || 'error');
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
// Reload so applied proposals show ✓ and any anchor-stale skips stay
|
|
1899
|
+
// pending; the memory files themselves were already updated on disk.
|
|
1900
|
+
// The message is stashed in pendingNotice because loadDreams → render
|
|
1901
|
+
// rewrites statsEl.
|
|
1902
|
+
pendingNotice = 'Applied ' + (r.data.applied || 0)
|
|
1903
|
+
+ (r.data.skipped ? ', skipped ' + r.data.skipped + ' (anchor changed / net-negative)' : '')
|
|
1904
|
+
+ (r.data.alreadyAccepted ? ', ' + r.data.alreadyAccepted + ' already applied' : '');
|
|
1905
|
+
loadDreams(true);
|
|
1906
|
+
}).catch(function(e){
|
|
1907
|
+
if (btn){ btn.disabled = false; btn.textContent = origText || 'Accept'; }
|
|
1908
|
+
statsEl.textContent = 'Apply error: ' + e.message;
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function renderRun(run){
|
|
1913
|
+
var box = el('div', 'dream-run');
|
|
1914
|
+
var head = el('div', 'dream-run-head');
|
|
1915
|
+
if (run.agent) head.appendChild(el('span', 'agent', run.agent));
|
|
1916
|
+
var modeCls = run.mode === 'auto' ? 'auto' : 'propose';
|
|
1917
|
+
head.appendChild(el('span', 'dream-badge ' + modeCls, run.mode));
|
|
1918
|
+
head.appendChild(el('span', 'dream-badge outcome', run.outcome));
|
|
1919
|
+
head.appendChild(el('span', 'when', run.iso));
|
|
1920
|
+
|
|
1921
|
+
// Pending = a propose-run proposal not yet manually accepted. Accept-all
|
|
1922
|
+
// shows only when there is something to apply.
|
|
1923
|
+
var pending = (run.proposals || []).filter(function(p){ return run.mode !== 'auto' && !p.accepted; });
|
|
1924
|
+
if (pending.length){
|
|
1925
|
+
var allBtn = el('button', 'dream-accept-all', 'Accept all (' + pending.length + ')');
|
|
1926
|
+
allBtn.title = 'Apply every pending proposal in this run to memory';
|
|
1927
|
+
allBtn.addEventListener('click', function(){
|
|
1928
|
+
acceptDreams(run.agent, run.ts, pending.map(function(p){ return p.index; }), allBtn);
|
|
1929
|
+
});
|
|
1930
|
+
head.appendChild(allBtn);
|
|
1931
|
+
}
|
|
1932
|
+
box.appendChild(head);
|
|
1933
|
+
|
|
1934
|
+
if (run.summary) box.appendChild(el('div', 'dream-summary', run.summary));
|
|
1935
|
+
|
|
1936
|
+
if (run.proposals && run.proposals.length){
|
|
1937
|
+
var props = el('div', 'dream-props');
|
|
1938
|
+
run.proposals.forEach(function(p){
|
|
1939
|
+
var pe = el('div', 'dream-prop' + (p.accepted ? ' accepted' : ''));
|
|
1940
|
+
var top = el('div');
|
|
1941
|
+
top.appendChild(el('span', 'op ' + (p.op || ''), p.op || '?'));
|
|
1942
|
+
top.appendChild(el('span', 'file', p.file || ''));
|
|
1943
|
+
top.appendChild(el('span', 'score', 'score ' + (p.score != null ? p.score : '?') + ' · recall ' + (p.recallCount != null ? p.recallCount : '?')));
|
|
1944
|
+
pe.appendChild(top);
|
|
1945
|
+
if (p.reason) pe.appendChild(el('div', 'reason', p.reason));
|
|
1946
|
+
if (p.target) pe.appendChild(el('div', 'anchor', 'anchor: ' + p.target));
|
|
1947
|
+
if (p.content) pe.appendChild(el('div', 'content', p.content));
|
|
1948
|
+
|
|
1949
|
+
// Per-proposal status + accept action.
|
|
1950
|
+
var actions = el('div', 'dream-prop-actions');
|
|
1951
|
+
if (p.accepted){
|
|
1952
|
+
actions.appendChild(el('span', 'prop-status applied', '✓ applied'));
|
|
1953
|
+
} else if (run.mode === 'auto'){
|
|
1954
|
+
actions.appendChild(el('span', 'prop-status applied-auto', '✓ applied (auto)'));
|
|
1955
|
+
} else {
|
|
1956
|
+
actions.appendChild(el('span', 'prop-status pending', 'pending'));
|
|
1957
|
+
var accBtn = el('button', 'dream-accept-btn', 'Accept');
|
|
1958
|
+
accBtn.title = 'Apply this proposal to ' + (p.file || 'memory');
|
|
1959
|
+
accBtn.addEventListener('click', function(){
|
|
1960
|
+
acceptDreams(run.agent, run.ts, [p.index], accBtn);
|
|
1961
|
+
});
|
|
1962
|
+
actions.appendChild(accBtn);
|
|
1963
|
+
}
|
|
1964
|
+
pe.appendChild(actions);
|
|
1965
|
+
props.appendChild(pe);
|
|
1966
|
+
});
|
|
1967
|
+
box.appendChild(props);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
var applied = (run.applied != null) ? (', applied ' + run.applied) : '';
|
|
1971
|
+
box.appendChild(el('div', 'dream-meta', 'tokens ' + (run.tokens||0) + ' · sessions ' + (run.sessions||0) + applied));
|
|
1972
|
+
return box;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function render(){
|
|
1976
|
+
var filter = agentSel ? agentSel.value : '';
|
|
1977
|
+
var runs = filter ? allRuns.filter(function(r){ return r.agent === filter; }) : allRuns;
|
|
1978
|
+
listEl.textContent = '';
|
|
1979
|
+
if (!runs.length){
|
|
1980
|
+
emptyEl.textContent = allRuns.length ? 'No dream runs for this agent.' : 'No dream runs yet. The nightly dreaming pass writes here after it first runs (auto or propose mode).';
|
|
1981
|
+
emptyEl.style.display = '';
|
|
1982
|
+
statsEl.textContent = '';
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
emptyEl.style.display = 'none';
|
|
1986
|
+
var pendingTotal = 0;
|
|
1987
|
+
runs.forEach(function(r){
|
|
1988
|
+
if (r.mode === 'auto') return;
|
|
1989
|
+
(r.proposals || []).forEach(function(p){ if (!p.accepted) pendingTotal++; });
|
|
1990
|
+
});
|
|
1991
|
+
statsEl.textContent = runs.length + ' run' + (runs.length === 1 ? '' : 's')
|
|
1992
|
+
+ (pendingTotal ? ' · ' + pendingTotal + ' pending' : '');
|
|
1993
|
+
if (pendingNotice){
|
|
1994
|
+
statsEl.textContent += ' — ' + pendingNotice;
|
|
1995
|
+
pendingNotice = '';
|
|
1996
|
+
}
|
|
1997
|
+
runs.forEach(function(r){ listEl.appendChild(renderRun(r)); });
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function loadDreams(force){
|
|
2001
|
+
if (loaded && !force) return;
|
|
2002
|
+
loaded = true;
|
|
2003
|
+
statsEl.textContent = 'Loading…';
|
|
2004
|
+
fetch(apiUrl('/knowledge/dreams'), { headers: { 'Accept': 'application/json' } }).then(function(res){
|
|
2005
|
+
if (res.status === 401) { onUnauthorized(); return null; }
|
|
2006
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2007
|
+
return res.json();
|
|
2008
|
+
}).then(function(data){
|
|
2009
|
+
if (!data) return;
|
|
2010
|
+
allRuns = data.runs || [];
|
|
2011
|
+
// Populate the agent filter, preserving the current pick.
|
|
2012
|
+
if (agentSel){
|
|
2013
|
+
var prev = agentSel.value;
|
|
2014
|
+
agentSel.textContent = '';
|
|
2015
|
+
agentSel.appendChild(el('option', null, 'All agents')); agentSel.lastChild.value = '';
|
|
2016
|
+
(data.agents || []).forEach(function(a){ var o = el('option', null, a); o.value = a; agentSel.appendChild(o); });
|
|
2017
|
+
agentSel.value = prev; if (agentSel.value !== prev) agentSel.value = '';
|
|
2018
|
+
}
|
|
2019
|
+
render();
|
|
2020
|
+
}).catch(function(err){
|
|
2021
|
+
statsEl.textContent = ''; emptyEl.textContent = 'Failed to load dreams: ' + err.message; emptyEl.style.display = '';
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
if (agentSel) agentSel.addEventListener('change', render);
|
|
2026
|
+
if (refreshBtn) refreshBtn.addEventListener('click', function(){ loadDreams(true); });
|
|
2027
|
+
// Exposed so the tab switcher (in the kb-graph script) can lazy-load on first open.
|
|
2028
|
+
window.__loadDreams = loadDreams;
|
|
2029
|
+
})();
|
|
2030
|
+
</script>
|
|
908
2031
|
</body>
|
|
909
2032
|
</html>`;
|
|
910
2033
|
}
|