claude-memory-layer 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/AGENTS.md +60 -0
  2. package/README.md +166 -2
  3. package/bootstrap-kb/decisions/decisions.md +244 -0
  4. package/bootstrap-kb/glossary/glossary.md +46 -0
  5. package/bootstrap-kb/modules/.claude-plugin.md +22 -0
  6. package/bootstrap-kb/modules/agents.md.md +15 -0
  7. package/bootstrap-kb/modules/claude.md.md +15 -0
  8. package/bootstrap-kb/modules/context.md.md +15 -0
  9. package/bootstrap-kb/modules/docs.md +18 -0
  10. package/bootstrap-kb/modules/handoff.md.md +15 -0
  11. package/bootstrap-kb/modules/package-lock.json.md +15 -0
  12. package/bootstrap-kb/modules/package.json.md +15 -0
  13. package/bootstrap-kb/modules/plan.md.md +15 -0
  14. package/bootstrap-kb/modules/readme.md.md +15 -0
  15. package/bootstrap-kb/modules/scripts.md +26 -0
  16. package/bootstrap-kb/modules/spec.md.md +15 -0
  17. package/bootstrap-kb/modules/specs.md +20 -0
  18. package/bootstrap-kb/modules/src.md +51 -0
  19. package/bootstrap-kb/modules/tests.md +42 -0
  20. package/bootstrap-kb/modules/tsconfig.json.md +15 -0
  21. package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
  22. package/bootstrap-kb/overview/overview.md +40 -0
  23. package/bootstrap-kb/sources/manifest.json +950 -0
  24. package/bootstrap-kb/sources/manifest.md +227 -0
  25. package/bootstrap-kb/timeline/timeline.md +57 -0
  26. package/d.sh +3 -0
  27. package/deploy.sh +3 -0
  28. package/dist/cli/index.js +3577 -389
  29. package/dist/cli/index.js.map +4 -4
  30. package/dist/core/index.js +1383 -138
  31. package/dist/core/index.js.map +4 -4
  32. package/dist/hooks/post-tool-use.js +1917 -214
  33. package/dist/hooks/post-tool-use.js.map +4 -4
  34. package/dist/hooks/session-end.js +1813 -231
  35. package/dist/hooks/session-end.js.map +4 -4
  36. package/dist/hooks/session-start.js +1802 -205
  37. package/dist/hooks/session-start.js.map +4 -4
  38. package/dist/hooks/stop.js +1909 -248
  39. package/dist/hooks/stop.js.map +4 -4
  40. package/dist/hooks/user-prompt-submit.js +1861 -206
  41. package/dist/hooks/user-prompt-submit.js.map +4 -4
  42. package/dist/server/api/index.js +2341 -217
  43. package/dist/server/api/index.js.map +4 -4
  44. package/dist/server/index.js +2350 -226
  45. package/dist/server/index.js.map +4 -4
  46. package/dist/services/memory-service.js +1805 -206
  47. package/dist/services/memory-service.js.map +4 -4
  48. package/dist/ui/app.js +1447 -55
  49. package/dist/ui/index.html +318 -147
  50. package/dist/ui/style.css +892 -0
  51. package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
  52. package/docs/MEMU_ADOPTION.md +40 -0
  53. package/docs/OPERATIONS.md +18 -0
  54. package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
  55. package/memory/_index.md +405 -0
  56. package/memory/default/uncategorized/2026-02-25.md +4839 -0
  57. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
  58. package/memory/specs/citations-system/2026-02-25.md +1121 -0
  59. package/memory/specs/endless-mode/2026-02-25.md +1392 -0
  60. package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
  61. package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
  62. package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
  63. package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
  64. package/memory/specs/private-tags/2026-02-25.md +1057 -0
  65. package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
  66. package/memory/specs/task-entity-system/2026-02-25.md +924 -0
  67. package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
  68. package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
  69. package/package.json +9 -2
  70. package/scripts/build.ts +6 -0
  71. package/scripts/fix-sync-gap.js +32 -0
  72. package/scripts/heartbeat-memory-orchestrator.sh +28 -0
  73. package/scripts/report-sync-gap.js +26 -0
  74. package/scripts/review-queue-auto-resolve.js +21 -0
  75. package/scripts/sync-gap-auto-heal.sh +17 -0
  76. package/specs/20260207-dashboard-upgrade/context.md +38 -0
  77. package/specs/20260207-dashboard-upgrade/spec.md +96 -0
  78. package/src/cli/index.ts +391 -60
  79. package/src/core/consolidated-store.ts +63 -1
  80. package/src/core/consolidation-worker.ts +115 -6
  81. package/src/core/event-store.ts +14 -0
  82. package/src/core/index.ts +1 -0
  83. package/src/core/ingest-interceptor.ts +80 -0
  84. package/src/core/markdown-mirror.ts +70 -0
  85. package/src/core/md-mirror.ts +92 -0
  86. package/src/core/mongo-sync-config.ts +165 -0
  87. package/src/core/mongo-sync-worker.ts +381 -0
  88. package/src/core/retriever.ts +540 -150
  89. package/src/core/sqlite-event-store.ts +794 -7
  90. package/src/core/sqlite-wrapper.ts +8 -0
  91. package/src/core/tag-taxonomy.ts +51 -0
  92. package/src/core/turn-state.ts +159 -0
  93. package/src/core/types.ts +51 -8
  94. package/src/core/vector-store.ts +21 -3
  95. package/src/hooks/post-tool-use.ts +68 -23
  96. package/src/hooks/session-end.ts +8 -3
  97. package/src/hooks/stop.ts +96 -25
  98. package/src/hooks/user-prompt-submit.ts +44 -5
  99. package/src/server/api/chat.ts +244 -0
  100. package/src/server/api/citations.ts +3 -3
  101. package/src/server/api/events.ts +30 -5
  102. package/src/server/api/health.ts +53 -0
  103. package/src/server/api/index.ts +9 -1
  104. package/src/server/api/projects.ts +74 -0
  105. package/src/server/api/search.ts +3 -3
  106. package/src/server/api/sessions.ts +3 -3
  107. package/src/server/api/stats.ts +89 -8
  108. package/src/server/api/turns.ts +143 -0
  109. package/src/server/api/utils.ts +46 -0
  110. package/src/services/bootstrap-organizer.ts +443 -0
  111. package/src/services/codex-session-history-importer.ts +474 -0
  112. package/src/services/memory-service.ts +508 -71
  113. package/src/services/session-history-importer.ts +215 -51
  114. package/src/ui/app.js +1447 -55
  115. package/src/ui/index.html +318 -147
  116. package/src/ui/style.css +892 -0
  117. package/tests/bootstrap-organizer.test.ts +111 -0
  118. package/tests/consolidation-worker.test.ts +75 -0
  119. package/tests/ingest-interceptor.test.ts +38 -0
  120. package/tests/markdown-mirror.test.ts +85 -0
  121. package/tests/md-mirror.test.ts +50 -0
  122. package/tests/retriever-fallback-chain.test.ts +223 -0
  123. package/tests/retriever-strategy-scope.test.ts +97 -0
  124. package/tests/retriever.memu-adoption.test.ts +122 -0
  125. package/tests/sqlite-event-store-replication.test.ts +92 -0
  126. package/.claude/settings.local.json +0 -27
  127. package/.claude-memory/test.sqlite +0 -0
  128. package/.history/package_20260201112328.json +0 -45
  129. package/.history/package_20260201113602.json +0 -45
  130. package/.history/package_20260201113713.json +0 -45
  131. package/.history/package_20260201114110.json +0 -45
  132. package/.history/package_20260201114632.json +0 -46
  133. package/.history/package_20260201133143.json +0 -45
  134. package/.history/package_20260201134319.json +0 -45
  135. package/.history/package_20260201134326.json +0 -45
  136. package/.history/package_20260201134334.json +0 -45
  137. package/.history/package_20260201134912.json +0 -45
  138. package/.history/package_20260201142928.json +0 -46
  139. package/.history/package_20260201192048.json +0 -47
  140. package/.history/package_20260202114053.json +0 -49
  141. package/.history/package_20260202121115.json +0 -49
  142. package/test_access.js +0 -49
package/dist/ui/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Code Memory Dashboard Logic
3
- * Handles state management, API calls, and UI updates.
3
+ * Handles state management, API calls, UI updates, modals, and navigation.
4
4
  */
5
5
 
6
6
  const API_BASE = '/api';
@@ -9,10 +9,23 @@ const API_BASE = '/api';
9
9
  const state = {
10
10
  stats: null,
11
11
  sharedStats: null,
12
+ mostAccessed: null,
13
+ helpfulness: null,
14
+ retrievalTraces: null,
12
15
  currentLevel: 'L0',
16
+ currentSort: 'recent',
17
+ currentView: 'overview',
18
+ currentProject: '', // empty = global
19
+ projects: [],
13
20
  events: [],
14
21
  isLoading: false,
15
- chartInstance: null
22
+ chartInstance: null,
23
+ chatMessages: [],
24
+ isChatOpen: false,
25
+ isChatStreaming: false,
26
+ chatAbortController: null,
27
+ chatConversationId: null,
28
+ chatCurrentTab: 'chat'
16
29
  };
17
30
 
18
31
  // Utils
@@ -27,6 +40,21 @@ const CHART_COLORS = {
27
40
  L4: '#FF4560'
28
41
  };
29
42
 
43
+ // --- API URL Helper ---
44
+
45
+ function apiUrl(path, params = {}) {
46
+ const url = new URL(path, window.location.origin);
47
+ if (state.currentProject) {
48
+ url.searchParams.set('project', state.currentProject);
49
+ }
50
+ for (const [key, value] of Object.entries(params)) {
51
+ if (value !== undefined && value !== null) {
52
+ url.searchParams.set(key, String(value));
53
+ }
54
+ }
55
+ return url.toString();
56
+ }
57
+
30
58
  // --- Initialization ---
31
59
 
32
60
  document.addEventListener('DOMContentLoaded', () => {
@@ -34,13 +62,38 @@ document.addEventListener('DOMContentLoaded', () => {
34
62
  });
35
63
 
36
64
  async function initDashboard() {
65
+ await loadProjects();
37
66
  await refreshData();
38
67
  setupEventListeners();
39
- initActivityChart();
68
+ await initActivityChart();
69
+ }
70
+
71
+ async function loadProjects() {
72
+ try {
73
+ const res = await fetch(`${API_BASE}/projects`);
74
+ const data = await res.json();
75
+ state.projects = data.projects || [];
76
+
77
+ const select = document.getElementById('project-select');
78
+ if (!select) return;
79
+
80
+ // Clear existing options except first
81
+ while (select.options.length > 1) select.remove(1);
82
+
83
+ // Add project options
84
+ state.projects.forEach(p => {
85
+ const option = document.createElement('option');
86
+ option.value = p.hash;
87
+ option.textContent = `${p.projectName} (${p.dbSizeHuman})`;
88
+ select.appendChild(option);
89
+ });
90
+ } catch (error) {
91
+ console.error('Failed to load projects:', error);
92
+ }
40
93
  }
41
94
 
42
95
  function setupEventListeners() {
43
- // Navigation
96
+ // Pipeline steps
44
97
  document.querySelectorAll('.p-step').forEach(step => {
45
98
  step.addEventListener('click', (e) => {
46
99
  const level = e.currentTarget.dataset.level;
@@ -48,17 +101,132 @@ function setupEventListeners() {
48
101
  });
49
102
  });
50
103
 
104
+ // Sort buttons
105
+ document.querySelectorAll('.sort-btn').forEach(btn => {
106
+ btn.addEventListener('click', (e) => {
107
+ const sort = e.currentTarget.dataset.sort;
108
+ if (sort) selectSort(sort);
109
+ });
110
+ });
111
+
51
112
  // Search
52
113
  const searchInput = document.getElementById('search-input');
53
114
  if (searchInput) {
54
115
  searchInput.addEventListener('input', debounce((e) => handleSearch(e.target.value), 300));
55
116
  }
56
117
 
118
+ // Project selector
119
+ const projectSelect = document.getElementById('project-select');
120
+ if (projectSelect) {
121
+ projectSelect.addEventListener('change', async (e) => {
122
+ state.currentProject = e.target.value;
123
+ await refreshData();
124
+ if (state.chartInstance) {
125
+ state.chartInstance.destroy();
126
+ state.chartInstance = null;
127
+ }
128
+ await initActivityChart();
129
+ // Reload current view if not overview
130
+ if (state.currentView !== 'overview') {
131
+ switchView(state.currentView);
132
+ }
133
+ updateChatProjectScope();
134
+ });
135
+ }
136
+
57
137
  // Refresh
58
138
  const refreshBtn = document.getElementById('refresh-btn');
59
139
  if (refreshBtn) {
60
140
  refreshBtn.addEventListener('click', refreshData);
61
141
  }
142
+
143
+ // Stat cards
144
+ document.querySelectorAll('.stat-card[data-stat]').forEach(card => {
145
+ card.addEventListener('click', () => {
146
+ handleStatClick(card.dataset.stat);
147
+ });
148
+ });
149
+
150
+ // Sidebar navigation
151
+ document.querySelectorAll('.nav-item[data-nav]').forEach(item => {
152
+ item.addEventListener('click', () => {
153
+ switchView(item.dataset.nav);
154
+ });
155
+ });
156
+
157
+ // Modal close buttons
158
+ document.querySelectorAll('.modal-close-btn').forEach(btn => {
159
+ btn.addEventListener('click', () => {
160
+ const modalId = btn.dataset.modal;
161
+ closeModal(modalId);
162
+ });
163
+ });
164
+
165
+ // Modal overlay click to close
166
+ document.querySelectorAll('.modal-overlay').forEach(overlay => {
167
+ overlay.addEventListener('click', (e) => {
168
+ if (e.target === overlay) {
169
+ closeModal(overlay.id);
170
+ }
171
+ });
172
+ });
173
+
174
+ // ESC key to close modals
175
+ document.addEventListener('keydown', (e) => {
176
+ if (e.key === 'Escape') {
177
+ if (state.isChatOpen) {
178
+ closeChatPanel();
179
+ } else {
180
+ closeAllModals();
181
+ }
182
+ }
183
+ });
184
+
185
+ // Chat panel
186
+ const chatToggle = document.getElementById('chat-toggle-btn');
187
+ if (chatToggle) {
188
+ chatToggle.addEventListener('click', toggleChatPanel);
189
+ }
190
+ const chatClose = document.getElementById('chat-close-btn');
191
+ if (chatClose) {
192
+ chatClose.addEventListener('click', () => closeChatPanel());
193
+ }
194
+
195
+ const chatInput = document.getElementById('chat-input');
196
+ const chatSendBtn = document.getElementById('chat-send-btn');
197
+ if (chatInput) {
198
+ chatInput.addEventListener('input', () => {
199
+ chatInput.style.height = 'auto';
200
+ chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
201
+ chatSendBtn.disabled = !chatInput.value.trim() || state.isChatStreaming;
202
+ });
203
+ chatInput.addEventListener('keydown', (e) => {
204
+ if (e.key === 'Enter' && !e.shiftKey) {
205
+ e.preventDefault();
206
+ if (chatInput.value.trim() && !state.isChatStreaming) {
207
+ sendChatMessage();
208
+ }
209
+ }
210
+ });
211
+ }
212
+ if (chatSendBtn) {
213
+ chatSendBtn.addEventListener('click', () => {
214
+ if (!state.isChatStreaming) sendChatMessage();
215
+ });
216
+ }
217
+
218
+ // Chat tabs
219
+ document.querySelectorAll('.chat-header-tab').forEach(tab => {
220
+ tab.addEventListener('click', () => {
221
+ switchChatTab(tab.dataset.chatTab);
222
+ });
223
+ });
224
+
225
+ // New conversation button
226
+ const chatNewBtn = document.getElementById('chat-new-btn');
227
+ if (chatNewBtn) {
228
+ chatNewBtn.addEventListener('click', startNewConversation);
229
+ }
62
230
  }
63
231
 
64
232
  // --- Data Fetching ---
@@ -66,21 +234,27 @@ function setupEventListeners() {
66
234
  async function refreshData() {
67
235
  const btn = document.getElementById('refresh-btn');
68
236
  if(btn) btn.classList.add('loading');
69
-
237
+
70
238
  try {
71
- const [stats, shared] = await Promise.all([
72
- fetch(`${API_BASE}/stats`).then(r => r.json()).catch(() => null),
73
- fetch(`${API_BASE}/stats/shared`).then(r => r.json()).catch(() => null)
239
+ const [stats, shared, mostAccessed, helpfulness, retrievalTraces] = await Promise.all([
240
+ fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
241
+ fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
242
+ fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
243
+ fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null),
244
+ fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null)
74
245
  ]);
75
246
 
76
247
  state.stats = stats;
77
248
  state.sharedStats = shared;
249
+ state.mostAccessed = mostAccessed;
250
+ state.helpfulness = helpfulness;
251
+ state.retrievalTraces = retrievalTraces;
78
252
 
79
253
  updateStatsUI();
80
254
  updateSharedUI();
255
+ updateMemoryUsageUI();
81
256
  await loadLevelEvents(state.currentLevel);
82
-
83
- // Update Endless Mode Status (Mocked if API missing)
257
+
84
258
  checkEndlessStatus();
85
259
 
86
260
  } catch (error) {
@@ -90,15 +264,13 @@ async function refreshData() {
90
264
  }
91
265
  }
92
266
 
93
- async function loadLevelEvents(level) {
267
+ async function loadLevelEvents(level, sort) {
268
+ if (sort) state.currentSort = sort;
94
269
  state.isLoading = true;
95
- updateEventsListUI(); // Show loading state
270
+ updateEventsListUI();
96
271
 
97
272
  try {
98
- // Determine API endpoint based on level
99
- // L0 -> /events, others might be filtered
100
- // For now, using the same pattern as original but adapted
101
- const response = await fetch(`${API_BASE}/events?level=${level}&limit=20`);
273
+ const response = await fetch(apiUrl(`${API_BASE}/events`, { level, limit: 20, sort: state.currentSort }));
102
274
  if (response.ok) {
103
275
  const data = await response.json();
104
276
  state.events = data.events || [];
@@ -119,52 +291,62 @@ async function loadLevelEvents(level) {
119
291
  function updateStatsUI() {
120
292
  if (!state.stats) return;
121
293
 
122
- const { total_events, total_sessions, total_vectors } = state.stats;
123
-
124
- document.getElementById('stat-events').textContent = formatNumber(total_events);
125
- document.getElementById('stat-sessions').textContent = formatNumber(total_sessions);
126
-
127
- // Consolidating shared stats as a simple sum for the header if needed,
128
- // or just using the shared object
129
- const sharedCount = state.sharedStats ?
130
- (state.sharedStats.troubleshooting + state.sharedStats.best_practices + state.sharedStats.common_errors) : 0;
131
-
294
+ const eventCount = state.stats.storage?.eventCount || 0;
295
+ const sessionCount = state.stats.sessions?.total || 0;
296
+ const vectorCount = state.stats.storage?.vectorCount || 0;
297
+
298
+ document.getElementById('stat-events').textContent = formatNumber(eventCount);
299
+ document.getElementById('stat-sessions').textContent = formatNumber(sessionCount);
300
+
301
+ const sharedCount = state.sharedStats ?
302
+ ((state.sharedStats.troubleshooting || 0) + (state.sharedStats.bestPractices || 0) + (state.sharedStats.commonErrors || 0)) : 0;
303
+
132
304
  document.getElementById('stat-shared').textContent = formatNumber(sharedCount);
133
- document.getElementById('stat-vectors').textContent = formatNumber(total_vectors);
305
+ document.getElementById('stat-vectors').textContent = formatNumber(vectorCount);
134
306
 
135
- // Update Pipeline Counts (Mock logic as original didn't have per-level counts easily accessible in stats object usually)
136
- // If stats has level breakdown use that, otherwise distribute for visual
137
- updatePipelineCounts(state.stats.level_counts || {});
307
+ const levelCounts = {};
308
+ if (state.stats.levelStats) {
309
+ state.stats.levelStats.forEach(item => { levelCounts[item.level] = item.count; });
310
+ }
311
+ updatePipelineCounts(levelCounts);
138
312
  }
139
313
 
140
314
  function updatePipelineCounts(counts) {
141
315
  document.querySelectorAll('.p-step').forEach(step => {
142
316
  const level = step.dataset.level;
143
317
  const countEl = step.querySelector('.p-step-count');
144
- // Default to 0 if not found
145
318
  countEl.textContent = formatNumber(counts[level] || 0);
146
319
  });
147
320
  }
148
321
 
149
322
  function updateSharedUI() {
150
323
  if (!state.sharedStats) return;
151
-
152
- document.getElementById('shared-troubleshooting').textContent = formatNumber(state.sharedStats.troubleshooting);
153
- document.getElementById('shared-best-practices').textContent = formatNumber(state.sharedStats.best_practices);
154
- document.getElementById('shared-errors').textContent = formatNumber(state.sharedStats.common_errors);
324
+
325
+ document.getElementById('shared-troubleshooting').textContent = formatNumber(state.sharedStats.troubleshooting || 0);
326
+ document.getElementById('shared-best-practices').textContent = formatNumber(state.sharedStats.bestPractices || 0);
327
+ document.getElementById('shared-errors').textContent = formatNumber(state.sharedStats.commonErrors || 0);
155
328
  }
156
329
 
157
330
  function selectLevel(level) {
158
331
  state.currentLevel = level;
159
-
160
- // Update Visuals
332
+
161
333
  document.querySelectorAll('.p-step').forEach(step => {
162
334
  step.classList.toggle('active', step.dataset.level === level);
163
335
  });
164
-
336
+
165
337
  loadLevelEvents(level);
166
338
  }
167
339
 
340
+ function selectSort(sort) {
341
+ state.currentSort = sort;
342
+
343
+ document.querySelectorAll('.sort-btn').forEach(btn => {
344
+ btn.classList.toggle('active', btn.dataset.sort === sort);
345
+ });
346
+
347
+ loadLevelEvents(state.currentLevel, sort);
348
+ }
349
+
168
350
  function updateEventsListUI() {
169
351
  const container = document.getElementById('event-list-container');
170
352
  container.innerHTML = '';
@@ -182,32 +364,220 @@ function updateEventsListUI() {
182
364
  state.events.forEach(event => {
183
365
  const el = document.createElement('div');
184
366
  el.className = 'event-item';
185
-
367
+ el.style.cursor = 'pointer';
368
+ el.addEventListener('click', () => openDetailModal(event.id));
369
+
186
370
  const time = new Date(event.timestamp).toLocaleString();
187
- const typeClass = `type-${event.type.toLowerCase().replace('_', '-')}`;
188
-
371
+ const eventType = event.eventType || event.type || 'unknown';
372
+ const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
373
+ const preview = event.preview || event.content || '';
374
+ const accessBadge = event.accessCount > 0
375
+ ? `<span class="access-badge"><i class="ri-eye-line"></i> ${event.accessCount}</span>`
376
+ : '';
377
+ const lastUsed = (state.currentSort === 'accessed' || state.currentSort === 'most-accessed') && event.lastAccessedAt
378
+ ? `<span class="event-time" style="color:var(--accent-secondary);">used ${new Date(event.lastAccessedAt).toLocaleString()}</span>`
379
+ : '';
380
+
189
381
  el.innerHTML = `
190
382
  <div class="event-header">
191
- <span class="event-type-badge ${typeClass}">${event.type}</span>
192
- <span class="event-time">${time}</span>
383
+ <span class="event-type-badge ${typeClass}">${eventType}</span>
384
+ <div style="display:flex; gap:8px; align-items:center;">
385
+ ${accessBadge}
386
+ ${lastUsed}
387
+ <span class="event-time">${time}</span>
388
+ </div>
193
389
  </div>
194
- <div class="event-content">${escapeHtml(event.content || '')}</div>
390
+ <div class="event-content">${escapeHtml(preview)}</div>
195
391
  `;
196
-
392
+
197
393
  container.appendChild(el);
198
394
  });
199
395
  }
200
396
 
397
+ // --- Memory Usage ---
398
+
399
+ function updateMemoryUsageUI() {
400
+ updateGraduationBars();
401
+ updateHelpfulnessUI();
402
+ updateMostHelpfulList();
403
+ updateRetrievalTraceUI();
404
+ }
405
+
406
+ function updateGraduationBars() {
407
+ const container = document.getElementById('graduation-bars');
408
+ if (!container || !state.stats?.levelStats) return;
409
+
410
+ const levels = ['L0', 'L1', 'L2', 'L3', 'L4'];
411
+ const colors = [CHART_COLORS.L0, CHART_COLORS.L1, CHART_COLORS.L2, CHART_COLORS.L3, CHART_COLORS.L4];
412
+
413
+ const counts = {};
414
+ state.stats.levelStats.forEach(s => { counts[s.level] = s.count; });
415
+ const total = Object.values(counts).reduce((a, b) => a + b, 0) || 1;
416
+
417
+ container.innerHTML = levels.map((level, i) => {
418
+ const count = counts[level] || 0;
419
+ const pct = ((count / total) * 100).toFixed(1);
420
+ return `
421
+ <div class="grad-bar-row">
422
+ <span class="grad-bar-label" style="color:${colors[i]}">${level}</span>
423
+ <div class="grad-bar-track">
424
+ <div class="grad-bar-fill" style="width:${pct}%; background:${colors[i]};"></div>
425
+ </div>
426
+ <span class="grad-bar-value">${count} (${pct}%)</span>
427
+ </div>
428
+ `;
429
+ }).join('');
430
+ }
431
+
432
+ function updateHelpfulnessUI() {
433
+ const container = document.getElementById('helpfulness-summary');
434
+ if (!container) return;
435
+
436
+ const h = state.helpfulness;
437
+ if (!h || h.totalEvaluated === 0) {
438
+ container.innerHTML = '<span style="color:var(--text-muted);">No evaluations yet. Helpfulness is measured automatically at session end.</span>';
439
+ return;
440
+ }
441
+
442
+ const scoreColor = h.avgScore >= 0.7 ? 'var(--success, #00E396)' : h.avgScore >= 0.4 ? 'var(--warning, #FEB019)' : 'var(--danger, #FF4560)';
443
+
444
+ container.innerHTML = `
445
+ <div style="display:flex; gap:16px; align-items:center; flex-wrap:wrap;">
446
+ <div style="display:flex; align-items:baseline; gap:4px;">
447
+ <span style="font-size:20px; font-weight:700; color:${scoreColor};">${h.avgScore}</span>
448
+ <span style="font-size:11px; color:var(--text-muted);">avg</span>
449
+ </div>
450
+ <div style="display:flex; gap:10px; font-size:12px;">
451
+ <span style="color:var(--success, #00E396);">${h.helpful} helpful</span>
452
+ <span style="color:var(--warning, #FEB019);">${h.neutral} neutral</span>
453
+ <span style="color:var(--danger, #FF4560);">${h.unhelpful} unhelpful</span>
454
+ </div>
455
+ <span style="font-size:11px; color:var(--text-muted);">${h.totalEvaluated} evaluated / ${h.totalRetrievals} retrieved</span>
456
+ </div>
457
+ `;
458
+ }
459
+
460
+ function updateMostHelpfulList() {
461
+ const container = document.getElementById('most-helpful-list');
462
+ if (!container) return;
463
+
464
+ const memories = state.helpfulness?.topMemories || [];
465
+
466
+ if (memories.length === 0) {
467
+ container.innerHTML = '<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">No helpful memories yet</div>';
468
+ return;
469
+ }
470
+
471
+ container.innerHTML = memories.slice(0, 5).map((m, i) => {
472
+ const scoreColor = m.helpfulnessScore >= 0.7 ? 'var(--success, #00E396)' : m.helpfulnessScore >= 0.4 ? 'var(--warning, #FEB019)' : 'var(--danger, #FF4560)';
473
+ return `
474
+ <div class="shared-item">
475
+ <div class="shared-info">
476
+ <div class="shared-icon" style="font-size:14px; font-weight:700; color:var(--accent-primary);">#${i + 1}</div>
477
+ <span style="font-size:13px; color:var(--text-secondary); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
478
+ ${escapeHtml(m.summary || '(no summary)')}
479
+ </span>
480
+ </div>
481
+ <div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px;">
482
+ <span style="font-size:14px; font-weight:600; color:${scoreColor};">${m.helpfulnessScore}</span>
483
+ <span style="font-size:10px; color:var(--text-muted);">${m.accessCount}x accessed</span>
484
+ </div>
485
+ </div>
486
+ `;
487
+ }).join('');
488
+ }
489
+
490
+
491
+ function updateRetrievalTraceUI() {
492
+ const summaryEl = document.getElementById('retrieval-trace-summary');
493
+ const listEl = document.getElementById('retrieval-trace-list');
494
+ if (!summaryEl || !listEl) return;
495
+
496
+ const payload = state.retrievalTraces;
497
+ const stats = payload?.stats;
498
+ const traces = payload?.traces || [];
499
+
500
+ if (!stats || !Number.isFinite(stats.totalQueries) || stats.totalQueries === 0) {
501
+ summaryEl.innerHTML = '<span style="color:var(--text-muted);">No retrieval traces yet.</span>';
502
+ listEl.innerHTML = '<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">No query/context trace data</div>';
503
+ return;
504
+ }
505
+
506
+ const selectionRate = ((stats.selectionRate || 0) * 100).toFixed(1);
507
+ summaryEl.innerHTML = `
508
+ <div style="display:flex; gap:14px; flex-wrap:wrap; font-size:12px;">
509
+ <span><strong>${formatNumber(stats.totalQueries)}</strong> queries</span>
510
+ <span><strong>${Number(stats.avgCandidateCount || 0).toFixed(1)}</strong> avg candidates</span>
511
+ <span><strong>${Number(stats.avgSelectedCount || 0).toFixed(1)}</strong> avg selected</span>
512
+ <span><strong>${selectionRate}%</strong> selection rate</span>
513
+ </div>
514
+ `;
515
+
516
+ listEl.innerHTML = traces.slice(0, 8).map((t) => {
517
+ const ts = t.createdAt ? new Date(t.createdAt).toLocaleString() : '-';
518
+ const confidence = t.confidence || 'n/a';
519
+ const selected = Number(t.selectedCount || 0);
520
+ const candidates = Number(t.candidateCount || 0);
521
+ const selectedDetails = (t.selectedDetails || []).slice(0, 2);
522
+ const candidateDetails = (t.candidateDetails || []).slice(0, 3);
523
+ const selectedIdsHtml = selectedDetails.length > 0
524
+ ? selectedDetails.map((d) => {
525
+ 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)}`;
526
+ return `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal('${d.eventId}')" title="${escapeHtml(breakdown)}">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`;
527
+ }).join(' ')
528
+ : ((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(' ') || '-');
529
+
530
+ const scoreBreakdownHtml = selectedDetails.length > 0
531
+ ? 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('')
532
+ : '';
533
+
534
+ return `
535
+ <div class="shared-item" style="align-items:flex-start;">
536
+ <div class="shared-info" style="align-items:flex-start; flex-direction:column; gap:4px;">
537
+ <span style="font-size:12px; color:var(--text-secondary);"><strong>Q:</strong> ${escapeHtml((t.queryText || '').slice(0, 120))}</span>
538
+ <span style="font-size:11px; color:var(--text-muted);">${ts} · strategy=${escapeHtml(t.strategy || 'auto')} · conf=${escapeHtml(confidence)}</span>
539
+ <span style="font-size:11px; color:var(--text-muted);">selected IDs: ${selectedIdsHtml}</span>
540
+ <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>
541
+ ${scoreBreakdownHtml}
542
+ </div>
543
+ <div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px; min-width:68px;">
544
+ <span style="font-size:13px; font-weight:600; color:var(--accent-primary);">${selected}/${candidates}</span>
545
+ <span style="font-size:10px; color:var(--text-muted);">sel/cand</span>
546
+ </div>
547
+ </div>
548
+ `;
549
+ }).join('');
550
+ }
551
+
552
+
201
553
  // --- Charts ---
202
554
 
203
- function initActivityChart() {
555
+ async function initActivityChart() {
204
556
  const chartEl = document.querySelector("#activity-chart");
205
557
  if (!chartEl) return;
206
558
 
559
+ let categories = [];
560
+ let seriesData = [];
561
+ try {
562
+ const res = await fetch(apiUrl(`${API_BASE}/stats/timeline`, { days: 14 }));
563
+ const data = await res.json();
564
+ if (data.daily && data.daily.length > 0) {
565
+ categories = data.daily.map(d => d.date);
566
+ seriesData = data.daily.map(d => d.total);
567
+ }
568
+ } catch (e) {
569
+ console.error('Failed to load timeline:', e);
570
+ }
571
+
572
+ if (seriesData.length === 0) {
573
+ categories = ['No data'];
574
+ seriesData = [0];
575
+ }
576
+
207
577
  const options = {
208
578
  series: [{
209
579
  name: 'Events',
210
- data: [30, 40, 35, 50, 49, 60, 70, 91, 125] // Placeholder data, would populate from API
580
+ data: seriesData
211
581
  }],
212
582
  chart: {
213
583
  type: 'area',
@@ -237,8 +607,12 @@ function initActivityChart() {
237
607
  strokeDashArray: 4,
238
608
  },
239
609
  xaxis: {
240
- categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'], // Placeholder
241
- labels: { style: { colors: '#8B9BB4' } },
610
+ categories: categories,
611
+ labels: {
612
+ style: { colors: '#8B9BB4' },
613
+ rotate: -45,
614
+ rotateAlways: categories.length > 7
615
+ },
242
616
  axisBorder: { show: false },
243
617
  axisTicks: { show: false }
244
618
  },
@@ -257,10 +631,8 @@ function initActivityChart() {
257
631
  async function checkEndlessStatus() {
258
632
  const statusEl = document.getElementById('status-dot');
259
633
  const textEl = document.getElementById('status-text');
260
-
261
- // Mock check - replace with real API if available
262
- // const isRunning = await fetch('/api/endless/status')...
263
- const isRunning = false;
634
+
635
+ const isRunning = false;
264
636
 
265
637
  if (statusEl && textEl) {
266
638
  if (isRunning) {
@@ -275,6 +647,650 @@ async function checkEndlessStatus() {
275
647
  }
276
648
  }
277
649
 
650
+ // =============================================
651
+ // Modal System
652
+ // =============================================
653
+
654
+ function openModal(modalId) {
655
+ const modal = document.getElementById(modalId);
656
+ if (modal) {
657
+ modal.style.display = 'flex';
658
+ document.body.style.overflow = 'hidden';
659
+ }
660
+ }
661
+
662
+ function closeModal(modalId) {
663
+ const modal = document.getElementById(modalId);
664
+ if (modal) {
665
+ modal.style.display = 'none';
666
+ document.body.style.overflow = '';
667
+ }
668
+ }
669
+
670
+ function closeAllModals() {
671
+ document.querySelectorAll('.modal-overlay').forEach(m => {
672
+ m.style.display = 'none';
673
+ });
674
+ document.body.style.overflow = '';
675
+ }
676
+
677
+ // --- Detail Modal ---
678
+
679
+ async function openDetailModal(eventId) {
680
+ const body = document.getElementById('detail-modal-body');
681
+ 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>';
682
+ openModal('detail-modal');
683
+
684
+ try {
685
+ const res = await fetch(apiUrl(`${API_BASE}/events/${eventId}`));
686
+ if (!res.ok) throw new Error('Event not found');
687
+ const data = await res.json();
688
+ const evt = data.event;
689
+ const ctx = data.context || [];
690
+
691
+ const eventType = evt.eventType || 'unknown';
692
+ const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
693
+ const time = new Date(evt.timestamp).toLocaleString();
694
+
695
+ let contextHtml = '';
696
+ if (ctx.length > 0) {
697
+ contextHtml = `
698
+ <div class="modal-section-title">Context (Surrounding Events)</div>
699
+ <div class="modal-context-list">
700
+ ${ctx.map(c => `
701
+ <div class="modal-context-item" onclick="openDetailModal('${c.id}')">
702
+ <span class="event-type-badge ${`type-${(c.eventType || '').toLowerCase().replace('_', '-')}`}" style="flex-shrink:0;">${c.eventType}</span>
703
+ <div style="flex:1; min-width:0;">
704
+ <div style="font-size:12px; color:var(--text-muted); margin-bottom:4px;">${new Date(c.timestamp).toLocaleString()}</div>
705
+ <div style="font-size:13px; color:var(--text-secondary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${escapeHtml(c.preview || '')}</div>
706
+ </div>
707
+ </div>
708
+ `).join('')}
709
+ </div>
710
+ `;
711
+ }
712
+
713
+ body.innerHTML = `
714
+ <div class="modal-meta">
715
+ <div class="modal-meta-item">
716
+ <i class="ri-price-tag-3-line"></i>
717
+ <span class="event-type-badge ${typeClass}">${eventType}</span>
718
+ </div>
719
+ <div class="modal-meta-item">
720
+ <i class="ri-time-line"></i>
721
+ ${time}
722
+ </div>
723
+ <div class="modal-meta-item">
724
+ <i class="ri-chat-1-line"></i>
725
+ Session: ${evt.sessionId ? evt.sessionId.slice(0, 12) + '...' : 'N/A'}
726
+ </div>
727
+ </div>
728
+ <div class="modal-section-title">Content</div>
729
+ <div class="modal-content-block">${escapeHtml(evt.content || '(empty)')}</div>
730
+ ${contextHtml}
731
+ `;
732
+ } catch (error) {
733
+ body.innerHTML = `<div style="text-align:center; padding:40px; color:var(--error);">Failed to load event: ${escapeHtml(error.message)}</div>`;
734
+ }
735
+ }
736
+
737
+ // --- Stat Card Click Handlers ---
738
+
739
+ function handleStatClick(statType) {
740
+ switch (statType) {
741
+ case 'events': showEventsListModal(); break;
742
+ case 'sessions': showSessionsModal(); break;
743
+ case 'shared': showSharedModal(); break;
744
+ case 'vectors': showVectorsModal(); break;
745
+ }
746
+ }
747
+
748
+ async function showEventsListModal() {
749
+ document.getElementById('list-modal-title').textContent = 'Total Events';
750
+ const body = document.getElementById('list-modal-body');
751
+ body.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-muted);">Loading events...</div>';
752
+ openModal('list-modal');
753
+
754
+ try {
755
+ const res = await fetch(apiUrl(`${API_BASE}/events`, { limit: 50 }));
756
+ const data = await res.json();
757
+ const events = data.events || [];
758
+
759
+ if (events.length === 0) {
760
+ body.innerHTML = '<div class="modal-list-empty">No events found</div>';
761
+ return;
762
+ }
763
+
764
+ body.innerHTML = events.map(e => {
765
+ const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
766
+ return `
767
+ <div class="modal-list-item" onclick="openDetailModal('${e.id}')">
768
+ <div class="modal-list-info">
769
+ <div class="title">
770
+ <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
771
+ ${escapeHtml((e.preview || '').slice(0, 80))}
772
+ </div>
773
+ <div class="subtitle">${new Date(e.timestamp).toLocaleString()} | Session: ${(e.sessionId || '').slice(0, 12)}...</div>
774
+ </div>
775
+ ${e.accessCount > 0 ? `<div class="modal-list-badge"><i class="ri-eye-line"></i> ${e.accessCount}</div>` : ''}
776
+ </div>
777
+ `;
778
+ }).join('');
779
+ } catch (error) {
780
+ body.innerHTML = `<div class="modal-list-empty">Failed to load events</div>`;
781
+ }
782
+ }
783
+
784
+ async function showSessionsModal() {
785
+ document.getElementById('list-modal-title').textContent = 'Active Sessions';
786
+ const body = document.getElementById('list-modal-body');
787
+ body.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-muted);">Loading sessions...</div>';
788
+ openModal('list-modal');
789
+
790
+ try {
791
+ const res = await fetch(apiUrl(`${API_BASE}/sessions`, { pageSize: 50 }));
792
+ const data = await res.json();
793
+ const sessions = data.sessions || [];
794
+
795
+ if (sessions.length === 0) {
796
+ body.innerHTML = '<div class="modal-list-empty">No sessions found</div>';
797
+ return;
798
+ }
799
+
800
+ body.innerHTML = sessions.map(s => {
801
+ const started = new Date(s.startedAt).toLocaleString();
802
+ const lastEvent = new Date(s.lastEventAt).toLocaleString();
803
+ return `
804
+ <div class="modal-list-item" onclick="showSessionDetailInModal('${s.id}')">
805
+ <div class="modal-list-info">
806
+ <div class="title"><i class="ri-chat-1-line" style="color:var(--accent-primary); margin-right:6px;"></i>${s.id.slice(0, 20)}...</div>
807
+ <div class="subtitle">Started: ${started} | Last: ${lastEvent}</div>
808
+ </div>
809
+ <div class="modal-list-badge">${s.eventCount} events</div>
810
+ </div>
811
+ `;
812
+ }).join('');
813
+ } catch (error) {
814
+ body.innerHTML = `<div class="modal-list-empty">Failed to load sessions</div>`;
815
+ }
816
+ }
817
+
818
+ async function showSessionDetailInModal(sessionId) {
819
+ document.getElementById('list-modal-title').textContent = 'Session Detail';
820
+ const body = document.getElementById('list-modal-body');
821
+ body.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-muted);">Loading session...</div>';
822
+
823
+ try {
824
+ const res = await fetch(apiUrl(`${API_BASE}/sessions/${sessionId}`));
825
+ const data = await res.json();
826
+ const session = data.session;
827
+ const events = data.events || [];
828
+ const stats = data.stats || {};
829
+
830
+ body.innerHTML = `
831
+ <div class="modal-meta">
832
+ <div class="modal-meta-item"><i class="ri-fingerprint-line"></i>${sessionId.slice(0, 20)}...</div>
833
+ <div class="modal-meta-item"><i class="ri-time-line"></i>${new Date(session.startedAt).toLocaleString()}</div>
834
+ <div class="modal-meta-item"><i class="ri-file-list-3-line"></i>${session.eventCount} events</div>
835
+ </div>
836
+ <div style="display:flex; gap:12px; margin-bottom:20px; flex-wrap:wrap;">
837
+ <div style="padding:10px 16px; background:rgba(59,130,246,0.1); border-radius:8px; font-size:13px;">
838
+ <span style="color:#60A5FA; font-weight:600;">${stats.user_prompt || 0}</span> <span style="color:var(--text-muted);">prompts</span>
839
+ </div>
840
+ <div style="padding:10px 16px; background:rgba(16,185,129,0.1); border-radius:8px; font-size:13px;">
841
+ <span style="color:#34D399; font-weight:600;">${stats.agent_response || 0}</span> <span style="color:var(--text-muted);">responses</span>
842
+ </div>
843
+ <div style="padding:10px 16px; background:rgba(245,158,11,0.1); border-radius:8px; font-size:13px;">
844
+ <span style="color:#FBBF24; font-weight:600;">${stats.tool_observation || 0}</span> <span style="color:var(--text-muted);">tools</span>
845
+ </div>
846
+ </div>
847
+ <div class="modal-section-title">Events</div>
848
+ ${events.map(e => {
849
+ const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
850
+ return `
851
+ <div class="modal-list-item" onclick="closeAllModals(); openDetailModal('${e.id}')">
852
+ <div class="modal-list-info">
853
+ <div class="title">
854
+ <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
855
+ ${escapeHtml((e.preview || '').slice(0, 80))}
856
+ </div>
857
+ <div class="subtitle">${new Date(e.timestamp).toLocaleString()}</div>
858
+ </div>
859
+ </div>
860
+ `;
861
+ }).join('')}
862
+ `;
863
+ } catch (error) {
864
+ body.innerHTML = `<div class="modal-list-empty">Failed to load session</div>`;
865
+ }
866
+ }
867
+
868
+ function showSharedModal() {
869
+ document.getElementById('list-modal-title').textContent = 'Shared Items';
870
+ const body = document.getElementById('list-modal-body');
871
+ const s = state.sharedStats || {};
872
+
873
+ const items = [
874
+ { icon: '🔧', label: 'Troubleshooting', count: s.troubleshooting || 0, color: '#60A5FA' },
875
+ { icon: '✨', label: 'Best Practices', count: s.bestPractices || 0, color: '#34D399' },
876
+ { icon: '⚠️', label: 'Common Errors', count: s.commonErrors || 0, color: '#FBBF24' }
877
+ ];
878
+
879
+ const total = items.reduce((a, b) => a + b.count, 0);
880
+ const lastUpdated = s.lastUpdated ? new Date(s.lastUpdated).toLocaleString() : 'N/A';
881
+
882
+ body.innerHTML = `
883
+ <div style="text-align:center; margin-bottom:24px;">
884
+ <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>
885
+ <div style="font-size:13px; color:var(--text-muted); margin-top:4px;">Total shared items</div>
886
+ </div>
887
+ ${items.map(item => `
888
+ <div class="modal-list-item" style="cursor:default;">
889
+ <div class="modal-list-info">
890
+ <div class="title">${item.icon} ${item.label}</div>
891
+ <div class="subtitle">Cross-project knowledge items</div>
892
+ </div>
893
+ <div class="modal-list-badge" style="background:${item.color}22; color:${item.color};">${formatNumber(item.count)}</div>
894
+ </div>
895
+ `).join('')}
896
+ <div style="text-align:center; margin-top:20px; font-size:12px; color:var(--text-muted);">
897
+ Total usage: ${formatNumber(s.totalUsageCount || 0)} | Last updated: ${lastUpdated}
898
+ </div>
899
+ `;
900
+
901
+ openModal('list-modal');
902
+ }
903
+
904
+ function showVectorsModal() {
905
+ document.getElementById('list-modal-title').textContent = 'Vector Nodes';
906
+ const body = document.getElementById('list-modal-body');
907
+ const stats = state.stats || {};
908
+ const vectorCount = stats.storage?.vectorCount || 0;
909
+ const memory = stats.memory || {};
910
+
911
+ body.innerHTML = `
912
+ <div style="text-align:center; margin-bottom:24px;">
913
+ <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>
914
+ <div style="font-size:13px; color:var(--text-muted); margin-top:4px;">Total vector nodes</div>
915
+ </div>
916
+ <div class="modal-list-item" style="cursor:default;">
917
+ <div class="modal-list-info">
918
+ <div class="title"><i class="ri-node-tree" style="color:var(--accent-primary); margin-right:6px;"></i>Embedded Vectors</div>
919
+ <div class="subtitle">Semantic search index entries</div>
920
+ </div>
921
+ <div class="modal-list-badge">${formatNumber(vectorCount)}</div>
922
+ </div>
923
+ <div class="modal-list-item" style="cursor:default;">
924
+ <div class="modal-list-info">
925
+ <div class="title"><i class="ri-cpu-line" style="color:var(--accent-secondary); margin-right:6px;"></i>Heap Used</div>
926
+ <div class="subtitle">Current memory usage</div>
927
+ </div>
928
+ <div class="modal-list-badge" style="background:rgba(0,240,255,0.1); color:var(--accent-secondary);">${memory.heapUsed || 0} MB</div>
929
+ </div>
930
+ <div class="modal-list-item" style="cursor:default;">
931
+ <div class="modal-list-info">
932
+ <div class="title"><i class="ri-hard-drive-2-line" style="color:var(--warning); margin-right:6px;"></i>Heap Total</div>
933
+ <div class="subtitle">Allocated memory</div>
934
+ </div>
935
+ <div class="modal-list-badge" style="background:rgba(254,176,25,0.1); color:var(--warning);">${memory.heapTotal || 0} MB</div>
936
+ </div>
937
+ `;
938
+
939
+ openModal('list-modal');
940
+ }
941
+
942
+ // =============================================
943
+ // Sidebar Navigation
944
+ // =============================================
945
+
946
+ function switchView(viewName) {
947
+ if (state.currentView === viewName) return;
948
+ state.currentView = viewName;
949
+
950
+ // Update nav active state
951
+ document.querySelectorAll('.nav-item[data-nav]').forEach(item => {
952
+ item.classList.toggle('active', item.dataset.nav === viewName);
953
+ });
954
+
955
+ // Switch page views
956
+ document.querySelectorAll('.page-view').forEach(view => {
957
+ view.classList.remove('active');
958
+ });
959
+ const targetView = document.getElementById(`view-${viewName}`);
960
+ if (targetView) {
961
+ targetView.classList.add('active');
962
+ }
963
+
964
+ // Load view content
965
+ switch (viewName) {
966
+ case 'knowledge-graph': loadKnowledgeGraphView(); break;
967
+ case 'memory-banks': loadMemoryBanksView(); break;
968
+ case 'configuration': loadConfigurationView(); break;
969
+ }
970
+ }
971
+
972
+ // --- Knowledge Graph View ---
973
+
974
+ async function loadKnowledgeGraphView() {
975
+ const container = document.getElementById('kg-content');
976
+ container.innerHTML = '<div style="text-align:center; padding:60px; color:var(--text-muted);">Loading knowledge graph...</div>';
977
+
978
+ try {
979
+ const [mostAccessedRes, helpfulnessRes] = await Promise.all([
980
+ fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 20 })).then(r => r.json()).catch(() => ({ memories: [] })),
981
+ fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 10 })).then(r => r.json()).catch(() => ({ topMemories: [] }))
982
+ ]);
983
+
984
+ const memories = mostAccessedRes.memories || [];
985
+ const helpful = helpfulnessRes.topMemories || [];
986
+
987
+ if (memories.length === 0 && helpful.length === 0) {
988
+ 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>';
989
+ return;
990
+ }
991
+
992
+ // Collect all topics
993
+ const topicMap = {};
994
+ memories.forEach(m => {
995
+ (m.topics || []).forEach(t => {
996
+ topicMap[t] = (topicMap[t] || 0) + 1;
997
+ });
998
+ });
999
+ const topTopics = Object.entries(topicMap).sort((a, b) => b[1] - a[1]).slice(0, 15);
1000
+
1001
+ let topicsHtml = '';
1002
+ if (topTopics.length > 0) {
1003
+ topicsHtml = `
1004
+ <div class="card" style="margin-bottom:24px;">
1005
+ <div class="card-header">
1006
+ <div class="card-title"><i class="ri-hashtag"></i><span>Top Topics</span></div>
1007
+ </div>
1008
+ <div class="kg-topic-list">
1009
+ ${topTopics.map(([topic, count]) => `
1010
+ <span class="kg-topic-tag">${escapeHtml(topic)} (${count})</span>
1011
+ `).join('')}
1012
+ </div>
1013
+ </div>
1014
+ `;
1015
+ }
1016
+
1017
+ const memoriesHtml = memories.length > 0 ? `
1018
+ <div class="card" style="margin-bottom:24px;">
1019
+ <div class="card-header">
1020
+ <div class="card-title"><i class="ri-star-line"></i><span>Most Accessed Memories</span></div>
1021
+ </div>
1022
+ <div class="kg-grid">
1023
+ ${memories.map((m, i) => `
1024
+ <div class="kg-memory-card" onclick="openDetailModalByMemory('${m.memoryId || ''}')">
1025
+ <div class="kg-memory-rank">#${i + 1}</div>
1026
+ <div class="kg-memory-summary">${escapeHtml(m.summary || '(no summary)')}</div>
1027
+ ${(m.topics || []).length > 0 ? `
1028
+ <div class="kg-topic-list">
1029
+ ${m.topics.slice(0, 3).map(t => `<span class="kg-topic-tag">${escapeHtml(t)}</span>`).join('')}
1030
+ </div>
1031
+ ` : ''}
1032
+ <div class="kg-memory-meta">
1033
+ <span><i class="ri-eye-line"></i> ${m.accessCount || 0}x accessed</span>
1034
+ <span><i class="ri-shield-check-line"></i> ${((m.confidence || 0) * 100).toFixed(0)}% confidence</span>
1035
+ </div>
1036
+ </div>
1037
+ `).join('')}
1038
+ </div>
1039
+ </div>
1040
+ ` : '';
1041
+
1042
+ const helpfulHtml = helpful.length > 0 ? `
1043
+ <div class="card">
1044
+ <div class="card-header">
1045
+ <div class="card-title"><i class="ri-thumb-up-line"></i><span>Most Helpful Memories</span></div>
1046
+ </div>
1047
+ ${helpful.map((m, i) => {
1048
+ const scoreColor = m.helpfulnessScore >= 0.7 ? 'var(--success)' : m.helpfulnessScore >= 0.4 ? 'var(--warning)' : 'var(--error)';
1049
+ return `
1050
+ <div class="modal-list-item" onclick="openDetailModalByEvent('${m.eventId || ''}')">
1051
+ <div class="modal-list-info">
1052
+ <div class="title">#${i + 1} ${escapeHtml(m.summary || '(no summary)')}</div>
1053
+ <div class="subtitle">${m.accessCount || 0}x accessed | ${m.evaluationCount || 0} evaluations</div>
1054
+ </div>
1055
+ <div class="modal-list-badge" style="color:${scoreColor}; background:${scoreColor}22;">${m.helpfulnessScore}</div>
1056
+ </div>
1057
+ `;
1058
+ }).join('')}
1059
+ </div>
1060
+ ` : '';
1061
+
1062
+ container.innerHTML = topicsHtml + memoriesHtml + helpfulHtml;
1063
+
1064
+ } catch (error) {
1065
+ container.innerHTML = `<div style="text-align:center; padding:60px; color:var(--error);">Failed to load knowledge graph: ${escapeHtml(error.message)}</div>`;
1066
+ }
1067
+ }
1068
+
1069
+ function openDetailModalByMemory(memoryId) {
1070
+ // memoryId might be an event ID - try to open it
1071
+ if (memoryId) openDetailModal(memoryId);
1072
+ }
1073
+
1074
+ function openDetailModalByEvent(eventId) {
1075
+ if (eventId) openDetailModal(eventId);
1076
+ }
1077
+
1078
+ // --- Memory Banks View ---
1079
+
1080
+ async function loadMemoryBanksView() {
1081
+ const container = document.getElementById('mb-content');
1082
+ container.innerHTML = '<div style="text-align:center; padding:60px; color:var(--text-muted);">Loading memory banks...</div>';
1083
+
1084
+ try {
1085
+ const [statsRes, graduationRes] = await Promise.all([
1086
+ fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
1087
+ fetch(apiUrl(`${API_BASE}/stats/graduation`)).then(r => r.json()).catch(() => null)
1088
+ ]);
1089
+
1090
+ const levelStats = statsRes?.levelStats || [];
1091
+ const levels = ['L0', 'L1', 'L2', 'L3', 'L4'];
1092
+ const levelNames = { L0: 'Raw Events', L1: 'Structured', L2: 'Validated', L3: 'Verified', L4: 'Active' };
1093
+ const levelCounts = {};
1094
+ levelStats.forEach(s => { levelCounts[s.level] = s.count; });
1095
+
1096
+ const criteria = graduationRes?.criteria || {};
1097
+
1098
+ container.innerHTML = `
1099
+ <div class="mb-level-tabs" id="mb-tabs">
1100
+ ${levels.map(level => `
1101
+ <button class="mb-level-tab ${level === 'L0' ? 'active' : ''}" data-level="${level}" style="border-left:3px solid ${CHART_COLORS[level]};">
1102
+ ${levelNames[level]} <span class="tab-count">(${levelCounts[level] || 0})</span>
1103
+ </button>
1104
+ `).join('')}
1105
+ </div>
1106
+ <div class="card" style="margin-bottom:24px;">
1107
+ <div class="card-header">
1108
+ <div class="card-title"><i class="ri-stack-line"></i><span>Level Events</span></div>
1109
+ </div>
1110
+ <div id="mb-events-list">
1111
+ <div style="text-align:center; padding:20px; color:var(--text-muted);">Select a level to view events</div>
1112
+ </div>
1113
+ </div>
1114
+ <div class="card">
1115
+ <div class="card-header">
1116
+ <div class="card-title"><i class="ri-graduation-cap-line"></i><span>Graduation Criteria</span></div>
1117
+ </div>
1118
+ ${Object.entries(criteria).map(([key, c]) => `
1119
+ <div style="margin-bottom:16px;">
1120
+ <div style="font-size:14px; font-weight:600; color:var(--accent-primary); margin-bottom:8px;">${key}</div>
1121
+ <div style="display:grid; grid-template-columns:repeat(2, 1fr); gap:8px;">
1122
+ <div class="cfg-row" style="padding:8px 12px; background:rgba(255,255,255,0.02); border-radius:8px; border:none;">
1123
+ <span class="cfg-row-label">Min Access</span>
1124
+ <span class="cfg-row-value">${c.minAccessCount}</span>
1125
+ </div>
1126
+ <div class="cfg-row" style="padding:8px 12px; background:rgba(255,255,255,0.02); border-radius:8px; border:none;">
1127
+ <span class="cfg-row-label">Min Confidence</span>
1128
+ <span class="cfg-row-value">${c.minConfidence}</span>
1129
+ </div>
1130
+ <div class="cfg-row" style="padding:8px 12px; background:rgba(255,255,255,0.02); border-radius:8px; border:none;">
1131
+ <span class="cfg-row-label">Cross-Session Refs</span>
1132
+ <span class="cfg-row-value">${c.minCrossSessionRefs}</span>
1133
+ </div>
1134
+ <div class="cfg-row" style="padding:8px 12px; background:rgba(255,255,255,0.02); border-radius:8px; border:none;">
1135
+ <span class="cfg-row-label">Max Age (days)</span>
1136
+ <span class="cfg-row-value">${c.maxAgeDays}</span>
1137
+ </div>
1138
+ </div>
1139
+ </div>
1140
+ `).join('')}
1141
+ </div>
1142
+ `;
1143
+
1144
+ // Setup level tab click handlers
1145
+ document.querySelectorAll('#mb-tabs .mb-level-tab').forEach(tab => {
1146
+ tab.addEventListener('click', () => {
1147
+ document.querySelectorAll('#mb-tabs .mb-level-tab').forEach(t => t.classList.remove('active'));
1148
+ tab.classList.add('active');
1149
+ loadMemoryBankLevel(tab.dataset.level);
1150
+ });
1151
+ });
1152
+
1153
+ // Load L0 by default
1154
+ await loadMemoryBankLevel('L0');
1155
+
1156
+ } catch (error) {
1157
+ container.innerHTML = `<div style="text-align:center; padding:60px; color:var(--error);">Failed to load memory banks: ${escapeHtml(error.message)}</div>`;
1158
+ }
1159
+ }
1160
+
1161
+ async function loadMemoryBankLevel(level) {
1162
+ const container = document.getElementById('mb-events-list');
1163
+ if (!container) return;
1164
+ container.innerHTML = '<div style="text-align:center; padding:20px; color:var(--text-muted);">Loading...</div>';
1165
+
1166
+ try {
1167
+ const res = await fetch(apiUrl(`${API_BASE}/stats/levels/${level}`, { limit: 30 }));
1168
+ const data = await res.json();
1169
+ const events = data.events || [];
1170
+
1171
+ if (events.length === 0) {
1172
+ container.innerHTML = `<div style="text-align:center; padding:20px; color:var(--text-muted);">No events at level ${level}</div>`;
1173
+ return;
1174
+ }
1175
+
1176
+ container.innerHTML = `
1177
+ <div class="mb-event-list">
1178
+ ${events.map(e => {
1179
+ const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
1180
+ return `
1181
+ <div class="mb-event-card" onclick="openDetailModal('${e.id}')">
1182
+ <div class="mb-event-header">
1183
+ <span class="event-type-badge ${typeClass}">${e.eventType}</span>
1184
+ <div style="display:flex; gap:8px; align-items:center;">
1185
+ ${e.accessCount > 0 ? `<span class="access-badge"><i class="ri-eye-line"></i> ${e.accessCount}</span>` : ''}
1186
+ <span class="event-time">${new Date(e.timestamp).toLocaleString()}</span>
1187
+ </div>
1188
+ </div>
1189
+ <div class="mb-event-content">${escapeHtml((e.content || '').slice(0, 200))}</div>
1190
+ </div>
1191
+ `;
1192
+ }).join('')}
1193
+ </div>
1194
+ ${data.hasMore ? `<div style="text-align:center; padding:16px; color:var(--text-muted); font-size:13px;">Showing ${events.length} of ${data.total} events</div>` : ''}
1195
+ `;
1196
+ } catch (error) {
1197
+ container.innerHTML = `<div style="text-align:center; padding:20px; color:var(--error);">Failed to load level ${level}</div>`;
1198
+ }
1199
+ }
1200
+
1201
+ // --- Configuration View ---
1202
+
1203
+ async function loadConfigurationView() {
1204
+ const container = document.getElementById('cfg-content');
1205
+ container.innerHTML = '<div style="text-align:center; padding:60px; color:var(--text-muted);">Loading configuration...</div>';
1206
+
1207
+ try {
1208
+ const [statsRes, graduationRes, endlessRes] = await Promise.all([
1209
+ fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
1210
+ fetch(apiUrl(`${API_BASE}/stats/graduation`)).then(r => r.json()).catch(() => null),
1211
+ fetch(apiUrl(`${API_BASE}/stats/endless`)).then(r => r.json()).catch(() => null)
1212
+ ]);
1213
+
1214
+ const memory = statsRes?.memory || {};
1215
+ const storage = statsRes?.storage || {};
1216
+ const criteria = graduationRes?.criteria || {};
1217
+ const descriptions = graduationRes?.description || {};
1218
+ const endless = endlessRes || {};
1219
+
1220
+ container.innerHTML = `
1221
+ <div class="cfg-grid">
1222
+ <div class="cfg-section">
1223
+ <div class="cfg-section-title"><i class="ri-database-2-line"></i>Storage</div>
1224
+ <div class="cfg-row">
1225
+ <span class="cfg-row-label">Total Events</span>
1226
+ <span class="cfg-row-value">${formatNumber(storage.eventCount || 0)}</span>
1227
+ </div>
1228
+ <div class="cfg-row">
1229
+ <span class="cfg-row-label">Vector Nodes</span>
1230
+ <span class="cfg-row-value">${formatNumber(storage.vectorCount || 0)}</span>
1231
+ </div>
1232
+ <div class="cfg-row">
1233
+ <span class="cfg-row-label">Heap Used</span>
1234
+ <span class="cfg-row-value">${memory.heapUsed || 0} MB</span>
1235
+ </div>
1236
+ <div class="cfg-row">
1237
+ <span class="cfg-row-label">Heap Total</span>
1238
+ <span class="cfg-row-value">${memory.heapTotal || 0} MB</span>
1239
+ </div>
1240
+ </div>
1241
+
1242
+ <div class="cfg-section">
1243
+ <div class="cfg-section-title"><i class="ri-infinite-loop-line"></i>Endless Mode</div>
1244
+ <div class="cfg-row">
1245
+ <span class="cfg-row-label">Mode</span>
1246
+ <span class="cfg-row-value">${endless.mode || 'session'}</span>
1247
+ </div>
1248
+ <div class="cfg-row">
1249
+ <span class="cfg-row-label">Continuity Score</span>
1250
+ <span class="cfg-row-value">${endless.continuityScore || 0}</span>
1251
+ </div>
1252
+ <div class="cfg-row">
1253
+ <span class="cfg-row-label">Working Set Size</span>
1254
+ <span class="cfg-row-value">${endless.workingSetSize || 0}</span>
1255
+ </div>
1256
+ <div class="cfg-row">
1257
+ <span class="cfg-row-label">Consolidated</span>
1258
+ <span class="cfg-row-value">${endless.consolidatedCount || 0}</span>
1259
+ </div>
1260
+ <div class="cfg-row">
1261
+ <span class="cfg-row-label">Last Consolidation</span>
1262
+ <span class="cfg-row-value">${endless.lastConsolidation ? new Date(endless.lastConsolidation).toLocaleDateString() : 'Never'}</span>
1263
+ </div>
1264
+ </div>
1265
+ </div>
1266
+
1267
+ <div class="card" style="margin-top:24px;">
1268
+ <div class="card-header">
1269
+ <div class="card-title"><i class="ri-graduation-cap-line"></i><span>Graduation Criteria</span></div>
1270
+ </div>
1271
+ <div style="margin-bottom:16px; font-size:13px; color:var(--text-muted);">
1272
+ ${Object.entries(descriptions).map(([key, desc]) => `
1273
+ <div style="margin-bottom:4px;"><strong style="color:var(--text-secondary);">${key}</strong>: ${desc}</div>
1274
+ `).join('')}
1275
+ </div>
1276
+ <div style="display:grid; grid-template-columns:repeat(2, 1fr); gap:16px;">
1277
+ ${Object.entries(criteria).map(([key, c]) => `
1278
+ <div style="background:var(--bg-panel); border-radius:12px; padding:16px;">
1279
+ <div style="font-size:14px; font-weight:600; color:var(--accent-primary); margin-bottom:12px;">${key}</div>
1280
+ <div class="cfg-row"><span class="cfg-row-label">Min Access Count</span><span class="cfg-row-value">${c.minAccessCount}</span></div>
1281
+ <div class="cfg-row"><span class="cfg-row-label">Min Confidence</span><span class="cfg-row-value">${c.minConfidence}</span></div>
1282
+ <div class="cfg-row"><span class="cfg-row-label">Cross-Session Refs</span><span class="cfg-row-value">${c.minCrossSessionRefs}</span></div>
1283
+ <div class="cfg-row"><span class="cfg-row-label">Max Age (days)</span><span class="cfg-row-value">${c.maxAgeDays}</span></div>
1284
+ </div>
1285
+ `).join('')}
1286
+ </div>
1287
+ </div>
1288
+ `;
1289
+ } catch (error) {
1290
+ container.innerHTML = `<div style="text-align:center; padding:60px; color:var(--error);">Failed to load configuration: ${escapeHtml(error.message)}</div>`;
1291
+ }
1292
+ }
1293
+
278
1294
  // --- Helpers ---
279
1295
 
280
1296
  function debounce(func, wait) {
@@ -291,14 +1307,390 @@ function debounce(func, wait) {
291
1307
 
292
1308
  function handleSearch(query) {
293
1309
  console.log('Searching for:', query);
294
- // Implement search logic here
295
1310
  }
296
1311
 
297
1312
  function escapeHtml(unsafe) {
298
- return unsafe
1313
+ return String(unsafe)
299
1314
  .replace(/&/g, "&amp;")
300
1315
  .replace(/</g, "&lt;")
301
1316
  .replace(/>/g, "&gt;")
302
1317
  .replace(/"/g, "&quot;")
303
1318
  .replace(/'/g, "&#039;");
304
1319
  }
1320
+
1321
+ // --- Chat Panel ---
1322
+
1323
+ const CHAT_STORAGE_KEY = 'code-memory-chat-history';
1324
+
1325
+ function loadChatHistory() {
1326
+ try {
1327
+ const raw = localStorage.getItem(CHAT_STORAGE_KEY);
1328
+ return raw ? JSON.parse(raw) : [];
1329
+ } catch { return []; }
1330
+ }
1331
+
1332
+ function saveChatHistory(conversations) {
1333
+ try {
1334
+ // Keep last 50 conversations max
1335
+ const trimmed = conversations.slice(-50);
1336
+ localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(trimmed));
1337
+ } catch { /* storage full or unavailable */ }
1338
+ }
1339
+
1340
+ function saveCurrentConversation() {
1341
+ if (state.chatMessages.length === 0) return;
1342
+ const conversations = loadChatHistory();
1343
+ const firstUserMsg = state.chatMessages.find(m => m.role === 'user');
1344
+ const title = firstUserMsg ? firstUserMsg.content.slice(0, 80) : 'Untitled';
1345
+
1346
+ if (state.chatConversationId) {
1347
+ // Update existing
1348
+ const idx = conversations.findIndex(c => c.id === state.chatConversationId);
1349
+ if (idx >= 0) {
1350
+ conversations[idx].messages = [...state.chatMessages];
1351
+ conversations[idx].updatedAt = new Date().toISOString();
1352
+ conversations[idx].title = title;
1353
+ }
1354
+ } else {
1355
+ // Create new
1356
+ state.chatConversationId = 'chat-' + Date.now();
1357
+ conversations.push({
1358
+ id: state.chatConversationId,
1359
+ title,
1360
+ messages: [...state.chatMessages],
1361
+ createdAt: new Date().toISOString(),
1362
+ updatedAt: new Date().toISOString(),
1363
+ project: state.currentProject || 'global'
1364
+ });
1365
+ }
1366
+ saveChatHistory(conversations);
1367
+ }
1368
+
1369
+ function startNewConversation() {
1370
+ saveCurrentConversation();
1371
+ state.chatMessages = [];
1372
+ state.chatConversationId = null;
1373
+
1374
+ const container = document.getElementById('chat-messages');
1375
+ container.innerHTML = `
1376
+ <div class="chat-welcome">
1377
+ <div class="chat-welcome-icon">🧠</div>
1378
+ <div class="chat-welcome-title">Ask about your memories</div>
1379
+ <div class="chat-welcome-text">
1380
+ I can search through your coding sessions, tool usage, and stored knowledge to answer questions.
1381
+ </div>
1382
+ </div>
1383
+ `;
1384
+ switchChatTab('chat');
1385
+ }
1386
+
1387
+ function loadConversation(id) {
1388
+ const conversations = loadChatHistory();
1389
+ const conv = conversations.find(c => c.id === id);
1390
+ if (!conv) return;
1391
+
1392
+ // Save current first
1393
+ if (state.chatMessages.length > 0 && state.chatConversationId !== id) {
1394
+ saveCurrentConversation();
1395
+ }
1396
+
1397
+ state.chatConversationId = conv.id;
1398
+ state.chatMessages = [...conv.messages];
1399
+
1400
+ // Render messages
1401
+ const container = document.getElementById('chat-messages');
1402
+ container.innerHTML = '';
1403
+ for (const msg of conv.messages) {
1404
+ appendChatMessage(msg.role, msg.content);
1405
+ }
1406
+
1407
+ switchChatTab('chat');
1408
+ }
1409
+
1410
+ function deleteConversation(id, evt) {
1411
+ evt.stopPropagation();
1412
+ const conversations = loadChatHistory().filter(c => c.id !== id);
1413
+ saveChatHistory(conversations);
1414
+ if (state.chatConversationId === id) {
1415
+ state.chatMessages = [];
1416
+ state.chatConversationId = null;
1417
+ const container = document.getElementById('chat-messages');
1418
+ container.innerHTML = `
1419
+ <div class="chat-welcome">
1420
+ <div class="chat-welcome-icon">🧠</div>
1421
+ <div class="chat-welcome-title">Ask about your memories</div>
1422
+ <div class="chat-welcome-text">
1423
+ I can search through your coding sessions, tool usage, and stored knowledge to answer questions.
1424
+ </div>
1425
+ </div>
1426
+ `;
1427
+ }
1428
+ renderHistoryList();
1429
+ }
1430
+
1431
+ function renderHistoryList() {
1432
+ const container = document.getElementById('chat-history-view');
1433
+ const conversations = loadChatHistory().reverse(); // newest first
1434
+
1435
+ if (conversations.length === 0) {
1436
+ container.innerHTML = '<div class="chat-history-empty">No conversation history yet.</div>';
1437
+ return;
1438
+ }
1439
+
1440
+ container.innerHTML = conversations.map(conv => {
1441
+ const date = new Date(conv.updatedAt || conv.createdAt);
1442
+ const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1443
+ const msgCount = conv.messages.length;
1444
+ const isActive = conv.id === state.chatConversationId;
1445
+ return `
1446
+ <div class="chat-history-item${isActive ? ' active' : ''}" onclick="loadConversation('${conv.id}')"
1447
+ style="${isActive ? 'border-color:var(--accent-primary);background:rgba(123,97,255,0.08);' : ''}">
1448
+ <div class="chat-history-item-title">${escapeHtml(conv.title)}</div>
1449
+ <div class="chat-history-item-meta">
1450
+ <span>${dateStr} &middot; ${msgCount} messages</span>
1451
+ <button class="chat-history-item-delete" onclick="deleteConversation('${conv.id}', event)" title="Delete">
1452
+ <i class="ri-delete-bin-line"></i>
1453
+ </button>
1454
+ </div>
1455
+ </div>
1456
+ `;
1457
+ }).join('');
1458
+ }
1459
+
1460
+ function switchChatTab(tab) {
1461
+ const msgContainer = document.getElementById('chat-messages');
1462
+ const historyContainer = document.getElementById('chat-history-view');
1463
+ const inputArea = document.querySelector('.chat-input-area');
1464
+
1465
+ document.querySelectorAll('.chat-header-tab').forEach(t => {
1466
+ t.classList.toggle('active', t.dataset.chatTab === tab);
1467
+ });
1468
+
1469
+ if (tab === 'chat') {
1470
+ msgContainer.classList.remove('hidden');
1471
+ historyContainer.classList.remove('active');
1472
+ if (inputArea) inputArea.style.display = '';
1473
+ } else {
1474
+ msgContainer.classList.add('hidden');
1475
+ historyContainer.classList.add('active');
1476
+ if (inputArea) inputArea.style.display = 'none';
1477
+ renderHistoryList();
1478
+ }
1479
+
1480
+ state.chatCurrentTab = tab;
1481
+ }
1482
+
1483
+ function toggleChatPanel() {
1484
+ if (state.isChatOpen) {
1485
+ closeChatPanel();
1486
+ } else {
1487
+ openChatPanel();
1488
+ }
1489
+ }
1490
+
1491
+ function openChatPanel() {
1492
+ const panel = document.getElementById('chat-panel');
1493
+ if (panel) {
1494
+ panel.classList.add('open');
1495
+ state.isChatOpen = true;
1496
+ updateChatProjectScope();
1497
+ setTimeout(() => {
1498
+ document.getElementById('chat-input')?.focus();
1499
+ }, 300);
1500
+ }
1501
+ }
1502
+
1503
+ function closeChatPanel() {
1504
+ const panel = document.getElementById('chat-panel');
1505
+ if (panel) {
1506
+ panel.classList.remove('open');
1507
+ state.isChatOpen = false;
1508
+ }
1509
+ if (state.chatAbortController) {
1510
+ state.chatAbortController.abort();
1511
+ state.chatAbortController = null;
1512
+ state.isChatStreaming = false;
1513
+ }
1514
+ // Auto-save on close
1515
+ saveCurrentConversation();
1516
+ }
1517
+
1518
+ function updateChatProjectScope() {
1519
+ const el = document.getElementById('chat-project-scope');
1520
+ if (!el) return;
1521
+ if (state.currentProject) {
1522
+ const proj = state.projects.find(p => p.hash === state.currentProject);
1523
+ el.textContent = `Scope: ${proj?.projectName || state.currentProject}`;
1524
+ } else {
1525
+ el.textContent = 'Scope: All (Global)';
1526
+ }
1527
+ }
1528
+
1529
+ async function sendChatMessage() {
1530
+ const input = document.getElementById('chat-input');
1531
+ const message = input.value.trim();
1532
+ if (!message) return;
1533
+
1534
+ input.value = '';
1535
+ input.style.height = 'auto';
1536
+ document.getElementById('chat-send-btn').disabled = true;
1537
+
1538
+ // Add user message
1539
+ state.chatMessages.push({ role: 'user', content: message });
1540
+ appendChatMessage('user', message);
1541
+
1542
+ // Remove welcome
1543
+ const welcome = document.querySelector('.chat-welcome');
1544
+ if (welcome) welcome.remove();
1545
+
1546
+ // Show loading
1547
+ const loadingEl = appendChatLoading();
1548
+
1549
+ state.isChatStreaming = true;
1550
+ state.chatAbortController = new AbortController();
1551
+
1552
+ try {
1553
+ const response = await fetch(apiUrl(`${API_BASE}/chat`), {
1554
+ method: 'POST',
1555
+ headers: { 'Content-Type': 'application/json' },
1556
+ body: JSON.stringify({
1557
+ message,
1558
+ history: state.chatMessages.slice(-10)
1559
+ }),
1560
+ signal: state.chatAbortController.signal
1561
+ });
1562
+
1563
+ if (!response.ok) {
1564
+ const err = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
1565
+ throw new Error(err.error || `Request failed: ${response.status}`);
1566
+ }
1567
+
1568
+ loadingEl.remove();
1569
+ const msgEl = appendChatMessage('assistant', '', true);
1570
+ let fullContent = '';
1571
+
1572
+ const reader = response.body.getReader();
1573
+ const decoder = new TextDecoder();
1574
+ let sseBuffer = '';
1575
+
1576
+ while (true) {
1577
+ const { done, value } = await reader.read();
1578
+ if (done) break;
1579
+
1580
+ sseBuffer += decoder.decode(value, { stream: true });
1581
+ const lines = sseBuffer.split('\n');
1582
+ sseBuffer = lines.pop() || '';
1583
+
1584
+ for (const line of lines) {
1585
+ if (line.startsWith('data: ')) {
1586
+ const dataStr = line.slice(6);
1587
+ try {
1588
+ const data = JSON.parse(dataStr);
1589
+ if (data.content) {
1590
+ fullContent += data.content;
1591
+ updateChatMessageContent(msgEl, fullContent);
1592
+ scrollChatToBottom();
1593
+ }
1594
+ if (data.error) {
1595
+ fullContent += `\n\n**Error:** ${data.error}`;
1596
+ updateChatMessageContent(msgEl, fullContent);
1597
+ }
1598
+ } catch { /* skip */ }
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ msgEl.classList.remove('streaming');
1604
+ if (fullContent) {
1605
+ state.chatMessages.push({ role: 'assistant', content: fullContent });
1606
+ }
1607
+
1608
+ // Auto-save after each response
1609
+ saveCurrentConversation();
1610
+
1611
+ } catch (err) {
1612
+ if (loadingEl.parentNode) loadingEl.remove();
1613
+ if (err.name !== 'AbortError') {
1614
+ appendChatMessage('assistant',
1615
+ `**Error:** ${err.message}\n\nMake sure the Claude CLI is installed and authenticated.`
1616
+ );
1617
+ }
1618
+ } finally {
1619
+ state.isChatStreaming = false;
1620
+ state.chatAbortController = null;
1621
+ const sendBtn = document.getElementById('chat-send-btn');
1622
+ const chatInput = document.getElementById('chat-input');
1623
+ if (sendBtn && chatInput) {
1624
+ sendBtn.disabled = !chatInput.value.trim();
1625
+ }
1626
+ }
1627
+ }
1628
+
1629
+ function appendChatMessage(role, content, streaming = false) {
1630
+ const container = document.getElementById('chat-messages');
1631
+ const el = document.createElement('div');
1632
+ el.className = `chat-msg ${role}${streaming ? ' streaming' : ''}`;
1633
+
1634
+ if (role === 'assistant') {
1635
+ el.innerHTML = renderMarkdown(content);
1636
+ } else {
1637
+ el.textContent = content;
1638
+ }
1639
+
1640
+ container.appendChild(el);
1641
+ scrollChatToBottom();
1642
+ return el;
1643
+ }
1644
+
1645
+ function appendChatLoading() {
1646
+ const container = document.getElementById('chat-messages');
1647
+ const el = document.createElement('div');
1648
+ el.className = 'chat-loading';
1649
+ el.innerHTML = `
1650
+ <div class="chat-loading-dot"></div>
1651
+ <div class="chat-loading-dot"></div>
1652
+ <div class="chat-loading-dot"></div>
1653
+ `;
1654
+ container.appendChild(el);
1655
+ scrollChatToBottom();
1656
+ return el;
1657
+ }
1658
+
1659
+ function updateChatMessageContent(el, content) {
1660
+ el.innerHTML = renderMarkdown(content);
1661
+ }
1662
+
1663
+ function scrollChatToBottom() {
1664
+ const container = document.getElementById('chat-messages');
1665
+ if (container) container.scrollTop = container.scrollHeight;
1666
+ }
1667
+
1668
+ function renderMarkdown(text) {
1669
+ if (!text) return '';
1670
+
1671
+ let html = escapeHtml(text);
1672
+
1673
+ // Code blocks
1674
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
1675
+
1676
+ // Inline code
1677
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
1678
+
1679
+ // Bold
1680
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1681
+
1682
+ // Italic
1683
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
1684
+
1685
+ // Headers
1686
+ html = html.replace(/^### (.+)$/gm, '<div style="font-weight:600;color:var(--text-primary);margin:12px 0 4px;">$1</div>');
1687
+ html = html.replace(/^## (.+)$/gm, '<div style="font-size:15px;font-weight:600;color:var(--text-primary);margin:12px 0 4px;">$1</div>');
1688
+
1689
+ // Lists
1690
+ html = html.replace(/^- (.+)$/gm, '<div style="padding-left:16px;">&#8226; $1</div>');
1691
+
1692
+ // Line breaks
1693
+ html = html.replace(/\n/g, '<br>');
1694
+
1695
+ return html;
1696
+ }