@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.
@@ -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
+ });