@memextend/webui 0.1.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/dist/api/config.d.ts +2 -0
- package/dist/api/config.d.ts.map +1 -0
- package/dist/api/config.js +106 -0
- package/dist/api/config.js.map +1 -0
- package/dist/api/memories.d.ts +2 -0
- package/dist/api/memories.d.ts.map +1 -0
- package/dist/api/memories.js +213 -0
- package/dist/api/memories.js.map +1 -0
- package/dist/api/projects.d.ts +2 -0
- package/dist/api/projects.d.ts.map +1 -0
- package/dist/api/projects.js +136 -0
- package/dist/api/projects.js.map +1 -0
- package/dist/api/search.d.ts +2 -0
- package/dist/api/search.d.ts.map +1 -0
- package/dist/api/search.js +97 -0
- package/dist/api/search.js.map +1 -0
- package/dist/api/stats.d.ts +2 -0
- package/dist/api/stats.d.ts.map +1 -0
- package/dist/api/stats.js +183 -0
- package/dist/api/stats.js.map +1 -0
- package/dist/public/app.js +953 -0
- package/dist/public/index.html +551 -0
- package/dist/public/styles.css +1436 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +84 -0
- package/dist/server.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
// apps/webui/src/public/app.js
|
|
2
|
+
// memextend Web UI Frontend
|
|
3
|
+
// Copyright (c) 2026 ZodTTD LLC. MIT License.
|
|
4
|
+
|
|
5
|
+
const API_BASE = '/api';
|
|
6
|
+
|
|
7
|
+
// State
|
|
8
|
+
const state = {
|
|
9
|
+
currentView: 'dashboard',
|
|
10
|
+
memories: [],
|
|
11
|
+
projects: [],
|
|
12
|
+
globalProfiles: [],
|
|
13
|
+
stats: null,
|
|
14
|
+
settings: null,
|
|
15
|
+
pagination: {
|
|
16
|
+
limit: 50,
|
|
17
|
+
offset: 0,
|
|
18
|
+
total: 0
|
|
19
|
+
},
|
|
20
|
+
filters: {
|
|
21
|
+
projectId: '',
|
|
22
|
+
type: '',
|
|
23
|
+
tool: '',
|
|
24
|
+
startDate: '',
|
|
25
|
+
endDate: ''
|
|
26
|
+
},
|
|
27
|
+
selectedMemory: null
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// API Helpers
|
|
31
|
+
async function api(endpoint, options = {}) {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
34
|
+
...options,
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
...options.headers
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const error = await response.json();
|
|
43
|
+
throw new Error(error.error || 'API Error');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return await response.json();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('API Error:', error);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Toast Notifications
|
|
54
|
+
function showToast(message, type = 'info') {
|
|
55
|
+
const container = document.getElementById('toast-container');
|
|
56
|
+
const toast = document.createElement('div');
|
|
57
|
+
toast.className = `toast ${type}`;
|
|
58
|
+
toast.textContent = message;
|
|
59
|
+
container.appendChild(toast);
|
|
60
|
+
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
toast.remove();
|
|
63
|
+
}, 3000);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Format date
|
|
67
|
+
function formatDate(isoDate) {
|
|
68
|
+
const date = new Date(isoDate);
|
|
69
|
+
return date.toLocaleDateString('en-US', {
|
|
70
|
+
year: 'numeric',
|
|
71
|
+
month: 'short',
|
|
72
|
+
day: 'numeric',
|
|
73
|
+
hour: '2-digit',
|
|
74
|
+
minute: '2-digit'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Format short date
|
|
79
|
+
function formatShortDate(isoDate) {
|
|
80
|
+
const date = new Date(isoDate);
|
|
81
|
+
return date.toLocaleDateString('en-US', {
|
|
82
|
+
month: 'short',
|
|
83
|
+
day: 'numeric'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Truncate text
|
|
88
|
+
function truncate(text, maxLength = 150) {
|
|
89
|
+
if (text.length <= maxLength) return text;
|
|
90
|
+
return text.slice(0, maxLength) + '...';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Escape HTML
|
|
94
|
+
function escapeHtml(text) {
|
|
95
|
+
const div = document.createElement('div');
|
|
96
|
+
div.textContent = text;
|
|
97
|
+
return div.innerHTML;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Navigation
|
|
101
|
+
function setupNavigation() {
|
|
102
|
+
const navBtns = document.querySelectorAll('.nav-btn');
|
|
103
|
+
navBtns.forEach(btn => {
|
|
104
|
+
btn.addEventListener('click', () => {
|
|
105
|
+
const view = btn.dataset.view;
|
|
106
|
+
switchView(view);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function switchView(view) {
|
|
112
|
+
state.currentView = view;
|
|
113
|
+
|
|
114
|
+
// Update nav buttons
|
|
115
|
+
document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
116
|
+
btn.classList.toggle('active', btn.dataset.view === view);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Update views
|
|
120
|
+
document.querySelectorAll('.view').forEach(v => {
|
|
121
|
+
v.classList.toggle('active', v.id === `view-${view}`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Load view data
|
|
125
|
+
switch (view) {
|
|
126
|
+
case 'dashboard':
|
|
127
|
+
loadDashboard();
|
|
128
|
+
break;
|
|
129
|
+
case 'memories':
|
|
130
|
+
loadMemories();
|
|
131
|
+
loadProjects();
|
|
132
|
+
break;
|
|
133
|
+
case 'search':
|
|
134
|
+
loadProjects();
|
|
135
|
+
break;
|
|
136
|
+
case 'global':
|
|
137
|
+
loadGlobalProfiles();
|
|
138
|
+
break;
|
|
139
|
+
case 'settings':
|
|
140
|
+
loadSettings();
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Dashboard
|
|
146
|
+
async function loadDashboard() {
|
|
147
|
+
try {
|
|
148
|
+
const stats = await api('/stats');
|
|
149
|
+
state.stats = stats;
|
|
150
|
+
renderDashboard(stats);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
showToast('Failed to load dashboard', 'error');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderDashboard(stats) {
|
|
157
|
+
// Stats cards
|
|
158
|
+
document.getElementById('stat-memories').textContent = stats.overview.totalMemories.toLocaleString();
|
|
159
|
+
document.getElementById('stat-vectors').textContent = stats.overview.totalVectors.toLocaleString();
|
|
160
|
+
document.getElementById('stat-projects').textContent = stats.overview.totalProjects.toLocaleString();
|
|
161
|
+
document.getElementById('stat-storage').textContent = stats.storage.total.sizeFormatted;
|
|
162
|
+
|
|
163
|
+
// Activity chart
|
|
164
|
+
const chartContainer = document.getElementById('activity-chart');
|
|
165
|
+
const dates = Object.keys(stats.activity.dateDistribution).sort();
|
|
166
|
+
const maxCount = Math.max(...Object.values(stats.activity.dateDistribution), 1);
|
|
167
|
+
|
|
168
|
+
chartContainer.innerHTML = dates.map(date => {
|
|
169
|
+
const count = stats.activity.dateDistribution[date];
|
|
170
|
+
const height = (count / maxCount) * 100;
|
|
171
|
+
return `<div class="activity-bar" style="height: ${Math.max(height, 4)}%" data-count="${count}" title="${formatShortDate(date)}: ${count}"></div>`;
|
|
172
|
+
}).join('');
|
|
173
|
+
|
|
174
|
+
// Type breakdown
|
|
175
|
+
const typeBreakdown = document.getElementById('type-breakdown');
|
|
176
|
+
typeBreakdown.innerHTML = Object.entries(stats.breakdowns.byType)
|
|
177
|
+
.sort((a, b) => b[1] - a[1])
|
|
178
|
+
.map(([type, count]) => `
|
|
179
|
+
<div class="breakdown-item">
|
|
180
|
+
<span class="breakdown-label">${type}</span>
|
|
181
|
+
<span class="breakdown-value">${count}</span>
|
|
182
|
+
</div>
|
|
183
|
+
`).join('') || '<div class="empty-state">No data</div>';
|
|
184
|
+
|
|
185
|
+
// Source breakdown
|
|
186
|
+
const sourceBreakdown = document.getElementById('source-breakdown');
|
|
187
|
+
sourceBreakdown.innerHTML = Object.entries(stats.breakdowns.bySource)
|
|
188
|
+
.sort((a, b) => b[1] - a[1])
|
|
189
|
+
.map(([source, count]) => `
|
|
190
|
+
<div class="breakdown-item">
|
|
191
|
+
<span class="breakdown-label">${source}</span>
|
|
192
|
+
<span class="breakdown-value">${count}</span>
|
|
193
|
+
</div>
|
|
194
|
+
`).join('') || '<div class="empty-state">No data</div>';
|
|
195
|
+
|
|
196
|
+
// Recent memories
|
|
197
|
+
const recentMemories = document.getElementById('recent-memories');
|
|
198
|
+
if (stats.recentMemories.length === 0) {
|
|
199
|
+
recentMemories.innerHTML = '<div class="empty-state">No memories yet</div>';
|
|
200
|
+
} else {
|
|
201
|
+
recentMemories.innerHTML = stats.recentMemories.map(m => `
|
|
202
|
+
<div class="memory-item" data-id="${m.id}">
|
|
203
|
+
<div class="memory-item-header">
|
|
204
|
+
<div class="memory-item-meta">
|
|
205
|
+
<span class="memory-type-badge ${m.type}">${m.type}</span>
|
|
206
|
+
</div>
|
|
207
|
+
<span class="memory-date">${formatDate(m.createdAt)}</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="memory-content">${escapeHtml(m.preview)}</div>
|
|
210
|
+
</div>
|
|
211
|
+
`).join('');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Model status
|
|
215
|
+
const modelStatus = document.getElementById('model-status');
|
|
216
|
+
modelStatus.innerHTML = `
|
|
217
|
+
<div class="model-status-indicator ${stats.embedding.modelAvailable ? 'available' : 'unavailable'}"></div>
|
|
218
|
+
<div class="model-status-text">
|
|
219
|
+
<strong>${stats.embedding.modelName}</strong>
|
|
220
|
+
<span style="color: var(--text-secondary);">
|
|
221
|
+
- ${stats.embedding.modelAvailable ? 'Ready' : 'Not downloaded (using fallback)'}
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Memories
|
|
228
|
+
async function loadMemories() {
|
|
229
|
+
try {
|
|
230
|
+
const params = new URLSearchParams({
|
|
231
|
+
limit: state.pagination.limit,
|
|
232
|
+
offset: state.pagination.offset
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (state.filters.projectId) params.append('projectId', state.filters.projectId);
|
|
236
|
+
if (state.filters.type) params.append('type', state.filters.type);
|
|
237
|
+
if (state.filters.tool) params.append('tool', state.filters.tool);
|
|
238
|
+
if (state.filters.startDate) params.append('startDate', state.filters.startDate);
|
|
239
|
+
if (state.filters.endDate) params.append('endDate', state.filters.endDate);
|
|
240
|
+
|
|
241
|
+
const result = await api(`/memories?${params}`);
|
|
242
|
+
state.memories = result.memories;
|
|
243
|
+
state.pagination = { ...state.pagination, ...result.pagination };
|
|
244
|
+
|
|
245
|
+
renderMemories();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
showToast('Failed to load memories', 'error');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderMemories() {
|
|
252
|
+
const container = document.getElementById('memories-list');
|
|
253
|
+
|
|
254
|
+
if (state.memories.length === 0) {
|
|
255
|
+
container.innerHTML = `
|
|
256
|
+
<div class="empty-state">
|
|
257
|
+
<h3>No memories found</h3>
|
|
258
|
+
<p>Try adjusting your filters or search criteria.</p>
|
|
259
|
+
</div>
|
|
260
|
+
`;
|
|
261
|
+
document.getElementById('memories-pagination').innerHTML = '';
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
container.innerHTML = state.memories.map(m => `
|
|
266
|
+
<div class="memory-item" data-id="${m.id}">
|
|
267
|
+
<div class="memory-item-header">
|
|
268
|
+
<div class="memory-item-meta">
|
|
269
|
+
<span class="memory-type-badge ${m.type}">${m.type}</span>
|
|
270
|
+
${m.sourceTool ? `<span class="memory-source">${m.sourceTool}</span>` : ''}
|
|
271
|
+
</div>
|
|
272
|
+
<span class="memory-date">${formatDate(m.createdAt)}</span>
|
|
273
|
+
</div>
|
|
274
|
+
<div class="memory-content">${escapeHtml(truncate(m.content, 200))}</div>
|
|
275
|
+
<div class="memory-id">ID: ${m.id}</div>
|
|
276
|
+
</div>
|
|
277
|
+
`).join('');
|
|
278
|
+
|
|
279
|
+
// Pagination
|
|
280
|
+
const paginationContainer = document.getElementById('memories-pagination');
|
|
281
|
+
const totalPages = Math.ceil(state.pagination.total / state.pagination.limit);
|
|
282
|
+
const currentPage = Math.floor(state.pagination.offset / state.pagination.limit) + 1;
|
|
283
|
+
|
|
284
|
+
paginationContainer.innerHTML = `
|
|
285
|
+
<button ${currentPage === 1 ? 'disabled' : ''} onclick="goToPage(${currentPage - 1})">Previous</button>
|
|
286
|
+
<span class="page-info">Page ${currentPage} of ${totalPages} (${state.pagination.total} total)</span>
|
|
287
|
+
<button ${!state.pagination.hasMore ? 'disabled' : ''} onclick="goToPage(${currentPage + 1})">Next</button>
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function goToPage(page) {
|
|
292
|
+
state.pagination.offset = (page - 1) * state.pagination.limit;
|
|
293
|
+
loadMemories();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Projects
|
|
297
|
+
async function loadProjects() {
|
|
298
|
+
try {
|
|
299
|
+
const result = await api('/projects');
|
|
300
|
+
state.projects = result.projects;
|
|
301
|
+
populateProjectSelectors();
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error('Failed to load projects:', error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function populateProjectSelectors() {
|
|
308
|
+
const selectors = [
|
|
309
|
+
document.getElementById('filter-project'),
|
|
310
|
+
document.getElementById('search-project')
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
selectors.forEach(selector => {
|
|
314
|
+
if (!selector) return;
|
|
315
|
+
|
|
316
|
+
const currentValue = selector.value;
|
|
317
|
+
selector.innerHTML = `<option value="">${selector.id === 'filter-project' ? 'All Projects' : 'Select Project'}</option>`;
|
|
318
|
+
|
|
319
|
+
state.projects.forEach(p => {
|
|
320
|
+
const option = document.createElement('option');
|
|
321
|
+
option.value = p.id;
|
|
322
|
+
option.textContent = `${p.name} (${p.memoryCount})`;
|
|
323
|
+
selector.appendChild(option);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
selector.value = currentValue;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Filters
|
|
331
|
+
function setupFilters() {
|
|
332
|
+
const filterProject = document.getElementById('filter-project');
|
|
333
|
+
const deleteProjectBtn = document.getElementById('delete-project-btn');
|
|
334
|
+
|
|
335
|
+
document.getElementById('apply-filters').addEventListener('click', () => {
|
|
336
|
+
state.filters.projectId = filterProject.value;
|
|
337
|
+
state.filters.type = document.getElementById('filter-type').value;
|
|
338
|
+
state.filters.tool = document.getElementById('filter-tool').value;
|
|
339
|
+
state.filters.startDate = document.getElementById('filter-start-date').value;
|
|
340
|
+
state.filters.endDate = document.getElementById('filter-end-date').value;
|
|
341
|
+
state.pagination.offset = 0;
|
|
342
|
+
loadMemories();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
document.getElementById('clear-filters').addEventListener('click', () => {
|
|
346
|
+
filterProject.value = '';
|
|
347
|
+
document.getElementById('filter-type').value = '';
|
|
348
|
+
document.getElementById('filter-tool').value = '';
|
|
349
|
+
document.getElementById('filter-start-date').value = '';
|
|
350
|
+
document.getElementById('filter-end-date').value = '';
|
|
351
|
+
state.filters = { projectId: '', type: '', tool: '', startDate: '', endDate: '' };
|
|
352
|
+
state.pagination.offset = 0;
|
|
353
|
+
deleteProjectBtn.style.display = 'none';
|
|
354
|
+
loadMemories();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Show/hide delete project button when project is selected
|
|
358
|
+
filterProject.addEventListener('change', () => {
|
|
359
|
+
deleteProjectBtn.style.display = filterProject.value ? 'inline-block' : 'none';
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Delete project button
|
|
363
|
+
deleteProjectBtn.addEventListener('click', () => {
|
|
364
|
+
const projectId = filterProject.value;
|
|
365
|
+
if (!projectId) return;
|
|
366
|
+
|
|
367
|
+
const project = state.projects.find(p => p.id === projectId);
|
|
368
|
+
if (!project) return;
|
|
369
|
+
|
|
370
|
+
document.getElementById('delete-project-name').textContent = project.name;
|
|
371
|
+
document.getElementById('delete-project-count').textContent = project.memoryCount;
|
|
372
|
+
document.getElementById('delete-project-modal').classList.add('active');
|
|
373
|
+
state.selectedProjectToDelete = projectId;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Setup delete project modal
|
|
378
|
+
function setupDeleteProjectModal() {
|
|
379
|
+
const modal = document.getElementById('delete-project-modal');
|
|
380
|
+
|
|
381
|
+
document.getElementById('cancel-delete-project').addEventListener('click', closeModals);
|
|
382
|
+
|
|
383
|
+
document.getElementById('confirm-delete-project').addEventListener('click', async () => {
|
|
384
|
+
if (state.selectedProjectToDelete) {
|
|
385
|
+
await deleteProject(state.selectedProjectToDelete);
|
|
386
|
+
state.selectedProjectToDelete = null;
|
|
387
|
+
document.getElementById('filter-project').value = '';
|
|
388
|
+
document.getElementById('delete-project-btn').style.display = 'none';
|
|
389
|
+
state.filters.projectId = '';
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
modal.addEventListener('click', (e) => {
|
|
394
|
+
if (e.target === modal) closeModals();
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Setup clear global profile modal
|
|
399
|
+
function setupClearGlobalModal() {
|
|
400
|
+
const modal = document.getElementById('clear-global-modal');
|
|
401
|
+
const clearBtn = document.getElementById('clear-global-btn');
|
|
402
|
+
|
|
403
|
+
if (clearBtn) {
|
|
404
|
+
clearBtn.addEventListener('click', () => {
|
|
405
|
+
document.getElementById('clear-global-count').textContent = state.globalProfiles.length;
|
|
406
|
+
modal.classList.add('active');
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
document.getElementById('cancel-clear-global').addEventListener('click', closeModals);
|
|
411
|
+
|
|
412
|
+
document.getElementById('confirm-clear-global').addEventListener('click', clearAllGlobalProfiles);
|
|
413
|
+
|
|
414
|
+
modal.addEventListener('click', (e) => {
|
|
415
|
+
if (e.target === modal) closeModals();
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Search
|
|
420
|
+
function setupSearch() {
|
|
421
|
+
const searchBtn = document.getElementById('search-btn');
|
|
422
|
+
const searchInput = document.getElementById('search-input');
|
|
423
|
+
|
|
424
|
+
searchBtn.addEventListener('click', performSearch);
|
|
425
|
+
searchInput.addEventListener('keypress', (e) => {
|
|
426
|
+
if (e.key === 'Enter') performSearch();
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function performSearch() {
|
|
431
|
+
const query = document.getElementById('search-input').value.trim();
|
|
432
|
+
if (!query) {
|
|
433
|
+
showToast('Please enter a search query', 'info');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const scope = document.querySelector('input[name="search-scope"]:checked').value;
|
|
438
|
+
const projectId = document.getElementById('search-project').value;
|
|
439
|
+
|
|
440
|
+
const container = document.getElementById('search-results');
|
|
441
|
+
const infoContainer = document.getElementById('search-info');
|
|
442
|
+
|
|
443
|
+
container.innerHTML = '<div class="loading">Searching...</div>';
|
|
444
|
+
infoContainer.innerHTML = '';
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const params = new URLSearchParams({ q: query, scope, limit: 20 });
|
|
448
|
+
if (scope === 'project' && projectId) {
|
|
449
|
+
params.append('projectId', projectId);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const result = await api(`/search?${params}`);
|
|
453
|
+
|
|
454
|
+
if (result.results.length === 0) {
|
|
455
|
+
container.innerHTML = `
|
|
456
|
+
<div class="empty-state">
|
|
457
|
+
<h3>No results found</h3>
|
|
458
|
+
<p>Try a different search query or scope.</p>
|
|
459
|
+
</div>
|
|
460
|
+
`;
|
|
461
|
+
} else {
|
|
462
|
+
container.innerHTML = result.results.map(r => {
|
|
463
|
+
if (r.type === 'global_profile') {
|
|
464
|
+
return `
|
|
465
|
+
<div class="global-item">
|
|
466
|
+
<div class="global-item-header">
|
|
467
|
+
<span class="global-key-badge">${r.item.key}</span>
|
|
468
|
+
<span class="memory-date">${formatDate(r.item.createdAt)}</span>
|
|
469
|
+
</div>
|
|
470
|
+
<div class="global-content">${escapeHtml(r.item.content)}</div>
|
|
471
|
+
</div>
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return `
|
|
476
|
+
<div class="memory-item" data-id="${r.item.id}">
|
|
477
|
+
<div class="memory-item-header">
|
|
478
|
+
<div class="memory-item-meta">
|
|
479
|
+
<span class="memory-type-badge ${r.item.type}">${r.item.type}</span>
|
|
480
|
+
<span class="search-score">Score: ${r.score.toFixed(3)}</span>
|
|
481
|
+
<span class="memory-source">${r.source}</span>
|
|
482
|
+
</div>
|
|
483
|
+
<span class="memory-date">${formatDate(r.item.createdAt)}</span>
|
|
484
|
+
</div>
|
|
485
|
+
<div class="memory-content">${escapeHtml(truncate(r.item.content, 200))}</div>
|
|
486
|
+
<div class="memory-id">ID: ${r.item.id}</div>
|
|
487
|
+
</div>
|
|
488
|
+
`;
|
|
489
|
+
}).join('');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
infoContainer.innerHTML = `
|
|
493
|
+
Found ${result.total} results for "<strong>${escapeHtml(query)}</strong>"
|
|
494
|
+
(Scope: ${scope}${result.usingRealEmbeddings ? '' : ' - using fallback embeddings'})
|
|
495
|
+
`;
|
|
496
|
+
} catch (error) {
|
|
497
|
+
container.innerHTML = `<div class="empty-state"><h3>Search failed</h3><p>${error.message}</p></div>`;
|
|
498
|
+
showToast('Search failed', 'error');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Global Profiles
|
|
503
|
+
async function loadGlobalProfiles() {
|
|
504
|
+
try {
|
|
505
|
+
const result = await api('/stats/global');
|
|
506
|
+
state.globalProfiles = result.profiles;
|
|
507
|
+
renderGlobalProfiles();
|
|
508
|
+
} catch (error) {
|
|
509
|
+
showToast('Failed to load global profiles', 'error');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function renderGlobalProfiles() {
|
|
514
|
+
const container = document.getElementById('global-profiles');
|
|
515
|
+
const clearBtn = document.getElementById('clear-global-btn');
|
|
516
|
+
|
|
517
|
+
// Show/hide clear button based on whether there are profiles
|
|
518
|
+
if (clearBtn) {
|
|
519
|
+
clearBtn.style.display = state.globalProfiles.length > 0 ? 'inline-block' : 'none';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (state.globalProfiles.length === 0) {
|
|
523
|
+
container.innerHTML = `
|
|
524
|
+
<div class="empty-state">
|
|
525
|
+
<h3>No global profiles yet</h3>
|
|
526
|
+
<p>Use Claude's memextend_save_global tool to add cross-project preferences.</p>
|
|
527
|
+
</div>
|
|
528
|
+
`;
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
container.innerHTML = state.globalProfiles.map(p => `
|
|
533
|
+
<div class="global-item" data-global-id="${p.id}">
|
|
534
|
+
<div class="global-item-header">
|
|
535
|
+
<span class="global-key-badge">${p.key}</span>
|
|
536
|
+
<span class="memory-date">${formatDate(p.createdAt)}</span>
|
|
537
|
+
<button class="btn btn-danger btn-tiny delete-global-item" data-id="${p.id}" title="Delete this entry">×</button>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="global-content">${escapeHtml(p.content)}</div>
|
|
540
|
+
<div class="memory-id">ID: ${p.id}</div>
|
|
541
|
+
</div>
|
|
542
|
+
`).join('');
|
|
543
|
+
|
|
544
|
+
// Add click handlers for individual delete buttons
|
|
545
|
+
container.querySelectorAll('.delete-global-item').forEach(btn => {
|
|
546
|
+
btn.addEventListener('click', async (e) => {
|
|
547
|
+
e.stopPropagation();
|
|
548
|
+
const id = btn.dataset.id;
|
|
549
|
+
if (confirm('Delete this global profile entry?')) {
|
|
550
|
+
await deleteGlobalProfile(id);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Delete a single global profile
|
|
557
|
+
async function deleteGlobalProfile(id) {
|
|
558
|
+
try {
|
|
559
|
+
await api(`/stats/global/${id}`, { method: 'DELETE' });
|
|
560
|
+
showToast('Global profile entry deleted', 'success');
|
|
561
|
+
loadGlobalProfiles();
|
|
562
|
+
} catch (error) {
|
|
563
|
+
showToast('Failed to delete global profile entry', 'error');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Clear all global profiles
|
|
568
|
+
async function clearAllGlobalProfiles() {
|
|
569
|
+
try {
|
|
570
|
+
const result = await api('/stats/global', { method: 'DELETE' });
|
|
571
|
+
showToast(`Cleared ${result.deleted} global profile entries`, 'success');
|
|
572
|
+
closeModals();
|
|
573
|
+
loadGlobalProfiles();
|
|
574
|
+
} catch (error) {
|
|
575
|
+
showToast('Failed to clear global profiles', 'error');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Delete a project and all its memories
|
|
580
|
+
async function deleteProject(projectId) {
|
|
581
|
+
try {
|
|
582
|
+
const result = await api(`/projects/${projectId}`, { method: 'DELETE' });
|
|
583
|
+
showToast(`Deleted project with ${result.memoriesDeleted} memories`, 'success');
|
|
584
|
+
closeModals();
|
|
585
|
+
loadProjects();
|
|
586
|
+
loadMemories();
|
|
587
|
+
if (state.currentView === 'dashboard') {
|
|
588
|
+
loadDashboard();
|
|
589
|
+
}
|
|
590
|
+
} catch (error) {
|
|
591
|
+
showToast('Failed to delete project', 'error');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Memory Modal
|
|
596
|
+
function setupModal() {
|
|
597
|
+
const modal = document.getElementById('memory-modal');
|
|
598
|
+
const deleteModal = document.getElementById('delete-modal');
|
|
599
|
+
|
|
600
|
+
// Close buttons
|
|
601
|
+
document.querySelectorAll('.modal-close').forEach(btn => {
|
|
602
|
+
btn.addEventListener('click', closeModals);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
document.getElementById('modal-cancel').addEventListener('click', closeModals);
|
|
606
|
+
document.getElementById('cancel-delete').addEventListener('click', closeModals);
|
|
607
|
+
|
|
608
|
+
// Click outside to close
|
|
609
|
+
modal.addEventListener('click', (e) => {
|
|
610
|
+
if (e.target === modal) closeModals();
|
|
611
|
+
});
|
|
612
|
+
deleteModal.addEventListener('click', (e) => {
|
|
613
|
+
if (e.target === deleteModal) closeModals();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Save button
|
|
617
|
+
document.getElementById('modal-save').addEventListener('click', saveMemory);
|
|
618
|
+
|
|
619
|
+
// Delete button
|
|
620
|
+
document.getElementById('modal-delete').addEventListener('click', () => {
|
|
621
|
+
document.getElementById('delete-modal').classList.add('active');
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Confirm delete
|
|
625
|
+
document.getElementById('confirm-delete').addEventListener('click', deleteMemory);
|
|
626
|
+
|
|
627
|
+
// Create memory buttons
|
|
628
|
+
document.getElementById('create-memory-btn').addEventListener('click', () => openCreateModal(false));
|
|
629
|
+
document.getElementById('create-global-btn').addEventListener('click', () => openCreateModal(true));
|
|
630
|
+
|
|
631
|
+
// Create modal events
|
|
632
|
+
document.getElementById('create-cancel').addEventListener('click', closeModals);
|
|
633
|
+
document.getElementById('create-save').addEventListener('click', createMemory);
|
|
634
|
+
|
|
635
|
+
const createModal = document.getElementById('create-modal');
|
|
636
|
+
createModal.addEventListener('click', (e) => {
|
|
637
|
+
if (e.target === createModal) closeModals();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Scope toggle in create modal
|
|
641
|
+
document.getElementById('create-scope').addEventListener('change', (e) => {
|
|
642
|
+
const projectGroup = document.getElementById('create-project-group');
|
|
643
|
+
projectGroup.style.display = e.target.value === 'project' ? 'block' : 'none';
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Memory item clicks
|
|
647
|
+
document.addEventListener('click', (e) => {
|
|
648
|
+
const memoryItem = e.target.closest('.memory-item');
|
|
649
|
+
if (memoryItem && memoryItem.dataset.id) {
|
|
650
|
+
openMemoryModal(memoryItem.dataset.id);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function openMemoryModal(id) {
|
|
656
|
+
try {
|
|
657
|
+
const memory = await api(`/memories/${id}`);
|
|
658
|
+
state.selectedMemory = memory;
|
|
659
|
+
|
|
660
|
+
document.getElementById('modal-id').textContent = memory.id;
|
|
661
|
+
document.getElementById('modal-type').textContent = memory.type;
|
|
662
|
+
document.getElementById('modal-type').className = `memory-type-badge ${memory.type}`;
|
|
663
|
+
document.getElementById('modal-date').textContent = formatDate(memory.createdAt);
|
|
664
|
+
document.getElementById('modal-content').value = memory.content;
|
|
665
|
+
document.getElementById('modal-source').textContent = memory.sourceTool || (memory.type === 'reasoning' ? 'reasoning' : 'manual');
|
|
666
|
+
document.getElementById('modal-project').textContent = memory.projectId || 'global';
|
|
667
|
+
|
|
668
|
+
document.getElementById('memory-modal').classList.add('active');
|
|
669
|
+
} catch (error) {
|
|
670
|
+
showToast('Failed to load memory details', 'error');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function closeModals() {
|
|
675
|
+
document.getElementById('memory-modal').classList.remove('active');
|
|
676
|
+
document.getElementById('delete-modal').classList.remove('active');
|
|
677
|
+
document.getElementById('create-modal').classList.remove('active');
|
|
678
|
+
document.getElementById('delete-project-modal').classList.remove('active');
|
|
679
|
+
document.getElementById('clear-global-modal').classList.remove('active');
|
|
680
|
+
state.selectedMemory = null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Create memory modal
|
|
684
|
+
function openCreateModal(isGlobal = false) {
|
|
685
|
+
const scopeSelect = document.getElementById('create-scope');
|
|
686
|
+
const projectGroup = document.getElementById('create-project-group');
|
|
687
|
+
const projectSelect = document.getElementById('create-project');
|
|
688
|
+
const contentArea = document.getElementById('create-content');
|
|
689
|
+
|
|
690
|
+
// Reset form
|
|
691
|
+
contentArea.value = '';
|
|
692
|
+
|
|
693
|
+
if (isGlobal) {
|
|
694
|
+
scopeSelect.value = 'global';
|
|
695
|
+
projectGroup.style.display = 'none';
|
|
696
|
+
} else {
|
|
697
|
+
scopeSelect.value = 'project';
|
|
698
|
+
projectGroup.style.display = 'block';
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Populate project dropdown
|
|
702
|
+
projectSelect.innerHTML = '';
|
|
703
|
+
if (state.projects && state.projects.length > 0) {
|
|
704
|
+
state.projects.forEach(p => {
|
|
705
|
+
const opt = document.createElement('option');
|
|
706
|
+
opt.value = p;
|
|
707
|
+
opt.textContent = p.length > 40 ? p.slice(0, 40) + '...' : p;
|
|
708
|
+
projectSelect.appendChild(opt);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
document.getElementById('create-modal').classList.add('active');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function createMemory() {
|
|
716
|
+
const scope = document.getElementById('create-scope').value;
|
|
717
|
+
const content = document.getElementById('create-content').value.trim();
|
|
718
|
+
|
|
719
|
+
if (!content) {
|
|
720
|
+
showToast('Content cannot be empty', 'error');
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let projectId = null;
|
|
725
|
+
if (scope === 'project') {
|
|
726
|
+
projectId = document.getElementById('create-project').value;
|
|
727
|
+
if (!projectId) {
|
|
728
|
+
showToast('Please select a project', 'error');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
try {
|
|
734
|
+
await api('/memories', {
|
|
735
|
+
method: 'POST',
|
|
736
|
+
body: JSON.stringify({
|
|
737
|
+
content,
|
|
738
|
+
projectId,
|
|
739
|
+
type: projectId ? 'manual' : 'global'
|
|
740
|
+
})
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
showToast('Memory created successfully', 'success');
|
|
744
|
+
closeModals();
|
|
745
|
+
|
|
746
|
+
// Refresh current view
|
|
747
|
+
if (state.currentView === 'dashboard') {
|
|
748
|
+
loadDashboard();
|
|
749
|
+
} else if (state.currentView === 'memories') {
|
|
750
|
+
loadMemories();
|
|
751
|
+
} else if (state.currentView === 'global') {
|
|
752
|
+
loadGlobalProfiles();
|
|
753
|
+
}
|
|
754
|
+
} catch (error) {
|
|
755
|
+
showToast('Failed to create memory', 'error');
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function saveMemory() {
|
|
760
|
+
if (!state.selectedMemory) return;
|
|
761
|
+
|
|
762
|
+
const content = document.getElementById('modal-content').value.trim();
|
|
763
|
+
if (!content) {
|
|
764
|
+
showToast('Content cannot be empty', 'error');
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
await api(`/memories/${state.selectedMemory.id}`, {
|
|
770
|
+
method: 'PUT',
|
|
771
|
+
body: JSON.stringify({ content })
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
showToast('Memory updated successfully', 'success');
|
|
775
|
+
closeModals();
|
|
776
|
+
|
|
777
|
+
// Refresh current view
|
|
778
|
+
if (state.currentView === 'dashboard') {
|
|
779
|
+
loadDashboard();
|
|
780
|
+
} else if (state.currentView === 'memories') {
|
|
781
|
+
loadMemories();
|
|
782
|
+
}
|
|
783
|
+
} catch (error) {
|
|
784
|
+
showToast('Failed to update memory', 'error');
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function deleteMemory() {
|
|
789
|
+
if (!state.selectedMemory) return;
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
await api(`/memories/${state.selectedMemory.id}`, {
|
|
793
|
+
method: 'DELETE'
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
showToast('Memory deleted successfully', 'success');
|
|
797
|
+
closeModals();
|
|
798
|
+
|
|
799
|
+
// Refresh current view
|
|
800
|
+
if (state.currentView === 'dashboard') {
|
|
801
|
+
loadDashboard();
|
|
802
|
+
} else if (state.currentView === 'memories') {
|
|
803
|
+
loadMemories();
|
|
804
|
+
}
|
|
805
|
+
} catch (error) {
|
|
806
|
+
showToast('Failed to delete memory', 'error');
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Settings
|
|
811
|
+
async function loadSettings() {
|
|
812
|
+
try {
|
|
813
|
+
const config = await api('/config');
|
|
814
|
+
state.settings = config;
|
|
815
|
+
populateSettingsForm(config);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
showToast('Failed to load settings', 'error');
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function populateSettingsForm(config) {
|
|
822
|
+
// Capture settings
|
|
823
|
+
document.getElementById('setting-capture-reasoning').checked = config.capture?.captureReasoning ?? true;
|
|
824
|
+
document.getElementById('setting-max-reasoning').value = config.capture?.maxReasoningLength ?? 10000;
|
|
825
|
+
document.getElementById('setting-max-tool').value = config.capture?.maxToolOutputLength ?? 2000;
|
|
826
|
+
|
|
827
|
+
// Tool toggles
|
|
828
|
+
document.getElementById('setting-tool-edit').checked = config.capture?.tools?.Edit ?? false;
|
|
829
|
+
document.getElementById('setting-tool-write').checked = config.capture?.tools?.Write ?? false;
|
|
830
|
+
document.getElementById('setting-tool-bash').checked = config.capture?.tools?.Bash ?? false;
|
|
831
|
+
document.getElementById('setting-tool-task').checked = config.capture?.tools?.Task ?? false;
|
|
832
|
+
|
|
833
|
+
// Retrieval settings
|
|
834
|
+
document.getElementById('setting-auto-inject').checked = config.retrieval?.autoInject ?? true;
|
|
835
|
+
document.getElementById('setting-max-memories').value = config.retrieval?.maxMemories ?? 0;
|
|
836
|
+
document.getElementById('setting-recent-days').value = config.retrieval?.recentDays ?? 0;
|
|
837
|
+
document.getElementById('setting-include-global').checked = config.retrieval?.includeGlobal ?? true;
|
|
838
|
+
document.getElementById('setting-dedup-threshold').value = config.retrieval?.deduplicationThreshold ?? 0.85;
|
|
839
|
+
document.getElementById('setting-session-max-chars').value = config.retrieval?.sessionMaxChars ?? 10000;
|
|
840
|
+
document.getElementById('setting-compact-max-chars').value = config.retrieval?.compactMaxChars ?? 2000;
|
|
841
|
+
|
|
842
|
+
// Storage limits
|
|
843
|
+
document.getElementById('setting-max-per-project').value = config.storage?.maxMemoriesPerProject ?? 500;
|
|
844
|
+
document.getElementById('setting-max-total').value = config.storage?.maxTotalMemories ?? 5000;
|
|
845
|
+
document.getElementById('setting-dedupe-on-prune').checked = config.storage?.deduplicateOnPrune ?? true;
|
|
846
|
+
|
|
847
|
+
// System settings
|
|
848
|
+
document.getElementById('setting-debug').checked = config.debug ?? false;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function getSettingsFromForm() {
|
|
852
|
+
return {
|
|
853
|
+
capture: {
|
|
854
|
+
captureReasoning: document.getElementById('setting-capture-reasoning').checked,
|
|
855
|
+
maxReasoningLength: parseInt(document.getElementById('setting-max-reasoning').value, 10),
|
|
856
|
+
maxToolOutputLength: parseInt(document.getElementById('setting-max-tool').value, 10),
|
|
857
|
+
tools: {
|
|
858
|
+
Edit: document.getElementById('setting-tool-edit').checked,
|
|
859
|
+
Write: document.getElementById('setting-tool-write').checked,
|
|
860
|
+
Bash: document.getElementById('setting-tool-bash').checked,
|
|
861
|
+
Task: document.getElementById('setting-tool-task').checked
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
retrieval: {
|
|
865
|
+
autoInject: document.getElementById('setting-auto-inject').checked,
|
|
866
|
+
maxMemories: parseInt(document.getElementById('setting-max-memories').value, 10),
|
|
867
|
+
recentDays: parseInt(document.getElementById('setting-recent-days').value, 10),
|
|
868
|
+
includeGlobal: document.getElementById('setting-include-global').checked,
|
|
869
|
+
deduplicationThreshold: parseFloat(document.getElementById('setting-dedup-threshold').value),
|
|
870
|
+
sessionMaxChars: parseInt(document.getElementById('setting-session-max-chars').value, 10),
|
|
871
|
+
compactMaxChars: parseInt(document.getElementById('setting-compact-max-chars').value, 10)
|
|
872
|
+
},
|
|
873
|
+
storage: {
|
|
874
|
+
maxMemoriesPerProject: parseInt(document.getElementById('setting-max-per-project').value, 10),
|
|
875
|
+
maxTotalMemories: parseInt(document.getElementById('setting-max-total').value, 10),
|
|
876
|
+
deduplicateOnPrune: document.getElementById('setting-dedupe-on-prune').checked
|
|
877
|
+
},
|
|
878
|
+
debug: document.getElementById('setting-debug').checked
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async function saveSettings() {
|
|
883
|
+
const config = getSettingsFromForm();
|
|
884
|
+
|
|
885
|
+
// Validation
|
|
886
|
+
if (config.capture.maxReasoningLength < 100 || config.capture.maxReasoningLength > 100000) {
|
|
887
|
+
showToast('Max reasoning length must be between 100 and 100,000', 'error');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (config.capture.maxToolOutputLength < 100 || config.capture.maxToolOutputLength > 50000) {
|
|
891
|
+
showToast('Max tool output length must be between 100 and 50,000', 'error');
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (config.retrieval.maxMemories < 0 || config.retrieval.maxMemories > 100) {
|
|
895
|
+
showToast('Max memories must be between 0 and 100 (0 = unlimited)', 'error');
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (config.retrieval.recentDays < 0 || config.retrieval.recentDays > 365) {
|
|
899
|
+
showToast('Recent days must be between 0 and 365 (0 = unlimited)', 'error');
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (config.retrieval.deduplicationThreshold < 0 || config.retrieval.deduplicationThreshold > 1) {
|
|
903
|
+
showToast('Deduplication threshold must be between 0 and 1', 'error');
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
await api('/config', {
|
|
909
|
+
method: 'PUT',
|
|
910
|
+
body: JSON.stringify(config)
|
|
911
|
+
});
|
|
912
|
+
state.settings = config;
|
|
913
|
+
showToast('Settings saved successfully', 'success');
|
|
914
|
+
} catch (error) {
|
|
915
|
+
showToast('Failed to save settings', 'error');
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async function resetSettings() {
|
|
920
|
+
try {
|
|
921
|
+
const defaults = await api('/config/defaults');
|
|
922
|
+
populateSettingsForm(defaults);
|
|
923
|
+
showToast('Settings reset to defaults (not saved yet)', 'info');
|
|
924
|
+
} catch (error) {
|
|
925
|
+
showToast('Failed to load defaults', 'error');
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function setupSettings() {
|
|
930
|
+
const saveBtn = document.getElementById('settings-save');
|
|
931
|
+
const resetBtn = document.getElementById('settings-reset');
|
|
932
|
+
|
|
933
|
+
if (saveBtn) {
|
|
934
|
+
saveBtn.addEventListener('click', saveSettings);
|
|
935
|
+
}
|
|
936
|
+
if (resetBtn) {
|
|
937
|
+
resetBtn.addEventListener('click', resetSettings);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Initialize
|
|
942
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
943
|
+
setupNavigation();
|
|
944
|
+
setupFilters();
|
|
945
|
+
setupSearch();
|
|
946
|
+
setupModal();
|
|
947
|
+
setupSettings();
|
|
948
|
+
setupDeleteProjectModal();
|
|
949
|
+
setupClearGlobalModal();
|
|
950
|
+
|
|
951
|
+
// Load initial view
|
|
952
|
+
loadDashboard();
|
|
953
|
+
});
|