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.
- package/config/kpi-thresholds.json +7 -0
- package/dist/cli/index.js +532 -79
- package/dist/cli/index.js.map +3 -3
- package/dist/core/index.js +49 -4
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +140 -3
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +140 -3
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +140 -3
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +140 -3
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +379 -34
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +467 -34
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +474 -41
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +140 -3
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +362 -4
- package/dist/ui/index.html +90 -0
- package/dist/ui/style.css +41 -0
- package/memory/_index.md +3 -0
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/cli/index.ts +23 -1
- package/src/core/embedder.ts +3 -2
- package/src/core/sqlite-event-store.ts +32 -0
- package/src/core/types.ts +2 -2
- package/src/core/vector-store.ts +20 -0
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +7 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +119 -2
- package/src/ui/app.js +362 -4
- package/src/ui/index.html +90 -0
- package/src/ui/style.css +41 -0
package/dist/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
|
-
<
|
|
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/dist/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">
|