claude-memory-layer 1.0.18 → 1.0.20

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 (42) hide show
  1. package/config/kpi-thresholds.json +7 -0
  2. package/dist/cli/index.js +532 -79
  3. package/dist/cli/index.js.map +3 -3
  4. package/dist/core/index.js +49 -4
  5. package/dist/core/index.js.map +2 -2
  6. package/dist/hooks/post-tool-use.js +140 -3
  7. package/dist/hooks/post-tool-use.js.map +2 -2
  8. package/dist/hooks/session-end.js +140 -3
  9. package/dist/hooks/session-end.js.map +2 -2
  10. package/dist/hooks/session-start.js +140 -3
  11. package/dist/hooks/session-start.js.map +2 -2
  12. package/dist/hooks/stop.js +140 -3
  13. package/dist/hooks/stop.js.map +2 -2
  14. package/dist/hooks/user-prompt-submit.js +379 -34
  15. package/dist/hooks/user-prompt-submit.js.map +3 -3
  16. package/dist/server/api/index.js +467 -34
  17. package/dist/server/api/index.js.map +3 -3
  18. package/dist/server/index.js +474 -41
  19. package/dist/server/index.js.map +3 -3
  20. package/dist/services/memory-service.js +140 -3
  21. package/dist/services/memory-service.js.map +2 -2
  22. package/dist/ui/app.js +362 -4
  23. package/dist/ui/index.html +90 -0
  24. package/dist/ui/style.css +41 -0
  25. package/memory/_index.md +3 -0
  26. package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
  27. package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
  28. package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
  29. package/package.json +3 -2
  30. package/scripts/delete-unknown-projects.js +154 -0
  31. package/src/cli/index.ts +23 -1
  32. package/src/core/embedder.ts +3 -2
  33. package/src/core/sqlite-event-store.ts +32 -0
  34. package/src/core/types.ts +2 -2
  35. package/src/core/vector-store.ts +20 -0
  36. package/src/hooks/user-prompt-submit.ts +225 -29
  37. package/src/server/api/events.ts +7 -0
  38. package/src/server/api/stats.ts +346 -0
  39. package/src/services/memory-service.ts +119 -2
  40. package/src/ui/app.js +362 -4
  41. package/src/ui/index.html +90 -0
  42. package/src/ui/style.css +41 -0
package/src/ui/app.js CHANGED
@@ -12,6 +12,12 @@ const state = {
12
12
  mostAccessed: null,
13
13
  helpfulness: null,
14
14
  retrievalTraces: null,
15
+ adherenceSummary: null,
16
+ adherenceWindow: '24h',
17
+ userPromptSearchQuery: '',
18
+ userPromptItems: [],
19
+ userPromptPage: 1,
20
+ userPromptPageSize: 30,
15
21
  currentLevel: 'L0',
16
22
  currentSort: 'recent',
17
23
  currentView: 'overview',
@@ -20,6 +26,9 @@ const state = {
20
26
  events: [],
21
27
  isLoading: false,
22
28
  chartInstance: null,
29
+ kpiChartInstance: null,
30
+ kpiWindow: '7d',
31
+ kpi: null,
23
32
  chatMessages: [],
24
33
  isChatOpen: false,
25
34
  isChatStreaming: false,
@@ -102,19 +111,81 @@ function setupEventListeners() {
102
111
  });
103
112
 
104
113
  // Sort buttons
105
- document.querySelectorAll('.sort-btn').forEach(btn => {
114
+ document.querySelectorAll('.sort-btn[data-sort]').forEach(btn => {
106
115
  btn.addEventListener('click', (e) => {
107
116
  const sort = e.currentTarget.dataset.sort;
108
117
  if (sort) selectSort(sort);
109
118
  });
110
119
  });
111
120
 
121
+ // Adherence window controls
122
+ document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(btn => {
123
+ btn.addEventListener('click', async (e) => {
124
+ const window = e.currentTarget.dataset.adhWindow;
125
+ if (!window || state.adherenceWindow === window) return;
126
+ state.adherenceWindow = window;
127
+ document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(b => {
128
+ b.classList.toggle('active', b.dataset.adhWindow === window);
129
+ });
130
+ state.adherenceSummary = await fetchAdherenceSummary().catch(() => null);
131
+ updateAdherenceSummaryUI();
132
+ });
133
+ });
134
+
135
+ // KPI window controls
136
+ document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(btn => {
137
+ btn.addEventListener('click', async (e) => {
138
+ const window = e.currentTarget.dataset.kpiWindow;
139
+ if (!window || state.kpiWindow === window) return;
140
+ state.kpiWindow = window;
141
+ document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(b => {
142
+ b.classList.toggle('active', b.dataset.kpiWindow === window);
143
+ });
144
+ await loadKpiData();
145
+ updateKpiCardsUI();
146
+ renderKpiTrendChart();
147
+ });
148
+ });
149
+
112
150
  // Search
113
151
  const searchInput = document.getElementById('search-input');
114
152
  if (searchInput) {
115
153
  searchInput.addEventListener('input', debounce((e) => handleSearch(e.target.value), 300));
116
154
  }
117
155
 
156
+ // User prompt search
157
+ const userPromptSearch = document.getElementById('user-prompt-search');
158
+ if (userPromptSearch) {
159
+ userPromptSearch.addEventListener('input', debounce(async (e) => {
160
+ state.userPromptSearchQuery = e.target.value || '';
161
+ state.userPromptPage = 1;
162
+ await loadUserPromptsView();
163
+ }, 250));
164
+ }
165
+ const userPromptRefresh = document.getElementById('user-prompt-refresh');
166
+ if (userPromptRefresh) {
167
+ userPromptRefresh.addEventListener('click', async () => {
168
+ await loadUserPromptsView();
169
+ });
170
+ }
171
+ const userPromptPrev = document.getElementById('user-prompt-prev');
172
+ if (userPromptPrev) {
173
+ userPromptPrev.addEventListener('click', async () => {
174
+ if (state.userPromptPage <= 1) return;
175
+ state.userPromptPage -= 1;
176
+ await renderUserPromptList();
177
+ });
178
+ }
179
+ const userPromptNext = document.getElementById('user-prompt-next');
180
+ if (userPromptNext) {
181
+ userPromptNext.addEventListener('click', async () => {
182
+ const totalPages = Math.max(1, Math.ceil((state.userPromptItems?.length || 0) / state.userPromptPageSize));
183
+ if (state.userPromptPage >= totalPages) return;
184
+ state.userPromptPage += 1;
185
+ await renderUserPromptList();
186
+ });
187
+ }
188
+
118
189
  // Project selector
119
190
  const projectSelect = document.getElementById('project-select');
120
191
  if (projectSelect) {
@@ -125,6 +196,10 @@ function setupEventListeners() {
125
196
  state.chartInstance.destroy();
126
197
  state.chartInstance = null;
127
198
  }
199
+ if (state.kpiChartInstance) {
200
+ state.kpiChartInstance.destroy();
201
+ state.kpiChartInstance = null;
202
+ }
128
203
  await initActivityChart();
129
204
  // Reload current view if not overview
130
205
  if (state.currentView !== 'overview') {
@@ -231,17 +306,24 @@ function setupEventListeners() {
231
306
 
232
307
  // --- Data Fetching ---
233
308
 
309
+ async function loadKpiData() {
310
+ state.kpi = await fetch(apiUrl(`${API_BASE}/stats/kpi`, { window: state.kpiWindow }))
311
+ .then(r => r.json())
312
+ .catch(() => null);
313
+ }
314
+
234
315
  async function refreshData() {
235
316
  const btn = document.getElementById('refresh-btn');
236
317
  if(btn) btn.classList.add('loading');
237
318
 
238
319
  try {
239
- const [stats, shared, mostAccessed, helpfulness, retrievalTraces] = await Promise.all([
320
+ const [stats, shared, mostAccessed, helpfulness, retrievalTraces, adherenceSummary] = await Promise.all([
240
321
  fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
241
322
  fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
242
323
  fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
243
324
  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)
325
+ fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null),
326
+ fetchAdherenceSummary().catch(() => null)
245
327
  ]);
246
328
 
247
329
  state.stats = stats;
@@ -249,10 +331,15 @@ async function refreshData() {
249
331
  state.mostAccessed = mostAccessed;
250
332
  state.helpfulness = helpfulness;
251
333
  state.retrievalTraces = retrievalTraces;
334
+ state.adherenceSummary = adherenceSummary;
335
+
336
+ await loadKpiData();
252
337
 
253
338
  updateStatsUI();
254
339
  updateSharedUI();
255
340
  updateMemoryUsageUI();
341
+ updateKpiCardsUI();
342
+ renderKpiTrendChart();
256
343
  await loadLevelEvents(state.currentLevel);
257
344
 
258
345
  checkEndlessStatus();
@@ -327,6 +414,98 @@ function updateSharedUI() {
327
414
  document.getElementById('shared-errors').textContent = formatNumber(state.sharedStats.commonErrors || 0);
328
415
  }
329
416
 
417
+ function percentText(v) {
418
+ return `${((v || 0) * 100).toFixed(1)}%`;
419
+ }
420
+
421
+ function renderDelta(id, value, lowerIsBetter = false, asPercent = true) {
422
+ const el = document.getElementById(id);
423
+ if (!el) return;
424
+ const v = Number(value || 0);
425
+ const sign = v > 0 ? '+' : '';
426
+ const text = asPercent ? `${sign}${(v * 100).toFixed(1)}%` : `${sign}${v.toFixed(2)}`;
427
+
428
+ let positive = v > 0;
429
+ if (lowerIsBetter) positive = v < 0;
430
+ const cls = v === 0 ? 'neutral' : (positive ? 'good' : 'bad');
431
+ const arrow = v === 0 ? '→' : (positive ? '▲' : '▼');
432
+
433
+ el.className = `kpi-delta ${cls}`;
434
+ el.textContent = `${arrow} ${text} vs prev`;
435
+ }
436
+
437
+ function updateKpiCardsUI() {
438
+ const m = state.kpi?.metrics;
439
+ const d = state.kpi?.deltas;
440
+ if (!m) return;
441
+ const set = (id, value) => {
442
+ const el = document.getElementById(id);
443
+ if (el) el.textContent = value;
444
+ };
445
+ set('kpi-useful-recall', percentText(m.usefulRecallRate));
446
+ set('kpi-completion-turns', Number(m.avgCompletionTurns || 0).toFixed(2));
447
+ set('kpi-rework-rate', percentText(m.reworkRate));
448
+ set('kpi-failure-rate', percentText(m.postChangeFailureRate));
449
+
450
+ if (d) {
451
+ renderDelta('kpi-useful-recall-delta', d.usefulRecallRate, false, true);
452
+ renderDelta('kpi-completion-turns-delta', d.avgCompletionTurns, true, false);
453
+ renderDelta('kpi-rework-rate-delta', d.reworkRate, true, true);
454
+ renderDelta('kpi-failure-rate-delta', d.postChangeFailureRate, true, true);
455
+ }
456
+
457
+ const alertsEl = document.getElementById('kpi-alerts');
458
+ if (alertsEl) {
459
+ const alerts = state.kpi?.alerts || [];
460
+ if (alerts.length === 0) {
461
+ alertsEl.innerHTML = '<span style="color:var(--success);">No KPI alerts in current window.</span>';
462
+ } else {
463
+ alertsEl.innerHTML = alerts.slice(0, 3).map(a => `⚠️ ${escapeHtml(a.message)} (${a.metric})`).join(' · ');
464
+ }
465
+ }
466
+ }
467
+
468
+ function renderKpiTrendChart() {
469
+ const chartEl = document.querySelector('#kpi-trend-chart');
470
+ if (!chartEl) return;
471
+
472
+ const daily = state.kpi?.trend?.daily || [];
473
+ const categories = daily.map(d => d.date);
474
+ const useful = daily.map(d => Number(d.usefulRecallRate || 0) * 100);
475
+ const rework = daily.map(d => Number(d.reworkRate || 0) * 100);
476
+ const fail = daily.map(d => Number(d.postChangeFailureRate || 0) * 100);
477
+
478
+ if (state.kpiChartInstance) {
479
+ state.kpiChartInstance.destroy();
480
+ state.kpiChartInstance = null;
481
+ }
482
+
483
+ const options = {
484
+ series: [
485
+ { name: 'Useful Recall %', data: useful },
486
+ { name: 'Rework %', data: rework },
487
+ { name: 'Failure %', data: fail }
488
+ ],
489
+ chart: {
490
+ type: 'line',
491
+ height: 240,
492
+ background: 'transparent',
493
+ toolbar: { show: false },
494
+ fontFamily: 'Outfit, sans-serif'
495
+ },
496
+ stroke: { curve: 'smooth', width: 2 },
497
+ dataLabels: { enabled: false },
498
+ xaxis: { categories, labels: { style: { colors: '#8B9BB4' } } },
499
+ yaxis: { labels: { formatter: (v) => `${v.toFixed(0)}%`, style: { colors: '#8B9BB4' } } },
500
+ theme: { mode: 'dark' },
501
+ grid: { borderColor: 'rgba(255,255,255,0.05)', strokeDashArray: 4 },
502
+ colors: ['#34D399', '#FEB019', '#FF4560']
503
+ };
504
+
505
+ state.kpiChartInstance = new ApexCharts(chartEl, options);
506
+ state.kpiChartInstance.render();
507
+ }
508
+
330
509
  function selectLevel(level) {
331
510
  state.currentLevel = level;
332
511
 
@@ -347,6 +526,23 @@ function selectSort(sort) {
347
526
  loadLevelEvents(state.currentLevel, sort);
348
527
  }
349
528
 
529
+ function getAdherenceInfo(event) {
530
+ const adherence = event?.metadata?.adherence || event?.meta?.adherence || null;
531
+ if (!adherence || typeof adherence !== 'object') return null;
532
+ const reason = adherence.reason || 'unknown';
533
+ const checked = Boolean(adherence.checked);
534
+ const turn = adherence.turn;
535
+ return { reason, checked, turn };
536
+ }
537
+
538
+ function renderAdherenceBadge(event) {
539
+ const info = getAdherenceInfo(event);
540
+ if (!info) return '';
541
+ const modeClass = info.checked ? 'adherence-checked' : 'adherence-skipped';
542
+ const turnText = Number.isFinite(info.turn) ? ` · T${info.turn}` : '';
543
+ return `<span class="adherence-badge ${modeClass}" title="adherence ${info.checked ? 'checked' : 'skipped'}${turnText}">adh:${escapeHtml(info.reason)}</span>`;
544
+ }
545
+
350
546
  function updateEventsListUI() {
351
547
  const container = document.getElementById('event-list-container');
352
548
  container.innerHTML = '';
@@ -374,13 +570,17 @@ function updateEventsListUI() {
374
570
  const accessBadge = event.accessCount > 0
375
571
  ? `<span class="access-badge"><i class="ri-eye-line"></i> ${event.accessCount}</span>`
376
572
  : '';
573
+ const adherenceBadge = renderAdherenceBadge(event);
377
574
  const lastUsed = (state.currentSort === 'accessed' || state.currentSort === 'most-accessed') && event.lastAccessedAt
378
575
  ? `<span class="event-time" style="color:var(--accent-secondary);">used ${new Date(event.lastAccessedAt).toLocaleString()}</span>`
379
576
  : '';
380
577
 
381
578
  el.innerHTML = `
382
579
  <div class="event-header">
383
- <span class="event-type-badge ${typeClass}">${eventType}</span>
580
+ <div style="display:flex; gap:8px; align-items:center;">
581
+ <span class="event-type-badge ${typeClass}">${eventType}</span>
582
+ ${adherenceBadge}
583
+ </div>
384
584
  <div style="display:flex; gap:8px; align-items:center;">
385
585
  ${accessBadge}
386
586
  ${lastUsed}
@@ -400,9 +600,72 @@ function updateMemoryUsageUI() {
400
600
  updateGraduationBars();
401
601
  updateHelpfulnessUI();
402
602
  updateMostHelpfulList();
603
+ updateAdherenceSummaryUI();
403
604
  updateRetrievalTraceUI();
404
605
  }
405
606
 
607
+ function adherenceWindowToMs(window) {
608
+ if (window === '24h') return 24 * 60 * 60 * 1000;
609
+ if (window === '7d') return 7 * 24 * 60 * 60 * 1000;
610
+ if (window === '30d') return 30 * 24 * 60 * 60 * 1000;
611
+ return 24 * 60 * 60 * 1000;
612
+ }
613
+
614
+ async function fetchAdherenceSummary() {
615
+ const res = await fetch(apiUrl(`${API_BASE}/events`, { level: 'L0', limit: 500, sort: 'recent' }));
616
+ if (!res.ok) return null;
617
+ const data = await res.json();
618
+ const events = data.events || [];
619
+
620
+ const counts = {};
621
+ let checked = 0;
622
+ let skipped = 0;
623
+ let total = 0;
624
+
625
+ const now = Date.now();
626
+ const windowMs = adherenceWindowToMs(state.adherenceWindow);
627
+
628
+ for (const e of events) {
629
+ const ts = e?.timestamp ? new Date(e.timestamp).getTime() : 0;
630
+ if (!ts || now - ts > windowMs) continue;
631
+
632
+ const adherence = e?.metadata?.adherence || e?.meta?.adherence;
633
+ if (!adherence) continue;
634
+ total++;
635
+ const reason = adherence.reason || 'unknown';
636
+ counts[reason] = (counts[reason] || 0) + 1;
637
+ if (adherence.checked) checked++; else skipped++;
638
+ }
639
+
640
+ return { total, checked, skipped, counts, window: state.adherenceWindow };
641
+ }
642
+
643
+ function updateAdherenceSummaryUI() {
644
+ const el = document.getElementById('adherence-summary');
645
+ if (!el) return;
646
+
647
+ const s = state.adherenceSummary;
648
+ if (!s || !s.total) {
649
+ el.innerHTML = '<span style="color:var(--text-muted);">No adherence metadata yet.</span>';
650
+ return;
651
+ }
652
+
653
+ const top = Object.entries(s.counts || {})
654
+ .sort((a, b) => b[1] - a[1])
655
+ .slice(0, 5)
656
+ .map(([reason, count]) => `<span class="adherence-badge adherence-checked" style="margin-right:6px;">${escapeHtml(reason)}: ${count}</span>`)
657
+ .join('');
658
+
659
+ el.innerHTML = `
660
+ <div style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:8px;">
661
+ <span><strong>${s.total}</strong> tagged prompts (${escapeHtml(s.window || state.adherenceWindow)})</span>
662
+ <span style="color:var(--success);"><strong>${s.checked}</strong> checked</span>
663
+ <span style="color:var(--text-muted);"><strong>${s.skipped}</strong> skipped</span>
664
+ </div>
665
+ <div>${top}</div>
666
+ `;
667
+ }
668
+
406
669
  function updateGraduationBars() {
407
670
  const container = document.getElementById('graduation-bars');
408
671
  if (!container || !state.stats?.levelStats) return;
@@ -691,6 +954,7 @@ async function openDetailModal(eventId) {
691
954
  const eventType = evt.eventType || 'unknown';
692
955
  const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
693
956
  const time = new Date(evt.timestamp).toLocaleString();
957
+ const adherenceBadge = renderAdherenceBadge(evt);
694
958
 
695
959
  let contextHtml = '';
696
960
  if (ctx.length > 0) {
@@ -716,6 +980,7 @@ async function openDetailModal(eventId) {
716
980
  <i class="ri-price-tag-3-line"></i>
717
981
  <span class="event-type-badge ${typeClass}">${eventType}</span>
718
982
  </div>
983
+ ${adherenceBadge ? `<div class="modal-meta-item">${adherenceBadge}</div>` : ''}
719
984
  <div class="modal-meta-item">
720
985
  <i class="ri-time-line"></i>
721
986
  ${time}
@@ -763,11 +1028,13 @@ async function showEventsListModal() {
763
1028
 
764
1029
  body.innerHTML = events.map(e => {
765
1030
  const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
1031
+ const adherenceBadge = renderAdherenceBadge(e);
766
1032
  return `
767
1033
  <div class="modal-list-item" onclick="openDetailModal('${e.id}')">
768
1034
  <div class="modal-list-info">
769
1035
  <div class="title">
770
1036
  <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
1037
+ ${adherenceBadge}
771
1038
  ${escapeHtml((e.preview || '').slice(0, 80))}
772
1039
  </div>
773
1040
  <div class="subtitle">${new Date(e.timestamp).toLocaleString()} | Session: ${(e.sessionId || '').slice(0, 12)}...</div>
@@ -847,11 +1114,13 @@ async function showSessionDetailInModal(sessionId) {
847
1114
  <div class="modal-section-title">Events</div>
848
1115
  ${events.map(e => {
849
1116
  const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
1117
+ const adherenceBadge = renderAdherenceBadge(e);
850
1118
  return `
851
1119
  <div class="modal-list-item" onclick="closeAllModals(); openDetailModal('${e.id}')">
852
1120
  <div class="modal-list-info">
853
1121
  <div class="title">
854
1122
  <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
1123
+ ${adherenceBadge}
855
1124
  ${escapeHtml((e.preview || '').slice(0, 80))}
856
1125
  </div>
857
1126
  <div class="subtitle">${new Date(e.timestamp).toLocaleString()}</div>
@@ -965,6 +1234,7 @@ function switchView(viewName) {
965
1234
  switch (viewName) {
966
1235
  case 'knowledge-graph': loadKnowledgeGraphView(); break;
967
1236
  case 'memory-banks': loadMemoryBanksView(); break;
1237
+ case 'user-prompts': loadUserPromptsView(); break;
968
1238
  case 'configuration': loadConfigurationView(); break;
969
1239
  }
970
1240
  }
@@ -1198,6 +1468,94 @@ async function loadMemoryBankLevel(level) {
1198
1468
  }
1199
1469
  }
1200
1470
 
1471
+ // --- User Prompts View ---
1472
+
1473
+ async function renderUserPromptList() {
1474
+ const listEl = document.getElementById('user-prompt-list');
1475
+ const pageEl = document.getElementById('user-prompt-page');
1476
+ const prevBtn = document.getElementById('user-prompt-prev');
1477
+ const nextBtn = document.getElementById('user-prompt-next');
1478
+ const metaEl = document.getElementById('user-prompt-meta');
1479
+ if (!listEl) return;
1480
+
1481
+ const items = state.userPromptItems || [];
1482
+ const pageSize = state.userPromptPageSize;
1483
+ const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
1484
+ if (state.userPromptPage > totalPages) state.userPromptPage = totalPages;
1485
+
1486
+ const start = (state.userPromptPage - 1) * pageSize;
1487
+ const paged = items.slice(start, start + pageSize);
1488
+
1489
+ if (pageEl) pageEl.textContent = `${state.userPromptPage} / ${totalPages}`;
1490
+ if (prevBtn) prevBtn.disabled = state.userPromptPage <= 1;
1491
+ if (nextBtn) nextBtn.disabled = state.userPromptPage >= totalPages;
1492
+
1493
+ if (metaEl) {
1494
+ const sessionCount = new Set(items.map(i => i.sessionId)).size;
1495
+ metaEl.textContent = `${items.length} prompts · ${sessionCount} sessions${state.userPromptSearchQuery ? ` · query: "${state.userPromptSearchQuery}"` : ''}`;
1496
+ }
1497
+
1498
+ if (paged.length === 0) {
1499
+ listEl.innerHTML = '<div style="padding:20px; text-align:center; color:var(--text-muted);">No user prompts found.</div>';
1500
+ return;
1501
+ }
1502
+
1503
+ // Group current page by session
1504
+ const groups = new Map();
1505
+ for (const e of paged) {
1506
+ const key = e.sessionId || 'unknown';
1507
+ const arr = groups.get(key) || [];
1508
+ arr.push(e);
1509
+ groups.set(key, arr);
1510
+ }
1511
+
1512
+ const html = Array.from(groups.entries()).map(([sessionId, sessionItems]) => {
1513
+ const heading = `
1514
+ <div style="margin:10px 0 6px; font-size:12px; color:var(--text-muted); font-weight:600;">
1515
+ <i class="ri-chat-1-line"></i> Session ${escapeHtml((sessionId || '').slice(0, 16))}... · ${sessionItems.length} prompts
1516
+ </div>
1517
+ `;
1518
+
1519
+ const cards = sessionItems.map((e) => `
1520
+ <div class="event-item" style="cursor:pointer;" onclick="openDetailModal('${e.id}')">
1521
+ <div class="event-header">
1522
+ <span class="event-type-badge type-user-prompt">user_prompt</span>
1523
+ <span class="event-time">${new Date(e.timestamp).toLocaleString()}</span>
1524
+ </div>
1525
+ <div class="event-content" style="-webkit-line-clamp:4;">${escapeHtml(e.preview || '')}</div>
1526
+ </div>
1527
+ `).join('');
1528
+
1529
+ return heading + cards;
1530
+ }).join('');
1531
+
1532
+ listEl.innerHTML = html;
1533
+ }
1534
+
1535
+ async function loadUserPromptsView() {
1536
+ const listEl = document.getElementById('user-prompt-list');
1537
+ if (!listEl) return;
1538
+
1539
+ listEl.innerHTML = '<div style="padding:20px; text-align:center; color:var(--text-muted);">Loading user prompts...</div>';
1540
+
1541
+ try {
1542
+ const params = {
1543
+ type: 'user_prompt',
1544
+ sort: 'recent',
1545
+ limit: 500,
1546
+ q: state.userPromptSearchQuery || undefined
1547
+ };
1548
+ const res = await fetch(apiUrl(`${API_BASE}/events`, params));
1549
+ const data = await res.json();
1550
+ const items = data.events || [];
1551
+ state.userPromptItems = items;
1552
+
1553
+ await renderUserPromptList();
1554
+ } catch (error) {
1555
+ listEl.innerHTML = `<div style="padding:20px; text-align:center; color:var(--error);">Failed to load user prompts: ${escapeHtml(error.message)}</div>`;
1556
+ }
1557
+ }
1558
+
1201
1559
  // --- Configuration View ---
1202
1560
 
1203
1561
  async function loadConfigurationView() {
package/src/ui/index.html CHANGED
@@ -51,6 +51,10 @@
51
51
  <i class="ri-brain-line"></i>
52
52
  <span>Memory Banks</span>
53
53
  </li>
54
+ <li class="nav-item" data-nav="user-prompts">
55
+ <i class="ri-message-2-line"></i>
56
+ <span>User Prompts</span>
57
+ </li>
54
58
  <li class="nav-item" data-nav="configuration">
55
59
  <i class="ri-settings-4-line"></i>
56
60
  <span>Configuration</span>
@@ -120,6 +124,45 @@
120
124
  </div>
121
125
  </div>
122
126
 
127
+ <!-- KPI Cards -->
128
+ <div class="stats-grid kpi-grid" style="margin-top:-4px;">
129
+ <div class="stat-card kpi-card">
130
+ <div class="stat-value" id="kpi-useful-recall">-</div>
131
+ <div class="kpi-delta" id="kpi-useful-recall-delta">-</div>
132
+ <div class="stat-label"><i class="ri-thumb-up-line"></i> Useful Recall Rate</div>
133
+ </div>
134
+ <div class="stat-card kpi-card">
135
+ <div class="stat-value" id="kpi-completion-turns">-</div>
136
+ <div class="kpi-delta" id="kpi-completion-turns-delta">-</div>
137
+ <div class="stat-label"><i class="ri-repeat-line"></i> Avg Completion Turns</div>
138
+ </div>
139
+ <div class="stat-card kpi-card">
140
+ <div class="stat-value" id="kpi-rework-rate">-</div>
141
+ <div class="kpi-delta" id="kpi-rework-rate-delta">-</div>
142
+ <div class="stat-label"><i class="ri-hammer-line"></i> Rework Rate</div>
143
+ </div>
144
+ <div class="stat-card kpi-card">
145
+ <div class="stat-value" id="kpi-failure-rate">-</div>
146
+ <div class="kpi-delta" id="kpi-failure-rate-delta">-</div>
147
+ <div class="stat-label"><i class="ri-error-warning-line"></i> Post-change Failure</div>
148
+ </div>
149
+ </div>
150
+
151
+ <div class="card" style="margin-bottom:24px;">
152
+ <div class="card-header">
153
+ <div class="card-title">
154
+ <i class="ri-line-chart-line"></i>
155
+ <span>KPI Trend</span>
156
+ </div>
157
+ <div style="display:flex; gap:8px;">
158
+ <button class="sort-btn active" data-kpi-window="7d">7d</button>
159
+ <button class="sort-btn" data-kpi-window="30d">30d</button>
160
+ </div>
161
+ </div>
162
+ <div id="kpi-alerts" style="font-size:12px; color:var(--text-muted); margin-bottom:10px;">Loading...</div>
163
+ <div id="kpi-trend-chart" style="height:240px;"></div>
164
+ </div>
165
+
123
166
  <!-- Main Grid -->
124
167
  <div class="dashboard-grid">
125
168
 
@@ -272,6 +315,18 @@
272
315
  </div>
273
316
  </div>
274
317
 
318
+ <div style="margin-top:20px;">
319
+ <div class="section-label" style="display:flex; justify-content:space-between; align-items:center;">
320
+ <span>Adherence Reasons</span>
321
+ <span id="adherence-window-controls" style="display:flex; gap:6px;">
322
+ <button class="sort-btn active" data-adh-window="24h">24h</button>
323
+ <button class="sort-btn" data-adh-window="7d">7d</button>
324
+ <button class="sort-btn" data-adh-window="30d">30d</button>
325
+ </span>
326
+ </div>
327
+ <div id="adherence-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
328
+ </div>
329
+
275
330
  <div style="margin-top:20px;">
276
331
  <div class="section-label">Retrieval Trace (1:1)</div>
277
332
  <div id="retrieval-trace-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
@@ -312,6 +367,41 @@
312
367
  </div>
313
368
  </div>
314
369
 
370
+ <!-- ========== VIEW: User Prompts ========== -->
371
+ <div id="view-user-prompts" class="page-view">
372
+ <header class="top-header">
373
+ <div class="page-title">
374
+ <h1>User Prompts</h1>
375
+ <p>Search and browse recent user prompts</p>
376
+ </div>
377
+ </header>
378
+ <div class="card" style="margin-bottom:16px;">
379
+ <div style="display:flex; gap:10px; align-items:center;">
380
+ <div class="search-wrapper" style="width:420px; max-width:100%;">
381
+ <i class="ri-search-line"></i>
382
+ <input type="text" id="user-prompt-search" class="search-input" placeholder="Search user prompts...">
383
+ </div>
384
+ <button id="user-prompt-refresh" class="btn btn-secondary"><i class="ri-refresh-line"></i><span>Refresh</span></button>
385
+ </div>
386
+ </div>
387
+ <div class="card">
388
+ <div class="card-header" style="align-items:flex-end;">
389
+ <div>
390
+ <div class="card-title"><i class="ri-history-line"></i><span>Latest User Prompt History</span></div>
391
+ <div id="user-prompt-meta" style="font-size:12px; color:var(--text-muted); margin-top:6px;"></div>
392
+ </div>
393
+ <div style="display:flex; gap:8px; align-items:center;">
394
+ <button id="user-prompt-prev" class="sort-btn">Prev</button>
395
+ <span id="user-prompt-page" style="font-size:12px; color:var(--text-muted);">1 / 1</span>
396
+ <button id="user-prompt-next" class="sort-btn">Next</button>
397
+ </div>
398
+ </div>
399
+ <div id="user-prompt-list" class="event-list">
400
+ <div style="padding:20px; text-align:center; color:var(--text-muted);">Loading...</div>
401
+ </div>
402
+ </div>
403
+ </div>
404
+
315
405
  <!-- ========== VIEW: Configuration ========== -->
316
406
  <div id="view-configuration" class="page-view">
317
407
  <header class="top-header">