claude-memory-layer 1.0.26 → 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.
- package/.env.example +7 -0
- package/AGENTS.md +11 -0
- package/README.md +184 -41
- package/benchmarks/replay/anonymized-real-sessions.json +48 -0
- package/dist/cli/index.js +10097 -6003
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +9745 -5587
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +6545 -5270
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/semantic-daemon.js +6646 -5354
- package/dist/hooks/semantic-daemon.js.map +4 -4
- package/dist/hooks/session-end.js +6618 -5347
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +6619 -5354
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +6614 -5325
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +6702 -5356
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/index.js +13537 -0
- package/dist/index.js.map +7 -0
- package/dist/mcp/index.js +20770 -0
- package/dist/mcp/index.js.map +7 -0
- package/dist/server/api/index.js +6632 -5319
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +6667 -5340
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +6568 -5350
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/assets/js/bootstrap.js +244 -0
- package/dist/ui/assets/js/chat.js +373 -0
- package/dist/ui/assets/js/disclosure.js +232 -0
- package/dist/ui/assets/js/modals.js +298 -0
- package/dist/ui/assets/js/overview.js +655 -0
- package/dist/ui/assets/js/state.js +72 -0
- package/dist/ui/assets/js/views.js +468 -0
- package/dist/ui/index.html +43 -1
- package/dist/ui/index.ts +3 -0
- package/dist/ui/style.css +222 -0
- package/docs/ARCHITECTURE_COMPARISON_AND_RECOMMENDATIONS.md +627 -0
- package/docs/HERMES_MEMORY_INGESTION_ANALYSIS.md +440 -0
- package/docs/MEMORY_USEFULNESS_AUDIT.md +371 -0
- package/docs/MEMORY_USEFULNESS_AUDIT_RAW.json +80 -0
- package/docs/MEMSEARCH_PROJECT_STRUCTURE_ANALYSIS.md +333 -0
- package/docs/PRODUCT_VALIDATION_MATRIX.md +82 -0
- package/docs/PROJECT_STRUCTURE_ANALYSIS.md +421 -0
- package/docs/REFACTORING_MILESTONES_AND_ISSUES.md +501 -0
- package/docs/REFACTORING_PLAN_THIN_CORE.md +414 -0
- package/docs/REFERENCE_PROJECT_ANALYSES.md +25 -0
- package/docs/SUPERLOCALMEMORY_PROJECT_STRUCTURE_ANALYSIS.md +452 -0
- package/docs/TARGET_ARCHITECTURE_AND_FOLDER_STRUCTURE.md +446 -0
- package/docs/architecture/comparison-index.md +47 -0
- package/docs/reports/codex-real-data-validation-20260505T040447Z.md +46 -0
- package/package.json +9 -5
- package/scripts/build.ts +25 -8
- package/scripts/generate-session-qrels.ts +126 -0
- package/scripts/replay-retrieval-benchmark.ts +69 -0
- package/specs/thin-core-refactor/context.md +275 -0
- package/specs/thin-core-refactor/plan.md +536 -0
- package/specs/thin-core-refactor/spec.md +465 -0
- package/src/adapters/claude/capture/index.ts +3 -0
- package/src/adapters/claude/context/index.ts +3 -0
- package/src/adapters/claude/hooks/index.ts +21 -0
- package/src/adapters/claude/hooks/post-tool-use.ts +239 -0
- package/src/adapters/claude/hooks/prompt-injection-policy.ts +104 -0
- package/src/adapters/claude/hooks/semantic-daemon-client.ts +209 -0
- package/src/adapters/claude/hooks/semantic-daemon.ts +283 -0
- package/src/adapters/claude/hooks/session-end.ts +59 -0
- package/src/adapters/claude/hooks/session-start.ts +73 -0
- package/src/adapters/claude/hooks/stop.ts +128 -0
- package/src/adapters/claude/hooks/user-prompt-submit.ts +361 -0
- package/src/adapters/claude/index.ts +4 -0
- package/src/adapters/claude/transcript/index.ts +4 -0
- package/src/adapters/claude/transcript/transcript-reader.ts +57 -0
- package/src/adapters/claude/transcript/turn-reconstructor.ts +65 -0
- package/src/apps/cli/claude-settings-hooks.ts +138 -0
- package/src/apps/cli/codex-import-runner.ts +125 -0
- package/src/apps/cli/codex-validation-output.ts +95 -0
- package/src/apps/cli/hermes-import-runner.ts +130 -0
- package/src/apps/cli/hermes-validation-output.ts +91 -0
- package/src/apps/cli/index.ts +1731 -0
- package/src/apps/cli/mcp-install.ts +106 -0
- package/src/apps/cli/retrieval-disclosure-output.ts +196 -0
- package/src/apps/dashboard/assets/js/bootstrap.js +244 -0
- package/src/apps/dashboard/assets/js/chat.js +373 -0
- package/src/apps/dashboard/assets/js/disclosure.js +232 -0
- package/src/apps/dashboard/assets/js/modals.js +298 -0
- package/src/apps/dashboard/assets/js/overview.js +655 -0
- package/src/apps/dashboard/assets/js/state.js +72 -0
- package/src/apps/dashboard/assets/js/views.js +468 -0
- package/src/{ui → apps/dashboard}/index.html +43 -1
- package/src/apps/dashboard/index.ts +3 -0
- package/src/{ui → apps/dashboard}/style.css +222 -0
- package/src/apps/index.ts +5 -0
- package/src/apps/server/api/chat.ts +244 -0
- package/src/apps/server/api/citations.ts +105 -0
- package/src/apps/server/api/events.ts +137 -0
- package/src/apps/server/api/health.ts +53 -0
- package/src/apps/server/api/index.ts +26 -0
- package/src/apps/server/api/projects.ts +74 -0
- package/src/apps/server/api/search.ts +184 -0
- package/src/apps/server/api/sessions.ts +115 -0
- package/src/apps/server/api/stats.ts +723 -0
- package/src/apps/server/api/turns.ts +143 -0
- package/src/apps/server/api/utils.ts +65 -0
- package/src/apps/server/index.ts +111 -0
- package/src/cli/index.ts +2 -1311
- package/src/cli/retrieval-disclosure-output.ts +2 -0
- package/src/compat/index.ts +5 -0
- package/src/core/derive/fact-deriver.ts +170 -0
- package/src/core/derive/index.ts +2 -0
- package/src/core/derive/summary-deriver.ts +76 -0
- package/src/core/embedder.ts +4 -152
- package/src/core/engine/embedding-maintenance-service.ts +187 -0
- package/src/core/engine/endless-memory-services.ts +4 -0
- package/src/core/engine/index.ts +19 -0
- package/src/core/engine/memory-engine-services.ts +170 -0
- package/src/core/engine/memory-ingest-service.ts +317 -0
- package/src/core/engine/memory-query-service.ts +173 -0
- package/src/core/engine/memory-runtime-service.ts +162 -0
- package/src/core/engine/memory-service-composition.ts +231 -0
- package/src/core/engine/retrieval-analytics-service.ts +181 -0
- package/src/core/engine/retrieval-disclosure-service.ts +420 -0
- package/src/core/engine/retrieval-orchestrator.ts +377 -0
- package/src/core/engine/retrieval-services.ts +176 -0
- package/src/core/engine/shared-memory-services.ts +4 -0
- package/src/core/entity-repo.ts +1 -3
- package/src/core/event-store.ts +3 -3
- package/src/core/evidence-aligner.ts +2 -2
- package/src/core/external-market-context.ts +582 -0
- package/src/core/graduation.ts +2 -3
- package/src/core/index.ts +21 -0
- package/src/core/matcher.ts +2 -4
- package/src/core/model/memory-fact.ts +30 -0
- package/src/core/model/memory-rule.ts +14 -0
- package/src/core/model/memory-summary.ts +21 -0
- package/src/core/model/raw-event.ts +28 -0
- package/src/core/model/retrieval-result.ts +35 -0
- package/src/core/privacy/filter.ts +21 -10
- package/src/core/product-validation-matrix.ts +314 -0
- package/src/core/progressive-retriever.ts +1 -2
- package/src/core/registry/project-path.ts +54 -0
- package/src/core/registry/session-registry.ts +69 -0
- package/src/core/replay-evaluator.ts +625 -0
- package/src/core/retrieval-benchmark.ts +117 -0
- package/src/core/retrieval-quality.ts +109 -0
- package/src/core/retriever.ts +53 -15
- package/src/core/session-qrels.ts +360 -0
- package/src/core/shared-event-store.ts +1 -1
- package/src/core/sqlite-event-store.ts +35 -11
- package/src/core/task/blocker-resolver.ts +2 -2
- package/src/core/task/task-resolver.ts +0 -1
- package/src/core/vector-outbox.ts +1 -10
- package/src/core/vector-worker.ts +1 -1
- package/src/extensions/endless-memory/endless-memory-services.ts +350 -0
- package/src/extensions/endless-memory/index.ts +1 -0
- package/src/extensions/index.ts +5 -0
- package/src/extensions/mcp/handlers.ts +960 -0
- package/src/extensions/mcp/index.ts +48 -0
- package/src/extensions/mcp/tools.ts +252 -0
- package/src/extensions/shared-memory/index.ts +1 -0
- package/src/extensions/shared-memory/shared-memory-services.ts +211 -0
- package/src/extensions/vector/embedder.ts +197 -0
- package/src/extensions/vector/index.ts +1 -0
- package/src/hooks/post-tool-use.ts +3 -236
- package/src/hooks/semantic-daemon-client.ts +1 -208
- package/src/hooks/semantic-daemon.ts +6 -271
- package/src/hooks/session-end.ts +4 -79
- package/src/hooks/session-start.ts +4 -73
- package/src/hooks/stop.ts +3 -173
- package/src/hooks/user-prompt-submit.ts +3 -338
- package/src/index.ts +13 -0
- package/src/mcp/handlers.ts +2 -212
- package/src/mcp/index.ts +3 -46
- package/src/mcp/tools.ts +2 -78
- package/src/server/api/chat.ts +2 -244
- package/src/server/api/citations.ts +2 -105
- package/src/server/api/events.ts +2 -137
- package/src/server/api/health.ts +2 -53
- package/src/server/api/index.ts +2 -26
- package/src/server/api/projects.ts +2 -74
- package/src/server/api/search.ts +2 -102
- package/src/server/api/sessions.ts +2 -115
- package/src/server/api/stats.ts +2 -724
- package/src/server/api/turns.ts +2 -143
- package/src/server/api/utils.ts +2 -46
- package/src/server/index.ts +2 -100
- package/src/services/bootstrap-organizer.ts +46 -26
- package/src/services/codex-session-history-importer.ts +521 -29
- package/src/services/hermes-session-history-importer.ts +733 -0
- package/src/services/memory-service-config.ts +36 -0
- package/src/services/memory-service-registry.ts +150 -0
- package/src/services/memory-service.ts +211 -1325
- package/src/services/session-history-importer.ts +58 -14
- package/tests/README.md +23 -0
- package/tests/adapters/claude/claude-semantic-daemon-adapter.test.ts +54 -0
- package/tests/adapters/claude/claude-transcript-reconstructor.test.ts +98 -0
- package/tests/adapters/claude-hook-prompt-injection-policy.test.ts +99 -0
- package/tests/apps/app-layer-boundary.test.ts +48 -0
- package/tests/apps/claude-settings-hooks.test.ts +107 -0
- package/tests/apps/cli-disclosure-output.test.ts +212 -0
- package/tests/apps/codex-import-runner.test.ts +99 -0
- package/tests/apps/codex-validation-output.test.ts +100 -0
- package/tests/apps/hermes-import-runner.test.ts +99 -0
- package/tests/apps/mcp-install-command.test.ts +59 -0
- package/tests/apps/package-build-entrypoints.test.ts +30 -0
- package/tests/apps/search-api-disclosure.test.ts +162 -0
- package/tests/apps/stats-api-lightweight.test.ts +67 -0
- package/tests/apps/ui-disclosure-output.test.ts +140 -0
- package/tests/{bootstrap-organizer.test.ts → core/bootstrap-organizer.test.ts} +1 -1
- package/tests/{canonical-key.test.ts → core/canonical-key.test.ts} +1 -1
- package/tests/core/codex-session-history-importer-validation.test.ts +185 -0
- package/tests/{consolidation-worker.test.ts → core/consolidation-worker.test.ts} +2 -2
- package/tests/core/embedding-maintenance-service.test.ts +282 -0
- package/tests/{evidence-aligner.test.ts → core/evidence-aligner.test.ts} +1 -1
- package/tests/core/external-market-context.test.ts +209 -0
- package/tests/core/fact-deriver.test.ts +79 -0
- package/tests/core/hermes-session-history-importer-validation.test.ts +609 -0
- package/tests/{ingest-interceptor.test.ts → core/ingest-interceptor.test.ts} +1 -1
- package/tests/{markdown-mirror.test.ts → core/markdown-mirror.test.ts} +2 -2
- package/tests/{matcher.test.ts → core/matcher.test.ts} +1 -1
- package/tests/{md-mirror.test.ts → core/md-mirror.test.ts} +2 -2
- package/tests/core/memory-engine-services.test.ts +240 -0
- package/tests/core/memory-ingest-service.test.ts +296 -0
- package/tests/core/memory-query-service.test.ts +129 -0
- package/tests/core/memory-runtime-service.test.ts +201 -0
- package/tests/core/memory-service-composition.test.ts +192 -0
- package/tests/core/memory-service-config.test.ts +41 -0
- package/tests/core/memory-service-facade.test.ts +30 -0
- package/tests/core/memory-service-registry.test.ts +206 -0
- package/tests/core/product-validation-matrix.test.ts +61 -0
- package/tests/core/project-registry.test.ts +78 -0
- package/tests/core/replay-evaluator.test.ts +181 -0
- package/tests/core/retrieval-analytics-service.test.ts +210 -0
- package/tests/core/retrieval-benchmark.test.ts +93 -0
- package/tests/core/retrieval-disclosure-service.test.ts +264 -0
- package/tests/core/retrieval-orchestrator.test.ts +403 -0
- package/tests/core/retrieval-quality.test.ts +31 -0
- package/tests/core/retrieval-services.test.ts +185 -0
- package/tests/{retriever-fallback-chain.test.ts → core/retriever-fallback-chain.test.ts} +3 -3
- package/tests/{retriever-strategy-scope.test.ts → core/retriever-strategy-scope.test.ts} +70 -3
- package/tests/{retriever.memu-adoption.test.ts → core/retriever.memu-adoption.test.ts} +3 -3
- package/tests/core/session-history-importer-filter.test.ts +78 -0
- package/tests/core/session-qrels.test.ts +250 -0
- package/tests/{sqlite-event-store-replication.test.ts → core/sqlite-event-store-replication.test.ts} +36 -1
- package/tests/core/summary-deriver.test.ts +66 -0
- package/tests/extensions/embedder-warning-suppression.test.ts +53 -0
- package/tests/extensions/endless-memory-extension-boundary.test.ts +17 -0
- package/tests/extensions/endless-memory-services.test.ts +325 -0
- package/tests/extensions/mcp-context-tools.test.ts +905 -0
- package/tests/extensions/mcp-extension-boundary.test.ts +21 -0
- package/tests/extensions/mcp-package-build.test.ts +22 -0
- package/tests/extensions/mcp-project-aware-tools.test.ts +102 -0
- package/tests/extensions/shared-memory-extension-boundary.test.ts +24 -0
- package/tests/extensions/shared-memory-services.test.ts +309 -0
- package/tests/extensions/vector-extension-boundary.test.ts +21 -0
- package/.claude/settings.local.json +0 -25
- package/.npm-cache/_cacache/content-v2/sha512/04/76/c098f88dfe584a2b80870bff7421b05d17d3d9ee1027f77772332a22d3f93a9a57101a2855107f6ad82077a818bba912b2bc317f2361b5ddb09ad284d9ce +0 -0
- package/.npm-cache/_cacache/content-v2/sha512/60/25/d2ecd39cfc7cab58351162814be77f935c6d6491c10c3745d456da7ddb2117ffd90c10e53fe3c0f1ed16b403307841543634504398b16ee4e6b6dd8e0c45 +0 -0
- package/.npm-cache/_cacache/index-v5/2b/9a/7f8f40206ed8a2e0a84efaa953ccaed1f5d001e14b931083f2e7a0738007 +0 -2
- package/.npm-cache/_cacache/index-v5/2e/d9/fcfa5c6a6abdc2a3644ab84a95936047298c465a2f47ee03db8f7fe1e946 +0 -3
- package/.npm-cache/_cacache/index-v5/a9/42/e519633356d12d3d2f19da66a8301016d496c8f5c3e0554124aaa62dc043 +0 -2
- package/.npm-cache/_logs/2026-02-26T12_04_52_729Z-debug-0.log +0 -256
- package/.npm-cache/_logs/2026-02-26T12_05_36_835Z-debug-0.log +0 -18
- package/.npm-cache/_logs/2026-02-26T12_05_45_982Z-debug-0.log +0 -32
- package/.npm-cache/_logs/2026-02-26T12_05_48_515Z-debug-0.log +0 -260
- package/.npm-cache/_logs/2026-02-26T12_05_53_567Z-debug-0.log +0 -69
- package/.npm-cache/_update-notifier-last-checked +0 -0
- package/bootstrap-kb/decisions/decisions.md +0 -244
- package/bootstrap-kb/glossary/glossary.md +0 -46
- package/bootstrap-kb/modules/.claude-plugin.md +0 -22
- package/bootstrap-kb/modules/agents.md.md +0 -15
- package/bootstrap-kb/modules/claude.md.md +0 -15
- package/bootstrap-kb/modules/context.md.md +0 -15
- package/bootstrap-kb/modules/docs.md +0 -18
- package/bootstrap-kb/modules/handoff.md.md +0 -15
- package/bootstrap-kb/modules/package-lock.json.md +0 -15
- package/bootstrap-kb/modules/package.json.md +0 -15
- package/bootstrap-kb/modules/plan.md.md +0 -15
- package/bootstrap-kb/modules/readme.md.md +0 -15
- package/bootstrap-kb/modules/scripts.md +0 -26
- package/bootstrap-kb/modules/spec.md.md +0 -15
- package/bootstrap-kb/modules/specs.md +0 -20
- package/bootstrap-kb/modules/src.md +0 -51
- package/bootstrap-kb/modules/tests.md +0 -42
- package/bootstrap-kb/modules/tsconfig.json.md +0 -15
- package/bootstrap-kb/modules/vitest.config.ts.md +0 -15
- package/bootstrap-kb/overview/overview.md +0 -40
- package/bootstrap-kb/sources/manifest.json +0 -950
- package/bootstrap-kb/sources/manifest.md +0 -227
- package/bootstrap-kb/timeline/timeline.md +0 -57
- package/claude-memory-layer-1.0.14.tgz +0 -0
- package/d.sh +0 -3
- package/deploy.sh +0 -3
- package/dist/ui/app.js +0 -2101
- package/memory/.claude-plugin/commands/2026-02-25.md +0 -263
- package/memory/_index.md +0 -418
- package/memory/agent_response/uncategorized/2026-02-26.md +0 -176
- package/memory/agent_response/uncategorized/2026-03-03.md +0 -14
- package/memory/agent_response/uncategorized/2026-03-04.md +0 -1421
- package/memory/agent_response/uncategorized/2026-03-05.md +0 -48
- package/memory/default/uncategorized/2026-02-25.md +0 -4839
- package/memory/session_summary/uncategorized/2026-02-26.md +0 -13
- package/memory/session_summary/uncategorized/2026-03-03.md +0 -5
- package/memory/session_summary/uncategorized/2026-03-04.md +0 -50
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +0 -142
- package/memory/specs/citations-system/2026-02-25.md +0 -1121
- package/memory/specs/endless-mode/2026-02-25.md +0 -1392
- package/memory/specs/entity-edge-model/2026-02-25.md +0 -1263
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +0 -1028
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +0 -1334
- package/memory/specs/post-tool-use-hook/2026-02-25.md +0 -1164
- package/memory/specs/private-tags/2026-02-25.md +0 -1057
- package/memory/specs/progressive-disclosure/2026-02-25.md +0 -1436
- package/memory/specs/task-entity-system/2026-02-25.md +0 -924
- package/memory/specs/vector-outbox-v2/2026-02-25.md +0 -1510
- package/memory/specs/web-viewer-ui/2026-02-25.md +0 -1709
- package/memory/tool_observation/uncategorized/2026-02-26.md +0 -209
- package/memory/tool_observation/uncategorized/2026-03-03.md +0 -21
- package/memory/tool_observation/uncategorized/2026-03-04.md +0 -1033
- package/memory/tool_observation/uncategorized/2026-03-05.md +0 -29
- package/memory/user_prompt/uncategorized/2026-02-26.md +0 -25
- package/memory/user_prompt/uncategorized/2026-03-04.md +0 -634
- package/specs/optional-duckdb/context.md +0 -77
- package/specs/optional-duckdb/plan.md +0 -142
- package/specs/optional-duckdb/spec.md +0 -35
- 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, "&")
|
|
1720
|
-
.replace(/</g, "<")
|
|
1721
|
-
.replace(/>/g, ">")
|
|
1722
|
-
.replace(/"/g, """)
|
|
1723
|
-
.replace(/'/g, "'");
|
|
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} · ${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;">• $1</div>');
|
|
2096
|
-
|
|
2097
|
-
// Line breaks
|
|
2098
|
-
html = html.replace(/\n/g, '<br>');
|
|
2099
|
-
|
|
2100
|
-
return html;
|
|
2101
|
-
}
|