claude-memory-layer 1.0.27 → 1.0.28

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.
Files changed (329) hide show
  1. package/.env.example +7 -0
  2. package/AGENTS.md +11 -0
  3. package/README.md +184 -41
  4. package/benchmarks/replay/anonymized-real-sessions.json +48 -0
  5. package/dist/cli/index.js +10097 -6003
  6. package/dist/cli/index.js.map +4 -4
  7. package/dist/core/index.js +9745 -5587
  8. package/dist/core/index.js.map +4 -4
  9. package/dist/hooks/post-tool-use.js +6545 -5270
  10. package/dist/hooks/post-tool-use.js.map +4 -4
  11. package/dist/hooks/semantic-daemon.js +6646 -5354
  12. package/dist/hooks/semantic-daemon.js.map +4 -4
  13. package/dist/hooks/session-end.js +6618 -5347
  14. package/dist/hooks/session-end.js.map +4 -4
  15. package/dist/hooks/session-start.js +6619 -5354
  16. package/dist/hooks/session-start.js.map +4 -4
  17. package/dist/hooks/stop.js +6614 -5325
  18. package/dist/hooks/stop.js.map +4 -4
  19. package/dist/hooks/user-prompt-submit.js +6702 -5356
  20. package/dist/hooks/user-prompt-submit.js.map +4 -4
  21. package/dist/index.js +13537 -0
  22. package/dist/index.js.map +7 -0
  23. package/dist/mcp/index.js +20770 -0
  24. package/dist/mcp/index.js.map +7 -0
  25. package/dist/server/api/index.js +6632 -5319
  26. package/dist/server/api/index.js.map +4 -4
  27. package/dist/server/index.js +6667 -5340
  28. package/dist/server/index.js.map +4 -4
  29. package/dist/services/memory-service.js +6568 -5350
  30. package/dist/services/memory-service.js.map +4 -4
  31. package/dist/ui/assets/js/bootstrap.js +244 -0
  32. package/dist/ui/assets/js/chat.js +373 -0
  33. package/dist/ui/assets/js/disclosure.js +232 -0
  34. package/dist/ui/assets/js/modals.js +298 -0
  35. package/dist/ui/assets/js/overview.js +655 -0
  36. package/dist/ui/assets/js/state.js +72 -0
  37. package/dist/ui/assets/js/views.js +468 -0
  38. package/dist/ui/index.html +43 -1
  39. package/dist/ui/index.ts +3 -0
  40. package/dist/ui/style.css +222 -0
  41. package/docs/ARCHITECTURE_COMPARISON_AND_RECOMMENDATIONS.md +627 -0
  42. package/docs/HERMES_MEMORY_INGESTION_ANALYSIS.md +440 -0
  43. package/docs/MEMORY_USEFULNESS_AUDIT.md +371 -0
  44. package/docs/MEMORY_USEFULNESS_AUDIT_RAW.json +80 -0
  45. package/docs/MEMSEARCH_PROJECT_STRUCTURE_ANALYSIS.md +333 -0
  46. package/docs/PRODUCT_VALIDATION_MATRIX.md +82 -0
  47. package/docs/PROJECT_STRUCTURE_ANALYSIS.md +421 -0
  48. package/docs/REFACTORING_MILESTONES_AND_ISSUES.md +501 -0
  49. package/docs/REFACTORING_PLAN_THIN_CORE.md +414 -0
  50. package/docs/REFERENCE_PROJECT_ANALYSES.md +25 -0
  51. package/docs/SUPERLOCALMEMORY_PROJECT_STRUCTURE_ANALYSIS.md +452 -0
  52. package/docs/TARGET_ARCHITECTURE_AND_FOLDER_STRUCTURE.md +446 -0
  53. package/docs/architecture/comparison-index.md +47 -0
  54. package/docs/reports/codex-real-data-validation-20260505T040447Z.md +46 -0
  55. package/package.json +9 -5
  56. package/scripts/build.ts +25 -8
  57. package/scripts/generate-session-qrels.ts +126 -0
  58. package/scripts/replay-retrieval-benchmark.ts +69 -0
  59. package/specs/thin-core-refactor/context.md +275 -0
  60. package/specs/thin-core-refactor/plan.md +536 -0
  61. package/specs/thin-core-refactor/spec.md +465 -0
  62. package/src/adapters/claude/capture/index.ts +3 -0
  63. package/src/adapters/claude/context/index.ts +3 -0
  64. package/src/adapters/claude/hooks/index.ts +21 -0
  65. package/src/adapters/claude/hooks/post-tool-use.ts +239 -0
  66. package/src/adapters/claude/hooks/prompt-injection-policy.ts +104 -0
  67. package/src/adapters/claude/hooks/semantic-daemon-client.ts +209 -0
  68. package/src/adapters/claude/hooks/semantic-daemon.ts +283 -0
  69. package/src/adapters/claude/hooks/session-end.ts +59 -0
  70. package/src/adapters/claude/hooks/session-start.ts +73 -0
  71. package/src/adapters/claude/hooks/stop.ts +128 -0
  72. package/src/adapters/claude/hooks/user-prompt-submit.ts +361 -0
  73. package/src/adapters/claude/index.ts +4 -0
  74. package/src/adapters/claude/transcript/index.ts +4 -0
  75. package/src/adapters/claude/transcript/transcript-reader.ts +57 -0
  76. package/src/adapters/claude/transcript/turn-reconstructor.ts +65 -0
  77. package/src/apps/cli/claude-settings-hooks.ts +138 -0
  78. package/src/apps/cli/codex-import-runner.ts +125 -0
  79. package/src/apps/cli/codex-validation-output.ts +95 -0
  80. package/src/apps/cli/hermes-import-runner.ts +130 -0
  81. package/src/apps/cli/hermes-validation-output.ts +91 -0
  82. package/src/apps/cli/index.ts +1731 -0
  83. package/src/apps/cli/mcp-install.ts +106 -0
  84. package/src/apps/cli/retrieval-disclosure-output.ts +196 -0
  85. package/src/apps/dashboard/assets/js/bootstrap.js +244 -0
  86. package/src/apps/dashboard/assets/js/chat.js +373 -0
  87. package/src/apps/dashboard/assets/js/disclosure.js +232 -0
  88. package/src/apps/dashboard/assets/js/modals.js +298 -0
  89. package/src/apps/dashboard/assets/js/overview.js +655 -0
  90. package/src/apps/dashboard/assets/js/state.js +72 -0
  91. package/src/apps/dashboard/assets/js/views.js +468 -0
  92. package/src/{ui → apps/dashboard}/index.html +43 -1
  93. package/src/apps/dashboard/index.ts +3 -0
  94. package/src/{ui → apps/dashboard}/style.css +222 -0
  95. package/src/apps/index.ts +5 -0
  96. package/src/apps/server/api/chat.ts +244 -0
  97. package/src/apps/server/api/citations.ts +105 -0
  98. package/src/apps/server/api/events.ts +137 -0
  99. package/src/apps/server/api/health.ts +53 -0
  100. package/src/apps/server/api/index.ts +26 -0
  101. package/src/apps/server/api/projects.ts +74 -0
  102. package/src/apps/server/api/search.ts +184 -0
  103. package/src/apps/server/api/sessions.ts +115 -0
  104. package/src/apps/server/api/stats.ts +723 -0
  105. package/src/apps/server/api/turns.ts +143 -0
  106. package/src/apps/server/api/utils.ts +65 -0
  107. package/src/apps/server/index.ts +111 -0
  108. package/src/cli/index.ts +2 -1311
  109. package/src/cli/retrieval-disclosure-output.ts +2 -0
  110. package/src/compat/index.ts +5 -0
  111. package/src/core/derive/fact-deriver.ts +170 -0
  112. package/src/core/derive/index.ts +2 -0
  113. package/src/core/derive/summary-deriver.ts +76 -0
  114. package/src/core/embedder.ts +4 -152
  115. package/src/core/engine/embedding-maintenance-service.ts +187 -0
  116. package/src/core/engine/endless-memory-services.ts +4 -0
  117. package/src/core/engine/index.ts +19 -0
  118. package/src/core/engine/memory-engine-services.ts +170 -0
  119. package/src/core/engine/memory-ingest-service.ts +317 -0
  120. package/src/core/engine/memory-query-service.ts +173 -0
  121. package/src/core/engine/memory-runtime-service.ts +162 -0
  122. package/src/core/engine/memory-service-composition.ts +231 -0
  123. package/src/core/engine/retrieval-analytics-service.ts +181 -0
  124. package/src/core/engine/retrieval-disclosure-service.ts +420 -0
  125. package/src/core/engine/retrieval-orchestrator.ts +377 -0
  126. package/src/core/engine/retrieval-services.ts +176 -0
  127. package/src/core/engine/shared-memory-services.ts +4 -0
  128. package/src/core/entity-repo.ts +1 -3
  129. package/src/core/event-store.ts +3 -3
  130. package/src/core/evidence-aligner.ts +2 -2
  131. package/src/core/external-market-context.ts +582 -0
  132. package/src/core/graduation.ts +2 -3
  133. package/src/core/index.ts +21 -0
  134. package/src/core/matcher.ts +2 -4
  135. package/src/core/model/memory-fact.ts +30 -0
  136. package/src/core/model/memory-rule.ts +14 -0
  137. package/src/core/model/memory-summary.ts +21 -0
  138. package/src/core/model/raw-event.ts +28 -0
  139. package/src/core/model/retrieval-result.ts +35 -0
  140. package/src/core/privacy/filter.ts +21 -10
  141. package/src/core/product-validation-matrix.ts +314 -0
  142. package/src/core/progressive-retriever.ts +1 -2
  143. package/src/core/registry/project-path.ts +54 -0
  144. package/src/core/registry/session-registry.ts +69 -0
  145. package/src/core/replay-evaluator.ts +625 -0
  146. package/src/core/retrieval-benchmark.ts +117 -0
  147. package/src/core/retrieval-quality.ts +109 -0
  148. package/src/core/retriever.ts +53 -15
  149. package/src/core/session-qrels.ts +360 -0
  150. package/src/core/shared-event-store.ts +1 -1
  151. package/src/core/sqlite-event-store.ts +35 -11
  152. package/src/core/task/blocker-resolver.ts +2 -2
  153. package/src/core/task/task-resolver.ts +0 -1
  154. package/src/core/vector-outbox.ts +1 -10
  155. package/src/core/vector-worker.ts +1 -1
  156. package/src/extensions/endless-memory/endless-memory-services.ts +350 -0
  157. package/src/extensions/endless-memory/index.ts +1 -0
  158. package/src/extensions/index.ts +5 -0
  159. package/src/extensions/mcp/handlers.ts +960 -0
  160. package/src/extensions/mcp/index.ts +48 -0
  161. package/src/extensions/mcp/tools.ts +252 -0
  162. package/src/extensions/shared-memory/index.ts +1 -0
  163. package/src/extensions/shared-memory/shared-memory-services.ts +211 -0
  164. package/src/extensions/vector/embedder.ts +197 -0
  165. package/src/extensions/vector/index.ts +1 -0
  166. package/src/hooks/post-tool-use.ts +3 -236
  167. package/src/hooks/semantic-daemon-client.ts +1 -208
  168. package/src/hooks/semantic-daemon.ts +6 -271
  169. package/src/hooks/session-end.ts +4 -79
  170. package/src/hooks/session-start.ts +4 -73
  171. package/src/hooks/stop.ts +3 -173
  172. package/src/hooks/user-prompt-submit.ts +3 -338
  173. package/src/index.ts +13 -0
  174. package/src/mcp/handlers.ts +2 -212
  175. package/src/mcp/index.ts +3 -46
  176. package/src/mcp/tools.ts +2 -78
  177. package/src/server/api/chat.ts +2 -244
  178. package/src/server/api/citations.ts +2 -105
  179. package/src/server/api/events.ts +2 -137
  180. package/src/server/api/health.ts +2 -53
  181. package/src/server/api/index.ts +2 -26
  182. package/src/server/api/projects.ts +2 -74
  183. package/src/server/api/search.ts +2 -102
  184. package/src/server/api/sessions.ts +2 -115
  185. package/src/server/api/stats.ts +2 -724
  186. package/src/server/api/turns.ts +2 -143
  187. package/src/server/api/utils.ts +2 -46
  188. package/src/server/index.ts +2 -100
  189. package/src/services/bootstrap-organizer.ts +46 -26
  190. package/src/services/codex-session-history-importer.ts +521 -29
  191. package/src/services/hermes-session-history-importer.ts +733 -0
  192. package/src/services/memory-service-config.ts +36 -0
  193. package/src/services/memory-service-registry.ts +150 -0
  194. package/src/services/memory-service.ts +211 -1325
  195. package/src/services/session-history-importer.ts +58 -14
  196. package/tests/README.md +23 -0
  197. package/tests/adapters/claude/claude-semantic-daemon-adapter.test.ts +54 -0
  198. package/tests/adapters/claude/claude-transcript-reconstructor.test.ts +98 -0
  199. package/tests/adapters/claude-hook-prompt-injection-policy.test.ts +99 -0
  200. package/tests/apps/app-layer-boundary.test.ts +48 -0
  201. package/tests/apps/claude-settings-hooks.test.ts +107 -0
  202. package/tests/apps/cli-disclosure-output.test.ts +212 -0
  203. package/tests/apps/codex-import-runner.test.ts +99 -0
  204. package/tests/apps/codex-validation-output.test.ts +100 -0
  205. package/tests/apps/hermes-import-runner.test.ts +99 -0
  206. package/tests/apps/mcp-install-command.test.ts +59 -0
  207. package/tests/apps/package-build-entrypoints.test.ts +30 -0
  208. package/tests/apps/search-api-disclosure.test.ts +162 -0
  209. package/tests/apps/stats-api-lightweight.test.ts +67 -0
  210. package/tests/apps/ui-disclosure-output.test.ts +140 -0
  211. package/tests/{bootstrap-organizer.test.ts → core/bootstrap-organizer.test.ts} +1 -1
  212. package/tests/{canonical-key.test.ts → core/canonical-key.test.ts} +1 -1
  213. package/tests/core/codex-session-history-importer-validation.test.ts +185 -0
  214. package/tests/{consolidation-worker.test.ts → core/consolidation-worker.test.ts} +2 -2
  215. package/tests/core/embedding-maintenance-service.test.ts +282 -0
  216. package/tests/{evidence-aligner.test.ts → core/evidence-aligner.test.ts} +1 -1
  217. package/tests/core/external-market-context.test.ts +209 -0
  218. package/tests/core/fact-deriver.test.ts +79 -0
  219. package/tests/core/hermes-session-history-importer-validation.test.ts +609 -0
  220. package/tests/{ingest-interceptor.test.ts → core/ingest-interceptor.test.ts} +1 -1
  221. package/tests/{markdown-mirror.test.ts → core/markdown-mirror.test.ts} +2 -2
  222. package/tests/{matcher.test.ts → core/matcher.test.ts} +1 -1
  223. package/tests/{md-mirror.test.ts → core/md-mirror.test.ts} +2 -2
  224. package/tests/core/memory-engine-services.test.ts +240 -0
  225. package/tests/core/memory-ingest-service.test.ts +296 -0
  226. package/tests/core/memory-query-service.test.ts +129 -0
  227. package/tests/core/memory-runtime-service.test.ts +201 -0
  228. package/tests/core/memory-service-composition.test.ts +192 -0
  229. package/tests/core/memory-service-config.test.ts +41 -0
  230. package/tests/core/memory-service-facade.test.ts +30 -0
  231. package/tests/core/memory-service-registry.test.ts +206 -0
  232. package/tests/core/product-validation-matrix.test.ts +61 -0
  233. package/tests/core/project-registry.test.ts +78 -0
  234. package/tests/core/replay-evaluator.test.ts +181 -0
  235. package/tests/core/retrieval-analytics-service.test.ts +210 -0
  236. package/tests/core/retrieval-benchmark.test.ts +93 -0
  237. package/tests/core/retrieval-disclosure-service.test.ts +264 -0
  238. package/tests/core/retrieval-orchestrator.test.ts +403 -0
  239. package/tests/core/retrieval-quality.test.ts +31 -0
  240. package/tests/core/retrieval-services.test.ts +185 -0
  241. package/tests/{retriever-fallback-chain.test.ts → core/retriever-fallback-chain.test.ts} +3 -3
  242. package/tests/{retriever-strategy-scope.test.ts → core/retriever-strategy-scope.test.ts} +70 -3
  243. package/tests/{retriever.memu-adoption.test.ts → core/retriever.memu-adoption.test.ts} +3 -3
  244. package/tests/core/session-history-importer-filter.test.ts +78 -0
  245. package/tests/core/session-qrels.test.ts +250 -0
  246. package/tests/{sqlite-event-store-replication.test.ts → core/sqlite-event-store-replication.test.ts} +36 -1
  247. package/tests/core/summary-deriver.test.ts +66 -0
  248. package/tests/extensions/embedder-warning-suppression.test.ts +53 -0
  249. package/tests/extensions/endless-memory-extension-boundary.test.ts +17 -0
  250. package/tests/extensions/endless-memory-services.test.ts +325 -0
  251. package/tests/extensions/mcp-context-tools.test.ts +905 -0
  252. package/tests/extensions/mcp-extension-boundary.test.ts +21 -0
  253. package/tests/extensions/mcp-package-build.test.ts +22 -0
  254. package/tests/extensions/mcp-project-aware-tools.test.ts +102 -0
  255. package/tests/extensions/shared-memory-extension-boundary.test.ts +24 -0
  256. package/tests/extensions/shared-memory-services.test.ts +309 -0
  257. package/tests/extensions/vector-extension-boundary.test.ts +21 -0
  258. package/.claude/settings.local.json +0 -25
  259. package/.npm-cache/_cacache/content-v2/sha512/04/76/c098f88dfe584a2b80870bff7421b05d17d3d9ee1027f77772332a22d3f93a9a57101a2855107f6ad82077a818bba912b2bc317f2361b5ddb09ad284d9ce +0 -0
  260. package/.npm-cache/_cacache/content-v2/sha512/60/25/d2ecd39cfc7cab58351162814be77f935c6d6491c10c3745d456da7ddb2117ffd90c10e53fe3c0f1ed16b403307841543634504398b16ee4e6b6dd8e0c45 +0 -0
  261. package/.npm-cache/_cacache/index-v5/2b/9a/7f8f40206ed8a2e0a84efaa953ccaed1f5d001e14b931083f2e7a0738007 +0 -2
  262. package/.npm-cache/_cacache/index-v5/2e/d9/fcfa5c6a6abdc2a3644ab84a95936047298c465a2f47ee03db8f7fe1e946 +0 -3
  263. package/.npm-cache/_cacache/index-v5/a9/42/e519633356d12d3d2f19da66a8301016d496c8f5c3e0554124aaa62dc043 +0 -2
  264. package/.npm-cache/_logs/2026-02-26T12_04_52_729Z-debug-0.log +0 -256
  265. package/.npm-cache/_logs/2026-02-26T12_05_36_835Z-debug-0.log +0 -18
  266. package/.npm-cache/_logs/2026-02-26T12_05_45_982Z-debug-0.log +0 -32
  267. package/.npm-cache/_logs/2026-02-26T12_05_48_515Z-debug-0.log +0 -260
  268. package/.npm-cache/_logs/2026-02-26T12_05_53_567Z-debug-0.log +0 -69
  269. package/.npm-cache/_update-notifier-last-checked +0 -0
  270. package/bootstrap-kb/decisions/decisions.md +0 -244
  271. package/bootstrap-kb/glossary/glossary.md +0 -46
  272. package/bootstrap-kb/modules/.claude-plugin.md +0 -22
  273. package/bootstrap-kb/modules/agents.md.md +0 -15
  274. package/bootstrap-kb/modules/claude.md.md +0 -15
  275. package/bootstrap-kb/modules/context.md.md +0 -15
  276. package/bootstrap-kb/modules/docs.md +0 -18
  277. package/bootstrap-kb/modules/handoff.md.md +0 -15
  278. package/bootstrap-kb/modules/package-lock.json.md +0 -15
  279. package/bootstrap-kb/modules/package.json.md +0 -15
  280. package/bootstrap-kb/modules/plan.md.md +0 -15
  281. package/bootstrap-kb/modules/readme.md.md +0 -15
  282. package/bootstrap-kb/modules/scripts.md +0 -26
  283. package/bootstrap-kb/modules/spec.md.md +0 -15
  284. package/bootstrap-kb/modules/specs.md +0 -20
  285. package/bootstrap-kb/modules/src.md +0 -51
  286. package/bootstrap-kb/modules/tests.md +0 -42
  287. package/bootstrap-kb/modules/tsconfig.json.md +0 -15
  288. package/bootstrap-kb/modules/vitest.config.ts.md +0 -15
  289. package/bootstrap-kb/overview/overview.md +0 -40
  290. package/bootstrap-kb/sources/manifest.json +0 -950
  291. package/bootstrap-kb/sources/manifest.md +0 -227
  292. package/bootstrap-kb/timeline/timeline.md +0 -57
  293. package/claude-memory-layer-1.0.14.tgz +0 -0
  294. package/d.sh +0 -3
  295. package/deploy.sh +0 -3
  296. package/dist/ui/app.js +0 -2101
  297. package/memory/.claude-plugin/commands/2026-02-25.md +0 -263
  298. package/memory/_index.md +0 -419
  299. package/memory/agent_response/uncategorized/2026-02-26.md +0 -176
  300. package/memory/agent_response/uncategorized/2026-03-03.md +0 -14
  301. package/memory/agent_response/uncategorized/2026-03-04.md +0 -1421
  302. package/memory/agent_response/uncategorized/2026-03-05.md +0 -157
  303. package/memory/default/uncategorized/2026-02-25.md +0 -4839
  304. package/memory/session_summary/uncategorized/2026-02-26.md +0 -13
  305. package/memory/session_summary/uncategorized/2026-03-03.md +0 -5
  306. package/memory/session_summary/uncategorized/2026-03-04.md +0 -50
  307. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +0 -142
  308. package/memory/specs/citations-system/2026-02-25.md +0 -1121
  309. package/memory/specs/endless-mode/2026-02-25.md +0 -1392
  310. package/memory/specs/entity-edge-model/2026-02-25.md +0 -1263
  311. package/memory/specs/evidence-aligner-v2/2026-02-25.md +0 -1028
  312. package/memory/specs/mcp-desktop-integration/2026-02-25.md +0 -1334
  313. package/memory/specs/post-tool-use-hook/2026-02-25.md +0 -1164
  314. package/memory/specs/private-tags/2026-02-25.md +0 -1057
  315. package/memory/specs/progressive-disclosure/2026-02-25.md +0 -1436
  316. package/memory/specs/task-entity-system/2026-02-25.md +0 -924
  317. package/memory/specs/vector-outbox-v2/2026-02-25.md +0 -1510
  318. package/memory/specs/web-viewer-ui/2026-02-25.md +0 -1709
  319. package/memory/tool_observation/uncategorized/2026-02-26.md +0 -209
  320. package/memory/tool_observation/uncategorized/2026-03-03.md +0 -21
  321. package/memory/tool_observation/uncategorized/2026-03-04.md +0 -1033
  322. package/memory/tool_observation/uncategorized/2026-03-05.md +0 -33
  323. package/memory/user_prompt/uncategorized/2026-02-26.md +0 -25
  324. package/memory/user_prompt/uncategorized/2026-03-04.md +0 -634
  325. package/memory/user_prompt/uncategorized/2026-03-05.md +0 -6
  326. package/specs/optional-duckdb/context.md +0 -77
  327. package/specs/optional-duckdb/plan.md +0 -142
  328. package/specs/optional-duckdb/spec.md +0 -35
  329. package/src/ui/app.js +0 -2101
package/dist/ui/app.js DELETED
@@ -1,2101 +0,0 @@
1
- /**
2
- * Code Memory Dashboard Logic
3
- * Handles state management, API calls, UI updates, modals, and navigation.
4
- */
5
-
6
- const API_BASE = '/api';
7
-
8
- // State
9
- const state = {
10
- stats: null,
11
- sharedStats: null,
12
- mostAccessed: null,
13
- helpfulness: null,
14
- retrievalTraces: null,
15
- adherenceSummary: null,
16
- adherenceWindow: '24h',
17
- userPromptSearchQuery: '',
18
- userPromptItems: [],
19
- userPromptPage: 1,
20
- userPromptPageSize: 30,
21
- currentLevel: 'L0',
22
- currentSort: 'recent',
23
- currentView: 'overview',
24
- currentProject: '', // empty = global
25
- projects: [],
26
- events: [],
27
- isLoading: false,
28
- chartInstance: null,
29
- kpiChartInstance: null,
30
- kpiWindow: '7d',
31
- kpi: null,
32
- chatMessages: [],
33
- isChatOpen: false,
34
- isChatStreaming: false,
35
- chatAbortController: null,
36
- chatConversationId: null,
37
- chatCurrentTab: 'chat'
38
- };
39
-
40
- // Utils
41
- const formatNumber = (num) => new Intl.NumberFormat().format(num || 0);
42
-
43
- // Colors for Chart
44
- const CHART_COLORS = {
45
- L0: '#7B61FF',
46
- L1: '#00F0FF',
47
- L2: '#00E396',
48
- L3: '#FEB019',
49
- L4: '#FF4560'
50
- };
51
-
52
- // --- API URL Helper ---
53
-
54
- function apiUrl(path, params = {}) {
55
- const url = new URL(path, window.location.origin);
56
- if (state.currentProject) {
57
- url.searchParams.set('project', state.currentProject);
58
- }
59
- for (const [key, value] of Object.entries(params)) {
60
- if (value !== undefined && value !== null) {
61
- url.searchParams.set(key, String(value));
62
- }
63
- }
64
- return url.toString();
65
- }
66
-
67
- // --- Initialization ---
68
-
69
- document.addEventListener('DOMContentLoaded', () => {
70
- initDashboard();
71
- });
72
-
73
- async function initDashboard() {
74
- await loadProjects();
75
- await refreshData();
76
- setupEventListeners();
77
- await initActivityChart();
78
- }
79
-
80
- async function loadProjects() {
81
- try {
82
- const res = await fetch(`${API_BASE}/projects`);
83
- const data = await res.json();
84
- state.projects = data.projects || [];
85
-
86
- const select = document.getElementById('project-select');
87
- if (!select) return;
88
-
89
- // Clear existing options except first
90
- while (select.options.length > 1) select.remove(1);
91
-
92
- // Add project options
93
- state.projects.forEach(p => {
94
- const option = document.createElement('option');
95
- option.value = p.hash;
96
- option.textContent = `${p.projectName} (${p.dbSizeHuman})`;
97
- select.appendChild(option);
98
- });
99
- } catch (error) {
100
- console.error('Failed to load projects:', error);
101
- }
102
- }
103
-
104
- function setupEventListeners() {
105
- // Pipeline steps
106
- document.querySelectorAll('.p-step').forEach(step => {
107
- step.addEventListener('click', (e) => {
108
- const level = e.currentTarget.dataset.level;
109
- if (level) selectLevel(level);
110
- });
111
- });
112
-
113
- // Sort buttons
114
- document.querySelectorAll('.sort-btn[data-sort]').forEach(btn => {
115
- btn.addEventListener('click', (e) => {
116
- const sort = e.currentTarget.dataset.sort;
117
- if (sort) selectSort(sort);
118
- });
119
- });
120
-
121
- // Adherence window controls
122
- document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(btn => {
123
- btn.addEventListener('click', async (e) => {
124
- const window = e.currentTarget.dataset.adhWindow;
125
- if (!window || state.adherenceWindow === window) return;
126
- state.adherenceWindow = window;
127
- document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(b => {
128
- b.classList.toggle('active', b.dataset.adhWindow === window);
129
- });
130
- state.adherenceSummary = await fetchAdherenceSummary().catch(() => null);
131
- updateAdherenceSummaryUI();
132
- });
133
- });
134
-
135
- // KPI window controls
136
- document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(btn => {
137
- btn.addEventListener('click', async (e) => {
138
- const window = e.currentTarget.dataset.kpiWindow;
139
- if (!window || state.kpiWindow === window) return;
140
- state.kpiWindow = window;
141
- document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(b => {
142
- b.classList.toggle('active', b.dataset.kpiWindow === window);
143
- });
144
- await loadKpiData();
145
- updateKpiCardsUI();
146
- renderKpiTrendChart();
147
- });
148
- });
149
-
150
- // Search
151
- const searchInput = document.getElementById('search-input');
152
- if (searchInput) {
153
- searchInput.addEventListener('input', debounce((e) => handleSearch(e.target.value), 300));
154
- }
155
-
156
- // User prompt search
157
- const userPromptSearch = document.getElementById('user-prompt-search');
158
- if (userPromptSearch) {
159
- userPromptSearch.addEventListener('input', debounce(async (e) => {
160
- state.userPromptSearchQuery = e.target.value || '';
161
- state.userPromptPage = 1;
162
- await loadUserPromptsView();
163
- }, 250));
164
- }
165
- const userPromptRefresh = document.getElementById('user-prompt-refresh');
166
- if (userPromptRefresh) {
167
- userPromptRefresh.addEventListener('click', async () => {
168
- await loadUserPromptsView();
169
- });
170
- }
171
- const userPromptPrev = document.getElementById('user-prompt-prev');
172
- if (userPromptPrev) {
173
- userPromptPrev.addEventListener('click', async () => {
174
- if (state.userPromptPage <= 1) return;
175
- state.userPromptPage -= 1;
176
- await renderUserPromptList();
177
- });
178
- }
179
- const userPromptNext = document.getElementById('user-prompt-next');
180
- if (userPromptNext) {
181
- userPromptNext.addEventListener('click', async () => {
182
- const totalPages = Math.max(1, Math.ceil((state.userPromptItems?.length || 0) / state.userPromptPageSize));
183
- if (state.userPromptPage >= totalPages) return;
184
- state.userPromptPage += 1;
185
- await renderUserPromptList();
186
- });
187
- }
188
-
189
- // Project selector
190
- const projectSelect = document.getElementById('project-select');
191
- if (projectSelect) {
192
- projectSelect.addEventListener('change', async (e) => {
193
- state.currentProject = e.target.value;
194
- await refreshData();
195
- if (state.chartInstance) {
196
- state.chartInstance.destroy();
197
- state.chartInstance = null;
198
- }
199
- if (state.kpiChartInstance) {
200
- state.kpiChartInstance.destroy();
201
- state.kpiChartInstance = null;
202
- }
203
- await initActivityChart();
204
- // Reload current view if not overview
205
- if (state.currentView !== 'overview') {
206
- switchView(state.currentView);
207
- }
208
- updateChatProjectScope();
209
- });
210
- }
211
-
212
- // Refresh
213
- const refreshBtn = document.getElementById('refresh-btn');
214
- if (refreshBtn) {
215
- refreshBtn.addEventListener('click', refreshData);
216
- }
217
-
218
- // Stat cards
219
- document.querySelectorAll('.stat-card[data-stat]').forEach(card => {
220
- card.addEventListener('click', () => {
221
- handleStatClick(card.dataset.stat);
222
- });
223
- });
224
-
225
- // Sidebar navigation
226
- document.querySelectorAll('.nav-item[data-nav]').forEach(item => {
227
- item.addEventListener('click', () => {
228
- switchView(item.dataset.nav);
229
- });
230
- });
231
-
232
- // Modal close buttons
233
- document.querySelectorAll('.modal-close-btn').forEach(btn => {
234
- btn.addEventListener('click', () => {
235
- const modalId = btn.dataset.modal;
236
- closeModal(modalId);
237
- });
238
- });
239
-
240
- // Modal overlay click to close
241
- document.querySelectorAll('.modal-overlay').forEach(overlay => {
242
- overlay.addEventListener('click', (e) => {
243
- if (e.target === overlay) {
244
- closeModal(overlay.id);
245
- }
246
- });
247
- });
248
-
249
- // ESC key to close modals
250
- document.addEventListener('keydown', (e) => {
251
- if (e.key === 'Escape') {
252
- if (state.isChatOpen) {
253
- closeChatPanel();
254
- } else {
255
- closeAllModals();
256
- }
257
- }
258
- });
259
-
260
- // Chat panel
261
- const chatToggle = document.getElementById('chat-toggle-btn');
262
- if (chatToggle) {
263
- chatToggle.addEventListener('click', toggleChatPanel);
264
- }
265
- const chatClose = document.getElementById('chat-close-btn');
266
- if (chatClose) {
267
- chatClose.addEventListener('click', () => closeChatPanel());
268
- }
269
-
270
- const chatInput = document.getElementById('chat-input');
271
- const chatSendBtn = document.getElementById('chat-send-btn');
272
- if (chatInput) {
273
- chatInput.addEventListener('input', () => {
274
- chatInput.style.height = 'auto';
275
- chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
276
- chatSendBtn.disabled = !chatInput.value.trim() || state.isChatStreaming;
277
- });
278
- chatInput.addEventListener('keydown', (e) => {
279
- if (e.key === 'Enter' && !e.shiftKey) {
280
- e.preventDefault();
281
- if (chatInput.value.trim() && !state.isChatStreaming) {
282
- sendChatMessage();
283
- }
284
- }
285
- });
286
- }
287
- if (chatSendBtn) {
288
- chatSendBtn.addEventListener('click', () => {
289
- if (!state.isChatStreaming) sendChatMessage();
290
- });
291
- }
292
-
293
- // Chat tabs
294
- document.querySelectorAll('.chat-header-tab').forEach(tab => {
295
- tab.addEventListener('click', () => {
296
- switchChatTab(tab.dataset.chatTab);
297
- });
298
- });
299
-
300
- // New conversation button
301
- const chatNewBtn = document.getElementById('chat-new-btn');
302
- if (chatNewBtn) {
303
- chatNewBtn.addEventListener('click', startNewConversation);
304
- }
305
- }
306
-
307
- // --- Data Fetching ---
308
-
309
- async function loadKpiData() {
310
- state.kpi = await fetch(apiUrl(`${API_BASE}/stats/kpi`, { window: state.kpiWindow }))
311
- .then(r => r.json())
312
- .catch(() => null);
313
- }
314
-
315
- async function refreshData() {
316
- const btn = document.getElementById('refresh-btn');
317
- if(btn) btn.classList.add('loading');
318
-
319
- try {
320
- const [stats, shared, mostAccessed, helpfulness, retrievalTraces, adherenceSummary] = await Promise.all([
321
- fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
322
- fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
323
- fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
324
- fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null),
325
- fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null),
326
- fetchAdherenceSummary().catch(() => null)
327
- ]);
328
-
329
- state.stats = stats;
330
- state.sharedStats = shared;
331
- state.mostAccessed = mostAccessed;
332
- state.helpfulness = helpfulness;
333
- state.retrievalTraces = retrievalTraces;
334
- state.adherenceSummary = adherenceSummary;
335
-
336
- await loadKpiData();
337
-
338
- updateStatsUI();
339
- updateSharedUI();
340
- updateMemoryUsageUI();
341
- updateKpiCardsUI();
342
- renderKpiTrendChart();
343
- await loadLevelEvents(state.currentLevel);
344
-
345
- checkEndlessStatus();
346
-
347
- } catch (error) {
348
- console.error('Failed to refresh data:', error);
349
- } finally {
350
- if(btn) btn.classList.remove('loading');
351
- }
352
- }
353
-
354
- async function loadLevelEvents(level, sort) {
355
- if (sort) state.currentSort = sort;
356
- state.isLoading = true;
357
- updateEventsListUI();
358
-
359
- try {
360
- const response = await fetch(apiUrl(`${API_BASE}/events`, { level, limit: 20, sort: state.currentSort }));
361
- if (response.ok) {
362
- const data = await response.json();
363
- state.events = data.events || [];
364
- } else {
365
- state.events = [];
366
- }
367
- } catch (error) {
368
- console.error(`Failed to load events for ${level}:`, error);
369
- state.events = [];
370
- } finally {
371
- state.isLoading = false;
372
- updateEventsListUI();
373
- }
374
- }
375
-
376
- // --- UI Updates ---
377
-
378
- function updateStatsUI() {
379
- if (!state.stats) return;
380
-
381
- const eventCount = state.stats.storage?.eventCount || 0;
382
- const sessionCount = state.stats.sessions?.total || 0;
383
- const vectorCount = state.stats.storage?.vectorCount || 0;
384
-
385
- document.getElementById('stat-events').textContent = formatNumber(eventCount);
386
- document.getElementById('stat-sessions').textContent = formatNumber(sessionCount);
387
-
388
- const sharedCount = state.sharedStats ?
389
- ((state.sharedStats.troubleshooting || 0) + (state.sharedStats.bestPractices || 0) + (state.sharedStats.commonErrors || 0)) : 0;
390
-
391
- document.getElementById('stat-vectors').textContent = formatNumber(vectorCount);
392
-
393
- // Retrieval quality stat card
394
- const rtStats = state.retrievalTraces?.stats;
395
- const totalQueries = rtStats?.totalQueries || 0;
396
- const selRate = rtStats ? ((rtStats.selectionRate || 0) * 100).toFixed(0) : null;
397
- document.getElementById('stat-retrieval-queries').textContent = formatNumber(totalQueries);
398
- const rateEl = document.getElementById('stat-retrieval-rate');
399
- if (rateEl) {
400
- rateEl.textContent = totalQueries > 0 && selRate !== null
401
- ? `${selRate}% selection rate`
402
- : totalQueries > 0 ? '' : 'no queries yet';
403
- }
404
-
405
- const levelCounts = {};
406
- if (state.stats.levelStats) {
407
- state.stats.levelStats.forEach(item => { levelCounts[item.level] = item.count; });
408
- }
409
- updatePipelineCounts(levelCounts);
410
- }
411
-
412
- function updatePipelineCounts(counts) {
413
- document.querySelectorAll('.p-step').forEach(step => {
414
- const level = step.dataset.level;
415
- const countEl = step.querySelector('.p-step-count');
416
- countEl.textContent = formatNumber(counts[level] || 0);
417
- });
418
- }
419
-
420
- function updateSharedUI() {
421
- if (!state.sharedStats) return;
422
-
423
- document.getElementById('shared-troubleshooting').textContent = formatNumber(state.sharedStats.troubleshooting || 0);
424
- document.getElementById('shared-best-practices').textContent = formatNumber(state.sharedStats.bestPractices || 0);
425
- document.getElementById('shared-errors').textContent = formatNumber(state.sharedStats.commonErrors || 0);
426
- }
427
-
428
- function percentText(v) {
429
- return `${((v || 0) * 100).toFixed(1)}%`;
430
- }
431
-
432
- function renderDelta(id, value, lowerIsBetter = false, asPercent = true) {
433
- const el = document.getElementById(id);
434
- if (!el) return;
435
- const v = Number(value || 0);
436
- const sign = v > 0 ? '+' : '';
437
- const text = asPercent ? `${sign}${(v * 100).toFixed(1)}%` : `${sign}${v.toFixed(2)}`;
438
-
439
- let positive = v > 0;
440
- if (lowerIsBetter) positive = v < 0;
441
- const cls = v === 0 ? 'neutral' : (positive ? 'good' : 'bad');
442
- const arrow = v === 0 ? '→' : (positive ? '▲' : '▼');
443
-
444
- el.className = `kpi-delta ${cls}`;
445
- el.textContent = `${arrow} ${text} vs prev`;
446
- }
447
-
448
- function updateKpiCardsUI() {
449
- const m = state.kpi?.metrics;
450
- const d = state.kpi?.deltas;
451
- if (!m) return;
452
- const set = (id, value) => {
453
- const el = document.getElementById(id);
454
- if (el) el.textContent = value;
455
- };
456
- set('kpi-useful-recall', percentText(m.usefulRecallRate));
457
- set('kpi-completion-turns', Number(m.avgCompletionTurns || 0).toFixed(2));
458
- set('kpi-rework-rate', percentText(m.reworkRate));
459
- set('kpi-failure-rate', percentText(m.postChangeFailureRate));
460
-
461
- if (d) {
462
- renderDelta('kpi-useful-recall-delta', d.usefulRecallRate, false, true);
463
- renderDelta('kpi-completion-turns-delta', d.avgCompletionTurns, true, false);
464
- renderDelta('kpi-rework-rate-delta', d.reworkRate, true, true);
465
- renderDelta('kpi-failure-rate-delta', d.postChangeFailureRate, true, true);
466
- }
467
-
468
- const alertsEl = document.getElementById('kpi-alerts');
469
- if (alertsEl) {
470
- const alerts = state.kpi?.alerts || [];
471
- if (alerts.length === 0) {
472
- alertsEl.innerHTML = '<span style="color:var(--success);">No KPI alerts in current window.</span>';
473
- } else {
474
- alertsEl.innerHTML = alerts.slice(0, 3).map(a => `⚠️ ${escapeHtml(a.message)} (${a.metric})`).join(' · ');
475
- }
476
- }
477
- }
478
-
479
- function renderKpiTrendChart() {
480
- const chartEl = document.querySelector('#kpi-trend-chart');
481
- if (!chartEl) return;
482
-
483
- const daily = state.kpi?.trend?.daily || [];
484
- const categories = daily.map(d => d.date);
485
- const useful = daily.map(d => Number(d.usefulRecallRate || 0) * 100);
486
- const rework = daily.map(d => Number(d.reworkRate || 0) * 100);
487
- const fail = daily.map(d => Number(d.postChangeFailureRate || 0) * 100);
488
-
489
- if (state.kpiChartInstance) {
490
- state.kpiChartInstance.destroy();
491
- state.kpiChartInstance = null;
492
- }
493
-
494
- const options = {
495
- series: [
496
- { name: 'Useful Recall %', data: useful },
497
- { name: 'Rework %', data: rework },
498
- { name: 'Failure %', data: fail }
499
- ],
500
- chart: {
501
- type: 'line',
502
- height: 240,
503
- background: 'transparent',
504
- toolbar: { show: false },
505
- fontFamily: 'Outfit, sans-serif'
506
- },
507
- stroke: { curve: 'smooth', width: 2 },
508
- dataLabels: { enabled: false },
509
- xaxis: { categories, labels: { style: { colors: '#8B9BB4' } } },
510
- yaxis: { labels: { formatter: (v) => `${v.toFixed(0)}%`, style: { colors: '#8B9BB4' } } },
511
- theme: { mode: 'dark' },
512
- grid: { borderColor: 'rgba(255,255,255,0.05)', strokeDashArray: 4 },
513
- colors: ['#34D399', '#FEB019', '#FF4560']
514
- };
515
-
516
- state.kpiChartInstance = new ApexCharts(chartEl, options);
517
- state.kpiChartInstance.render();
518
- }
519
-
520
- function selectLevel(level) {
521
- state.currentLevel = level;
522
-
523
- document.querySelectorAll('.p-step').forEach(step => {
524
- step.classList.toggle('active', step.dataset.level === level);
525
- });
526
-
527
- loadLevelEvents(level);
528
- }
529
-
530
- function selectSort(sort) {
531
- state.currentSort = sort;
532
-
533
- document.querySelectorAll('.sort-btn').forEach(btn => {
534
- btn.classList.toggle('active', btn.dataset.sort === sort);
535
- });
536
-
537
- loadLevelEvents(state.currentLevel, sort);
538
- }
539
-
540
- function getAdherenceInfo(event) {
541
- const adherence = event?.metadata?.adherence || event?.meta?.adherence || null;
542
- if (!adherence || typeof adherence !== 'object') return null;
543
- const reason = adherence.reason || 'unknown';
544
- const checked = Boolean(adherence.checked);
545
- const turn = adherence.turn;
546
- return { reason, checked, turn };
547
- }
548
-
549
- function renderAdherenceBadge(event) {
550
- const info = getAdherenceInfo(event);
551
- if (!info) return '';
552
- const modeClass = info.checked ? 'adherence-checked' : 'adherence-skipped';
553
- const turnText = Number.isFinite(info.turn) ? ` · T${info.turn}` : '';
554
- return `<span class="adherence-badge ${modeClass}" title="adherence ${info.checked ? 'checked' : 'skipped'}${turnText}">adh:${escapeHtml(info.reason)}</span>`;
555
- }
556
-
557
- function updateEventsListUI() {
558
- const container = document.getElementById('event-list-container');
559
- container.innerHTML = '';
560
-
561
- if (state.isLoading) {
562
- container.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-muted);">Loading events...</div>';
563
- return;
564
- }
565
-
566
- if (state.events.length === 0) {
567
- container.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-muted);">No events found for this level.</div>';
568
- return;
569
- }
570
-
571
- state.events.forEach(event => {
572
- const el = document.createElement('div');
573
- el.className = 'event-item';
574
- el.style.cursor = 'pointer';
575
- el.addEventListener('click', () => openDetailModal(event.id));
576
-
577
- const time = new Date(event.timestamp).toLocaleString();
578
- const eventType = event.eventType || event.type || 'unknown';
579
- const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
580
- const preview = event.preview || event.content || '';
581
- const accessBadge = event.accessCount > 0
582
- ? `<span class="access-badge"><i class="ri-eye-line"></i> ${event.accessCount}</span>`
583
- : '';
584
- const adherenceBadge = renderAdherenceBadge(event);
585
- const lastUsed = (state.currentSort === 'accessed' || state.currentSort === 'most-accessed') && event.lastAccessedAt
586
- ? `<span class="event-time" style="color:var(--accent-secondary);">used ${new Date(event.lastAccessedAt).toLocaleString()}</span>`
587
- : '';
588
-
589
- el.innerHTML = `
590
- <div class="event-header">
591
- <div style="display:flex; gap:8px; align-items:center;">
592
- <span class="event-type-badge ${typeClass}">${eventType}</span>
593
- ${adherenceBadge}
594
- </div>
595
- <div style="display:flex; gap:8px; align-items:center;">
596
- ${accessBadge}
597
- ${lastUsed}
598
- <span class="event-time">${time}</span>
599
- </div>
600
- </div>
601
- <div class="event-content">${escapeHtml(preview)}</div>
602
- `;
603
-
604
- container.appendChild(el);
605
- });
606
- }
607
-
608
- // --- Memory Usage ---
609
-
610
- function updateTopAccessedEventsUI() {
611
- const container = document.getElementById('top-accessed-events-list');
612
- if (!container) return;
613
-
614
- const events = (state.mostAccessed?.events || state.mostAccessed?.memories || []);
615
- const filtered = events.filter(e => (e.accessCount || 0) > 0).slice(0, 5);
616
-
617
- if (filtered.length === 0) {
618
- container.innerHTML = '<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">No accessed memories yet</div>';
619
- return;
620
- }
621
-
622
- container.innerHTML = filtered.map((m, i) => {
623
- const type = m.eventType || m.type || 'memory';
624
- const preview = (m.summary || m.preview || m.content || '').replace(/<[^>]*>/g, '').slice(0, 80);
625
- const lastAccessed = m.lastAccessedAt ? new Date(m.lastAccessedAt).toLocaleDateString() : (m.lastAccessed ? new Date(m.lastAccessed).toLocaleDateString() : '-');
626
- const id = m.id || m.memoryId || '';
627
- return `
628
- <div class="shared-item" style="cursor:pointer;" ${id ? `onclick="openDetailModal('${id}')"` : ''}>
629
- <div class="shared-info" style="flex-direction:column; align-items:flex-start; gap:2px;">
630
- <div style="display:flex; gap:6px; align-items:center;">
631
- <span class="event-type-badge type-${type.replace('_','-')}">${type}</span>
632
- <span style="font-size:10px; color:var(--text-muted);">last: ${lastAccessed}</span>
633
- </div>
634
- <span style="font-size:12px; color:var(--text-secondary); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:200px;" title="${escapeHtml(preview)}">${escapeHtml(preview) || '(no preview)'}</span>
635
- </div>
636
- <div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px; min-width:40px;">
637
- <span style="font-size:15px; font-weight:700; color:var(--accent-primary);">${m.accessCount}</span>
638
- <span style="font-size:10px; color:var(--text-muted);">hits</span>
639
- </div>
640
- </div>
641
- `;
642
- }).join('');
643
- }
644
-
645
- function updateMemoryUsageUI() {
646
- updateGraduationBars();
647
- updateHelpfulnessUI();
648
- updateMostHelpfulList();
649
- updateTopAccessedEventsUI();
650
- updateAdherenceSummaryUI();
651
- updateRetrievalTraceUI();
652
- }
653
-
654
- function adherenceWindowToMs(window) {
655
- if (window === '24h') return 24 * 60 * 60 * 1000;
656
- if (window === '7d') return 7 * 24 * 60 * 60 * 1000;
657
- if (window === '30d') return 30 * 24 * 60 * 60 * 1000;
658
- return 24 * 60 * 60 * 1000;
659
- }
660
-
661
- async function fetchAdherenceSummary() {
662
- const res = await fetch(apiUrl(`${API_BASE}/events`, { level: 'L0', limit: 500, sort: 'recent' }));
663
- if (!res.ok) return null;
664
- const data = await res.json();
665
- const events = data.events || [];
666
-
667
- const counts = {};
668
- let checked = 0;
669
- let skipped = 0;
670
- let total = 0;
671
-
672
- const now = Date.now();
673
- const windowMs = adherenceWindowToMs(state.adherenceWindow);
674
-
675
- for (const e of events) {
676
- const ts = e?.timestamp ? new Date(e.timestamp).getTime() : 0;
677
- if (!ts || now - ts > windowMs) continue;
678
-
679
- const adherence = e?.metadata?.adherence || e?.meta?.adherence;
680
- if (!adherence) continue;
681
- total++;
682
- const reason = adherence.reason || 'unknown';
683
- counts[reason] = (counts[reason] || 0) + 1;
684
- if (adherence.checked) checked++; else skipped++;
685
- }
686
-
687
- return { total, checked, skipped, counts, window: state.adherenceWindow };
688
- }
689
-
690
- function updateAdherenceSummaryUI() {
691
- const el = document.getElementById('adherence-summary');
692
- if (!el) return;
693
-
694
- const s = state.adherenceSummary;
695
- if (!s || !s.total) {
696
- el.innerHTML = '<span style="color:var(--text-muted);">No adherence metadata yet.</span>';
697
- return;
698
- }
699
-
700
- const top = Object.entries(s.counts || {})
701
- .sort((a, b) => b[1] - a[1])
702
- .slice(0, 5)
703
- .map(([reason, count]) => `<span class="adherence-badge adherence-checked" style="margin-right:6px;">${escapeHtml(reason)}: ${count}</span>`)
704
- .join('');
705
-
706
- el.innerHTML = `
707
- <div style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:8px;">
708
- <span><strong>${s.total}</strong> tagged prompts (${escapeHtml(s.window || state.adherenceWindow)})</span>
709
- <span style="color:var(--success);"><strong>${s.checked}</strong> checked</span>
710
- <span style="color:var(--text-muted);"><strong>${s.skipped}</strong> skipped</span>
711
- </div>
712
- <div>${top}</div>
713
- `;
714
- }
715
-
716
- function updateGraduationBars() {
717
- const container = document.getElementById('graduation-bars');
718
- if (!container || !state.stats?.levelStats) return;
719
-
720
- const levels = ['L0', 'L1', 'L2', 'L3', 'L4'];
721
- const colors = [CHART_COLORS.L0, CHART_COLORS.L1, CHART_COLORS.L2, CHART_COLORS.L3, CHART_COLORS.L4];
722
-
723
- const counts = {};
724
- state.stats.levelStats.forEach(s => { counts[s.level] = s.count; });
725
- const total = Object.values(counts).reduce((a, b) => a + b, 0) || 1;
726
-
727
- container.innerHTML = levels.map((level, i) => {
728
- const count = counts[level] || 0;
729
- const pct = ((count / total) * 100).toFixed(1);
730
- return `
731
- <div class="grad-bar-row">
732
- <span class="grad-bar-label" style="color:${colors[i]}">${level}</span>
733
- <div class="grad-bar-track">
734
- <div class="grad-bar-fill" style="width:${pct}%; background:${colors[i]};"></div>
735
- </div>
736
- <span class="grad-bar-value">${count} (${pct}%)</span>
737
- </div>
738
- `;
739
- }).join('');
740
- }
741
-
742
- function updateHelpfulnessUI() {
743
- const container = document.getElementById('helpfulness-summary');
744
- if (!container) return;
745
-
746
- const h = state.helpfulness;
747
- if (!h || h.totalEvaluated === 0) {
748
- container.innerHTML = '<span style="color:var(--text-muted);">No evaluations yet. Helpfulness is measured automatically at session end.</span>';
749
- return;
750
- }
751
-
752
- const scoreColor = h.avgScore >= 0.7 ? 'var(--success, #00E396)' : h.avgScore >= 0.4 ? 'var(--warning, #FEB019)' : 'var(--danger, #FF4560)';
753
-
754
- container.innerHTML = `
755
- <div style="display:flex; gap:16px; align-items:center; flex-wrap:wrap;">
756
- <div style="display:flex; align-items:baseline; gap:4px;">
757
- <span style="font-size:20px; font-weight:700; color:${scoreColor};">${h.avgScore}</span>
758
- <span style="font-size:11px; color:var(--text-muted);">avg</span>
759
- </div>
760
- <div style="display:flex; gap:10px; font-size:12px;">
761
- <span style="color:var(--success, #00E396);">${h.helpful} helpful</span>
762
- <span style="color:var(--warning, #FEB019);">${h.neutral} neutral</span>
763
- <span style="color:var(--danger, #FF4560);">${h.unhelpful} unhelpful</span>
764
- </div>
765
- <span style="font-size:11px; color:var(--text-muted);">${h.totalEvaluated} evaluated / ${h.totalRetrievals} retrieved</span>
766
- </div>
767
- `;
768
- }
769
-
770
- function updateMostHelpfulList() {
771
- const container = document.getElementById('most-helpful-list');
772
- if (!container) return;
773
-
774
- const memories = state.helpfulness?.topMemories || [];
775
-
776
- if (memories.length === 0) {
777
- container.innerHTML = '<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">No helpful memories yet</div>';
778
- return;
779
- }
780
-
781
- container.innerHTML = memories.slice(0, 5).map((m, i) => {
782
- const scoreColor = m.helpfulnessScore >= 0.7 ? 'var(--success, #00E396)' : m.helpfulnessScore >= 0.4 ? 'var(--warning, #FEB019)' : 'var(--danger, #FF4560)';
783
- return `
784
- <div class="shared-item">
785
- <div class="shared-info">
786
- <div class="shared-icon" style="font-size:14px; font-weight:700; color:var(--accent-primary);">#${i + 1}</div>
787
- <span style="font-size:13px; color:var(--text-secondary); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
788
- ${escapeHtml(m.summary || '(no summary)')}
789
- </span>
790
- </div>
791
- <div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px;">
792
- <span style="font-size:14px; font-weight:600; color:${scoreColor};">${m.helpfulnessScore}</span>
793
- <span style="font-size:10px; color:var(--text-muted);">${m.accessCount}x accessed</span>
794
- </div>
795
- </div>
796
- `;
797
- }).join('');
798
- }
799
-
800
-
801
- function updateRetrievalTraceUI() {
802
- const summaryEl = document.getElementById('retrieval-trace-summary');
803
- const listEl = document.getElementById('retrieval-trace-list');
804
- if (!summaryEl || !listEl) return;
805
-
806
- const payload = state.retrievalTraces;
807
- const stats = payload?.stats;
808
- const traces = payload?.traces || [];
809
-
810
- if (!stats || !Number.isFinite(stats.totalQueries) || stats.totalQueries === 0) {
811
- summaryEl.innerHTML = '<span style="color:var(--text-muted);">No retrieval traces yet.</span>';
812
- listEl.innerHTML = '<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">No query/context trace data</div>';
813
- return;
814
- }
815
-
816
- const selectionRate = ((stats.selectionRate || 0) * 100).toFixed(1);
817
- summaryEl.innerHTML = `
818
- <div style="display:flex; gap:14px; flex-wrap:wrap; font-size:12px;">
819
- <span><strong>${formatNumber(stats.totalQueries)}</strong> queries</span>
820
- <span><strong>${Number(stats.avgCandidateCount || 0).toFixed(1)}</strong> avg candidates</span>
821
- <span><strong>${Number(stats.avgSelectedCount || 0).toFixed(1)}</strong> avg selected</span>
822
- <span><strong>${selectionRate}%</strong> selection rate</span>
823
- </div>
824
- `;
825
-
826
- listEl.innerHTML = traces.slice(0, 8).map((t) => {
827
- const ts = t.createdAt ? new Date(t.createdAt).toLocaleString() : '-';
828
- const confidence = t.confidence || 'n/a';
829
- const selected = Number(t.selectedCount || 0);
830
- const candidates = Number(t.candidateCount || 0);
831
- const selectedDetails = (t.selectedDetails || []).slice(0, 2);
832
- const candidateDetails = (t.candidateDetails || []).slice(0, 3);
833
- const selectedIdsHtml = selectedDetails.length > 0
834
- ? selectedDetails.map((d) => {
835
- const breakdown = `score=${Number(d.score || 0).toFixed(3)} · s=${Number(d.semanticScore || 0).toFixed(3)} · l=${Number(d.lexicalScore || 0).toFixed(3)} · r=${Number(d.recencyScore || 0).toFixed(3)}`;
836
- return `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal('${d.eventId}')" title="${escapeHtml(breakdown)}">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`;
837
- }).join(' ')
838
- : ((t.selectedEventIds || []).slice(0, 2).map((id) => `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal('${id}')">${escapeHtml((id || '').slice(0, 8))}...</span>`).join(' ') || '-');
839
-
840
- const scoreBreakdownHtml = selectedDetails.length > 0
841
- ? selectedDetails.map((d) => `<div style="font-size:10px; color:var(--text-muted);">${escapeHtml((d.eventId || '').slice(0, 8))}... → score ${Number(d.score || 0).toFixed(3)} (s ${Number(d.semanticScore || 0).toFixed(3)}, l ${Number(d.lexicalScore || 0).toFixed(3)}, r ${Number(d.recencyScore || 0).toFixed(3)})</div>`).join('')
842
- : '';
843
-
844
- return `
845
- <div class="shared-item" style="align-items:flex-start;">
846
- <div class="shared-info" style="align-items:flex-start; flex-direction:column; gap:4px;">
847
- <span style="font-size:12px; color:var(--text-secondary);"><strong>Q:</strong> ${escapeHtml((t.queryText || '').slice(0, 120))}</span>
848
- <span style="font-size:11px; color:var(--text-muted);">${ts} · strategy=${escapeHtml(t.strategy || 'auto')} · conf=${escapeHtml(confidence)}</span>
849
- <span style="font-size:11px; color:var(--text-muted);">selected IDs: ${selectedIdsHtml}</span>
850
- <span style="font-size:11px; color:var(--text-muted);">candidates: ${candidateDetails.map((d) => `<span class=\"event-type-badge\" style=\"cursor:pointer;\" onclick=\"openDetailModal('${d.eventId}')\">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`).join(' ') || '-'}</span>
851
- ${scoreBreakdownHtml}
852
- </div>
853
- <div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px; min-width:68px;">
854
- <span style="font-size:13px; font-weight:600; color:var(--accent-primary);">${selected}/${candidates}</span>
855
- <span style="font-size:10px; color:var(--text-muted);">sel/cand</span>
856
- </div>
857
- </div>
858
- `;
859
- }).join('');
860
- }
861
-
862
-
863
- // --- Charts ---
864
-
865
- async function initActivityChart() {
866
- const chartEl = document.querySelector("#activity-chart");
867
- if (!chartEl) return;
868
-
869
- let categories = [];
870
- let seriesData = [];
871
- try {
872
- const res = await fetch(apiUrl(`${API_BASE}/stats/timeline`, { days: 14 }));
873
- const data = await res.json();
874
- if (data.daily && data.daily.length > 0) {
875
- categories = data.daily.map(d => d.date);
876
- seriesData = data.daily.map(d => d.total);
877
- }
878
- } catch (e) {
879
- console.error('Failed to load timeline:', e);
880
- }
881
-
882
- if (seriesData.length === 0) {
883
- categories = ['No data'];
884
- seriesData = [0];
885
- }
886
-
887
- const options = {
888
- series: [{
889
- name: 'Events',
890
- data: seriesData
891
- }],
892
- chart: {
893
- type: 'area',
894
- height: 300,
895
- background: 'transparent',
896
- toolbar: { show: false },
897
- fontFamily: 'Outfit, sans-serif'
898
- },
899
- theme: { mode: 'dark' },
900
- stroke: {
901
- curve: 'smooth',
902
- width: 3,
903
- colors: [CHART_COLORS.L0]
904
- },
905
- fill: {
906
- type: 'gradient',
907
- gradient: {
908
- shadeIntensity: 1,
909
- opacityFrom: 0.7,
910
- opacityTo: 0.1,
911
- stops: [0, 90, 100]
912
- }
913
- },
914
- dataLabels: { enabled: false },
915
- grid: {
916
- borderColor: 'rgba(255,255,255,0.05)',
917
- strokeDashArray: 4,
918
- },
919
- xaxis: {
920
- categories: categories,
921
- labels: {
922
- style: { colors: '#8B9BB4' },
923
- rotate: -45,
924
- rotateAlways: categories.length > 7
925
- },
926
- axisBorder: { show: false },
927
- axisTicks: { show: false }
928
- },
929
- yaxis: {
930
- labels: { style: { colors: '#8B9BB4' } }
931
- },
932
- colors: [CHART_COLORS.L0]
933
- };
934
-
935
- state.chartInstance = new ApexCharts(chartEl, options);
936
- state.chartInstance.render();
937
- }
938
-
939
- // --- Endless Mode ---
940
-
941
- async function checkEndlessStatus() {
942
- const statusEl = document.getElementById('status-dot');
943
- const textEl = document.getElementById('status-text');
944
-
945
- const isRunning = false;
946
-
947
- if (statusEl && textEl) {
948
- if (isRunning) {
949
- statusEl.classList.add('active');
950
- textEl.textContent = 'Active Background Processing';
951
- textEl.style.color = 'var(--success)';
952
- } else {
953
- statusEl.classList.remove('active');
954
- textEl.textContent = 'Idle';
955
- textEl.style.color = 'var(--text-muted)';
956
- }
957
- }
958
- }
959
-
960
- // =============================================
961
- // Modal System
962
- // =============================================
963
-
964
- function openModal(modalId) {
965
- const modal = document.getElementById(modalId);
966
- if (modal) {
967
- modal.style.display = 'flex';
968
- document.body.style.overflow = 'hidden';
969
- }
970
- }
971
-
972
- function closeModal(modalId) {
973
- const modal = document.getElementById(modalId);
974
- if (modal) {
975
- modal.style.display = 'none';
976
- document.body.style.overflow = '';
977
- }
978
- }
979
-
980
- function closeAllModals() {
981
- document.querySelectorAll('.modal-overlay').forEach(m => {
982
- m.style.display = 'none';
983
- });
984
- document.body.style.overflow = '';
985
- }
986
-
987
- // --- Detail Modal ---
988
-
989
- async function openDetailModal(eventId) {
990
- const body = document.getElementById('detail-modal-body');
991
- body.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-muted);"><i class="ri-loader-4-line" style="font-size:24px; animation: spin 1s linear infinite;"></i><br>Loading event details...</div>';
992
- openModal('detail-modal');
993
-
994
- try {
995
- const res = await fetch(apiUrl(`${API_BASE}/events/${eventId}`));
996
- if (!res.ok) throw new Error('Event not found');
997
- const data = await res.json();
998
- const evt = data.event;
999
- const ctx = data.context || [];
1000
-
1001
- const eventType = evt.eventType || 'unknown';
1002
- const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
1003
- const time = new Date(evt.timestamp).toLocaleString();
1004
- const adherenceBadge = renderAdherenceBadge(evt);
1005
-
1006
- let contextHtml = '';
1007
- if (ctx.length > 0) {
1008
- contextHtml = `
1009
- <div class="modal-section-title">Context (Surrounding Events)</div>
1010
- <div class="modal-context-list">
1011
- ${ctx.map(c => `
1012
- <div class="modal-context-item" onclick="openDetailModal('${c.id}')">
1013
- <span class="event-type-badge ${`type-${(c.eventType || '').toLowerCase().replace('_', '-')}`}" style="flex-shrink:0;">${c.eventType}</span>
1014
- <div style="flex:1; min-width:0;">
1015
- <div style="font-size:12px; color:var(--text-muted); margin-bottom:4px;">${new Date(c.timestamp).toLocaleString()}</div>
1016
- <div style="font-size:13px; color:var(--text-secondary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${escapeHtml(c.preview || '')}</div>
1017
- </div>
1018
- </div>
1019
- `).join('')}
1020
- </div>
1021
- `;
1022
- }
1023
-
1024
- body.innerHTML = `
1025
- <div class="modal-meta">
1026
- <div class="modal-meta-item">
1027
- <i class="ri-price-tag-3-line"></i>
1028
- <span class="event-type-badge ${typeClass}">${eventType}</span>
1029
- </div>
1030
- ${adherenceBadge ? `<div class="modal-meta-item">${adherenceBadge}</div>` : ''}
1031
- <div class="modal-meta-item">
1032
- <i class="ri-time-line"></i>
1033
- ${time}
1034
- </div>
1035
- <div class="modal-meta-item">
1036
- <i class="ri-chat-1-line"></i>
1037
- Session: ${evt.sessionId ? evt.sessionId.slice(0, 12) + '...' : 'N/A'}
1038
- </div>
1039
- </div>
1040
- <div class="modal-section-title">Content</div>
1041
- <div class="modal-content-block">${escapeHtml(evt.content || '(empty)')}</div>
1042
- ${contextHtml}
1043
- `;
1044
- } catch (error) {
1045
- body.innerHTML = `<div style="text-align:center; padding:40px; color:var(--error);">Failed to load event: ${escapeHtml(error.message)}</div>`;
1046
- }
1047
- }
1048
-
1049
- // --- Stat Card Click Handlers ---
1050
-
1051
- function handleStatClick(statType) {
1052
- switch (statType) {
1053
- case 'events': showEventsListModal(); break;
1054
- case 'sessions': showSessionsModal(); break;
1055
- case 'shared': showSharedModal(); break;
1056
- case 'vectors': showVectorsModal(); break;
1057
- }
1058
- }
1059
-
1060
- async function showEventsListModal() {
1061
- document.getElementById('list-modal-title').textContent = 'Total Events';
1062
- const body = document.getElementById('list-modal-body');
1063
- body.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-muted);">Loading events...</div>';
1064
- openModal('list-modal');
1065
-
1066
- try {
1067
- const res = await fetch(apiUrl(`${API_BASE}/events`, { limit: 50 }));
1068
- const data = await res.json();
1069
- const events = data.events || [];
1070
-
1071
- if (events.length === 0) {
1072
- body.innerHTML = '<div class="modal-list-empty">No events found</div>';
1073
- return;
1074
- }
1075
-
1076
- body.innerHTML = events.map(e => {
1077
- const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
1078
- const adherenceBadge = renderAdherenceBadge(e);
1079
- return `
1080
- <div class="modal-list-item" onclick="openDetailModal('${e.id}')">
1081
- <div class="modal-list-info">
1082
- <div class="title">
1083
- <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
1084
- ${adherenceBadge}
1085
- ${escapeHtml((e.preview || '').slice(0, 80))}
1086
- </div>
1087
- <div class="subtitle">${new Date(e.timestamp).toLocaleString()} | Session: ${(e.sessionId || '').slice(0, 12)}...</div>
1088
- </div>
1089
- ${e.accessCount > 0 ? `<div class="modal-list-badge"><i class="ri-eye-line"></i> ${e.accessCount}</div>` : ''}
1090
- </div>
1091
- `;
1092
- }).join('');
1093
- } catch (error) {
1094
- body.innerHTML = `<div class="modal-list-empty">Failed to load events</div>`;
1095
- }
1096
- }
1097
-
1098
- async function showSessionsModal() {
1099
- document.getElementById('list-modal-title').textContent = 'Active Sessions';
1100
- const body = document.getElementById('list-modal-body');
1101
- body.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-muted);">Loading sessions...</div>';
1102
- openModal('list-modal');
1103
-
1104
- try {
1105
- const res = await fetch(apiUrl(`${API_BASE}/sessions`, { pageSize: 50 }));
1106
- const data = await res.json();
1107
- const sessions = data.sessions || [];
1108
-
1109
- if (sessions.length === 0) {
1110
- body.innerHTML = '<div class="modal-list-empty">No sessions found</div>';
1111
- return;
1112
- }
1113
-
1114
- body.innerHTML = sessions.map(s => {
1115
- const started = new Date(s.startedAt).toLocaleString();
1116
- const lastEvent = new Date(s.lastEventAt).toLocaleString();
1117
- return `
1118
- <div class="modal-list-item" onclick="showSessionDetailInModal('${s.id}')">
1119
- <div class="modal-list-info">
1120
- <div class="title"><i class="ri-chat-1-line" style="color:var(--accent-primary); margin-right:6px;"></i>${s.id.slice(0, 20)}...</div>
1121
- <div class="subtitle">Started: ${started} | Last: ${lastEvent}</div>
1122
- </div>
1123
- <div class="modal-list-badge">${s.eventCount} events</div>
1124
- </div>
1125
- `;
1126
- }).join('');
1127
- } catch (error) {
1128
- body.innerHTML = `<div class="modal-list-empty">Failed to load sessions</div>`;
1129
- }
1130
- }
1131
-
1132
- async function showSessionDetailInModal(sessionId) {
1133
- document.getElementById('list-modal-title').textContent = 'Session Detail';
1134
- const body = document.getElementById('list-modal-body');
1135
- body.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-muted);">Loading session...</div>';
1136
-
1137
- try {
1138
- const res = await fetch(apiUrl(`${API_BASE}/sessions/${sessionId}`));
1139
- const data = await res.json();
1140
- const session = data.session;
1141
- const events = data.events || [];
1142
- const stats = data.stats || {};
1143
-
1144
- body.innerHTML = `
1145
- <div class="modal-meta">
1146
- <div class="modal-meta-item"><i class="ri-fingerprint-line"></i>${sessionId.slice(0, 20)}...</div>
1147
- <div class="modal-meta-item"><i class="ri-time-line"></i>${new Date(session.startedAt).toLocaleString()}</div>
1148
- <div class="modal-meta-item"><i class="ri-file-list-3-line"></i>${session.eventCount} events</div>
1149
- </div>
1150
- <div style="display:flex; gap:12px; margin-bottom:20px; flex-wrap:wrap;">
1151
- <div style="padding:10px 16px; background:rgba(59,130,246,0.1); border-radius:8px; font-size:13px;">
1152
- <span style="color:#60A5FA; font-weight:600;">${stats.user_prompt || 0}</span> <span style="color:var(--text-muted);">prompts</span>
1153
- </div>
1154
- <div style="padding:10px 16px; background:rgba(16,185,129,0.1); border-radius:8px; font-size:13px;">
1155
- <span style="color:#34D399; font-weight:600;">${stats.agent_response || 0}</span> <span style="color:var(--text-muted);">responses</span>
1156
- </div>
1157
- <div style="padding:10px 16px; background:rgba(245,158,11,0.1); border-radius:8px; font-size:13px;">
1158
- <span style="color:#FBBF24; font-weight:600;">${stats.tool_observation || 0}</span> <span style="color:var(--text-muted);">tools</span>
1159
- </div>
1160
- </div>
1161
- <div class="modal-section-title">Events</div>
1162
- ${events.map(e => {
1163
- const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
1164
- const adherenceBadge = renderAdherenceBadge(e);
1165
- return `
1166
- <div class="modal-list-item" onclick="closeAllModals(); openDetailModal('${e.id}')">
1167
- <div class="modal-list-info">
1168
- <div class="title">
1169
- <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
1170
- ${adherenceBadge}
1171
- ${escapeHtml((e.preview || '').slice(0, 80))}
1172
- </div>
1173
- <div class="subtitle">${new Date(e.timestamp).toLocaleString()}</div>
1174
- </div>
1175
- </div>
1176
- `;
1177
- }).join('')}
1178
- `;
1179
- } catch (error) {
1180
- body.innerHTML = `<div class="modal-list-empty">Failed to load session</div>`;
1181
- }
1182
- }
1183
-
1184
- function showSharedModal() {
1185
- document.getElementById('list-modal-title').textContent = 'Shared Items';
1186
- const body = document.getElementById('list-modal-body');
1187
- const s = state.sharedStats || {};
1188
-
1189
- const items = [
1190
- { icon: '🔧', label: 'Troubleshooting', count: s.troubleshooting || 0, color: '#60A5FA' },
1191
- { icon: '✨', label: 'Best Practices', count: s.bestPractices || 0, color: '#34D399' },
1192
- { icon: '⚠️', label: 'Common Errors', count: s.commonErrors || 0, color: '#FBBF24' }
1193
- ];
1194
-
1195
- const total = items.reduce((a, b) => a + b.count, 0);
1196
- const lastUpdated = s.lastUpdated ? new Date(s.lastUpdated).toLocaleString() : 'N/A';
1197
-
1198
- body.innerHTML = `
1199
- <div style="text-align:center; margin-bottom:24px;">
1200
- <div style="font-size:48px; font-weight:700; background:linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip:text; -webkit-text-fill-color:transparent;">${formatNumber(total)}</div>
1201
- <div style="font-size:13px; color:var(--text-muted); margin-top:4px;">Total shared items</div>
1202
- </div>
1203
- ${items.map(item => `
1204
- <div class="modal-list-item" style="cursor:default;">
1205
- <div class="modal-list-info">
1206
- <div class="title">${item.icon} ${item.label}</div>
1207
- <div class="subtitle">Cross-project knowledge items</div>
1208
- </div>
1209
- <div class="modal-list-badge" style="background:${item.color}22; color:${item.color};">${formatNumber(item.count)}</div>
1210
- </div>
1211
- `).join('')}
1212
- <div style="text-align:center; margin-top:20px; font-size:12px; color:var(--text-muted);">
1213
- Total usage: ${formatNumber(s.totalUsageCount || 0)} | Last updated: ${lastUpdated}
1214
- </div>
1215
- `;
1216
-
1217
- openModal('list-modal');
1218
- }
1219
-
1220
- function showVectorsModal() {
1221
- document.getElementById('list-modal-title').textContent = 'Vector Nodes';
1222
- const body = document.getElementById('list-modal-body');
1223
- const stats = state.stats || {};
1224
- const vectorCount = stats.storage?.vectorCount || 0;
1225
- const memory = stats.memory || {};
1226
-
1227
- body.innerHTML = `
1228
- <div style="text-align:center; margin-bottom:24px;">
1229
- <div style="font-size:48px; font-weight:700; background:linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip:text; -webkit-text-fill-color:transparent;">${formatNumber(vectorCount)}</div>
1230
- <div style="font-size:13px; color:var(--text-muted); margin-top:4px;">Total vector nodes</div>
1231
- </div>
1232
- <div class="modal-list-item" style="cursor:default;">
1233
- <div class="modal-list-info">
1234
- <div class="title"><i class="ri-node-tree" style="color:var(--accent-primary); margin-right:6px;"></i>Embedded Vectors</div>
1235
- <div class="subtitle">Semantic search index entries</div>
1236
- </div>
1237
- <div class="modal-list-badge">${formatNumber(vectorCount)}</div>
1238
- </div>
1239
- <div class="modal-list-item" style="cursor:default;">
1240
- <div class="modal-list-info">
1241
- <div class="title"><i class="ri-cpu-line" style="color:var(--accent-secondary); margin-right:6px;"></i>Heap Used</div>
1242
- <div class="subtitle">Current memory usage</div>
1243
- </div>
1244
- <div class="modal-list-badge" style="background:rgba(0,240,255,0.1); color:var(--accent-secondary);">${memory.heapUsed || 0} MB</div>
1245
- </div>
1246
- <div class="modal-list-item" style="cursor:default;">
1247
- <div class="modal-list-info">
1248
- <div class="title"><i class="ri-hard-drive-2-line" style="color:var(--warning); margin-right:6px;"></i>Heap Total</div>
1249
- <div class="subtitle">Allocated memory</div>
1250
- </div>
1251
- <div class="modal-list-badge" style="background:rgba(254,176,25,0.1); color:var(--warning);">${memory.heapTotal || 0} MB</div>
1252
- </div>
1253
- `;
1254
-
1255
- openModal('list-modal');
1256
- }
1257
-
1258
- // =============================================
1259
- // Sidebar Navigation
1260
- // =============================================
1261
-
1262
- function switchView(viewName) {
1263
- if (state.currentView === viewName) return;
1264
- state.currentView = viewName;
1265
-
1266
- // Update nav active state
1267
- document.querySelectorAll('.nav-item[data-nav]').forEach(item => {
1268
- item.classList.toggle('active', item.dataset.nav === viewName);
1269
- });
1270
-
1271
- // Switch page views
1272
- document.querySelectorAll('.page-view').forEach(view => {
1273
- view.classList.remove('active');
1274
- });
1275
- const targetView = document.getElementById(`view-${viewName}`);
1276
- if (targetView) {
1277
- targetView.classList.add('active');
1278
- }
1279
-
1280
- // Load view content
1281
- switch (viewName) {
1282
- case 'knowledge-graph': loadKnowledgeGraphView(); break;
1283
- case 'memory-banks': loadMemoryBanksView(); break;
1284
- case 'user-prompts': loadUserPromptsView(); break;
1285
- case 'configuration': loadConfigurationView(); break;
1286
- }
1287
- }
1288
-
1289
- // --- Knowledge Graph View ---
1290
-
1291
- async function loadKnowledgeGraphView() {
1292
- const container = document.getElementById('kg-content');
1293
- container.innerHTML = '<div style="text-align:center; padding:60px; color:var(--text-muted);">Loading knowledge graph...</div>';
1294
-
1295
- try {
1296
- const [mostAccessedRes, helpfulnessRes] = await Promise.all([
1297
- fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 20 })).then(r => r.json()).catch(() => ({ memories: [] })),
1298
- fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 10 })).then(r => r.json()).catch(() => ({ topMemories: [] }))
1299
- ]);
1300
-
1301
- const memories = mostAccessedRes.memories || [];
1302
- const helpful = helpfulnessRes.topMemories || [];
1303
-
1304
- if (memories.length === 0 && helpful.length === 0) {
1305
- container.innerHTML = '<div style="text-align:center; padding:60px; color:var(--text-muted);">No knowledge data available yet. Start using memories to build your knowledge graph.</div>';
1306
- return;
1307
- }
1308
-
1309
- // Collect all topics
1310
- const topicMap = {};
1311
- memories.forEach(m => {
1312
- (m.topics || []).forEach(t => {
1313
- topicMap[t] = (topicMap[t] || 0) + 1;
1314
- });
1315
- });
1316
- const topTopics = Object.entries(topicMap).sort((a, b) => b[1] - a[1]).slice(0, 15);
1317
-
1318
- let topicsHtml = '';
1319
- if (topTopics.length > 0) {
1320
- topicsHtml = `
1321
- <div class="card" style="margin-bottom:24px;">
1322
- <div class="card-header">
1323
- <div class="card-title"><i class="ri-hashtag"></i><span>Top Topics</span></div>
1324
- </div>
1325
- <div class="kg-topic-list">
1326
- ${topTopics.map(([topic, count]) => `
1327
- <span class="kg-topic-tag">${escapeHtml(topic)} (${count})</span>
1328
- `).join('')}
1329
- </div>
1330
- </div>
1331
- `;
1332
- }
1333
-
1334
- const memoriesHtml = memories.length > 0 ? `
1335
- <div class="card" style="margin-bottom:24px;">
1336
- <div class="card-header">
1337
- <div class="card-title"><i class="ri-star-line"></i><span>Most Accessed Memories</span></div>
1338
- </div>
1339
- <div class="kg-grid">
1340
- ${memories.map((m, i) => `
1341
- <div class="kg-memory-card" onclick="openDetailModalByMemory('${m.memoryId || ''}')">
1342
- <div class="kg-memory-rank">#${i + 1}</div>
1343
- <div class="kg-memory-summary">${escapeHtml(m.summary || '(no summary)')}</div>
1344
- ${(m.topics || []).length > 0 ? `
1345
- <div class="kg-topic-list">
1346
- ${m.topics.slice(0, 3).map(t => `<span class="kg-topic-tag">${escapeHtml(t)}</span>`).join('')}
1347
- </div>
1348
- ` : ''}
1349
- <div class="kg-memory-meta">
1350
- <span><i class="ri-eye-line"></i> ${m.accessCount || 0}x accessed</span>
1351
- <span><i class="ri-shield-check-line"></i> ${((m.confidence || 0) * 100).toFixed(0)}% confidence</span>
1352
- </div>
1353
- </div>
1354
- `).join('')}
1355
- </div>
1356
- </div>
1357
- ` : '';
1358
-
1359
- const helpfulHtml = helpful.length > 0 ? `
1360
- <div class="card">
1361
- <div class="card-header">
1362
- <div class="card-title"><i class="ri-thumb-up-line"></i><span>Most Helpful Memories</span></div>
1363
- </div>
1364
- ${helpful.map((m, i) => {
1365
- const scoreColor = m.helpfulnessScore >= 0.7 ? 'var(--success)' : m.helpfulnessScore >= 0.4 ? 'var(--warning)' : 'var(--error)';
1366
- return `
1367
- <div class="modal-list-item" onclick="openDetailModalByEvent('${m.eventId || ''}')">
1368
- <div class="modal-list-info">
1369
- <div class="title">#${i + 1} ${escapeHtml(m.summary || '(no summary)')}</div>
1370
- <div class="subtitle">${m.accessCount || 0}x accessed | ${m.evaluationCount || 0} evaluations</div>
1371
- </div>
1372
- <div class="modal-list-badge" style="color:${scoreColor}; background:${scoreColor}22;">${m.helpfulnessScore}</div>
1373
- </div>
1374
- `;
1375
- }).join('')}
1376
- </div>
1377
- ` : '';
1378
-
1379
- container.innerHTML = topicsHtml + memoriesHtml + helpfulHtml;
1380
-
1381
- } catch (error) {
1382
- container.innerHTML = `<div style="text-align:center; padding:60px; color:var(--error);">Failed to load knowledge graph: ${escapeHtml(error.message)}</div>`;
1383
- }
1384
- }
1385
-
1386
- function openDetailModalByMemory(memoryId) {
1387
- // memoryId might be an event ID - try to open it
1388
- if (memoryId) openDetailModal(memoryId);
1389
- }
1390
-
1391
- function openDetailModalByEvent(eventId) {
1392
- if (eventId) openDetailModal(eventId);
1393
- }
1394
-
1395
- // --- Memory Banks View ---
1396
-
1397
- async function loadMemoryBanksView() {
1398
- const container = document.getElementById('mb-content');
1399
- container.innerHTML = '<div style="text-align:center; padding:60px; color:var(--text-muted);">Loading memory banks...</div>';
1400
-
1401
- try {
1402
- const [statsRes, graduationRes] = await Promise.all([
1403
- fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
1404
- fetch(apiUrl(`${API_BASE}/stats/graduation`)).then(r => r.json()).catch(() => null)
1405
- ]);
1406
-
1407
- const levelStats = statsRes?.levelStats || [];
1408
- const levels = ['L0', 'L1', 'L2', 'L3', 'L4'];
1409
- const levelNames = { L0: 'Raw Events', L1: 'Structured', L2: 'Validated', L3: 'Verified', L4: 'Active' };
1410
- const levelCounts = {};
1411
- levelStats.forEach(s => { levelCounts[s.level] = s.count; });
1412
-
1413
- const criteria = graduationRes?.criteria || {};
1414
-
1415
- container.innerHTML = `
1416
- <div class="mb-level-tabs" id="mb-tabs">
1417
- ${levels.map(level => `
1418
- <button class="mb-level-tab ${level === 'L0' ? 'active' : ''}" data-level="${level}" style="border-left:3px solid ${CHART_COLORS[level]};">
1419
- ${levelNames[level]} <span class="tab-count">(${levelCounts[level] || 0})</span>
1420
- </button>
1421
- `).join('')}
1422
- </div>
1423
- <div class="card" style="margin-bottom:24px;">
1424
- <div class="card-header">
1425
- <div class="card-title"><i class="ri-stack-line"></i><span>Level Events</span></div>
1426
- </div>
1427
- <div id="mb-events-list">
1428
- <div style="text-align:center; padding:20px; color:var(--text-muted);">Select a level to view events</div>
1429
- </div>
1430
- </div>
1431
- <div class="card">
1432
- <div class="card-header">
1433
- <div class="card-title"><i class="ri-graduation-cap-line"></i><span>Graduation Criteria</span></div>
1434
- </div>
1435
- ${Object.entries(criteria).map(([key, c]) => `
1436
- <div style="margin-bottom:16px;">
1437
- <div style="font-size:14px; font-weight:600; color:var(--accent-primary); margin-bottom:8px;">${key}</div>
1438
- <div style="display:grid; grid-template-columns:repeat(2, 1fr); gap:8px;">
1439
- <div class="cfg-row" style="padding:8px 12px; background:rgba(255,255,255,0.02); border-radius:8px; border:none;">
1440
- <span class="cfg-row-label">Min Access</span>
1441
- <span class="cfg-row-value">${c.minAccessCount}</span>
1442
- </div>
1443
- <div class="cfg-row" style="padding:8px 12px; background:rgba(255,255,255,0.02); border-radius:8px; border:none;">
1444
- <span class="cfg-row-label">Min Confidence</span>
1445
- <span class="cfg-row-value">${c.minConfidence}</span>
1446
- </div>
1447
- <div class="cfg-row" style="padding:8px 12px; background:rgba(255,255,255,0.02); border-radius:8px; border:none;">
1448
- <span class="cfg-row-label">Cross-Session Refs</span>
1449
- <span class="cfg-row-value">${c.minCrossSessionRefs}</span>
1450
- </div>
1451
- <div class="cfg-row" style="padding:8px 12px; background:rgba(255,255,255,0.02); border-radius:8px; border:none;">
1452
- <span class="cfg-row-label">Max Age (days)</span>
1453
- <span class="cfg-row-value">${c.maxAgeDays}</span>
1454
- </div>
1455
- </div>
1456
- </div>
1457
- `).join('')}
1458
- </div>
1459
- `;
1460
-
1461
- // Setup level tab click handlers
1462
- document.querySelectorAll('#mb-tabs .mb-level-tab').forEach(tab => {
1463
- tab.addEventListener('click', () => {
1464
- document.querySelectorAll('#mb-tabs .mb-level-tab').forEach(t => t.classList.remove('active'));
1465
- tab.classList.add('active');
1466
- loadMemoryBankLevel(tab.dataset.level);
1467
- });
1468
- });
1469
-
1470
- // Load L0 by default
1471
- await loadMemoryBankLevel('L0');
1472
-
1473
- } catch (error) {
1474
- container.innerHTML = `<div style="text-align:center; padding:60px; color:var(--error);">Failed to load memory banks: ${escapeHtml(error.message)}</div>`;
1475
- }
1476
- }
1477
-
1478
- async function loadMemoryBankLevel(level) {
1479
- const container = document.getElementById('mb-events-list');
1480
- if (!container) return;
1481
- container.innerHTML = '<div style="text-align:center; padding:20px; color:var(--text-muted);">Loading...</div>';
1482
-
1483
- try {
1484
- const res = await fetch(apiUrl(`${API_BASE}/stats/levels/${level}`, { limit: 30 }));
1485
- const data = await res.json();
1486
- const events = data.events || [];
1487
-
1488
- if (events.length === 0) {
1489
- container.innerHTML = `<div style="text-align:center; padding:20px; color:var(--text-muted);">No events at level ${level}</div>`;
1490
- return;
1491
- }
1492
-
1493
- container.innerHTML = `
1494
- <div class="mb-event-list">
1495
- ${events.map(e => {
1496
- const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
1497
- return `
1498
- <div class="mb-event-card" onclick="openDetailModal('${e.id}')">
1499
- <div class="mb-event-header">
1500
- <span class="event-type-badge ${typeClass}">${e.eventType}</span>
1501
- <div style="display:flex; gap:8px; align-items:center;">
1502
- ${e.accessCount > 0 ? `<span class="access-badge"><i class="ri-eye-line"></i> ${e.accessCount}</span>` : ''}
1503
- <span class="event-time">${new Date(e.timestamp).toLocaleString()}</span>
1504
- </div>
1505
- </div>
1506
- <div class="mb-event-content">${escapeHtml((e.content || '').slice(0, 200))}</div>
1507
- </div>
1508
- `;
1509
- }).join('')}
1510
- </div>
1511
- ${data.hasMore ? `<div style="text-align:center; padding:16px; color:var(--text-muted); font-size:13px;">Showing ${events.length} of ${data.total} events</div>` : ''}
1512
- `;
1513
- } catch (error) {
1514
- container.innerHTML = `<div style="text-align:center; padding:20px; color:var(--error);">Failed to load level ${level}</div>`;
1515
- }
1516
- }
1517
-
1518
- // --- User Prompts View ---
1519
-
1520
- async function renderUserPromptList() {
1521
- const listEl = document.getElementById('user-prompt-list');
1522
- const pageEl = document.getElementById('user-prompt-page');
1523
- const prevBtn = document.getElementById('user-prompt-prev');
1524
- const nextBtn = document.getElementById('user-prompt-next');
1525
- const metaEl = document.getElementById('user-prompt-meta');
1526
- if (!listEl) return;
1527
-
1528
- const items = state.userPromptItems || [];
1529
- const pageSize = state.userPromptPageSize;
1530
- const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
1531
- if (state.userPromptPage > totalPages) state.userPromptPage = totalPages;
1532
-
1533
- const start = (state.userPromptPage - 1) * pageSize;
1534
- const paged = items.slice(start, start + pageSize);
1535
-
1536
- if (pageEl) pageEl.textContent = `${state.userPromptPage} / ${totalPages}`;
1537
- if (prevBtn) prevBtn.disabled = state.userPromptPage <= 1;
1538
- if (nextBtn) nextBtn.disabled = state.userPromptPage >= totalPages;
1539
-
1540
- if (metaEl) {
1541
- const sessionCount = new Set(items.map(i => i.sessionId)).size;
1542
- metaEl.textContent = `${items.length} prompts · ${sessionCount} sessions${state.userPromptSearchQuery ? ` · query: "${state.userPromptSearchQuery}"` : ''}`;
1543
- }
1544
-
1545
- if (paged.length === 0) {
1546
- listEl.innerHTML = '<div style="padding:20px; text-align:center; color:var(--text-muted);">No user prompts found.</div>';
1547
- return;
1548
- }
1549
-
1550
- // Group current page by session
1551
- const groups = new Map();
1552
- for (const e of paged) {
1553
- const key = e.sessionId || 'unknown';
1554
- const arr = groups.get(key) || [];
1555
- arr.push(e);
1556
- groups.set(key, arr);
1557
- }
1558
-
1559
- const html = Array.from(groups.entries()).map(([sessionId, sessionItems]) => {
1560
- const heading = `
1561
- <div style="margin:10px 0 6px; font-size:12px; color:var(--text-muted); font-weight:600;">
1562
- <i class="ri-chat-1-line"></i> Session ${escapeHtml((sessionId || '').slice(0, 16))}... · ${sessionItems.length} prompts
1563
- </div>
1564
- `;
1565
-
1566
- const cards = sessionItems.map((e) => `
1567
- <div class="event-item" style="cursor:pointer;" onclick="openDetailModal('${e.id}')">
1568
- <div class="event-header">
1569
- <span class="event-type-badge type-user-prompt">user_prompt</span>
1570
- <span class="event-time">${new Date(e.timestamp).toLocaleString()}</span>
1571
- </div>
1572
- <div class="event-content" style="-webkit-line-clamp:4;">${escapeHtml(e.preview || '')}</div>
1573
- </div>
1574
- `).join('');
1575
-
1576
- return heading + cards;
1577
- }).join('');
1578
-
1579
- listEl.innerHTML = html;
1580
- }
1581
-
1582
- async function loadUserPromptsView() {
1583
- const listEl = document.getElementById('user-prompt-list');
1584
- if (!listEl) return;
1585
-
1586
- listEl.innerHTML = '<div style="padding:20px; text-align:center; color:var(--text-muted);">Loading user prompts...</div>';
1587
-
1588
- try {
1589
- const params = {
1590
- type: 'user_prompt',
1591
- sort: 'recent',
1592
- limit: 500,
1593
- q: state.userPromptSearchQuery || undefined
1594
- };
1595
- const res = await fetch(apiUrl(`${API_BASE}/events`, params));
1596
- const data = await res.json();
1597
- const items = data.events || [];
1598
- state.userPromptItems = items;
1599
-
1600
- await renderUserPromptList();
1601
- } catch (error) {
1602
- listEl.innerHTML = `<div style="padding:20px; text-align:center; color:var(--error);">Failed to load user prompts: ${escapeHtml(error.message)}</div>`;
1603
- }
1604
- }
1605
-
1606
- // --- Configuration View ---
1607
-
1608
- async function loadConfigurationView() {
1609
- const container = document.getElementById('cfg-content');
1610
- container.innerHTML = '<div style="text-align:center; padding:60px; color:var(--text-muted);">Loading configuration...</div>';
1611
-
1612
- try {
1613
- const [statsRes, graduationRes, endlessRes] = await Promise.all([
1614
- fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
1615
- fetch(apiUrl(`${API_BASE}/stats/graduation`)).then(r => r.json()).catch(() => null),
1616
- fetch(apiUrl(`${API_BASE}/stats/endless`)).then(r => r.json()).catch(() => null)
1617
- ]);
1618
-
1619
- const memory = statsRes?.memory || {};
1620
- const storage = statsRes?.storage || {};
1621
- const criteria = graduationRes?.criteria || {};
1622
- const descriptions = graduationRes?.description || {};
1623
- const endless = endlessRes || {};
1624
-
1625
- container.innerHTML = `
1626
- <div class="cfg-grid">
1627
- <div class="cfg-section">
1628
- <div class="cfg-section-title"><i class="ri-database-2-line"></i>Storage</div>
1629
- <div class="cfg-row">
1630
- <span class="cfg-row-label">Total Events</span>
1631
- <span class="cfg-row-value">${formatNumber(storage.eventCount || 0)}</span>
1632
- </div>
1633
- <div class="cfg-row">
1634
- <span class="cfg-row-label">Vector Nodes</span>
1635
- <span class="cfg-row-value">${formatNumber(storage.vectorCount || 0)}</span>
1636
- </div>
1637
- <div class="cfg-row">
1638
- <span class="cfg-row-label">Heap Used</span>
1639
- <span class="cfg-row-value">${memory.heapUsed || 0} MB</span>
1640
- </div>
1641
- <div class="cfg-row">
1642
- <span class="cfg-row-label">Heap Total</span>
1643
- <span class="cfg-row-value">${memory.heapTotal || 0} MB</span>
1644
- </div>
1645
- </div>
1646
-
1647
- <div class="cfg-section">
1648
- <div class="cfg-section-title"><i class="ri-infinite-loop-line"></i>Endless Mode</div>
1649
- <div class="cfg-row">
1650
- <span class="cfg-row-label">Mode</span>
1651
- <span class="cfg-row-value">${endless.mode || 'session'}</span>
1652
- </div>
1653
- <div class="cfg-row">
1654
- <span class="cfg-row-label">Continuity Score</span>
1655
- <span class="cfg-row-value">${endless.continuityScore || 0}</span>
1656
- </div>
1657
- <div class="cfg-row">
1658
- <span class="cfg-row-label">Working Set Size</span>
1659
- <span class="cfg-row-value">${endless.workingSetSize || 0}</span>
1660
- </div>
1661
- <div class="cfg-row">
1662
- <span class="cfg-row-label">Consolidated</span>
1663
- <span class="cfg-row-value">${endless.consolidatedCount || 0}</span>
1664
- </div>
1665
- <div class="cfg-row">
1666
- <span class="cfg-row-label">Last Consolidation</span>
1667
- <span class="cfg-row-value">${endless.lastConsolidation ? new Date(endless.lastConsolidation).toLocaleDateString() : 'Never'}</span>
1668
- </div>
1669
- </div>
1670
- </div>
1671
-
1672
- <div class="card" style="margin-top:24px;">
1673
- <div class="card-header">
1674
- <div class="card-title"><i class="ri-graduation-cap-line"></i><span>Graduation Criteria</span></div>
1675
- </div>
1676
- <div style="margin-bottom:16px; font-size:13px; color:var(--text-muted);">
1677
- ${Object.entries(descriptions).map(([key, desc]) => `
1678
- <div style="margin-bottom:4px;"><strong style="color:var(--text-secondary);">${key}</strong>: ${desc}</div>
1679
- `).join('')}
1680
- </div>
1681
- <div style="display:grid; grid-template-columns:repeat(2, 1fr); gap:16px;">
1682
- ${Object.entries(criteria).map(([key, c]) => `
1683
- <div style="background:var(--bg-panel); border-radius:12px; padding:16px;">
1684
- <div style="font-size:14px; font-weight:600; color:var(--accent-primary); margin-bottom:12px;">${key}</div>
1685
- <div class="cfg-row"><span class="cfg-row-label">Min Access Count</span><span class="cfg-row-value">${c.minAccessCount}</span></div>
1686
- <div class="cfg-row"><span class="cfg-row-label">Min Confidence</span><span class="cfg-row-value">${c.minConfidence}</span></div>
1687
- <div class="cfg-row"><span class="cfg-row-label">Cross-Session Refs</span><span class="cfg-row-value">${c.minCrossSessionRefs}</span></div>
1688
- <div class="cfg-row"><span class="cfg-row-label">Max Age (days)</span><span class="cfg-row-value">${c.maxAgeDays}</span></div>
1689
- </div>
1690
- `).join('')}
1691
- </div>
1692
- </div>
1693
- `;
1694
- } catch (error) {
1695
- container.innerHTML = `<div style="text-align:center; padding:60px; color:var(--error);">Failed to load configuration: ${escapeHtml(error.message)}</div>`;
1696
- }
1697
- }
1698
-
1699
- // --- Helpers ---
1700
-
1701
- function debounce(func, wait) {
1702
- let timeout;
1703
- return function executedFunction(...args) {
1704
- const later = () => {
1705
- clearTimeout(timeout);
1706
- func(...args);
1707
- };
1708
- clearTimeout(timeout);
1709
- timeout = setTimeout(later, wait);
1710
- };
1711
- }
1712
-
1713
- function handleSearch(query) {
1714
- console.log('Searching for:', query);
1715
- }
1716
-
1717
- function escapeHtml(unsafe) {
1718
- return String(unsafe)
1719
- .replace(/&/g, "&amp;")
1720
- .replace(/</g, "&lt;")
1721
- .replace(/>/g, "&gt;")
1722
- .replace(/"/g, "&quot;")
1723
- .replace(/'/g, "&#039;");
1724
- }
1725
-
1726
- // --- Chat Panel ---
1727
-
1728
- const CHAT_STORAGE_KEY = 'code-memory-chat-history';
1729
-
1730
- function loadChatHistory() {
1731
- try {
1732
- const raw = localStorage.getItem(CHAT_STORAGE_KEY);
1733
- return raw ? JSON.parse(raw) : [];
1734
- } catch { return []; }
1735
- }
1736
-
1737
- function saveChatHistory(conversations) {
1738
- try {
1739
- // Keep last 50 conversations max
1740
- const trimmed = conversations.slice(-50);
1741
- localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(trimmed));
1742
- } catch { /* storage full or unavailable */ }
1743
- }
1744
-
1745
- function saveCurrentConversation() {
1746
- if (state.chatMessages.length === 0) return;
1747
- const conversations = loadChatHistory();
1748
- const firstUserMsg = state.chatMessages.find(m => m.role === 'user');
1749
- const title = firstUserMsg ? firstUserMsg.content.slice(0, 80) : 'Untitled';
1750
-
1751
- if (state.chatConversationId) {
1752
- // Update existing
1753
- const idx = conversations.findIndex(c => c.id === state.chatConversationId);
1754
- if (idx >= 0) {
1755
- conversations[idx].messages = [...state.chatMessages];
1756
- conversations[idx].updatedAt = new Date().toISOString();
1757
- conversations[idx].title = title;
1758
- }
1759
- } else {
1760
- // Create new
1761
- state.chatConversationId = 'chat-' + Date.now();
1762
- conversations.push({
1763
- id: state.chatConversationId,
1764
- title,
1765
- messages: [...state.chatMessages],
1766
- createdAt: new Date().toISOString(),
1767
- updatedAt: new Date().toISOString(),
1768
- project: state.currentProject || 'global'
1769
- });
1770
- }
1771
- saveChatHistory(conversations);
1772
- }
1773
-
1774
- function startNewConversation() {
1775
- saveCurrentConversation();
1776
- state.chatMessages = [];
1777
- state.chatConversationId = null;
1778
-
1779
- const container = document.getElementById('chat-messages');
1780
- container.innerHTML = `
1781
- <div class="chat-welcome">
1782
- <div class="chat-welcome-icon">🧠</div>
1783
- <div class="chat-welcome-title">Ask about your memories</div>
1784
- <div class="chat-welcome-text">
1785
- I can search through your coding sessions, tool usage, and stored knowledge to answer questions.
1786
- </div>
1787
- </div>
1788
- `;
1789
- switchChatTab('chat');
1790
- }
1791
-
1792
- function loadConversation(id) {
1793
- const conversations = loadChatHistory();
1794
- const conv = conversations.find(c => c.id === id);
1795
- if (!conv) return;
1796
-
1797
- // Save current first
1798
- if (state.chatMessages.length > 0 && state.chatConversationId !== id) {
1799
- saveCurrentConversation();
1800
- }
1801
-
1802
- state.chatConversationId = conv.id;
1803
- state.chatMessages = [...conv.messages];
1804
-
1805
- // Render messages
1806
- const container = document.getElementById('chat-messages');
1807
- container.innerHTML = '';
1808
- for (const msg of conv.messages) {
1809
- appendChatMessage(msg.role, msg.content);
1810
- }
1811
-
1812
- switchChatTab('chat');
1813
- }
1814
-
1815
- function deleteConversation(id, evt) {
1816
- evt.stopPropagation();
1817
- const conversations = loadChatHistory().filter(c => c.id !== id);
1818
- saveChatHistory(conversations);
1819
- if (state.chatConversationId === id) {
1820
- state.chatMessages = [];
1821
- state.chatConversationId = null;
1822
- const container = document.getElementById('chat-messages');
1823
- container.innerHTML = `
1824
- <div class="chat-welcome">
1825
- <div class="chat-welcome-icon">🧠</div>
1826
- <div class="chat-welcome-title">Ask about your memories</div>
1827
- <div class="chat-welcome-text">
1828
- I can search through your coding sessions, tool usage, and stored knowledge to answer questions.
1829
- </div>
1830
- </div>
1831
- `;
1832
- }
1833
- renderHistoryList();
1834
- }
1835
-
1836
- function renderHistoryList() {
1837
- const container = document.getElementById('chat-history-view');
1838
- const conversations = loadChatHistory().reverse(); // newest first
1839
-
1840
- if (conversations.length === 0) {
1841
- container.innerHTML = '<div class="chat-history-empty">No conversation history yet.</div>';
1842
- return;
1843
- }
1844
-
1845
- container.innerHTML = conversations.map(conv => {
1846
- const date = new Date(conv.updatedAt || conv.createdAt);
1847
- const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1848
- const msgCount = conv.messages.length;
1849
- const isActive = conv.id === state.chatConversationId;
1850
- return `
1851
- <div class="chat-history-item${isActive ? ' active' : ''}" onclick="loadConversation('${conv.id}')"
1852
- style="${isActive ? 'border-color:var(--accent-primary);background:rgba(123,97,255,0.08);' : ''}">
1853
- <div class="chat-history-item-title">${escapeHtml(conv.title)}</div>
1854
- <div class="chat-history-item-meta">
1855
- <span>${dateStr} &middot; ${msgCount} messages</span>
1856
- <button class="chat-history-item-delete" onclick="deleteConversation('${conv.id}', event)" title="Delete">
1857
- <i class="ri-delete-bin-line"></i>
1858
- </button>
1859
- </div>
1860
- </div>
1861
- `;
1862
- }).join('');
1863
- }
1864
-
1865
- function switchChatTab(tab) {
1866
- const msgContainer = document.getElementById('chat-messages');
1867
- const historyContainer = document.getElementById('chat-history-view');
1868
- const inputArea = document.querySelector('.chat-input-area');
1869
-
1870
- document.querySelectorAll('.chat-header-tab').forEach(t => {
1871
- t.classList.toggle('active', t.dataset.chatTab === tab);
1872
- });
1873
-
1874
- if (tab === 'chat') {
1875
- msgContainer.classList.remove('hidden');
1876
- historyContainer.classList.remove('active');
1877
- if (inputArea) inputArea.style.display = '';
1878
- } else {
1879
- msgContainer.classList.add('hidden');
1880
- historyContainer.classList.add('active');
1881
- if (inputArea) inputArea.style.display = 'none';
1882
- renderHistoryList();
1883
- }
1884
-
1885
- state.chatCurrentTab = tab;
1886
- }
1887
-
1888
- function toggleChatPanel() {
1889
- if (state.isChatOpen) {
1890
- closeChatPanel();
1891
- } else {
1892
- openChatPanel();
1893
- }
1894
- }
1895
-
1896
- function openChatPanel() {
1897
- const panel = document.getElementById('chat-panel');
1898
- if (panel) {
1899
- panel.classList.add('open');
1900
- state.isChatOpen = true;
1901
- updateChatProjectScope();
1902
- setTimeout(() => {
1903
- document.getElementById('chat-input')?.focus();
1904
- }, 300);
1905
- }
1906
- }
1907
-
1908
- function closeChatPanel() {
1909
- const panel = document.getElementById('chat-panel');
1910
- if (panel) {
1911
- panel.classList.remove('open');
1912
- state.isChatOpen = false;
1913
- }
1914
- if (state.chatAbortController) {
1915
- state.chatAbortController.abort();
1916
- state.chatAbortController = null;
1917
- state.isChatStreaming = false;
1918
- }
1919
- // Auto-save on close
1920
- saveCurrentConversation();
1921
- }
1922
-
1923
- function updateChatProjectScope() {
1924
- const el = document.getElementById('chat-project-scope');
1925
- if (!el) return;
1926
- if (state.currentProject) {
1927
- const proj = state.projects.find(p => p.hash === state.currentProject);
1928
- el.textContent = `Scope: ${proj?.projectName || state.currentProject}`;
1929
- } else {
1930
- el.textContent = 'Scope: All (Global)';
1931
- }
1932
- }
1933
-
1934
- async function sendChatMessage() {
1935
- const input = document.getElementById('chat-input');
1936
- const message = input.value.trim();
1937
- if (!message) return;
1938
-
1939
- input.value = '';
1940
- input.style.height = 'auto';
1941
- document.getElementById('chat-send-btn').disabled = true;
1942
-
1943
- // Add user message
1944
- state.chatMessages.push({ role: 'user', content: message });
1945
- appendChatMessage('user', message);
1946
-
1947
- // Remove welcome
1948
- const welcome = document.querySelector('.chat-welcome');
1949
- if (welcome) welcome.remove();
1950
-
1951
- // Show loading
1952
- const loadingEl = appendChatLoading();
1953
-
1954
- state.isChatStreaming = true;
1955
- state.chatAbortController = new AbortController();
1956
-
1957
- try {
1958
- const response = await fetch(apiUrl(`${API_BASE}/chat`), {
1959
- method: 'POST',
1960
- headers: { 'Content-Type': 'application/json' },
1961
- body: JSON.stringify({
1962
- message,
1963
- history: state.chatMessages.slice(-10)
1964
- }),
1965
- signal: state.chatAbortController.signal
1966
- });
1967
-
1968
- if (!response.ok) {
1969
- const err = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
1970
- throw new Error(err.error || `Request failed: ${response.status}`);
1971
- }
1972
-
1973
- loadingEl.remove();
1974
- const msgEl = appendChatMessage('assistant', '', true);
1975
- let fullContent = '';
1976
-
1977
- const reader = response.body.getReader();
1978
- const decoder = new TextDecoder();
1979
- let sseBuffer = '';
1980
-
1981
- while (true) {
1982
- const { done, value } = await reader.read();
1983
- if (done) break;
1984
-
1985
- sseBuffer += decoder.decode(value, { stream: true });
1986
- const lines = sseBuffer.split('\n');
1987
- sseBuffer = lines.pop() || '';
1988
-
1989
- for (const line of lines) {
1990
- if (line.startsWith('data: ')) {
1991
- const dataStr = line.slice(6);
1992
- try {
1993
- const data = JSON.parse(dataStr);
1994
- if (data.content) {
1995
- fullContent += data.content;
1996
- updateChatMessageContent(msgEl, fullContent);
1997
- scrollChatToBottom();
1998
- }
1999
- if (data.error) {
2000
- fullContent += `\n\n**Error:** ${data.error}`;
2001
- updateChatMessageContent(msgEl, fullContent);
2002
- }
2003
- } catch { /* skip */ }
2004
- }
2005
- }
2006
- }
2007
-
2008
- msgEl.classList.remove('streaming');
2009
- if (fullContent) {
2010
- state.chatMessages.push({ role: 'assistant', content: fullContent });
2011
- }
2012
-
2013
- // Auto-save after each response
2014
- saveCurrentConversation();
2015
-
2016
- } catch (err) {
2017
- if (loadingEl.parentNode) loadingEl.remove();
2018
- if (err.name !== 'AbortError') {
2019
- appendChatMessage('assistant',
2020
- `**Error:** ${err.message}\n\nMake sure the Claude CLI is installed and authenticated.`
2021
- );
2022
- }
2023
- } finally {
2024
- state.isChatStreaming = false;
2025
- state.chatAbortController = null;
2026
- const sendBtn = document.getElementById('chat-send-btn');
2027
- const chatInput = document.getElementById('chat-input');
2028
- if (sendBtn && chatInput) {
2029
- sendBtn.disabled = !chatInput.value.trim();
2030
- }
2031
- }
2032
- }
2033
-
2034
- function appendChatMessage(role, content, streaming = false) {
2035
- const container = document.getElementById('chat-messages');
2036
- const el = document.createElement('div');
2037
- el.className = `chat-msg ${role}${streaming ? ' streaming' : ''}`;
2038
-
2039
- if (role === 'assistant') {
2040
- el.innerHTML = renderMarkdown(content);
2041
- } else {
2042
- el.textContent = content;
2043
- }
2044
-
2045
- container.appendChild(el);
2046
- scrollChatToBottom();
2047
- return el;
2048
- }
2049
-
2050
- function appendChatLoading() {
2051
- const container = document.getElementById('chat-messages');
2052
- const el = document.createElement('div');
2053
- el.className = 'chat-loading';
2054
- el.innerHTML = `
2055
- <div class="chat-loading-dot"></div>
2056
- <div class="chat-loading-dot"></div>
2057
- <div class="chat-loading-dot"></div>
2058
- `;
2059
- container.appendChild(el);
2060
- scrollChatToBottom();
2061
- return el;
2062
- }
2063
-
2064
- function updateChatMessageContent(el, content) {
2065
- el.innerHTML = renderMarkdown(content);
2066
- }
2067
-
2068
- function scrollChatToBottom() {
2069
- const container = document.getElementById('chat-messages');
2070
- if (container) container.scrollTop = container.scrollHeight;
2071
- }
2072
-
2073
- function renderMarkdown(text) {
2074
- if (!text) return '';
2075
-
2076
- let html = escapeHtml(text);
2077
-
2078
- // Code blocks
2079
- html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
2080
-
2081
- // Inline code
2082
- html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
2083
-
2084
- // Bold
2085
- html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
2086
-
2087
- // Italic
2088
- html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
2089
-
2090
- // Headers
2091
- html = html.replace(/^### (.+)$/gm, '<div style="font-weight:600;color:var(--text-primary);margin:12px 0 4px;">$1</div>');
2092
- html = html.replace(/^## (.+)$/gm, '<div style="font-size:15px;font-weight:600;color:var(--text-primary);margin:12px 0 4px;">$1</div>');
2093
-
2094
- // Lists
2095
- html = html.replace(/^- (.+)$/gm, '<div style="padding-left:16px;">&#8226; $1</div>');
2096
-
2097
- // Line breaks
2098
- html = html.replace(/\n/g, '<br>');
2099
-
2100
- return html;
2101
- }