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.
- package/dist/cli/index.js +1373 -184
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +445 -7
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +705 -43
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +593 -52
- package/dist/hooks/session-end.js.map +3 -3
- package/dist/hooks/session-start.js +581 -25
- package/dist/hooks/session-start.js.map +3 -3
- package/dist/hooks/stop.js +693 -73
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +674 -94
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1045 -42
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1054 -51
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +599 -25
- package/dist/services/memory-service.js.map +3 -3
- package/dist/ui/app.js +1380 -55
- package/dist/ui/index.html +311 -148
- package/dist/ui/style.css +892 -0
- package/docs/OPERATIONS.md +18 -0
- package/package.json +8 -2
- package/scripts/fix-sync-gap.js +32 -0
- package/scripts/heartbeat-memory-orchestrator.sh +28 -0
- package/scripts/report-sync-gap.js +26 -0
- package/scripts/review-queue-auto-resolve.js +21 -0
- package/scripts/sync-gap-auto-heal.sh +17 -0
- package/specs/20260207-dashboard-upgrade/context.md +38 -0
- package/specs/20260207-dashboard-upgrade/spec.md +96 -0
- package/src/cli/index.ts +110 -58
- package/src/core/sqlite-event-store.ts +542 -6
- package/src/core/sqlite-wrapper.ts +8 -0
- package/src/core/turn-state.ts +159 -0
- package/src/core/types.ts +23 -8
- package/src/core/vector-store.ts +21 -3
- package/src/hooks/post-tool-use.ts +68 -23
- package/src/hooks/session-end.ts +8 -3
- package/src/hooks/stop.ts +96 -25
- package/src/hooks/user-prompt-submit.ts +78 -65
- package/src/server/api/chat.ts +244 -0
- package/src/server/api/citations.ts +3 -3
- package/src/server/api/events.ts +30 -5
- package/src/server/api/index.ts +7 -1
- package/src/server/api/projects.ts +74 -0
- package/src/server/api/search.ts +3 -3
- package/src/server/api/sessions.ts +3 -3
- package/src/server/api/stats.ts +43 -7
- package/src/server/api/turns.ts +143 -0
- package/src/server/api/utils.ts +46 -0
- package/src/services/memory-service.ts +208 -9
- package/src/services/session-history-importer.ts +215 -51
- package/src/ui/app.js +1380 -55
- package/src/ui/index.html +311 -148
- package/src/ui/style.css +892 -0
- package/.claude/settings.local.json +0 -27
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201112328.json +0 -45
- package/.history/package_20260201113602.json +0 -45
- package/.history/package_20260201113713.json +0 -45
- package/.history/package_20260201114110.json +0 -45
- package/.history/package_20260201114632.json +0 -46
- package/.history/package_20260201133143.json +0 -45
- package/.history/package_20260201134319.json +0 -45
- package/.history/package_20260201134326.json +0 -45
- package/.history/package_20260201134334.json +0 -45
- package/.history/package_20260201134912.json +0 -45
- package/.history/package_20260201142928.json +0 -46
- package/.history/package_20260201192048.json +0 -47
- package/.history/package_20260202114053.json +0 -49
- package/test_access.js +0 -49
package/dist/ui/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Code Memory Dashboard Logic
|
|
3
|
-
* Handles state management, API calls,
|
|
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
|
-
//
|
|
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();
|
|
267
|
+
updateEventsListUI();
|
|
96
268
|
|
|
97
269
|
try {
|
|
98
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const sharedCount = state.sharedStats ?
|
|
130
|
-
(state.sharedStats.troubleshooting + state.sharedStats.
|
|
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(
|
|
302
|
+
document.getElementById('stat-vectors').textContent = formatNumber(vectorCount);
|
|
134
303
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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.
|
|
154
|
-
document.getElementById('shared-errors').textContent = formatNumber(state.sharedStats.
|
|
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
|
|
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}">${
|
|
192
|
-
<
|
|
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(
|
|
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:
|
|
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:
|
|
241
|
-
labels: {
|
|
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
|
-
|
|
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, "&")
|
|
300
1248
|
.replace(/</g, "<")
|
|
301
1249
|
.replace(/>/g, ">")
|
|
302
1250
|
.replace(/"/g, """)
|
|
303
1251
|
.replace(/'/g, "'");
|
|
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} · ${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;">• $1</div>');
|
|
1624
|
+
|
|
1625
|
+
// Line breaks
|
|
1626
|
+
html = html.replace(/\n/g, '<br>');
|
|
1627
|
+
|
|
1628
|
+
return html;
|
|
1629
|
+
}
|