claude-memory-layer 1.0.9 → 1.0.11

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