agent-knowledge 1.0.0 → 1.0.4
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/CHANGELOG.md +77 -77
- package/LICENSE +21 -21
- package/README.md +191 -191
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/knowledge/git.d.ts.map +1 -1
- package/dist/knowledge/git.js +91 -86
- package/dist/knowledge/git.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js.map +1 -1
- package/dist/ui/app.js +38 -114
- package/dist/ui/index.html +21 -65
- package/dist/ui/styles.css +3 -2
- package/dist/vectorstore/store.d.ts.map +1 -1
- package/dist/vectorstore/store.js +32 -32
- package/dist/vectorstore/store.js.map +1 -1
- package/docs/ARCHITECTURE.md +244 -244
- package/docs/DASHBOARD.md +133 -133
- package/docs/SETUP.md +178 -178
- package/package.json +1 -1
- package/scripts/copy-ui.js +6 -6
- package/dist/ui/ui/app.js +0 -811
- package/dist/ui/ui/index.html +0 -300
- package/dist/ui/ui/styles.css +0 -1154
package/dist/ui/ui/app.js
DELETED
|
@@ -1,811 +0,0 @@
|
|
|
1
|
-
/* agent-cortex dashboard — vanilla JS SPA */
|
|
2
|
-
(function () {
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
// ── State ──────────────────────────────────────────────────────────────────
|
|
6
|
-
const state = {
|
|
7
|
-
activeTab: 'knowledge',
|
|
8
|
-
knowledge: { entries: [], activeCategory: 'all' },
|
|
9
|
-
search: { query: '', results: [], role: 'all', ranked: true, loading: false },
|
|
10
|
-
sessions: { list: [], projectFilter: '', loading: false },
|
|
11
|
-
recall: { scope: 'all', query: '', results: [], loading: false },
|
|
12
|
-
panel: { open: false, type: null, data: null },
|
|
13
|
-
stats: { knowledgeCount: 0, sessionCount: 0 },
|
|
14
|
-
connected: false,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// ── DOM refs ───────────────────────────────────────────────────────────────
|
|
18
|
-
const $ = (id) => document.getElementById(id);
|
|
19
|
-
const el = {
|
|
20
|
-
tabs: { knowledge: $('tab-knowledge'), search: $('tab-search'), sessions: $('tab-sessions'), recall: $('tab-recall') },
|
|
21
|
-
views: { knowledge: $('view-knowledge'), search: $('view-search'), sessions: $('view-sessions'), recall: $('view-recall') },
|
|
22
|
-
knowledgeGrid: $('knowledge-grid'),
|
|
23
|
-
knowledgeEmpty: $('knowledge-empty'),
|
|
24
|
-
knowledgeCategories: $('knowledge-categories'),
|
|
25
|
-
searchInput: $('search-input'),
|
|
26
|
-
searchResults: $('search-results'),
|
|
27
|
-
searchEmpty: $('search-empty'),
|
|
28
|
-
searchRoleFilters: $('search-role-filters'),
|
|
29
|
-
modeRanked: $('mode-ranked'),
|
|
30
|
-
modeRegex: $('mode-regex'),
|
|
31
|
-
sessionsList: $('sessions-list'),
|
|
32
|
-
sessionsEmpty: $('sessions-empty'),
|
|
33
|
-
sessionProjectFilter: $('session-project-filter'),
|
|
34
|
-
recallInput: $('recall-input'),
|
|
35
|
-
recallResults: $('recall-results'),
|
|
36
|
-
recallEmpty: $('recall-empty'),
|
|
37
|
-
recallScopes: $('recall-scopes'),
|
|
38
|
-
sidePanel: $('side-panel'),
|
|
39
|
-
panelTitle: $('panel-title'),
|
|
40
|
-
panelBody: $('panel-body'),
|
|
41
|
-
panelClose: $('panel-close'),
|
|
42
|
-
connectionStatus: $('connection-status'),
|
|
43
|
-
statKnowledge: $('stat-knowledge'),
|
|
44
|
-
statSessions: $('stat-sessions'),
|
|
45
|
-
themeToggle: $('theme-toggle'),
|
|
46
|
-
version: $('version'),
|
|
47
|
-
loadingOverlay: $('loading-overlay'),
|
|
48
|
-
toastContainer: $('toast-container'),
|
|
49
|
-
contentWrapper: $('content-wrapper'),
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// ── Utilities ──────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
function esc(str) {
|
|
55
|
-
const d = document.createElement('div');
|
|
56
|
-
d.textContent = str;
|
|
57
|
-
return d.innerHTML;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function debounce(fn, ms) {
|
|
61
|
-
let t;
|
|
62
|
-
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function relativeTime(dateStr) {
|
|
66
|
-
if (!dateStr) return '';
|
|
67
|
-
const now = Date.now();
|
|
68
|
-
const then = new Date(dateStr).getTime();
|
|
69
|
-
const diff = now - then;
|
|
70
|
-
if (diff < 0) return 'just now';
|
|
71
|
-
const secs = Math.floor(diff / 1000);
|
|
72
|
-
if (secs < 60) return 'just now';
|
|
73
|
-
const mins = Math.floor(secs / 60);
|
|
74
|
-
if (mins < 60) return `${mins}m ago`;
|
|
75
|
-
const hrs = Math.floor(mins / 60);
|
|
76
|
-
if (hrs < 24) return `${hrs}h ago`;
|
|
77
|
-
const days = Math.floor(hrs / 24);
|
|
78
|
-
if (days < 30) return `${days}d ago`;
|
|
79
|
-
const months = Math.floor(days / 30);
|
|
80
|
-
if (months < 12) return `${months}mo ago`;
|
|
81
|
-
return `${Math.floor(months / 12)}y ago`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function renderMd(raw) {
|
|
85
|
-
if (!raw) return '';
|
|
86
|
-
try {
|
|
87
|
-
const html = marked.parse(raw, { breaks: true, gfm: true });
|
|
88
|
-
const clean = DOMPurify.sanitize(html, { ADD_TAGS: ['pre', 'code'] });
|
|
89
|
-
const wrapper = document.createElement('div');
|
|
90
|
-
wrapper.innerHTML = clean;
|
|
91
|
-
wrapper.querySelectorAll('pre code').forEach((block) => {
|
|
92
|
-
try { hljs.highlightElement(block); } catch (_) { /* noop */ }
|
|
93
|
-
});
|
|
94
|
-
return wrapper.innerHTML;
|
|
95
|
-
} catch {
|
|
96
|
-
return esc(raw);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function highlightExcerpt(text, query) {
|
|
101
|
-
if (!text || !query) return esc(text || '');
|
|
102
|
-
try {
|
|
103
|
-
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
104
|
-
const re = new RegExp(`(${escaped})`, 'gi');
|
|
105
|
-
return esc(text).replace(re, '<mark>$1</mark>');
|
|
106
|
-
} catch {
|
|
107
|
-
return esc(text);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function formatScore(score) {
|
|
112
|
-
if (score == null) return '';
|
|
113
|
-
const pct = Math.min(Math.max(score * 100, 0), 100);
|
|
114
|
-
return `<div class="score-bar" title="Score: ${score.toFixed(3)}">
|
|
115
|
-
<div class="score-fill" style="width:${pct}%"></div>
|
|
116
|
-
<span class="score-label">${score.toFixed(2)}</span>
|
|
117
|
-
</div>`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function toast(msg, type = 'info', duration = 4000) {
|
|
121
|
-
const t = document.createElement('div');
|
|
122
|
-
t.className = `toast toast-${type}`;
|
|
123
|
-
const icons = { info: 'info', success: 'check_circle', error: 'error', warning: 'warning' };
|
|
124
|
-
t.innerHTML = `<span class="material-symbols-outlined toast-icon">${icons[type] || 'info'}</span>
|
|
125
|
-
<span class="toast-msg">${esc(msg)}</span>`;
|
|
126
|
-
el.toastContainer.appendChild(t);
|
|
127
|
-
requestAnimationFrame(() => t.classList.add('show'));
|
|
128
|
-
setTimeout(() => {
|
|
129
|
-
t.classList.remove('show');
|
|
130
|
-
t.addEventListener('transitionend', () => t.remove());
|
|
131
|
-
}, duration);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── API ────────────────────────────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
async function api(path) {
|
|
137
|
-
const res = await fetch(`/api${path}`);
|
|
138
|
-
if (!res.ok) throw new Error(`API ${res.status}: ${res.statusText}`);
|
|
139
|
-
return res.json();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ── WebSocket ──────────────────────────────────────────────────────────────
|
|
143
|
-
|
|
144
|
-
let ws = null;
|
|
145
|
-
let wsRetry = null;
|
|
146
|
-
|
|
147
|
-
function wsConnect() {
|
|
148
|
-
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
149
|
-
ws = new WebSocket(`${proto}://${location.host}`);
|
|
150
|
-
|
|
151
|
-
ws.addEventListener('open', () => {
|
|
152
|
-
state.connected = true;
|
|
153
|
-
updateConnectionStatus();
|
|
154
|
-
el.loadingOverlay.classList.add('hidden');
|
|
155
|
-
if (wsRetry) { clearTimeout(wsRetry); wsRetry = null; }
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
ws.addEventListener('message', (evt) => {
|
|
159
|
-
try {
|
|
160
|
-
const msg = JSON.parse(evt.data);
|
|
161
|
-
handleWsMessage(msg);
|
|
162
|
-
} catch { /* ignore non-json */ }
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
ws.addEventListener('close', () => {
|
|
166
|
-
state.connected = false;
|
|
167
|
-
updateConnectionStatus();
|
|
168
|
-
scheduleReconnect();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
ws.addEventListener('error', () => {
|
|
172
|
-
state.connected = false;
|
|
173
|
-
updateConnectionStatus();
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function scheduleReconnect() {
|
|
178
|
-
if (wsRetry) return;
|
|
179
|
-
wsRetry = setTimeout(() => { wsRetry = null; wsConnect(); }, 3000);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function handleWsMessage(msg) {
|
|
183
|
-
switch (msg.type) {
|
|
184
|
-
case 'reload':
|
|
185
|
-
location.reload();
|
|
186
|
-
return;
|
|
187
|
-
case 'state':
|
|
188
|
-
if (msg.knowledge) {
|
|
189
|
-
state.knowledge.entries = msg.knowledge;
|
|
190
|
-
renderKnowledge();
|
|
191
|
-
}
|
|
192
|
-
if (msg.sessions) {
|
|
193
|
-
state.sessions.list = msg.sessions;
|
|
194
|
-
renderSessions();
|
|
195
|
-
}
|
|
196
|
-
if (msg.stats) {
|
|
197
|
-
state.stats.knowledgeCount = msg.stats.knowledge_entries || 0;
|
|
198
|
-
state.stats.sessionCount = msg.stats.session_count || 0;
|
|
199
|
-
if (msg.stats.version) el.version.textContent = 'v' + msg.stats.version;
|
|
200
|
-
updateStats();
|
|
201
|
-
}
|
|
202
|
-
el.loadingOverlay.classList.add('hidden');
|
|
203
|
-
break;
|
|
204
|
-
case 'knowledge:update':
|
|
205
|
-
case 'knowledge:change':
|
|
206
|
-
loadKnowledge();
|
|
207
|
-
break;
|
|
208
|
-
case 'session:update':
|
|
209
|
-
case 'session:new':
|
|
210
|
-
loadSessions();
|
|
211
|
-
break;
|
|
212
|
-
case 'stats':
|
|
213
|
-
if (msg.data) {
|
|
214
|
-
if (msg.data.knowledgeCount != null) state.stats.knowledgeCount = msg.data.knowledgeCount;
|
|
215
|
-
if (msg.data.sessionCount != null) state.stats.sessionCount = msg.data.sessionCount;
|
|
216
|
-
updateStats();
|
|
217
|
-
}
|
|
218
|
-
break;
|
|
219
|
-
case 'version':
|
|
220
|
-
if (msg.data) el.version.textContent = msg.data;
|
|
221
|
-
break;
|
|
222
|
-
default:
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function updateConnectionStatus() {
|
|
228
|
-
const s = el.connectionStatus;
|
|
229
|
-
if (state.connected) {
|
|
230
|
-
s.className = 'status-badge connected';
|
|
231
|
-
s.textContent = 'Connected';
|
|
232
|
-
} else {
|
|
233
|
-
s.className = 'status-badge disconnected';
|
|
234
|
-
s.textContent = 'Disconnected';
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ── Stats ──────────────────────────────────────────────────────────────────
|
|
239
|
-
|
|
240
|
-
function updateStats() {
|
|
241
|
-
el.statKnowledge.querySelector('.stat-value').textContent = state.stats.knowledgeCount;
|
|
242
|
-
el.statSessions.querySelector('.stat-value').textContent = state.stats.sessionCount;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ── Theme ──────────────────────────────────────────────────────────────────
|
|
246
|
-
|
|
247
|
-
function initTheme() {
|
|
248
|
-
const saved = localStorage.getItem('agent-cortex-theme');
|
|
249
|
-
const theme = saved || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
250
|
-
applyTheme(theme);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function applyTheme(theme) {
|
|
254
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
255
|
-
localStorage.setItem('agent-cortex-theme', theme);
|
|
256
|
-
const icon = el.themeToggle.querySelector('.theme-icon');
|
|
257
|
-
if (icon) icon.textContent = theme === 'dark' ? 'light_mode' : 'dark_mode';
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function toggleTheme() {
|
|
261
|
-
const cur = document.documentElement.getAttribute('data-theme') || 'light';
|
|
262
|
-
applyTheme(cur === 'dark' ? 'light' : 'dark');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ── Tabs ───────────────────────────────────────────────────────────────────
|
|
266
|
-
|
|
267
|
-
function switchTab(name) {
|
|
268
|
-
if (state.activeTab === name) return;
|
|
269
|
-
state.activeTab = name;
|
|
270
|
-
|
|
271
|
-
Object.keys(el.tabs).forEach((k) => {
|
|
272
|
-
const active = k === name;
|
|
273
|
-
el.tabs[k].classList.toggle('active', active);
|
|
274
|
-
el.tabs[k].setAttribute('aria-selected', active);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
Object.keys(el.views).forEach((k) => {
|
|
278
|
-
const active = k === name;
|
|
279
|
-
el.views[k].classList.toggle('active', active);
|
|
280
|
-
el.views[k].hidden = !active;
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
if (name === 'knowledge' && state.knowledge.entries.length === 0) loadKnowledge();
|
|
284
|
-
if (name === 'sessions' && state.sessions.list.length === 0) loadSessions();
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ── Knowledge ──────────────────────────────────────────────────────────────
|
|
288
|
-
|
|
289
|
-
async function loadKnowledge() {
|
|
290
|
-
try {
|
|
291
|
-
const data = await api('/knowledge');
|
|
292
|
-
const entries = Array.isArray(data) ? data : (data.entries || []);
|
|
293
|
-
state.knowledge.entries = entries;
|
|
294
|
-
state.stats.knowledgeCount = entries.length;
|
|
295
|
-
updateStats();
|
|
296
|
-
renderKnowledge();
|
|
297
|
-
} catch (err) {
|
|
298
|
-
toast(`Failed to load knowledge: ${err.message}`, 'error');
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function renderKnowledge() {
|
|
303
|
-
const cat = state.knowledge.activeCategory;
|
|
304
|
-
const filtered = cat === 'all'
|
|
305
|
-
? state.knowledge.entries
|
|
306
|
-
: state.knowledge.entries.filter((e) => e.category === cat);
|
|
307
|
-
|
|
308
|
-
if (filtered.length === 0) {
|
|
309
|
-
el.knowledgeGrid.innerHTML = '';
|
|
310
|
-
el.knowledgeEmpty.classList.remove('hidden');
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
el.knowledgeEmpty.classList.add('hidden');
|
|
315
|
-
el.knowledgeGrid.innerHTML = filtered.map((entry) => {
|
|
316
|
-
const cat = entry.category || 'notes';
|
|
317
|
-
const catIcons = { projects: 'code', people: 'group', decisions: 'gavel', workflows: 'account_tree', notes: 'sticky_note_2' };
|
|
318
|
-
const icon = catIcons[cat] || 'article';
|
|
319
|
-
const title = entry.title || entry.path || entry.id || 'Untitled';
|
|
320
|
-
const preview = entry.preview || entry.excerpt || '';
|
|
321
|
-
const tags = (entry.tags || []).slice(0, 3).map((t) => `<span class="card-tag">${esc(t)}</span>`).join('');
|
|
322
|
-
const time = relativeTime(entry.updated || entry.created);
|
|
323
|
-
|
|
324
|
-
return `<div class="knowledge-card" data-path="${esc(entry.path || entry.id || '')}" tabindex="0" role="button">
|
|
325
|
-
<span class="card-category" data-cat="${esc(cat)}">
|
|
326
|
-
<span class="material-symbols-outlined" style="font-size:14px">${icon}</span>
|
|
327
|
-
${esc(cat)}
|
|
328
|
-
</span>
|
|
329
|
-
${time ? `<span class="card-date">${time}</span>` : ''}
|
|
330
|
-
<div class="card-title">${esc(title)}</div>
|
|
331
|
-
${tags ? `<div class="card-tags">${tags}</div>` : ''}
|
|
332
|
-
</div>`;
|
|
333
|
-
}).join('');
|
|
334
|
-
|
|
335
|
-
el.knowledgeGrid.querySelectorAll('.knowledge-card').forEach((card) => {
|
|
336
|
-
card.addEventListener('click', () => openKnowledgePanel(card.dataset.path));
|
|
337
|
-
card.addEventListener('keydown', (e) => { if (e.key === 'Enter') openKnowledgePanel(card.dataset.path); });
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async function openKnowledgePanel(path) {
|
|
342
|
-
if (!path) return;
|
|
343
|
-
try {
|
|
344
|
-
const data = await api(`/knowledge/${encodeURIComponent(path)}`);
|
|
345
|
-
const title = data.title || data.path || path;
|
|
346
|
-
const content = data.content || data.body || '';
|
|
347
|
-
openPanel('knowledge', { title, content, meta: data });
|
|
348
|
-
} catch (err) {
|
|
349
|
-
toast(`Failed to load entry: ${err.message}`, 'error');
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ── Search ─────────────────────────────────────────────────────────────────
|
|
354
|
-
|
|
355
|
-
const doSearch = debounce(async () => {
|
|
356
|
-
const q = state.search.query.trim();
|
|
357
|
-
if (!q) {
|
|
358
|
-
state.search.results = [];
|
|
359
|
-
renderSearchResults();
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
state.search.loading = true;
|
|
363
|
-
renderSearchResults();
|
|
364
|
-
try {
|
|
365
|
-
const params = new URLSearchParams({ q });
|
|
366
|
-
if (state.search.role !== 'all') params.set('role', state.search.role);
|
|
367
|
-
params.set('ranked', state.search.ranked);
|
|
368
|
-
const data = await api(`/sessions/search?${params}`);
|
|
369
|
-
state.search.results = Array.isArray(data) ? data : (data.results || []);
|
|
370
|
-
} catch (err) {
|
|
371
|
-
toast(`Search failed: ${err.message}`, 'error');
|
|
372
|
-
state.search.results = [];
|
|
373
|
-
}
|
|
374
|
-
state.search.loading = false;
|
|
375
|
-
renderSearchResults();
|
|
376
|
-
}, 300);
|
|
377
|
-
|
|
378
|
-
function renderSearchResults() {
|
|
379
|
-
const { results, loading, query } = state.search;
|
|
380
|
-
|
|
381
|
-
if (loading) {
|
|
382
|
-
el.searchResults.innerHTML = '<div class="loading-inline"><div class="loading-spinner small"></div><span>Searching...</span></div>';
|
|
383
|
-
el.searchEmpty.classList.add('hidden');
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (!query.trim()) {
|
|
388
|
-
el.searchResults.innerHTML = '';
|
|
389
|
-
el.searchEmpty.classList.remove('hidden');
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (results.length === 0) {
|
|
394
|
-
el.searchResults.innerHTML = '';
|
|
395
|
-
el.searchEmpty.querySelector('.empty-text').textContent = 'No results found';
|
|
396
|
-
el.searchEmpty.querySelector('.empty-hint').textContent = `No matches for "${query}"`;
|
|
397
|
-
el.searchEmpty.classList.remove('hidden');
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
el.searchEmpty.classList.add('hidden');
|
|
402
|
-
el.searchResults.innerHTML = results.map((r) => {
|
|
403
|
-
const sessionId = r.sessionId || r.session_id || '';
|
|
404
|
-
const excerpt = r.excerpt || r.text || r.content || '';
|
|
405
|
-
const role = r.role || '';
|
|
406
|
-
const score = r.score;
|
|
407
|
-
const project = r.project || '';
|
|
408
|
-
const time = relativeTime(r.timestamp || r.date);
|
|
409
|
-
const roleIcon = role === 'user' ? 'person' : role === 'assistant' ? 'smart_toy' : 'chat';
|
|
410
|
-
|
|
411
|
-
return `<div class="result-item" data-session-id="${esc(sessionId)}" tabindex="0" role="button">
|
|
412
|
-
<div class="result-meta">
|
|
413
|
-
<span class="role-badge" data-role="${esc(role)}"><span class="material-symbols-outlined" style="font-size:12px">${roleIcon}</span> ${esc(role)}</span>
|
|
414
|
-
${project ? `<span class="result-project">${esc(project)}</span>` : ''}
|
|
415
|
-
${time ? `<span class="result-date">${time}</span>` : ''}
|
|
416
|
-
${score != null ? `<span class="score-container">${formatScore(score)}</span>` : ''}
|
|
417
|
-
</div>
|
|
418
|
-
<div class="result-excerpt">${highlightExcerpt(excerpt, query)}</div>
|
|
419
|
-
</div>`;
|
|
420
|
-
}).join('');
|
|
421
|
-
|
|
422
|
-
el.searchResults.querySelectorAll('.result-item').forEach((card) => {
|
|
423
|
-
card.addEventListener('click', () => openSessionPanel(card.dataset.sessionId));
|
|
424
|
-
card.addEventListener('keydown', (e) => { if (e.key === 'Enter') openSessionPanel(card.dataset.sessionId); });
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// ── Sessions ───────────────────────────────────────────────────────────────
|
|
429
|
-
|
|
430
|
-
async function loadSessions() {
|
|
431
|
-
if (state.sessions.loading) return;
|
|
432
|
-
state.sessions.loading = true;
|
|
433
|
-
try {
|
|
434
|
-
const data = await api('/sessions');
|
|
435
|
-
const list = Array.isArray(data) ? data : (data.sessions || []);
|
|
436
|
-
state.sessions.list = list;
|
|
437
|
-
state.stats.sessionCount = list.length;
|
|
438
|
-
updateStats();
|
|
439
|
-
populateProjectFilter(list);
|
|
440
|
-
renderSessions();
|
|
441
|
-
} catch (err) {
|
|
442
|
-
toast(`Failed to load sessions: ${err.message}`, 'error');
|
|
443
|
-
}
|
|
444
|
-
state.sessions.loading = false;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function populateProjectFilter(sessions) {
|
|
448
|
-
const projects = [...new Set(sessions.map((s) => s.project).filter(Boolean))].sort();
|
|
449
|
-
const sel = el.sessionProjectFilter;
|
|
450
|
-
const current = sel.value;
|
|
451
|
-
sel.innerHTML = '<option value="">All projects</option>' +
|
|
452
|
-
projects.map((p) => `<option value="${esc(p)}">${esc(p)}</option>`).join('');
|
|
453
|
-
sel.value = current;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function renderSessions() {
|
|
457
|
-
const filter = state.sessions.projectFilter;
|
|
458
|
-
const list = filter
|
|
459
|
-
? state.sessions.list.filter((s) => s.project === filter)
|
|
460
|
-
: state.sessions.list;
|
|
461
|
-
|
|
462
|
-
if (list.length === 0) {
|
|
463
|
-
el.sessionsList.innerHTML = '';
|
|
464
|
-
el.sessionsEmpty.classList.remove('hidden');
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
el.sessionsEmpty.classList.add('hidden');
|
|
469
|
-
el.sessionsList.innerHTML = list.map((s) => {
|
|
470
|
-
const id = s.sessionId || s.id || '';
|
|
471
|
-
const project = s.project || '';
|
|
472
|
-
const branch = s.branch || s.gitBranch || s.git_branch || '';
|
|
473
|
-
const count = s.messageCount || s.message_count || s.count || 0;
|
|
474
|
-
const date = s.startTime || s.date || s.created || s.startedAt || '';
|
|
475
|
-
const preview = s.preview || '';
|
|
476
|
-
const time = relativeTime(date);
|
|
477
|
-
const dateStr = date ? new Date(date).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : '';
|
|
478
|
-
|
|
479
|
-
return `<div class="session-card" data-session-id="${esc(id)}" tabindex="0" role="button">
|
|
480
|
-
<div class="session-header">
|
|
481
|
-
<span class="session-project">${esc(project)}</span>
|
|
482
|
-
<span class="session-date">${dateStr || time || ''}</span>
|
|
483
|
-
</div>
|
|
484
|
-
<div class="session-meta">
|
|
485
|
-
${branch ? `<span class="session-meta-item"><span class="material-symbols-outlined">alt_route</span>${esc(branch)}</span>` : ''}
|
|
486
|
-
<span class="session-meta-item"><span class="material-symbols-outlined">chat</span>${count} messages</span>
|
|
487
|
-
<span class="session-meta-item"><span class="material-symbols-outlined">tag</span>${esc(id.slice(0, 8))}</span>
|
|
488
|
-
</div>
|
|
489
|
-
${preview ? `<div class="session-preview">${esc(preview)}</div>` : ''}
|
|
490
|
-
</div>`;
|
|
491
|
-
}).join('');
|
|
492
|
-
|
|
493
|
-
el.sessionsList.querySelectorAll('.session-card').forEach((card) => {
|
|
494
|
-
card.addEventListener('click', () => openSessionPanel(card.dataset.sessionId));
|
|
495
|
-
card.addEventListener('keydown', (e) => { if (e.key === 'Enter') openSessionPanel(card.dataset.sessionId); });
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
async function openSessionPanel(sessionId) {
|
|
500
|
-
if (!sessionId) return;
|
|
501
|
-
try {
|
|
502
|
-
const [session, summary] = await Promise.allSettled([
|
|
503
|
-
api(`/sessions/${encodeURIComponent(sessionId)}`),
|
|
504
|
-
api(`/sessions/${encodeURIComponent(sessionId)}/summary`),
|
|
505
|
-
]);
|
|
506
|
-
const sData = session.status === 'fulfilled' ? session.value : {};
|
|
507
|
-
const sumData = summary.status === 'fulfilled' ? summary.value : null;
|
|
508
|
-
openPanel('session', { session: sData, summary: sumData, sessionId });
|
|
509
|
-
} catch (err) {
|
|
510
|
-
toast(`Failed to load session: ${err.message}`, 'error');
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// ── Recall ─────────────────────────────────────────────────────────────────
|
|
515
|
-
|
|
516
|
-
const doRecall = debounce(async () => {
|
|
517
|
-
const q = state.recall.query.trim();
|
|
518
|
-
if (!q) {
|
|
519
|
-
state.recall.results = [];
|
|
520
|
-
renderRecallResults();
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
state.recall.loading = true;
|
|
524
|
-
renderRecallResults();
|
|
525
|
-
try {
|
|
526
|
-
const params = new URLSearchParams({ q });
|
|
527
|
-
if (state.recall.scope !== 'all') params.set('scope', state.recall.scope);
|
|
528
|
-
const data = await api(`/sessions/recall?${params}`);
|
|
529
|
-
state.recall.results = Array.isArray(data) ? data : (data.results || []);
|
|
530
|
-
} catch (err) {
|
|
531
|
-
toast(`Recall failed: ${err.message}`, 'error');
|
|
532
|
-
state.recall.results = [];
|
|
533
|
-
}
|
|
534
|
-
state.recall.loading = false;
|
|
535
|
-
renderRecallResults();
|
|
536
|
-
}, 300);
|
|
537
|
-
|
|
538
|
-
function renderRecallResults() {
|
|
539
|
-
const { results, loading, query } = state.recall;
|
|
540
|
-
|
|
541
|
-
if (loading) {
|
|
542
|
-
el.recallResults.innerHTML = '<div class="loading-inline"><div class="loading-spinner small"></div><span>Searching...</span></div>';
|
|
543
|
-
el.recallEmpty.classList.add('hidden');
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
if (!query.trim()) {
|
|
548
|
-
el.recallResults.innerHTML = '';
|
|
549
|
-
el.recallEmpty.classList.remove('hidden');
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (results.length === 0) {
|
|
554
|
-
el.recallResults.innerHTML = '';
|
|
555
|
-
el.recallEmpty.querySelector('.empty-text').textContent = 'No results found';
|
|
556
|
-
el.recallEmpty.querySelector('.empty-hint').textContent = `No matches for "${query}"`;
|
|
557
|
-
el.recallEmpty.classList.remove('hidden');
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
el.recallEmpty.classList.add('hidden');
|
|
562
|
-
el.recallResults.innerHTML = results.map((r) => {
|
|
563
|
-
const sessionId = r.sessionId || r.session_id || '';
|
|
564
|
-
const excerpt = r.excerpt || r.text || r.content || '';
|
|
565
|
-
const role = r.role || '';
|
|
566
|
-
const score = r.score;
|
|
567
|
-
const scope = r.scope || '';
|
|
568
|
-
const time = relativeTime(r.timestamp || r.date);
|
|
569
|
-
const roleIcon = role === 'user' ? 'person' : role === 'assistant' ? 'smart_toy' : 'chat';
|
|
570
|
-
|
|
571
|
-
return `<div class="result-card" data-session-id="${esc(sessionId)}" tabindex="0" role="button">
|
|
572
|
-
<div class="result-header">
|
|
573
|
-
<span class="material-symbols-outlined result-role-icon">${roleIcon}</span>
|
|
574
|
-
<span class="result-role">${esc(role)}</span>
|
|
575
|
-
${scope ? `<span class="result-scope">${esc(scope)}</span>` : ''}
|
|
576
|
-
${time ? `<span class="result-time">${time}</span>` : ''}
|
|
577
|
-
</div>
|
|
578
|
-
<div class="result-excerpt">${highlightExcerpt(excerpt, query)}</div>
|
|
579
|
-
${score != null ? formatScore(score) : ''}
|
|
580
|
-
</div>`;
|
|
581
|
-
}).join('');
|
|
582
|
-
|
|
583
|
-
el.recallResults.querySelectorAll('.result-card').forEach((card) => {
|
|
584
|
-
card.addEventListener('click', () => openSessionPanel(card.dataset.sessionId));
|
|
585
|
-
card.addEventListener('keydown', (e) => { if (e.key === 'Enter') openSessionPanel(card.dataset.sessionId); });
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// ── Side Panel ─────────────────────────────────────────────────────────────
|
|
590
|
-
|
|
591
|
-
function openPanel(type, data) {
|
|
592
|
-
state.panel = { open: true, type, data };
|
|
593
|
-
el.sidePanel.hidden = false;
|
|
594
|
-
requestAnimationFrame(() => el.sidePanel.classList.add('open'));
|
|
595
|
-
el.contentWrapper.classList.add('panel-visible');
|
|
596
|
-
|
|
597
|
-
if (type === 'knowledge') {
|
|
598
|
-
renderKnowledgePanel(data);
|
|
599
|
-
} else if (type === 'session') {
|
|
600
|
-
renderSessionPanel(data);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function closePanel() {
|
|
605
|
-
state.panel = { open: false, type: null, data: null };
|
|
606
|
-
el.sidePanel.classList.remove('open');
|
|
607
|
-
el.contentWrapper.classList.remove('panel-visible');
|
|
608
|
-
el.sidePanel.addEventListener('transitionend', function handler() {
|
|
609
|
-
if (!state.panel.open) el.sidePanel.hidden = true;
|
|
610
|
-
el.sidePanel.removeEventListener('transitionend', handler);
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function renderKnowledgePanel(data) {
|
|
615
|
-
el.panelTitle.innerHTML = `<span class="material-symbols-outlined panel-icon">article</span>${esc(data.title)}`;
|
|
616
|
-
const meta = data.meta || {};
|
|
617
|
-
const metaHtml = [];
|
|
618
|
-
if (meta.category) metaHtml.push(`<span class="panel-meta-item"><strong>Category:</strong> ${esc(meta.category)}</span>`);
|
|
619
|
-
if (meta.tags && meta.tags.length) metaHtml.push(`<span class="panel-meta-item"><strong>Tags:</strong> ${meta.tags.map((t) => esc(t)).join(', ')}</span>`);
|
|
620
|
-
if (meta.updated || meta.created) metaHtml.push(`<span class="panel-meta-item"><strong>Updated:</strong> ${relativeTime(meta.updated || meta.created)}</span>`);
|
|
621
|
-
|
|
622
|
-
el.panelBody.innerHTML =
|
|
623
|
-
(metaHtml.length ? `<div class="panel-meta">${metaHtml.join('')}</div>` : '') +
|
|
624
|
-
`<div class="panel-markdown">${renderMd(data.content)}</div>`;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function renderSessionPanel(data) {
|
|
628
|
-
const s = data.session || {};
|
|
629
|
-
const summary = data.summary;
|
|
630
|
-
const id = data.sessionId || s.id || '';
|
|
631
|
-
|
|
632
|
-
el.panelTitle.innerHTML = `<span class="material-symbols-outlined panel-icon">terminal</span>Session ${esc(id.slice(0, 12))}...`;
|
|
633
|
-
|
|
634
|
-
const metaParts = [];
|
|
635
|
-
if (s.project) metaParts.push(`<span class="panel-meta-item"><strong>Project:</strong> ${esc(s.project)}</span>`);
|
|
636
|
-
if (s.branch || s.git_branch) metaParts.push(`<span class="panel-meta-item"><strong>Branch:</strong> ${esc(s.branch || s.git_branch)}</span>`);
|
|
637
|
-
if (s.date || s.created || s.startedAt) metaParts.push(`<span class="panel-meta-item"><strong>Date:</strong> ${new Date(s.date || s.created || s.startedAt).toLocaleString()}</span>`);
|
|
638
|
-
|
|
639
|
-
let html = metaParts.length ? `<div class="panel-meta">${metaParts.join('')}</div>` : '';
|
|
640
|
-
|
|
641
|
-
if (summary) {
|
|
642
|
-
const sumText = typeof summary === 'string' ? summary : (summary.summary || summary.text || '');
|
|
643
|
-
if (sumText) {
|
|
644
|
-
html += `<div class="panel-summary"><h4>Summary</h4><div class="panel-markdown">${renderMd(sumText)}</div></div>`;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const messages = s.messages || s.conversation || [];
|
|
649
|
-
if (messages.length) {
|
|
650
|
-
html += '<div class="chat-messages">';
|
|
651
|
-
html += messages.map((m) => {
|
|
652
|
-
const role = m.role || 'unknown';
|
|
653
|
-
const text = m.content || m.text || '';
|
|
654
|
-
const isUser = role === 'user';
|
|
655
|
-
const isAssistant = role === 'assistant';
|
|
656
|
-
const bubbleClass = isUser ? 'chat-bubble user' : isAssistant ? 'chat-bubble assistant' : 'chat-bubble system';
|
|
657
|
-
const roleIcon = isUser ? 'person' : isAssistant ? 'smart_toy' : 'info';
|
|
658
|
-
const truncated = text.length > 2000 ? text.slice(0, 2000) + '...' : text;
|
|
659
|
-
|
|
660
|
-
return `<div class="${bubbleClass}">
|
|
661
|
-
<div class="bubble-header">
|
|
662
|
-
<span class="material-symbols-outlined bubble-icon">${roleIcon}</span>
|
|
663
|
-
<span class="bubble-role">${esc(role)}</span>
|
|
664
|
-
</div>
|
|
665
|
-
<div class="bubble-content">${renderMd(truncated)}</div>
|
|
666
|
-
</div>`;
|
|
667
|
-
}).join('');
|
|
668
|
-
html += '</div>';
|
|
669
|
-
} else {
|
|
670
|
-
html += '<div class="empty-state"><span class="material-symbols-outlined empty-icon">chat_bubble</span><div class="empty-text">No messages available</div></div>';
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
el.panelBody.innerHTML = html;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// ── Event Binding ──────────────────────────────────────────────────────────
|
|
677
|
-
|
|
678
|
-
function bindEvents() {
|
|
679
|
-
// Tabs
|
|
680
|
-
Object.keys(el.tabs).forEach((k) => {
|
|
681
|
-
el.tabs[k].addEventListener('click', () => switchTab(k));
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
// Theme
|
|
685
|
-
el.themeToggle.addEventListener('click', toggleTheme);
|
|
686
|
-
|
|
687
|
-
// Panel close
|
|
688
|
-
el.panelClose.addEventListener('click', closePanel);
|
|
689
|
-
|
|
690
|
-
// Knowledge category chips
|
|
691
|
-
el.knowledgeCategories.addEventListener('click', (e) => {
|
|
692
|
-
const chip = e.target.closest('.chip');
|
|
693
|
-
if (!chip) return;
|
|
694
|
-
el.knowledgeCategories.querySelectorAll('.chip').forEach((c) => c.classList.remove('active'));
|
|
695
|
-
chip.classList.add('active');
|
|
696
|
-
state.knowledge.activeCategory = chip.dataset.category;
|
|
697
|
-
renderKnowledge();
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
// Search input
|
|
701
|
-
el.searchInput.addEventListener('input', () => {
|
|
702
|
-
state.search.query = el.searchInput.value;
|
|
703
|
-
doSearch();
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
// Search role filters
|
|
707
|
-
el.searchRoleFilters.addEventListener('click', (e) => {
|
|
708
|
-
const chip = e.target.closest('.chip');
|
|
709
|
-
if (!chip) return;
|
|
710
|
-
el.searchRoleFilters.querySelectorAll('.chip').forEach((c) => c.classList.remove('active'));
|
|
711
|
-
chip.classList.add('active');
|
|
712
|
-
state.search.role = chip.dataset.role;
|
|
713
|
-
if (state.search.query.trim()) doSearch();
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
// Search mode toggle
|
|
717
|
-
el.modeRanked.addEventListener('click', () => {
|
|
718
|
-
state.search.ranked = true;
|
|
719
|
-
el.modeRanked.classList.add('active');
|
|
720
|
-
el.modeRegex.classList.remove('active');
|
|
721
|
-
if (state.search.query.trim()) doSearch();
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
el.modeRegex.addEventListener('click', () => {
|
|
725
|
-
state.search.ranked = false;
|
|
726
|
-
el.modeRanked.classList.remove('active');
|
|
727
|
-
el.modeRegex.classList.add('active');
|
|
728
|
-
if (state.search.query.trim()) doSearch();
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
// Session project filter
|
|
732
|
-
el.sessionProjectFilter.addEventListener('change', () => {
|
|
733
|
-
state.sessions.projectFilter = el.sessionProjectFilter.value;
|
|
734
|
-
renderSessions();
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
// Recall scope chips
|
|
738
|
-
el.recallScopes.addEventListener('click', (e) => {
|
|
739
|
-
const chip = e.target.closest('.chip');
|
|
740
|
-
if (!chip) return;
|
|
741
|
-
el.recallScopes.querySelectorAll('.chip').forEach((c) => c.classList.remove('active'));
|
|
742
|
-
chip.classList.add('active');
|
|
743
|
-
state.recall.scope = chip.dataset.scope;
|
|
744
|
-
if (state.recall.query.trim()) doRecall();
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
// Recall input
|
|
748
|
-
el.recallInput.addEventListener('input', () => {
|
|
749
|
-
state.recall.query = el.recallInput.value;
|
|
750
|
-
doRecall();
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
// Keyboard shortcuts
|
|
754
|
-
document.addEventListener('keydown', (e) => {
|
|
755
|
-
// Escape closes panel
|
|
756
|
-
if (e.key === 'Escape' && state.panel.open) {
|
|
757
|
-
e.preventDefault();
|
|
758
|
-
closePanel();
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// / or Ctrl+K focuses search
|
|
763
|
-
if ((e.key === '/' || (e.ctrlKey && e.key === 'k')) && !isInputFocused()) {
|
|
764
|
-
e.preventDefault();
|
|
765
|
-
switchTab('search');
|
|
766
|
-
el.searchInput.focus();
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
// Click outside panel to close
|
|
771
|
-
el.contentWrapper.addEventListener('click', (e) => {
|
|
772
|
-
if (state.panel.open && !el.sidePanel.contains(e.target)) {
|
|
773
|
-
// Only close if clicking on the main content area backdrop
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function isInputFocused() {
|
|
779
|
-
const active = document.activeElement;
|
|
780
|
-
if (!active) return false;
|
|
781
|
-
const tag = active.tagName.toLowerCase();
|
|
782
|
-
return tag === 'input' || tag === 'textarea' || tag === 'select' || active.isContentEditable;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// ── Init ───────────────────────────────────────────────────────────────────
|
|
786
|
-
|
|
787
|
-
async function init() {
|
|
788
|
-
initTheme();
|
|
789
|
-
bindEvents();
|
|
790
|
-
wsConnect();
|
|
791
|
-
|
|
792
|
-
// Load initial data in parallel
|
|
793
|
-
try {
|
|
794
|
-
await Promise.allSettled([loadKnowledge(), loadSessions()]);
|
|
795
|
-
} catch {
|
|
796
|
-
// individual loaders handle their own errors
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// Hide loading overlay after a short delay if ws hasn't connected
|
|
800
|
-
setTimeout(() => {
|
|
801
|
-
el.loadingOverlay.classList.add('hidden');
|
|
802
|
-
}, 5000);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// Start
|
|
806
|
-
if (document.readyState === 'loading') {
|
|
807
|
-
document.addEventListener('DOMContentLoaded', init);
|
|
808
|
-
} else {
|
|
809
|
-
init();
|
|
810
|
-
}
|
|
811
|
-
})();
|