@199-bio/engram 0.8.0 → 0.10.0
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/.env.example +5 -0
- package/boba-prompt.md +107 -0
- package/dist/consolidation/consolidator.d.ts.map +1 -1
- package/dist/consolidation/plan.d.ts.map +1 -0
- package/dist/index.js +170 -9
- package/dist/retrieval/hybrid.d.ts.map +1 -1
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/web/chat-handler.d.ts.map +1 -1
- package/nixpacks.toml +11 -0
- package/package.json +2 -1
- package/railway.json +13 -0
- package/src/consolidation/consolidator.ts +381 -29
- package/src/consolidation/plan.ts +444 -0
- package/src/index.ts +181 -10
- package/src/retrieval/hybrid.ts +69 -5
- package/src/storage/database.ts +358 -38
- package/src/transport/http.ts +111 -0
- package/src/transport/index.ts +24 -0
- package/src/web/chat-handler.ts +116 -70
- package/src/web/static/app.js +612 -360
- package/src/web/static/index.html +377 -130
- package/src/web/static/style.css +1249 -672
package/src/web/static/app.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Engram Web Interface
|
|
3
|
-
*
|
|
3
|
+
* Modern vanilla JavaScript - no build step required
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const API_BASE = '';
|
|
7
7
|
|
|
8
|
-
// State
|
|
8
|
+
// ============ State ============
|
|
9
9
|
let currentView = 'memories';
|
|
10
10
|
let editingMemoryId = null;
|
|
11
11
|
let memoriesOffset = 0;
|
|
12
12
|
const MEMORIES_PAGE_SIZE = 25;
|
|
13
13
|
|
|
14
|
-
// DOM Elements
|
|
14
|
+
// ============ DOM Elements ============
|
|
15
15
|
const views = {
|
|
16
16
|
memories: document.getElementById('memories-view'),
|
|
17
17
|
entities: document.getElementById('entities-view'),
|
|
@@ -20,11 +20,34 @@ const views = {
|
|
|
20
20
|
settings: document.getElementById('settings-view'),
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const viewTitles = {
|
|
24
|
+
memories: 'Memories',
|
|
25
|
+
entities: 'Entities',
|
|
26
|
+
graph: 'Knowledge Graph',
|
|
27
|
+
consolidation: 'Consolidation',
|
|
28
|
+
settings: 'Settings',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Sidebar elements
|
|
32
|
+
const sidebar = document.getElementById('sidebar');
|
|
33
|
+
const sidebarToggle = document.getElementById('sidebar-toggle');
|
|
34
|
+
|
|
35
|
+
// Stats elements
|
|
36
|
+
const statMemories = document.getElementById('stat-memories');
|
|
37
|
+
const statEntities = document.getElementById('stat-entities');
|
|
38
|
+
const statRelations = document.getElementById('stat-relations');
|
|
39
|
+
|
|
40
|
+
// Header elements
|
|
41
|
+
const viewTitle = document.getElementById('view-title');
|
|
42
|
+
const searchInput = document.getElementById('search-input');
|
|
43
|
+
const headerSearch = document.getElementById('header-search');
|
|
44
|
+
|
|
45
|
+
// List containers
|
|
24
46
|
const memoriesList = document.getElementById('memories-list');
|
|
25
47
|
const entitiesList = document.getElementById('entities-list');
|
|
26
48
|
const graphContainer = document.getElementById('graph-container');
|
|
27
|
-
|
|
49
|
+
|
|
50
|
+
// Filters
|
|
28
51
|
const entityTypeFilter = document.getElementById('entity-type-filter');
|
|
29
52
|
|
|
30
53
|
// Modal elements
|
|
@@ -40,7 +63,75 @@ const entityModal = document.getElementById('entity-modal');
|
|
|
40
63
|
const entityModalTitle = document.getElementById('entity-modal-title');
|
|
41
64
|
const entityModalBody = document.getElementById('entity-modal-body');
|
|
42
65
|
|
|
43
|
-
//
|
|
66
|
+
// Theme elements
|
|
67
|
+
const themeToggle = document.getElementById('theme-toggle');
|
|
68
|
+
const themeSelect = document.getElementById('theme-select');
|
|
69
|
+
|
|
70
|
+
// ============ Theme Management ============
|
|
71
|
+
function initTheme() {
|
|
72
|
+
const savedTheme = localStorage.getItem('engram-theme') || 'system';
|
|
73
|
+
applyTheme(savedTheme);
|
|
74
|
+
if (themeSelect) {
|
|
75
|
+
themeSelect.value = savedTheme;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function applyTheme(theme) {
|
|
80
|
+
if (theme === 'system') {
|
|
81
|
+
document.documentElement.removeAttribute('data-theme');
|
|
82
|
+
} else {
|
|
83
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
84
|
+
}
|
|
85
|
+
localStorage.setItem('engram-theme', theme);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toggleTheme() {
|
|
89
|
+
const current = document.documentElement.getAttribute('data-theme');
|
|
90
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
91
|
+
|
|
92
|
+
// Determine current effective theme
|
|
93
|
+
let effectiveDark = current === 'dark' || (current !== 'light' && prefersDark);
|
|
94
|
+
|
|
95
|
+
// Toggle to opposite
|
|
96
|
+
const newTheme = effectiveDark ? 'light' : 'dark';
|
|
97
|
+
applyTheme(newTheme);
|
|
98
|
+
|
|
99
|
+
if (themeSelect) {
|
|
100
|
+
themeSelect.value = newTheme;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Theme event listeners
|
|
105
|
+
if (themeToggle) {
|
|
106
|
+
themeToggle.addEventListener('click', toggleTheme);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (themeSelect) {
|
|
110
|
+
themeSelect.addEventListener('change', (e) => {
|
|
111
|
+
applyTheme(e.target.value);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============ Sidebar Management ============
|
|
116
|
+
function initSidebar() {
|
|
117
|
+
const collapsed = localStorage.getItem('engram-sidebar-collapsed') === 'true';
|
|
118
|
+
if (collapsed) {
|
|
119
|
+
sidebar.classList.add('collapsed');
|
|
120
|
+
document.body.classList.add('sidebar-collapsed');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toggleSidebar() {
|
|
125
|
+
sidebar.classList.toggle('collapsed');
|
|
126
|
+
document.body.classList.toggle('sidebar-collapsed');
|
|
127
|
+
localStorage.setItem('engram-sidebar-collapsed', sidebar.classList.contains('collapsed'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (sidebarToggle) {
|
|
131
|
+
sidebarToggle.addEventListener('click', toggleSidebar);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============ API Helper ============
|
|
44
135
|
async function api(path, options = {}) {
|
|
45
136
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
46
137
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -50,7 +141,7 @@ async function api(path, options = {}) {
|
|
|
50
141
|
return res.json();
|
|
51
142
|
}
|
|
52
143
|
|
|
53
|
-
//
|
|
144
|
+
// ============ Utilities ============
|
|
54
145
|
function formatDate(dateStr) {
|
|
55
146
|
const date = new Date(dateStr);
|
|
56
147
|
return date.toLocaleDateString('en-GB', {
|
|
@@ -62,20 +153,38 @@ function formatDate(dateStr) {
|
|
|
62
153
|
});
|
|
63
154
|
}
|
|
64
155
|
|
|
65
|
-
|
|
156
|
+
function escapeHtml(str) {
|
|
157
|
+
if (!str) return '';
|
|
158
|
+
return str
|
|
159
|
+
.replace(/&/g, '&')
|
|
160
|
+
.replace(/</g, '<')
|
|
161
|
+
.replace(/>/g, '>')
|
|
162
|
+
.replace(/"/g, '"')
|
|
163
|
+
.replace(/'/g, ''');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============ Stats ============
|
|
66
167
|
async function loadStats() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
168
|
+
try {
|
|
169
|
+
const stats = await api('/api/stats');
|
|
170
|
+
if (statMemories) statMemories.textContent = stats.memories.toLocaleString();
|
|
171
|
+
if (statEntities) statEntities.textContent = stats.entities.toLocaleString();
|
|
172
|
+
if (statRelations) statRelations.textContent = stats.relations.toLocaleString();
|
|
173
|
+
|
|
174
|
+
// Update consolidation badge
|
|
175
|
+
const badge = document.getElementById('consolidation-badge');
|
|
176
|
+
if (badge && stats.contradictions > 0) {
|
|
177
|
+
badge.textContent = stats.contradictions;
|
|
178
|
+
badge.style.display = 'inline-flex';
|
|
179
|
+
} else if (badge) {
|
|
180
|
+
badge.style.display = 'none';
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error('Failed to load stats', e);
|
|
74
184
|
}
|
|
75
|
-
statsEl.textContent = text;
|
|
76
185
|
}
|
|
77
186
|
|
|
78
|
-
//
|
|
187
|
+
// ============ Memories ============
|
|
79
188
|
async function loadMemories(query = '', append = false) {
|
|
80
189
|
if (!append) {
|
|
81
190
|
memoriesOffset = 0;
|
|
@@ -92,7 +201,16 @@ async function loadMemories(query = '', append = false) {
|
|
|
92
201
|
const data = await api(path);
|
|
93
202
|
|
|
94
203
|
if (data.memories.length === 0 && !append) {
|
|
95
|
-
memoriesList.innerHTML =
|
|
204
|
+
memoriesList.innerHTML = `
|
|
205
|
+
<div class="empty-state">
|
|
206
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
207
|
+
<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/>
|
|
208
|
+
<path d="M3.29 7L12 12l8.71-5M12 22V12"/>
|
|
209
|
+
</svg>
|
|
210
|
+
<h3>No memories found</h3>
|
|
211
|
+
<p>${query ? 'Try a different search term' : 'Add your first memory to get started'}</p>
|
|
212
|
+
</div>
|
|
213
|
+
`;
|
|
96
214
|
return;
|
|
97
215
|
}
|
|
98
216
|
|
|
@@ -100,20 +218,41 @@ async function loadMemories(query = '', append = false) {
|
|
|
100
218
|
<div class="list-item memory-item" data-id="${m.id}">
|
|
101
219
|
<div class="content">${escapeHtml(m.content)}</div>
|
|
102
220
|
<div class="meta">
|
|
103
|
-
<span
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
221
|
+
<span class="meta-item">
|
|
222
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
223
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
|
224
|
+
<path d="M16 2v4M8 2v4M3 10h18"/>
|
|
225
|
+
</svg>
|
|
226
|
+
${formatDate(m.timestamp)}
|
|
227
|
+
</span>
|
|
228
|
+
<span class="meta-item">
|
|
229
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
230
|
+
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/>
|
|
231
|
+
<circle cx="12" cy="7" r="4"/>
|
|
232
|
+
</svg>
|
|
233
|
+
${escapeHtml(m.source)}
|
|
234
|
+
</span>
|
|
235
|
+
${m.score ? `<span class="score-badge">${m.score.toFixed(4)}</span>` : ''}
|
|
107
236
|
</div>
|
|
108
237
|
<div class="actions">
|
|
109
|
-
<button class="edit-btn" data-id="${m.id}">
|
|
110
|
-
|
|
238
|
+
<button class="btn btn-sm btn-secondary edit-btn" data-id="${m.id}">
|
|
239
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
240
|
+
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
|
|
241
|
+
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
242
|
+
</svg>
|
|
243
|
+
Edit
|
|
244
|
+
</button>
|
|
245
|
+
<button class="btn btn-sm btn-ghost delete-btn" data-id="${m.id}">
|
|
246
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
247
|
+
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
248
|
+
</svg>
|
|
249
|
+
Delete
|
|
250
|
+
</button>
|
|
111
251
|
</div>
|
|
112
252
|
</div>
|
|
113
253
|
`).join('');
|
|
114
254
|
|
|
115
255
|
if (append) {
|
|
116
|
-
// Remove old "Load More" button if exists
|
|
117
256
|
const oldLoadMore = memoriesList.querySelector('.load-more-container');
|
|
118
257
|
if (oldLoadMore) oldLoadMore.remove();
|
|
119
258
|
memoriesList.insertAdjacentHTML('beforeend', memoriesHtml);
|
|
@@ -121,11 +260,11 @@ async function loadMemories(query = '', append = false) {
|
|
|
121
260
|
memoriesList.innerHTML = memoriesHtml;
|
|
122
261
|
}
|
|
123
262
|
|
|
124
|
-
// Add "Load More" button
|
|
263
|
+
// Add "Load More" button
|
|
125
264
|
if (!query && data.memories.length === MEMORIES_PAGE_SIZE) {
|
|
126
265
|
memoriesList.insertAdjacentHTML('beforeend', `
|
|
127
266
|
<div class="load-more-container">
|
|
128
|
-
<button class="load-more-btn">Load More</button>
|
|
267
|
+
<button class="btn btn-secondary load-more-btn">Load More</button>
|
|
129
268
|
</div>
|
|
130
269
|
`);
|
|
131
270
|
memoriesList.querySelector('.load-more-btn').addEventListener('click', () => {
|
|
@@ -134,7 +273,7 @@ async function loadMemories(query = '', append = false) {
|
|
|
134
273
|
});
|
|
135
274
|
}
|
|
136
275
|
|
|
137
|
-
// Attach event listeners
|
|
276
|
+
// Attach event listeners
|
|
138
277
|
memoriesList.querySelectorAll('.edit-btn').forEach(btn => {
|
|
139
278
|
btn.addEventListener('click', (e) => {
|
|
140
279
|
e.stopPropagation();
|
|
@@ -150,24 +289,36 @@ async function loadMemories(query = '', append = false) {
|
|
|
150
289
|
});
|
|
151
290
|
}
|
|
152
291
|
|
|
153
|
-
//
|
|
292
|
+
// ============ Entities ============
|
|
154
293
|
async function loadEntities(type = '') {
|
|
155
294
|
const path = type ? `/api/entities?type=${type}` : '/api/entities';
|
|
156
295
|
const data = await api(path);
|
|
157
296
|
|
|
158
297
|
if (data.entities.length === 0) {
|
|
159
|
-
entitiesList.innerHTML =
|
|
298
|
+
entitiesList.innerHTML = `
|
|
299
|
+
<div class="empty-state">
|
|
300
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
301
|
+
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
|
|
302
|
+
<circle cx="9" cy="7" r="4"/>
|
|
303
|
+
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>
|
|
304
|
+
</svg>
|
|
305
|
+
<h3>No entities found</h3>
|
|
306
|
+
<p>Entities are extracted automatically from memories</p>
|
|
307
|
+
</div>
|
|
308
|
+
`;
|
|
160
309
|
return;
|
|
161
310
|
}
|
|
162
311
|
|
|
163
312
|
entitiesList.innerHTML = data.entities.map(e => `
|
|
164
|
-
<div class="list-item entity-item" data-name="${escapeHtml(e.name)}">
|
|
165
|
-
<div class="name">${escapeHtml(e.name)}</div>
|
|
166
|
-
<div class="type"
|
|
313
|
+
<div class="list-item entity-item clickable" data-name="${escapeHtml(e.name)}">
|
|
314
|
+
<div class="entity-name">${escapeHtml(e.name)}</div>
|
|
315
|
+
<div class="entity-type">
|
|
316
|
+
${getEntityIcon(e.type)}
|
|
317
|
+
${e.type}
|
|
318
|
+
</div>
|
|
167
319
|
</div>
|
|
168
320
|
`).join('');
|
|
169
321
|
|
|
170
|
-
// Attach event listeners
|
|
171
322
|
entitiesList.querySelectorAll('.entity-item').forEach(item => {
|
|
172
323
|
item.addEventListener('click', () => {
|
|
173
324
|
showEntityDetails(item.dataset.name);
|
|
@@ -175,13 +326,29 @@ async function loadEntities(type = '') {
|
|
|
175
326
|
});
|
|
176
327
|
}
|
|
177
328
|
|
|
178
|
-
|
|
329
|
+
function getEntityIcon(type) {
|
|
330
|
+
const icons = {
|
|
331
|
+
person: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
|
|
332
|
+
organization: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M9 8h1M9 12h1M9 16h1M14 8h1M14 12h1M14 16h1M5 21V5a2 2 0 012-2h10a2 2 0 012 2v16"/></svg>',
|
|
333
|
+
place: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>',
|
|
334
|
+
concept: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3M12 17h.01"/></svg>',
|
|
335
|
+
event: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>',
|
|
336
|
+
};
|
|
337
|
+
return icons[type] || icons.concept;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ============ Entity Details ============
|
|
179
341
|
async function showEntityDetails(name) {
|
|
180
342
|
const data = await api(`/api/entities/${encodeURIComponent(name)}`);
|
|
181
343
|
|
|
182
344
|
entityModalTitle.textContent = data.name;
|
|
183
345
|
|
|
184
|
-
let html =
|
|
346
|
+
let html = `
|
|
347
|
+
<div class="entity-type">
|
|
348
|
+
${getEntityIcon(data.type)}
|
|
349
|
+
${data.type}
|
|
350
|
+
</div>
|
|
351
|
+
`;
|
|
185
352
|
|
|
186
353
|
if (data.observations && data.observations.length > 0) {
|
|
187
354
|
html += `<h3>Observations</h3><ul>`;
|
|
@@ -194,7 +361,7 @@ async function showEntityDetails(name) {
|
|
|
194
361
|
if (data.relationsFrom && data.relationsFrom.length > 0) {
|
|
195
362
|
html += `<h3>Relationships (outgoing)</h3><ul>`;
|
|
196
363
|
data.relationsFrom.forEach(r => {
|
|
197
|
-
html += `<li>${r.type}
|
|
364
|
+
html += `<li><span class="text-accent">${r.type}</span> → ${escapeHtml(r.targetEntity?.name || r.to)}</li>`;
|
|
198
365
|
});
|
|
199
366
|
html += `</ul>`;
|
|
200
367
|
}
|
|
@@ -202,7 +369,7 @@ async function showEntityDetails(name) {
|
|
|
202
369
|
if (data.relationsTo && data.relationsTo.length > 0) {
|
|
203
370
|
html += `<h3>Relationships (incoming)</h3><ul>`;
|
|
204
371
|
data.relationsTo.forEach(r => {
|
|
205
|
-
html += `<li>${escapeHtml(r.sourceEntity?.name || r.from)}
|
|
372
|
+
html += `<li>${escapeHtml(r.sourceEntity?.name || r.from)} → <span class="text-accent">${r.type}</span></li>`;
|
|
206
373
|
});
|
|
207
374
|
html += `</ul>`;
|
|
208
375
|
}
|
|
@@ -211,55 +378,73 @@ async function showEntityDetails(name) {
|
|
|
211
378
|
entityModal.classList.remove('hidden');
|
|
212
379
|
}
|
|
213
380
|
|
|
214
|
-
//
|
|
381
|
+
// ============ Graph ============
|
|
215
382
|
async function loadGraph() {
|
|
216
383
|
const data = await api('/api/graph');
|
|
217
384
|
|
|
218
|
-
// Simple visualization using CSS
|
|
219
385
|
if (data.nodes.length === 0) {
|
|
220
|
-
graphContainer.innerHTML =
|
|
386
|
+
graphContainer.innerHTML = `
|
|
387
|
+
<div class="empty-state">
|
|
388
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
389
|
+
<circle cx="18" cy="5" r="3"/>
|
|
390
|
+
<circle cx="6" cy="12" r="3"/>
|
|
391
|
+
<circle cx="18" cy="19" r="3"/>
|
|
392
|
+
<path d="M8.59 13.51l6.82 3.98M15.41 6.51l-6.82 3.98"/>
|
|
393
|
+
</svg>
|
|
394
|
+
<h3>No graph data</h3>
|
|
395
|
+
<p>Add memories with entities to build your knowledge graph</p>
|
|
396
|
+
</div>
|
|
397
|
+
`;
|
|
221
398
|
return;
|
|
222
399
|
}
|
|
223
400
|
|
|
224
|
-
//
|
|
225
|
-
let html = '<div style="padding: 2rem; font-size: 0.875rem;">';
|
|
226
|
-
html += '<p style="margin-bottom: 1rem; color: var(--text-muted);">Knowledge graph visualization. Click entities to see details.</p>';
|
|
227
|
-
|
|
228
|
-
// Group by type
|
|
401
|
+
// Group nodes by type
|
|
229
402
|
const byType = {};
|
|
230
403
|
data.nodes.forEach(n => {
|
|
231
404
|
if (!byType[n.type]) byType[n.type] = [];
|
|
232
405
|
byType[n.type].push(n);
|
|
233
406
|
});
|
|
234
407
|
|
|
408
|
+
let html = '<div style="padding: 24px;">';
|
|
409
|
+
html += '<p class="text-muted" style="margin-bottom: 24px;">Knowledge graph visualization. Click entities to see details.</p>';
|
|
410
|
+
|
|
235
411
|
for (const [type, nodes] of Object.entries(byType)) {
|
|
236
|
-
html +=
|
|
237
|
-
|
|
238
|
-
|
|
412
|
+
html += `
|
|
413
|
+
<div style="margin-bottom: 24px;">
|
|
414
|
+
<h3 style="font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-tertiary); margin-bottom: 12px;">${type}</h3>
|
|
415
|
+
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
|
416
|
+
`;
|
|
239
417
|
nodes.forEach(n => {
|
|
240
|
-
html += `<
|
|
418
|
+
html += `<button class="btn btn-sm btn-secondary graph-node" data-name="${escapeHtml(n.label)}">${escapeHtml(n.label)}</button>`;
|
|
241
419
|
});
|
|
242
|
-
html +=
|
|
420
|
+
html += '</div></div>';
|
|
243
421
|
}
|
|
244
422
|
|
|
245
423
|
if (data.edges.length > 0) {
|
|
246
|
-
html +=
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
424
|
+
html += `
|
|
425
|
+
<div style="margin-top: 32px;">
|
|
426
|
+
<h3 style="font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-tertiary); margin-bottom: 12px;">Relationships</h3>
|
|
427
|
+
<div class="list">
|
|
428
|
+
`;
|
|
429
|
+
data.edges.slice(0, 50).forEach(e => {
|
|
250
430
|
const fromNode = data.nodes.find(n => n.id === e.from);
|
|
251
431
|
const toNode = data.nodes.find(n => n.id === e.to);
|
|
252
432
|
if (fromNode && toNode) {
|
|
253
|
-
html +=
|
|
433
|
+
html += `
|
|
434
|
+
<div class="list-item" style="padding: 12px 16px;">
|
|
435
|
+
<span>${escapeHtml(fromNode.label)}</span>
|
|
436
|
+
<span class="text-accent" style="margin: 0 8px;">→ ${e.label} →</span>
|
|
437
|
+
<span>${escapeHtml(toNode.label)}</span>
|
|
438
|
+
</div>
|
|
439
|
+
`;
|
|
254
440
|
}
|
|
255
441
|
});
|
|
256
|
-
html +=
|
|
442
|
+
html += '</div></div>';
|
|
257
443
|
}
|
|
258
444
|
|
|
259
445
|
html += '</div>';
|
|
260
446
|
graphContainer.innerHTML = html;
|
|
261
447
|
|
|
262
|
-
// Attach click handlers
|
|
263
448
|
graphContainer.querySelectorAll('.graph-node').forEach(node => {
|
|
264
449
|
node.addEventListener('click', () => {
|
|
265
450
|
showEntityDetails(node.dataset.name);
|
|
@@ -267,7 +452,7 @@ async function loadGraph() {
|
|
|
267
452
|
});
|
|
268
453
|
}
|
|
269
454
|
|
|
270
|
-
//
|
|
455
|
+
// ============ Memory CRUD ============
|
|
271
456
|
async function editMemory(id) {
|
|
272
457
|
const data = await api('/api/memories');
|
|
273
458
|
const memory = data.memories.find(m => m.id === id);
|
|
@@ -282,16 +467,14 @@ async function editMemory(id) {
|
|
|
282
467
|
modal.classList.remove('hidden');
|
|
283
468
|
}
|
|
284
469
|
|
|
285
|
-
// Delete memory
|
|
286
470
|
async function deleteMemory(id) {
|
|
287
|
-
if (!confirm('Delete this memory?')) return;
|
|
471
|
+
if (!confirm('Delete this memory? This action cannot be undone.')) return;
|
|
288
472
|
|
|
289
473
|
await api(`/api/memories/${id}`, { method: 'DELETE' });
|
|
290
474
|
await loadMemories(searchInput.value);
|
|
291
475
|
await loadStats();
|
|
292
476
|
}
|
|
293
477
|
|
|
294
|
-
// Save memory
|
|
295
478
|
async function saveMemory() {
|
|
296
479
|
const content = modalContentInput.value.trim();
|
|
297
480
|
if (!content) return;
|
|
@@ -313,7 +496,6 @@ async function saveMemory() {
|
|
|
313
496
|
await loadStats();
|
|
314
497
|
}
|
|
315
498
|
|
|
316
|
-
// Close modal
|
|
317
499
|
function closeModal() {
|
|
318
500
|
modal.classList.add('hidden');
|
|
319
501
|
editingMemoryId = null;
|
|
@@ -323,41 +505,37 @@ function closeModal() {
|
|
|
323
505
|
importanceValue.textContent = '0.5';
|
|
324
506
|
}
|
|
325
507
|
|
|
326
|
-
//
|
|
327
|
-
function escapeHtml(str) {
|
|
328
|
-
if (!str) return '';
|
|
329
|
-
return str
|
|
330
|
-
.replace(/&/g, '&')
|
|
331
|
-
.replace(/</g, '<')
|
|
332
|
-
.replace(/>/g, '>')
|
|
333
|
-
.replace(/"/g, '"')
|
|
334
|
-
.replace(/'/g, ''');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Switch view
|
|
508
|
+
// ============ View Switching ============
|
|
338
509
|
function switchView(view) {
|
|
339
510
|
currentView = view;
|
|
340
511
|
|
|
341
|
-
// Update nav
|
|
342
|
-
document.querySelectorAll('.nav-
|
|
512
|
+
// Update nav items
|
|
513
|
+
document.querySelectorAll('.nav-item').forEach(btn => {
|
|
343
514
|
btn.classList.toggle('active', btn.dataset.view === view);
|
|
344
515
|
});
|
|
345
516
|
|
|
346
517
|
// Update views
|
|
347
518
|
Object.entries(views).forEach(([name, el]) => {
|
|
348
|
-
el.classList.toggle('active', name === view);
|
|
519
|
+
if (el) el.classList.toggle('active', name === view);
|
|
349
520
|
});
|
|
350
521
|
|
|
351
|
-
//
|
|
352
|
-
if (view
|
|
353
|
-
|
|
522
|
+
// Update header
|
|
523
|
+
if (viewTitle) viewTitle.textContent = viewTitles[view] || view;
|
|
524
|
+
|
|
525
|
+
// Show/hide search based on view
|
|
526
|
+
if (headerSearch) {
|
|
527
|
+
headerSearch.style.display = view === 'memories' ? 'block' : 'none';
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Load data
|
|
531
|
+
if (view === 'memories') loadMemories(searchInput?.value || '');
|
|
532
|
+
if (view === 'entities') loadEntities(entityTypeFilter?.value || '');
|
|
354
533
|
if (view === 'graph') loadGraph();
|
|
355
534
|
if (view === 'consolidation') loadConsolidation();
|
|
356
535
|
if (view === 'settings') loadSettings();
|
|
357
536
|
}
|
|
358
537
|
|
|
359
538
|
// ============ Consolidation ============
|
|
360
|
-
|
|
361
539
|
const contradictionsList = document.getElementById('contradictions-list');
|
|
362
540
|
const digestsList = document.getElementById('digests-list');
|
|
363
541
|
const unconsolidatedCount = document.getElementById('unconsolidated-count');
|
|
@@ -371,7 +549,6 @@ const contradictionResolution = document.getElementById('contradiction-resolutio
|
|
|
371
549
|
|
|
372
550
|
let currentContradictionId = null;
|
|
373
551
|
|
|
374
|
-
// Load consolidation view
|
|
375
552
|
async function loadConsolidation() {
|
|
376
553
|
await Promise.all([
|
|
377
554
|
loadConsolidationStatus(),
|
|
@@ -380,29 +557,39 @@ async function loadConsolidation() {
|
|
|
380
557
|
]);
|
|
381
558
|
}
|
|
382
559
|
|
|
383
|
-
// Load consolidation status
|
|
384
560
|
async function loadConsolidationStatus() {
|
|
385
561
|
try {
|
|
386
562
|
const status = await api('/api/consolidation/status');
|
|
387
|
-
unconsolidatedCount.textContent = status.unconsolidatedMemories;
|
|
388
|
-
digestsCount.textContent = status.totalDigests;
|
|
389
|
-
contradictionsCount.textContent = status.unresolvedContradictions;
|
|
390
|
-
runConsolidationBtn
|
|
391
|
-
|
|
392
|
-
|
|
563
|
+
if (unconsolidatedCount) unconsolidatedCount.textContent = status.unconsolidatedMemories;
|
|
564
|
+
if (digestsCount) digestsCount.textContent = status.totalDigests;
|
|
565
|
+
if (contradictionsCount) contradictionsCount.textContent = status.unresolvedContradictions;
|
|
566
|
+
if (runConsolidationBtn) {
|
|
567
|
+
runConsolidationBtn.disabled = !status.configured;
|
|
568
|
+
if (!status.configured) {
|
|
569
|
+
runConsolidationBtn.title = 'Configure ANTHROPIC_API_KEY to enable';
|
|
570
|
+
}
|
|
393
571
|
}
|
|
394
572
|
} catch (e) {
|
|
395
573
|
console.error('Failed to load consolidation status', e);
|
|
396
574
|
}
|
|
397
575
|
}
|
|
398
576
|
|
|
399
|
-
// Load contradictions
|
|
400
577
|
async function loadContradictions() {
|
|
578
|
+
if (!contradictionsList) return;
|
|
579
|
+
|
|
401
580
|
try {
|
|
402
581
|
const data = await api('/api/contradictions?resolved=false');
|
|
403
582
|
|
|
404
583
|
if (data.contradictions.length === 0) {
|
|
405
|
-
contradictionsList.innerHTML =
|
|
584
|
+
contradictionsList.innerHTML = `
|
|
585
|
+
<div class="empty-state">
|
|
586
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
587
|
+
<path d="M20 6L9 17l-5-5"/>
|
|
588
|
+
</svg>
|
|
589
|
+
<h3>No contradictions</h3>
|
|
590
|
+
<p>All conflicts have been resolved</p>
|
|
591
|
+
</div>
|
|
592
|
+
`;
|
|
406
593
|
return;
|
|
407
594
|
}
|
|
408
595
|
|
|
@@ -410,23 +597,25 @@ async function loadContradictions() {
|
|
|
410
597
|
<div class="list-item contradiction-item" data-id="${c.id}">
|
|
411
598
|
${c.entity ? `<span class="entity-tag">${escapeHtml(c.entity.name)}</span>` : ''}
|
|
412
599
|
<div class="description">${escapeHtml(c.description)}</div>
|
|
413
|
-
<div class="
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
<span class="date">${c.memory_a ? formatDate(c.memory_a.timestamp) : ''}</span>
|
|
417
|
-
</div>
|
|
418
|
-
<div class="memory-quote">
|
|
419
|
-
${escapeHtml(c.memory_b?.content || 'Memory deleted')}
|
|
420
|
-
<span class="date">${c.memory_b ? formatDate(c.memory_b.timestamp) : ''}</span>
|
|
421
|
-
</div>
|
|
600
|
+
<div class="memory-quote">
|
|
601
|
+
${escapeHtml(c.memory_a?.content || 'Memory deleted')}
|
|
602
|
+
<span class="date">${c.memory_a ? formatDate(c.memory_a.timestamp) : ''}</span>
|
|
422
603
|
</div>
|
|
423
|
-
<div class="
|
|
424
|
-
|
|
604
|
+
<div class="memory-quote">
|
|
605
|
+
${escapeHtml(c.memory_b?.content || 'Memory deleted')}
|
|
606
|
+
<span class="date">${c.memory_b ? formatDate(c.memory_b.timestamp) : ''}</span>
|
|
607
|
+
</div>
|
|
608
|
+
<div class="actions" style="margin-top: 16px;">
|
|
609
|
+
<button class="btn btn-sm btn-primary resolve-btn" data-id="${c.id}">
|
|
610
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
611
|
+
<path d="M20 6L9 17l-5-5"/>
|
|
612
|
+
</svg>
|
|
613
|
+
Resolve
|
|
614
|
+
</button>
|
|
425
615
|
</div>
|
|
426
616
|
</div>
|
|
427
617
|
`).join('');
|
|
428
618
|
|
|
429
|
-
// Attach event listeners
|
|
430
619
|
contradictionsList.querySelectorAll('.resolve-btn').forEach(btn => {
|
|
431
620
|
btn.addEventListener('click', (e) => {
|
|
432
621
|
e.stopPropagation();
|
|
@@ -435,21 +624,31 @@ async function loadContradictions() {
|
|
|
435
624
|
});
|
|
436
625
|
} catch (e) {
|
|
437
626
|
console.error('Failed to load contradictions', e);
|
|
438
|
-
contradictionsList.innerHTML = '<div class="empty-state">Failed to load contradictions</div>';
|
|
627
|
+
contradictionsList.innerHTML = '<div class="empty-state"><p>Failed to load contradictions</p></div>';
|
|
439
628
|
}
|
|
440
629
|
}
|
|
441
630
|
|
|
442
|
-
// Load digests with hierarchy visualization
|
|
443
631
|
async function loadDigests() {
|
|
632
|
+
if (!digestsList) return;
|
|
633
|
+
|
|
444
634
|
try {
|
|
445
635
|
const data = await api('/api/digests');
|
|
446
636
|
|
|
447
637
|
if (data.digests.length === 0) {
|
|
448
|
-
digestsList.innerHTML =
|
|
638
|
+
digestsList.innerHTML = `
|
|
639
|
+
<div class="empty-state">
|
|
640
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
641
|
+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
|
|
642
|
+
<path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/>
|
|
643
|
+
</svg>
|
|
644
|
+
<h3>No digests yet</h3>
|
|
645
|
+
<p>Run consolidation to create summaries</p>
|
|
646
|
+
</div>
|
|
647
|
+
`;
|
|
449
648
|
return;
|
|
450
649
|
}
|
|
451
650
|
|
|
452
|
-
// Group
|
|
651
|
+
// Group by level
|
|
453
652
|
const byLevel = { 1: [], 2: [], 3: [] };
|
|
454
653
|
data.digests.forEach(d => {
|
|
455
654
|
const level = d.level || 1;
|
|
@@ -457,12 +656,7 @@ async function loadDigests() {
|
|
|
457
656
|
byLevel[level].push(d);
|
|
458
657
|
});
|
|
459
658
|
|
|
460
|
-
const levelLabels = {
|
|
461
|
-
1: 'Session Summaries',
|
|
462
|
-
2: 'Topic Digests',
|
|
463
|
-
3: 'Entity Profiles'
|
|
464
|
-
};
|
|
465
|
-
|
|
659
|
+
const levelLabels = { 1: 'Session Summaries', 2: 'Topic Digests', 3: 'Entity Profiles' };
|
|
466
660
|
const levelDescs = {
|
|
467
661
|
1: 'Summaries of individual conversations',
|
|
468
662
|
2: 'Consolidated topic-based knowledge',
|
|
@@ -470,18 +664,18 @@ async function loadDigests() {
|
|
|
470
664
|
};
|
|
471
665
|
|
|
472
666
|
let html = '';
|
|
473
|
-
for (const level of [3, 2, 1]) {
|
|
667
|
+
for (const level of [3, 2, 1]) {
|
|
474
668
|
const digests = byLevel[level];
|
|
475
669
|
if (digests.length === 0) continue;
|
|
476
670
|
|
|
477
671
|
html += `
|
|
478
672
|
<div class="digest-level">
|
|
479
|
-
<
|
|
673
|
+
<div class="level-header">
|
|
480
674
|
<span class="level-badge">L${level}</span>
|
|
481
|
-
|
|
675
|
+
<span class="level-title">${levelLabels[level]}</span>
|
|
482
676
|
<span class="level-count">(${digests.length})</span>
|
|
483
|
-
</
|
|
484
|
-
<p class="
|
|
677
|
+
</div>
|
|
678
|
+
<p class="section-desc" style="margin-bottom: 16px;">${levelDescs[level]}</p>
|
|
485
679
|
<div class="digest-list">
|
|
486
680
|
`;
|
|
487
681
|
|
|
@@ -504,56 +698,65 @@ async function loadDigests() {
|
|
|
504
698
|
digestsList.innerHTML = html;
|
|
505
699
|
} catch (e) {
|
|
506
700
|
console.error('Failed to load digests', e);
|
|
507
|
-
digestsList.innerHTML = '<div class="empty-state">Failed to load digests</div>';
|
|
701
|
+
digestsList.innerHTML = '<div class="empty-state"><p>Failed to load digests</p></div>';
|
|
508
702
|
}
|
|
509
703
|
}
|
|
510
704
|
|
|
511
|
-
// Run consolidation
|
|
512
705
|
async function runConsolidation() {
|
|
706
|
+
if (!runConsolidationBtn) return;
|
|
707
|
+
|
|
513
708
|
runConsolidationBtn.disabled = true;
|
|
514
|
-
runConsolidationBtn.
|
|
709
|
+
runConsolidationBtn.innerHTML = `
|
|
710
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
|
|
711
|
+
<path d="M21 12a9 9 0 11-6.219-8.56"/>
|
|
712
|
+
</svg>
|
|
713
|
+
Consolidating...
|
|
714
|
+
`;
|
|
515
715
|
|
|
516
716
|
try {
|
|
517
717
|
const result = await api('/api/consolidation/run', { method: 'POST' });
|
|
518
718
|
alert(`Consolidation complete!\n\nDigests created: ${result.digestsCreated}\nContradictions found: ${result.contradictionsFound}\nMemories processed: ${result.memoriesProcessed}`);
|
|
519
719
|
await loadConsolidation();
|
|
720
|
+
await loadStats();
|
|
520
721
|
} catch (e) {
|
|
521
722
|
console.error('Consolidation failed', e);
|
|
522
723
|
alert('Consolidation failed. Check console for details.');
|
|
523
724
|
} finally {
|
|
524
725
|
runConsolidationBtn.disabled = false;
|
|
525
|
-
runConsolidationBtn.
|
|
726
|
+
runConsolidationBtn.innerHTML = `
|
|
727
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
728
|
+
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
|
|
729
|
+
</svg>
|
|
730
|
+
Run Consolidation
|
|
731
|
+
`;
|
|
526
732
|
}
|
|
527
733
|
}
|
|
528
734
|
|
|
529
|
-
// Open contradiction modal
|
|
530
735
|
function openContradictionModal(id) {
|
|
531
|
-
const item = contradictionsList
|
|
736
|
+
const item = contradictionsList?.querySelector(`[data-id="${id}"]`);
|
|
532
737
|
if (!item) return;
|
|
533
738
|
|
|
534
739
|
currentContradictionId = id;
|
|
535
|
-
|
|
536
|
-
// Copy the memories to the modal
|
|
537
|
-
const description = item.querySelector('.description').textContent;
|
|
740
|
+
const description = item.querySelector('.description')?.textContent || '';
|
|
538
741
|
const memories = item.querySelectorAll('.memory-quote');
|
|
539
742
|
|
|
540
|
-
contradictionModalBody
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
743
|
+
if (contradictionModalBody) {
|
|
744
|
+
contradictionModalBody.innerHTML = `
|
|
745
|
+
<p style="font-weight: 500; margin-bottom: 16px;">${escapeHtml(description)}</p>
|
|
746
|
+
<div class="memory-quote">${memories[0]?.innerHTML || ''}</div>
|
|
747
|
+
<div class="memory-quote">${memories[1]?.innerHTML || ''}</div>
|
|
748
|
+
`;
|
|
749
|
+
}
|
|
545
750
|
|
|
546
|
-
contradictionResolution.value = '';
|
|
547
|
-
contradictionModal
|
|
751
|
+
if (contradictionResolution) contradictionResolution.value = '';
|
|
752
|
+
contradictionModal?.classList.remove('hidden');
|
|
548
753
|
}
|
|
549
754
|
|
|
550
|
-
// Close contradiction modal
|
|
551
755
|
function closeContradictionModal() {
|
|
552
|
-
contradictionModal
|
|
756
|
+
contradictionModal?.classList.add('hidden');
|
|
553
757
|
currentContradictionId = null;
|
|
554
758
|
}
|
|
555
759
|
|
|
556
|
-
// Resolve contradiction
|
|
557
760
|
async function resolveContradiction(resolution) {
|
|
558
761
|
if (!currentContradictionId || !resolution.trim()) return;
|
|
559
762
|
|
|
@@ -571,7 +774,6 @@ async function resolveContradiction(resolution) {
|
|
|
571
774
|
}
|
|
572
775
|
}
|
|
573
776
|
|
|
574
|
-
// Dismiss contradiction
|
|
575
777
|
async function dismissContradiction() {
|
|
576
778
|
if (!currentContradictionId) return;
|
|
577
779
|
if (!confirm('Dismiss this contradiction without resolution?')) return;
|
|
@@ -587,70 +789,40 @@ async function dismissContradiction() {
|
|
|
587
789
|
}
|
|
588
790
|
}
|
|
589
791
|
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
document.getElementById('contradiction-cancel').addEventListener('click', closeContradictionModal);
|
|
599
|
-
document.getElementById('contradiction-dismiss').addEventListener('click', dismissContradiction);
|
|
600
|
-
|
|
601
|
-
contradictionModal.addEventListener('click', (e) => {
|
|
602
|
-
if (e.target === contradictionModal) closeContradictionModal();
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
// Event listeners
|
|
606
|
-
document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
607
|
-
btn.addEventListener('click', () => switchView(btn.dataset.view));
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
document.getElementById('search-btn').addEventListener('click', () => {
|
|
611
|
-
loadMemories(searchInput.value);
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
searchInput.addEventListener('keypress', (e) => {
|
|
615
|
-
if (e.key === 'Enter') loadMemories(searchInput.value);
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
entityTypeFilter.addEventListener('change', () => {
|
|
619
|
-
loadEntities(entityTypeFilter.value);
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
document.getElementById('add-memory-btn').addEventListener('click', () => {
|
|
623
|
-
editingMemoryId = null;
|
|
624
|
-
modalTitle.textContent = 'Add Memory';
|
|
625
|
-
modal.classList.remove('hidden');
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
document.getElementById('modal-cancel').addEventListener('click', closeModal);
|
|
629
|
-
|
|
630
|
-
modalForm.addEventListener('submit', (e) => {
|
|
631
|
-
e.preventDefault();
|
|
632
|
-
saveMemory();
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
modalImportance.addEventListener('input', () => {
|
|
636
|
-
importanceValue.textContent = modalImportance.value;
|
|
637
|
-
});
|
|
792
|
+
// ============ Settings ============
|
|
793
|
+
const apiStatusBadge = document.getElementById('api-status-badge');
|
|
794
|
+
const apiKeyInput = document.getElementById('api-key-input');
|
|
795
|
+
const toggleKeyVisibility = document.getElementById('toggle-key-visibility');
|
|
796
|
+
const saveApiKeyBtn = document.getElementById('save-api-key');
|
|
797
|
+
const clearApiKeyBtn = document.getElementById('clear-api-key');
|
|
638
798
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
799
|
+
async function loadSettings() {
|
|
800
|
+
try {
|
|
801
|
+
const settings = await api('/api/settings');
|
|
802
|
+
updateSettingsUI(settings);
|
|
803
|
+
} catch (e) {
|
|
804
|
+
console.error('Failed to load settings', e);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
642
807
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (e.target === modal) closeModal();
|
|
646
|
-
});
|
|
808
|
+
function updateSettingsUI(settings) {
|
|
809
|
+
if (!apiStatusBadge) return;
|
|
647
810
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
811
|
+
if (settings.has_api_key) {
|
|
812
|
+
apiStatusBadge.textContent = `Configured (${settings.api_key_source})`;
|
|
813
|
+
apiStatusBadge.className = 'status-badge configured';
|
|
814
|
+
if (apiKeyInput) {
|
|
815
|
+
apiKeyInput.placeholder = settings.api_key_preview || 'sk-ant-api03-...';
|
|
816
|
+
apiKeyInput.value = '';
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
apiStatusBadge.textContent = 'Not configured';
|
|
820
|
+
apiStatusBadge.className = 'status-badge not-configured';
|
|
821
|
+
if (apiKeyInput) apiKeyInput.placeholder = 'sk-ant-api03-...';
|
|
822
|
+
}
|
|
823
|
+
}
|
|
651
824
|
|
|
652
825
|
// ============ Chat Panel ============
|
|
653
|
-
|
|
654
826
|
const chatPanel = document.getElementById('chat-panel');
|
|
655
827
|
const chatToggle = document.getElementById('chat-toggle');
|
|
656
828
|
const chatClose = document.getElementById('chat-close');
|
|
@@ -662,68 +834,75 @@ const chatInput = document.getElementById('chat-input');
|
|
|
662
834
|
|
|
663
835
|
let chatConfigured = false;
|
|
664
836
|
|
|
665
|
-
// Check if chat is configured
|
|
666
837
|
async function checkChatStatus() {
|
|
667
838
|
try {
|
|
668
839
|
const data = await api('/api/chat/status');
|
|
669
840
|
chatConfigured = data.configured;
|
|
670
|
-
if (!chatConfigured) {
|
|
671
|
-
chatStatus.textContent = '
|
|
841
|
+
if (!chatConfigured && chatStatus) {
|
|
842
|
+
chatStatus.textContent = 'Configure ANTHROPIC_API_KEY to enable chat';
|
|
672
843
|
chatStatus.classList.add('error');
|
|
673
|
-
chatInput
|
|
674
|
-
|
|
675
|
-
|
|
844
|
+
if (chatInput) {
|
|
845
|
+
chatInput.disabled = true;
|
|
846
|
+
chatInput.placeholder = 'Chat disabled - API key not configured';
|
|
847
|
+
}
|
|
848
|
+
} else if (chatStatus) {
|
|
676
849
|
chatStatus.textContent = '';
|
|
677
850
|
chatStatus.classList.remove('error');
|
|
678
|
-
chatInput
|
|
679
|
-
|
|
851
|
+
if (chatInput) {
|
|
852
|
+
chatInput.disabled = false;
|
|
853
|
+
chatInput.placeholder = 'Ask me to manage entities...';
|
|
854
|
+
}
|
|
680
855
|
}
|
|
681
856
|
} catch (e) {
|
|
682
|
-
chatStatus
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
857
|
+
if (chatStatus) {
|
|
858
|
+
chatStatus.textContent = 'Failed to connect to chat service';
|
|
859
|
+
chatStatus.classList.add('error');
|
|
860
|
+
}
|
|
861
|
+
if (chatInput) {
|
|
862
|
+
chatInput.disabled = true;
|
|
863
|
+
chatInput.placeholder = 'Chat unavailable';
|
|
864
|
+
}
|
|
686
865
|
}
|
|
687
866
|
}
|
|
688
867
|
|
|
689
|
-
// Toggle chat panel
|
|
690
868
|
function toggleChat() {
|
|
869
|
+
if (!chatPanel) return;
|
|
870
|
+
|
|
691
871
|
const isHidden = chatPanel.classList.contains('hidden');
|
|
692
872
|
chatPanel.classList.toggle('hidden');
|
|
693
|
-
chatToggle
|
|
873
|
+
chatToggle?.classList.toggle('active', isHidden);
|
|
694
874
|
document.body.classList.toggle('chat-open', isHidden);
|
|
695
875
|
|
|
696
876
|
if (isHidden) {
|
|
697
877
|
checkChatStatus();
|
|
698
|
-
chatInput
|
|
878
|
+
chatInput?.focus();
|
|
699
879
|
}
|
|
700
880
|
}
|
|
701
881
|
|
|
702
|
-
// Add message to chat
|
|
703
882
|
function addChatMessage(content, role) {
|
|
883
|
+
if (!chatMessages) return;
|
|
884
|
+
|
|
704
885
|
const div = document.createElement('div');
|
|
705
886
|
div.className = `chat-message ${role}`;
|
|
887
|
+
div.innerHTML = `<p>${formatChatContent(content)}</p>`;
|
|
888
|
+
chatMessages.appendChild(div);
|
|
889
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
890
|
+
}
|
|
706
891
|
|
|
707
|
-
|
|
708
|
-
|
|
892
|
+
function formatChatContent(content) {
|
|
893
|
+
return content
|
|
709
894
|
.replace(/\n\n/g, '</p><p>')
|
|
710
895
|
.replace(/\n/g, '<br>')
|
|
711
896
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
712
897
|
.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
713
|
-
|
|
714
|
-
div.innerHTML = `<p>${formatted}</p>`;
|
|
715
|
-
chatMessages.appendChild(div);
|
|
716
|
-
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
717
898
|
}
|
|
718
899
|
|
|
719
|
-
// Send chat message with streaming
|
|
720
900
|
async function sendChatMessage(message) {
|
|
721
|
-
if (!message.trim()) return;
|
|
901
|
+
if (!message.trim() || !chatMessages) return;
|
|
722
902
|
|
|
723
|
-
// Add user message
|
|
724
903
|
addChatMessage(message, 'user');
|
|
725
|
-
chatInput.value = '';
|
|
726
|
-
chatInput.disabled = true;
|
|
904
|
+
if (chatInput) chatInput.value = '';
|
|
905
|
+
if (chatInput) chatInput.disabled = true;
|
|
727
906
|
|
|
728
907
|
// Create assistant message div for streaming
|
|
729
908
|
const responseDiv = document.createElement('div');
|
|
@@ -765,28 +944,64 @@ async function sendChatMessage(message) {
|
|
|
765
944
|
switch (event.type) {
|
|
766
945
|
case 'text':
|
|
767
946
|
currentContent += event.content;
|
|
768
|
-
contentEl.innerHTML = formatChatContent(currentContent);
|
|
947
|
+
if (contentEl) contentEl.innerHTML = formatChatContent(currentContent);
|
|
948
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
949
|
+
break;
|
|
950
|
+
|
|
951
|
+
case 'thinking':
|
|
952
|
+
// Handle streaming thinking content
|
|
953
|
+
let thinkingEl = contentEl?.querySelector('.thinking-indicator');
|
|
954
|
+
if (!thinkingEl) {
|
|
955
|
+
// Create thinking indicator on first thinking event
|
|
956
|
+
thinkingEl = document.createElement('div');
|
|
957
|
+
thinkingEl.className = 'tool-indicator thinking-indicator';
|
|
958
|
+
thinkingEl.innerHTML = `
|
|
959
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
960
|
+
<circle cx="12" cy="12" r="10"/>
|
|
961
|
+
<path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3M12 17h.01"/>
|
|
962
|
+
</svg>
|
|
963
|
+
<span class="thinking-text">Reasoning...</span>
|
|
964
|
+
`;
|
|
965
|
+
contentEl?.appendChild(thinkingEl);
|
|
966
|
+
}
|
|
967
|
+
// Optionally show truncated thinking preview (last 50 chars)
|
|
968
|
+
if (event.content && event.content.length > 0) {
|
|
969
|
+
const thinkingTextEl = thinkingEl.querySelector('.thinking-text');
|
|
970
|
+
if (thinkingTextEl) {
|
|
971
|
+
const preview = event.content.slice(-80).replace(/\n/g, ' ').trim();
|
|
972
|
+
thinkingTextEl.textContent = `Reasoning... ${preview.length > 60 ? '...' + preview.slice(-60) : preview}`;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
769
975
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
770
976
|
break;
|
|
771
977
|
|
|
772
978
|
case 'tool_start':
|
|
979
|
+
// Remove thinking indicator if present
|
|
980
|
+
const thinkingInd = contentEl?.querySelector('.thinking-indicator');
|
|
981
|
+
if (thinkingInd) thinkingInd.remove();
|
|
982
|
+
|
|
773
983
|
// Show tool execution indicator
|
|
774
|
-
const toolIndicator = document.createElement('
|
|
984
|
+
const toolIndicator = document.createElement('div');
|
|
775
985
|
toolIndicator.className = 'tool-indicator';
|
|
776
|
-
toolIndicator.
|
|
777
|
-
|
|
986
|
+
toolIndicator.innerHTML = `
|
|
987
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
988
|
+
<path d="M21 12a9 9 0 11-6.219-8.56"/>
|
|
989
|
+
</svg>
|
|
990
|
+
Using ${event.tool}...
|
|
991
|
+
`;
|
|
992
|
+
contentEl?.appendChild(toolIndicator);
|
|
778
993
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
779
994
|
break;
|
|
780
995
|
|
|
781
996
|
case 'tool_end':
|
|
782
997
|
// Remove tool indicator
|
|
783
|
-
const indicators = contentEl
|
|
784
|
-
indicators
|
|
998
|
+
const indicators = contentEl?.querySelectorAll('.tool-indicator');
|
|
999
|
+
indicators?.forEach(ind => ind.remove());
|
|
785
1000
|
break;
|
|
786
1001
|
|
|
787
1002
|
case 'error':
|
|
788
|
-
currentContent += `\n\
|
|
789
|
-
contentEl.innerHTML = formatChatContent(currentContent);
|
|
1003
|
+
currentContent += `\n\n**Error:** ${event.content}`;
|
|
1004
|
+
if (contentEl) contentEl.innerHTML = formatChatContent(currentContent);
|
|
790
1005
|
break;
|
|
791
1006
|
|
|
792
1007
|
case 'done':
|
|
@@ -802,66 +1017,50 @@ async function sendChatMessage(message) {
|
|
|
802
1017
|
|
|
803
1018
|
// Refresh data in case something changed
|
|
804
1019
|
loadStats();
|
|
805
|
-
if (currentView === 'entities') loadEntities(entityTypeFilter
|
|
1020
|
+
if (currentView === 'entities') loadEntities(entityTypeFilter?.value || '');
|
|
806
1021
|
if (currentView === 'graph') loadGraph();
|
|
807
|
-
if (currentView === 'memories') loadMemories(searchInput
|
|
1022
|
+
if (currentView === 'memories') loadMemories(searchInput?.value || '');
|
|
808
1023
|
|
|
809
1024
|
} catch (e) {
|
|
810
1025
|
responseDiv.classList.remove('streaming');
|
|
811
|
-
contentEl.innerHTML = formatChatContent(
|
|
1026
|
+
if (contentEl) contentEl.innerHTML = formatChatContent(`**Error:** ${e.message || 'Failed to get response'}`);
|
|
812
1027
|
}
|
|
813
1028
|
|
|
814
|
-
chatInput
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
// Format chat content (markdown-like)
|
|
819
|
-
function formatChatContent(content) {
|
|
820
|
-
return content
|
|
821
|
-
.replace(/\n\n/g, '</p><p>')
|
|
822
|
-
.replace(/\n/g, '<br>')
|
|
823
|
-
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
824
|
-
.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
1029
|
+
if (chatInput) {
|
|
1030
|
+
chatInput.disabled = false;
|
|
1031
|
+
chatInput.focus();
|
|
1032
|
+
}
|
|
825
1033
|
}
|
|
826
1034
|
|
|
827
|
-
// Clear chat history
|
|
828
1035
|
async function clearChatHistory() {
|
|
829
1036
|
try {
|
|
830
1037
|
await api('/api/chat/clear', { method: 'POST' });
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1038
|
+
if (chatMessages) {
|
|
1039
|
+
chatMessages.innerHTML = `
|
|
1040
|
+
<div class="chat-message assistant">
|
|
1041
|
+
<p>Hi! I can help you manage your memories and entities.</p>
|
|
1042
|
+
<p><strong>Examples:</strong></p>
|
|
1043
|
+
<ul>
|
|
1044
|
+
<li>"Show me all entities"</li>
|
|
1045
|
+
<li>"Find duplicates"</li>
|
|
1046
|
+
<li>"Merge Boris into Boris Djordjevic"</li>
|
|
1047
|
+
<li>"Delete the entity 'crashed'"</li>
|
|
1048
|
+
</ul>
|
|
1049
|
+
<p style="font-size: 0.8em; color: var(--text-tertiary); margin-top: 0.5rem;">Requires ANTHROPIC_API_KEY to be configured.</p>
|
|
1050
|
+
</div>
|
|
1051
|
+
`;
|
|
1052
|
+
}
|
|
845
1053
|
} catch (e) {
|
|
846
1054
|
console.error('Failed to clear chat history', e);
|
|
847
1055
|
}
|
|
848
1056
|
}
|
|
849
1057
|
|
|
850
|
-
// Chat event listeners
|
|
851
|
-
chatToggle.addEventListener('click', toggleChat);
|
|
852
|
-
chatClose.addEventListener('click', toggleChat);
|
|
853
|
-
chatClear.addEventListener('click', clearChatHistory);
|
|
854
|
-
|
|
855
|
-
chatForm.addEventListener('submit', (e) => {
|
|
856
|
-
e.preventDefault();
|
|
857
|
-
sendChatMessage(chatInput.value);
|
|
858
|
-
});
|
|
859
|
-
|
|
860
1058
|
// ============ API Status Indicator ============
|
|
861
|
-
|
|
862
1059
|
const apiStatusEl = document.getElementById('api-status');
|
|
863
1060
|
|
|
864
1061
|
async function checkApiStatus() {
|
|
1062
|
+
if (!apiStatusEl) return;
|
|
1063
|
+
|
|
865
1064
|
apiStatusEl.classList.remove('connected', 'disconnected');
|
|
866
1065
|
apiStatusEl.classList.add('checking');
|
|
867
1066
|
apiStatusEl.title = 'Checking API status...';
|
|
@@ -874,7 +1073,7 @@ async function checkApiStatus() {
|
|
|
874
1073
|
apiStatusEl.title = 'Anthropic API connected';
|
|
875
1074
|
} else {
|
|
876
1075
|
apiStatusEl.classList.add('disconnected');
|
|
877
|
-
apiStatusEl.title = 'API key not configured
|
|
1076
|
+
apiStatusEl.title = 'API key not configured';
|
|
878
1077
|
}
|
|
879
1078
|
} catch (e) {
|
|
880
1079
|
apiStatusEl.classList.remove('checking');
|
|
@@ -883,105 +1082,158 @@ async function checkApiStatus() {
|
|
|
883
1082
|
}
|
|
884
1083
|
}
|
|
885
1084
|
|
|
886
|
-
// ============
|
|
1085
|
+
// ============ Event Listeners ============
|
|
887
1086
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const clearApiKeyBtn = document.getElementById('clear-api-key');
|
|
1087
|
+
// Navigation
|
|
1088
|
+
document.querySelectorAll('.nav-item').forEach(btn => {
|
|
1089
|
+
btn.addEventListener('click', () => switchView(btn.dataset.view));
|
|
1090
|
+
});
|
|
893
1091
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
} catch (e) {
|
|
899
|
-
console.error('Failed to load settings', e);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
1092
|
+
// Search
|
|
1093
|
+
document.getElementById('search-input')?.addEventListener('keypress', (e) => {
|
|
1094
|
+
if (e.key === 'Enter') loadMemories(e.target.value);
|
|
1095
|
+
});
|
|
902
1096
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
apiKeyInput.placeholder = settings.api_key_preview || 'sk-ant-api03-...';
|
|
908
|
-
apiKeyInput.value = '';
|
|
909
|
-
} else {
|
|
910
|
-
apiStatusBadge.textContent = 'Not configured';
|
|
911
|
-
apiStatusBadge.className = 'status-badge not-configured';
|
|
912
|
-
apiKeyInput.placeholder = 'sk-ant-api03-...';
|
|
913
|
-
}
|
|
914
|
-
}
|
|
1097
|
+
// Entity filter
|
|
1098
|
+
entityTypeFilter?.addEventListener('change', () => {
|
|
1099
|
+
loadEntities(entityTypeFilter.value);
|
|
1100
|
+
});
|
|
915
1101
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
1102
|
+
// Add memory button
|
|
1103
|
+
document.getElementById('add-memory-btn')?.addEventListener('click', () => {
|
|
1104
|
+
editingMemoryId = null;
|
|
1105
|
+
if (modalTitle) modalTitle.textContent = 'Add Memory';
|
|
1106
|
+
modal?.classList.remove('hidden');
|
|
1107
|
+
});
|
|
923
1108
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1109
|
+
// Modal events
|
|
1110
|
+
document.getElementById('modal-cancel')?.addEventListener('click', closeModal);
|
|
1111
|
+
document.getElementById('modal-cancel-btn')?.addEventListener('click', closeModal);
|
|
1112
|
+
modalForm?.addEventListener('submit', (e) => {
|
|
1113
|
+
e.preventDefault();
|
|
1114
|
+
saveMemory();
|
|
1115
|
+
});
|
|
1116
|
+
modalImportance?.addEventListener('input', () => {
|
|
1117
|
+
if (importanceValue) importanceValue.textContent = modalImportance.value;
|
|
1118
|
+
});
|
|
1119
|
+
modal?.addEventListener('click', (e) => {
|
|
1120
|
+
if (e.target === modal) closeModal();
|
|
1121
|
+
});
|
|
931
1122
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1123
|
+
// Entity modal
|
|
1124
|
+
document.getElementById('entity-modal-close')?.addEventListener('click', () => {
|
|
1125
|
+
entityModal?.classList.add('hidden');
|
|
1126
|
+
});
|
|
1127
|
+
document.getElementById('entity-modal-close-btn')?.addEventListener('click', () => {
|
|
1128
|
+
entityModal?.classList.add('hidden');
|
|
1129
|
+
});
|
|
1130
|
+
entityModal?.addEventListener('click', (e) => {
|
|
1131
|
+
if (e.target === entityModal) entityModal.classList.add('hidden');
|
|
1132
|
+
});
|
|
936
1133
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1134
|
+
// Consolidation
|
|
1135
|
+
runConsolidationBtn?.addEventListener('click', runConsolidation);
|
|
1136
|
+
contradictionForm?.addEventListener('submit', (e) => {
|
|
1137
|
+
e.preventDefault();
|
|
1138
|
+
resolveContradiction(contradictionResolution?.value || '');
|
|
1139
|
+
});
|
|
1140
|
+
document.getElementById('contradiction-cancel')?.addEventListener('click', closeContradictionModal);
|
|
1141
|
+
document.getElementById('contradiction-close')?.addEventListener('click', closeContradictionModal);
|
|
1142
|
+
document.getElementById('contradiction-dismiss')?.addEventListener('click', dismissContradiction);
|
|
1143
|
+
contradictionModal?.addEventListener('click', (e) => {
|
|
1144
|
+
if (e.target === contradictionModal) closeContradictionModal();
|
|
1145
|
+
});
|
|
940
1146
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1147
|
+
// Settings
|
|
1148
|
+
toggleKeyVisibility?.addEventListener('click', () => {
|
|
1149
|
+
if (!apiKeyInput) return;
|
|
1150
|
+
const type = apiKeyInput.type === 'password' ? 'text' : 'password';
|
|
1151
|
+
apiKeyInput.type = type;
|
|
1152
|
+
const eyeIcon = document.getElementById('eye-icon');
|
|
1153
|
+
if (eyeIcon) {
|
|
1154
|
+
eyeIcon.innerHTML = type === 'password'
|
|
1155
|
+
? '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'
|
|
1156
|
+
: '<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/><path d="M1 1l22 22"/>';
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
945
1159
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
} finally {
|
|
958
|
-
saveApiKeyBtn.disabled = false;
|
|
959
|
-
saveApiKeyBtn.textContent = 'Save API Key';
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
}
|
|
1160
|
+
saveApiKeyBtn?.addEventListener('click', async () => {
|
|
1161
|
+
const apiKey = apiKeyInput?.value.trim();
|
|
1162
|
+
if (!apiKey) {
|
|
1163
|
+
alert('Please enter an API key');
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (!apiKey.startsWith('sk-ant-')) {
|
|
1168
|
+
alert('Invalid API key format. Should start with sk-ant-');
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
963
1171
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1172
|
+
try {
|
|
1173
|
+
saveApiKeyBtn.disabled = true;
|
|
1174
|
+
saveApiKeyBtn.textContent = 'Saving...';
|
|
967
1175
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1176
|
+
const result = await api('/api/settings', {
|
|
1177
|
+
method: 'POST',
|
|
1178
|
+
body: { anthropic_api_key: apiKey },
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
if (result.success) {
|
|
1182
|
+
if (apiKeyInput) apiKeyInput.value = '';
|
|
974
1183
|
await loadSettings();
|
|
975
1184
|
await checkApiStatus();
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
clearApiKeyBtn.disabled = false;
|
|
1185
|
+
alert('API key saved successfully!');
|
|
1186
|
+
} else {
|
|
1187
|
+
alert('Failed to save API key');
|
|
980
1188
|
}
|
|
981
|
-
})
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
console.error('Failed to save API key', e);
|
|
1191
|
+
alert('Error saving API key');
|
|
1192
|
+
} finally {
|
|
1193
|
+
saveApiKeyBtn.disabled = false;
|
|
1194
|
+
saveApiKeyBtn.textContent = 'Save API Key';
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
clearApiKeyBtn?.addEventListener('click', async () => {
|
|
1199
|
+
if (!confirm('Are you sure you want to clear the API key?')) return;
|
|
1200
|
+
|
|
1201
|
+
try {
|
|
1202
|
+
clearApiKeyBtn.disabled = true;
|
|
1203
|
+
await api('/api/settings', {
|
|
1204
|
+
method: 'POST',
|
|
1205
|
+
body: { anthropic_api_key: '' },
|
|
1206
|
+
});
|
|
1207
|
+
await loadSettings();
|
|
1208
|
+
await checkApiStatus();
|
|
1209
|
+
} catch (e) {
|
|
1210
|
+
console.error('Failed to clear API key', e);
|
|
1211
|
+
} finally {
|
|
1212
|
+
clearApiKeyBtn.disabled = false;
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
// Chat
|
|
1217
|
+
chatToggle?.addEventListener('click', toggleChat);
|
|
1218
|
+
chatClose?.addEventListener('click', toggleChat);
|
|
1219
|
+
chatClear?.addEventListener('click', clearChatHistory);
|
|
1220
|
+
chatForm?.addEventListener('submit', (e) => {
|
|
1221
|
+
e.preventDefault();
|
|
1222
|
+
sendChatMessage(chatInput?.value || '');
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// ============ Initialization ============
|
|
1226
|
+
function init() {
|
|
1227
|
+
initTheme();
|
|
1228
|
+
initSidebar();
|
|
1229
|
+
checkApiStatus();
|
|
1230
|
+
loadStats();
|
|
1231
|
+
loadMemories();
|
|
982
1232
|
}
|
|
983
1233
|
|
|
984
|
-
//
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1234
|
+
// Run on DOM ready
|
|
1235
|
+
if (document.readyState === 'loading') {
|
|
1236
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
1237
|
+
} else {
|
|
1238
|
+
init();
|
|
1239
|
+
}
|