superlocalmemory 2.4.2 → 2.5.1

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/ui/js/graph.js ADDED
@@ -0,0 +1,32 @@
1
+ // SuperLocalMemory V2 - Knowledge Graph (D3.js force-directed)
2
+ // Depends on: core.js, modal.js (openMemoryDetail)
3
+
4
+ var graphData = { nodes: [], links: [] };
5
+
6
+ async function loadGraph() {
7
+ var maxNodes = document.getElementById('graph-max-nodes').value;
8
+ try {
9
+ var response = await fetch('/api/graph?max_nodes=' + maxNodes);
10
+ graphData = await response.json();
11
+ renderGraph(graphData);
12
+ } catch (error) {
13
+ console.error('Error loading graph:', error);
14
+ }
15
+ }
16
+
17
+ function renderGraph(data) {
18
+ var container = document.getElementById('graph-container');
19
+ container.textContent = '';
20
+ var width = container.clientWidth || 1200;
21
+ var height = 600;
22
+ var svg = d3.select('#graph-container').append('svg').attr('width', width).attr('height', height);
23
+ var tooltip = d3.select('body').append('div').attr('class', 'tooltip-custom').style('opacity', 0);
24
+ var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
25
+ var simulation = d3.forceSimulation(data.nodes).force('link', d3.forceLink(data.links).id(function(d) { return d.id; }).distance(100)).force('charge', d3.forceManyBody().strength(-200)).force('center', d3.forceCenter(width / 2, height / 2)).force('collision', d3.forceCollide().radius(20));
26
+ var link = svg.append('g').selectAll('line').data(data.links).enter().append('line').attr('class', 'link').attr('stroke-width', function(d) { return Math.sqrt(d.weight * 2); });
27
+ var node = svg.append('g').selectAll('circle').data(data.nodes).enter().append('circle').attr('class', 'node').attr('r', function(d) { return 5 + (d.importance || 5); }).attr('fill', function(d) { return colorScale(d.cluster_id || 0); }).call(d3.drag().on('start', dragStarted).on('drag', dragged).on('end', dragEnded)).on('mouseover', function(event, d) { tooltip.transition().duration(200).style('opacity', .9); var label = d.category || d.project_name || 'Memory #' + d.id; tooltip.text(label + ': ' + (d.content_preview || d.summary || 'No content')).style('left', (event.pageX + 10) + 'px').style('top', (event.pageY - 28) + 'px'); }).on('mouseout', function() { tooltip.transition().duration(500).style('opacity', 0); }).on('click', function(event, d) { openMemoryDetail(d); });
28
+ simulation.on('tick', function() { link.attr('x1', function(d) { return d.source.x; }).attr('y1', function(d) { return d.source.y; }).attr('x2', function(d) { return d.target.x; }).attr('y2', function(d) { return d.target.y; }); node.attr('cx', function(d) { return d.x; }).attr('cy', function(d) { return d.y; }); });
29
+ function dragStarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }
30
+ function dragged(event, d) { d.fx = event.x; d.fy = event.y; }
31
+ function dragEnded(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }
32
+ }
package/ui/js/init.js ADDED
@@ -0,0 +1,31 @@
1
+ // SuperLocalMemory V2 - Event Listener Wiring
2
+ // Loads LAST — after all module scripts. Connects UI events to module functions.
3
+
4
+ document.getElementById('memories-tab').addEventListener('shown.bs.tab', loadMemories);
5
+ document.getElementById('clusters-tab').addEventListener('shown.bs.tab', loadClusters);
6
+ document.getElementById('patterns-tab').addEventListener('shown.bs.tab', loadPatterns);
7
+ document.getElementById('timeline-tab').addEventListener('shown.bs.tab', loadTimeline);
8
+ document.getElementById('settings-tab').addEventListener('shown.bs.tab', loadSettings);
9
+ document.getElementById('search-query').addEventListener('keypress', function(e) { if (e.key === 'Enter') searchMemories(); });
10
+
11
+ document.getElementById('profile-select').addEventListener('change', function() {
12
+ switchProfile(this.value);
13
+ });
14
+
15
+ document.getElementById('add-profile-btn').addEventListener('click', function() {
16
+ createProfile();
17
+ });
18
+
19
+ var newProfileInput = document.getElementById('new-profile-name');
20
+ if (newProfileInput) {
21
+ newProfileInput.addEventListener('keypress', function(e) {
22
+ if (e.key === 'Enter') createProfile();
23
+ });
24
+ }
25
+
26
+ // v2.5 tabs (graceful — elements may not exist on older installs)
27
+ var eventsTab = document.getElementById('events-tab');
28
+ if (eventsTab) eventsTab.addEventListener('shown.bs.tab', loadEventStats);
29
+
30
+ var agentsTab = document.getElementById('agents-tab');
31
+ if (agentsTab) agentsTab.addEventListener('shown.bs.tab', loadAgents);
@@ -0,0 +1,149 @@
1
+ // SuperLocalMemory V2 - Memories List + Sorting
2
+ // Depends on: core.js, modal.js (openMemoryDetail)
3
+ //
4
+ // Security: All dynamic values are escaped via escapeHtml() before DOM insertion.
5
+ // The innerHTML usage in renderMemoriesTable is safe because every interpolated
6
+ // value passes through escapeHtml(). Data comes from our own local SQLite DB only.
7
+ // nosemgrep: innerHTML-xss — all dynamic values escaped via escapeHtml()
8
+
9
+ async function loadMemories() {
10
+ var category = document.getElementById('filter-category').value;
11
+ var project = document.getElementById('filter-project').value;
12
+ var url = '/api/memories?limit=50';
13
+ if (category) url += '&category=' + encodeURIComponent(category);
14
+ if (project) url += '&project_name=' + encodeURIComponent(project);
15
+
16
+ showLoading('memories-list', 'Loading memories...');
17
+ try {
18
+ var response = await fetch(url);
19
+ var data = await response.json();
20
+ lastSearchResults = null;
21
+ var exportBtn = document.getElementById('export-search-btn');
22
+ if (exportBtn) exportBtn.style.display = 'none';
23
+ renderMemoriesTable(data.memories, false);
24
+ } catch (error) {
25
+ console.error('Error loading memories:', error);
26
+ showEmpty('memories-list', 'exclamation-triangle', 'Failed to load memories');
27
+ }
28
+ }
29
+
30
+ function renderMemoriesTable(memories, showScores) {
31
+ var container = document.getElementById('memories-list');
32
+ if (!memories || memories.length === 0) {
33
+ showEmpty('memories-list', 'journal-x', 'No memories found. Try a different search or filter.');
34
+ return;
35
+ }
36
+
37
+ window._slmMemories = memories;
38
+ var scoreHeader = showScores ? '<th>Score</th>' : '';
39
+
40
+ var rows = '';
41
+ memories.forEach(function(mem, idx) {
42
+ var content = mem.summary || mem.content || '';
43
+ var contentPreview = content.length > 100 ? content.substring(0, 100) + '...' : content;
44
+ var importance = mem.importance || 5;
45
+ var importanceClass = importance >= 8 ? 'success' : importance >= 5 ? 'warning' : 'secondary';
46
+
47
+ var scoreCell = '';
48
+ if (showScores) {
49
+ var score = mem.score || 0;
50
+ var pct = Math.round(score * 100);
51
+ var barColor = pct >= 70 ? '#43e97b' : pct >= 40 ? '#f9c74f' : '#f94144';
52
+ scoreCell = '<td><span class="score-label">' + escapeHtml(String(pct)) + '%</span>'
53
+ + '<div class="score-bar-container"><div class="score-bar">'
54
+ + '<div class="score-bar-fill" style="width:' + pct + '%;background:' + barColor + '"></div>'
55
+ + '</div></div></td>';
56
+ }
57
+
58
+ rows += '<tr data-mem-idx="' + idx + '">'
59
+ + '<td>' + escapeHtml(String(mem.id)) + '</td>'
60
+ + '<td><span class="badge bg-primary">' + escapeHtml(mem.category || 'None') + '</span></td>'
61
+ + '<td><small>' + escapeHtml(mem.project_name || '-') + '</small></td>'
62
+ + '<td class="memory-content" title="' + escapeHtml(content) + '">' + escapeHtml(contentPreview) + '</td>'
63
+ + scoreCell
64
+ + '<td><span class="badge bg-' + importanceClass + ' badge-importance">' + escapeHtml(String(importance)) + '</span></td>'
65
+ + '<td>' + escapeHtml(String(mem.cluster_id || '-')) + '</td>'
66
+ + '<td><small>' + escapeHtml(formatDate(mem.created_at)) + '</small></td>'
67
+ + '</tr>';
68
+ });
69
+
70
+ var tableHtml = '<table class="table table-hover memory-table"><thead><tr>'
71
+ + '<th class="sortable" data-sort="id">ID</th>'
72
+ + '<th class="sortable" data-sort="category">Category</th>'
73
+ + '<th class="sortable" data-sort="project">Project</th>'
74
+ + '<th>Content</th>'
75
+ + scoreHeader
76
+ + '<th class="sortable" data-sort="importance">Importance</th>'
77
+ + '<th>Cluster</th>'
78
+ + '<th class="sortable" data-sort="created">Created</th>'
79
+ + '</tr></thead><tbody>' + rows + '</tbody></table>';
80
+
81
+ // All values above escaped via escapeHtml() — safe for trusted local data
82
+ container.textContent = '';
83
+ container.insertAdjacentHTML('beforeend', tableHtml);
84
+
85
+ var table = container.querySelector('table');
86
+ if (table) {
87
+ table.addEventListener('click', function(e) {
88
+ var th = e.target.closest('th.sortable');
89
+ if (th) { handleSort(th); return; }
90
+ var row = e.target.closest('tr[data-mem-idx]');
91
+ if (row) {
92
+ var idx = parseInt(row.getAttribute('data-mem-idx'), 10);
93
+ if (window._slmMemories && window._slmMemories[idx]) {
94
+ openMemoryDetail(window._slmMemories[idx]);
95
+ }
96
+ }
97
+ });
98
+ }
99
+ }
100
+
101
+ // ============================================================================
102
+ // Column Sorting
103
+ // ============================================================================
104
+
105
+ var currentSort = { column: null, direction: 'asc' };
106
+
107
+ function handleSort(th) {
108
+ var col = th.getAttribute('data-sort');
109
+ if (!col) return;
110
+
111
+ if (currentSort.column === col) {
112
+ currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
113
+ } else {
114
+ currentSort.column = col;
115
+ currentSort.direction = 'asc';
116
+ }
117
+
118
+ document.querySelectorAll('#memories-list th.sortable').forEach(function(h) {
119
+ h.classList.remove('sort-asc', 'sort-desc');
120
+ });
121
+ th.classList.add('sort-' + currentSort.direction);
122
+
123
+ if (!window._slmMemories) return;
124
+ var memories = window._slmMemories.slice();
125
+ var dir = currentSort.direction === 'asc' ? 1 : -1;
126
+
127
+ memories.sort(function(a, b) {
128
+ var av, bv;
129
+ switch (col) {
130
+ case 'id': return ((a.id || 0) - (b.id || 0)) * dir;
131
+ case 'importance': return ((a.importance || 0) - (b.importance || 0)) * dir;
132
+ case 'category':
133
+ av = (a.category || '').toLowerCase(); bv = (b.category || '').toLowerCase();
134
+ return av < bv ? -dir : av > bv ? dir : 0;
135
+ case 'project':
136
+ av = (a.project_name || '').toLowerCase(); bv = (b.project_name || '').toLowerCase();
137
+ return av < bv ? -dir : av > bv ? dir : 0;
138
+ case 'created':
139
+ av = a.created_at || ''; bv = b.created_at || '';
140
+ return av < bv ? -dir : av > bv ? dir : 0;
141
+ case 'score': return ((a.score || 0) - (b.score || 0)) * dir;
142
+ default: return 0;
143
+ }
144
+ });
145
+
146
+ window._slmMemories = memories;
147
+ var showScores = memories.length > 0 && typeof memories[0].score === 'number';
148
+ renderMemoriesTable(memories, showScores);
149
+ }
package/ui/js/modal.js ADDED
@@ -0,0 +1,139 @@
1
+ // SuperLocalMemory V2 - Memory Detail Modal + Copy/Export
2
+ // Depends on: core.js
3
+ //
4
+ // Security: All dynamic values escaped via escapeHtml(). Data from local DB only.
5
+ // nosemgrep: innerHTML-xss — all dynamic values escaped
6
+
7
+ var currentMemoryDetail = null;
8
+
9
+ function openMemoryDetail(mem) {
10
+ currentMemoryDetail = mem;
11
+ var body = document.getElementById('memory-detail-body');
12
+ if (!mem) {
13
+ body.textContent = 'No memory data';
14
+ return;
15
+ }
16
+
17
+ var content = mem.content || mem.summary || '(no content)';
18
+ var tags = mem.tags || '';
19
+ var importance = mem.importance || 5;
20
+ var importanceClass = importance >= 8 ? 'success' : importance >= 5 ? 'warning' : 'secondary';
21
+
22
+ // Build detail using DOM nodes for safety
23
+ body.textContent = '';
24
+
25
+ var contentDiv = document.createElement('div');
26
+ contentDiv.className = 'memory-detail-content';
27
+ contentDiv.textContent = content;
28
+ body.appendChild(contentDiv);
29
+
30
+ body.appendChild(document.createElement('hr'));
31
+
32
+ var dl = document.createElement('dl');
33
+ dl.className = 'memory-detail-meta row';
34
+
35
+ // Left column
36
+ var col1 = document.createElement('div');
37
+ col1.className = 'col-md-6';
38
+ addDetailRow(col1, 'ID', String(mem.id || '-'));
39
+ addDetailBadgeRow(col1, 'Category', mem.category || 'None', 'bg-primary');
40
+ addDetailRow(col1, 'Project', mem.project_name || '-');
41
+ addDetailTagsRow(col1, 'Tags', tags);
42
+ dl.appendChild(col1);
43
+
44
+ // Right column
45
+ var col2 = document.createElement('div');
46
+ col2.className = 'col-md-6';
47
+ addDetailBadgeRow(col2, 'Importance', importance + '/10', 'bg-' + importanceClass);
48
+ addDetailRow(col2, 'Cluster', String(mem.cluster_id || '-'));
49
+ addDetailRow(col2, 'Created', formatDateFull(mem.created_at));
50
+ if (mem.updated_at) addDetailRow(col2, 'Updated', formatDateFull(mem.updated_at));
51
+
52
+ if (typeof mem.score === 'number') {
53
+ var pct = Math.round(mem.score * 100);
54
+ addDetailRow(col2, 'Relevance Score', pct + '%');
55
+ }
56
+ dl.appendChild(col2);
57
+
58
+ body.appendChild(dl);
59
+
60
+ var modal = new bootstrap.Modal(document.getElementById('memoryDetailModal'));
61
+ modal.show();
62
+ }
63
+
64
+ function addDetailRow(parent, label, value) {
65
+ var dt = document.createElement('dt');
66
+ dt.textContent = label;
67
+ parent.appendChild(dt);
68
+ var dd = document.createElement('dd');
69
+ dd.textContent = value;
70
+ parent.appendChild(dd);
71
+ }
72
+
73
+ function addDetailBadgeRow(parent, label, value, badgeClass) {
74
+ var dt = document.createElement('dt');
75
+ dt.textContent = label;
76
+ parent.appendChild(dt);
77
+ var dd = document.createElement('dd');
78
+ var badge = document.createElement('span');
79
+ badge.className = 'badge ' + badgeClass;
80
+ badge.textContent = value;
81
+ dd.appendChild(badge);
82
+ parent.appendChild(dd);
83
+ }
84
+
85
+ function addDetailTagsRow(parent, label, tags) {
86
+ var dt = document.createElement('dt');
87
+ dt.textContent = label;
88
+ parent.appendChild(dt);
89
+ var dd = document.createElement('dd');
90
+ var tagList = typeof tags === 'string' ? tags.split(',') : (tags || []);
91
+ if (tagList.length === 0 || (tagList.length === 1 && !tagList[0].trim())) {
92
+ dd.className = 'text-muted';
93
+ dd.textContent = 'None';
94
+ } else {
95
+ tagList.forEach(function(t) {
96
+ var tag = t.trim();
97
+ if (tag) {
98
+ var badge = document.createElement('span');
99
+ badge.className = 'badge bg-secondary me-1';
100
+ badge.textContent = tag;
101
+ dd.appendChild(badge);
102
+ }
103
+ });
104
+ }
105
+ parent.appendChild(dd);
106
+ }
107
+
108
+ function copyMemoryToClipboard() {
109
+ if (!currentMemoryDetail) return;
110
+ var text = currentMemoryDetail.content || currentMemoryDetail.summary || '';
111
+ navigator.clipboard.writeText(text).then(function() {
112
+ showToast('Copied to clipboard');
113
+ }).catch(function() {
114
+ var ta = document.createElement('textarea');
115
+ ta.value = text;
116
+ document.body.appendChild(ta);
117
+ ta.select();
118
+ document.execCommand('copy');
119
+ document.body.removeChild(ta);
120
+ showToast('Copied to clipboard');
121
+ });
122
+ }
123
+
124
+ function exportMemoryAsMarkdown() {
125
+ if (!currentMemoryDetail) return;
126
+ var mem = currentMemoryDetail;
127
+ var md = '# Memory #' + (mem.id || 'unknown') + '\n\n';
128
+ md += '**Category:** ' + (mem.category || 'None') + ' \n';
129
+ md += '**Project:** ' + (mem.project_name || '-') + ' \n';
130
+ md += '**Importance:** ' + (mem.importance || 5) + '/10 \n';
131
+ md += '**Tags:** ' + (mem.tags || 'None') + ' \n';
132
+ md += '**Created:** ' + (mem.created_at || '-') + ' \n';
133
+ if (mem.cluster_id) md += '**Cluster:** ' + mem.cluster_id + ' \n';
134
+ md += '\n---\n\n';
135
+ md += mem.content || mem.summary || '(no content)';
136
+ md += '\n\n---\n*Exported from SuperLocalMemory V2*\n';
137
+
138
+ downloadFile('memory-' + (mem.id || 'export') + '.md', md, 'text/markdown');
139
+ }
@@ -0,0 +1,93 @@
1
+ // SuperLocalMemory V2 - Patterns View (Layer 4)
2
+ // Depends on: core.js
3
+
4
+ async function loadPatterns() {
5
+ showLoading('patterns-list', 'Loading patterns...');
6
+ try {
7
+ var response = await fetch('/api/patterns');
8
+ var data = await response.json();
9
+ renderPatterns(data.patterns);
10
+ } catch (error) {
11
+ console.error('Error loading patterns:', error);
12
+ showEmpty('patterns-list', 'puzzle', 'Failed to load patterns');
13
+ }
14
+ }
15
+
16
+ function renderPatterns(patterns) {
17
+ var container = document.getElementById('patterns-list');
18
+ if (!patterns || Object.keys(patterns).length === 0) {
19
+ showEmpty('patterns-list', 'puzzle', 'No patterns learned yet. Use SuperLocalMemory for a while to build patterns.');
20
+ return;
21
+ }
22
+
23
+ var typeIcons = { preference: 'heart', style: 'palette', terminology: 'code-slash' };
24
+ var typeLabels = { preference: 'Preferences', style: 'Coding Style', terminology: 'Terminology' };
25
+
26
+ container.textContent = '';
27
+
28
+ for (var type in patterns) {
29
+ if (!patterns.hasOwnProperty(type)) continue;
30
+ var items = patterns[type];
31
+
32
+ var header = document.createElement('h6');
33
+ header.className = 'mt-3 mb-2';
34
+ var icon = document.createElement('i');
35
+ icon.className = 'bi bi-' + (typeIcons[type] || 'puzzle') + ' me-1';
36
+ header.appendChild(icon);
37
+ header.appendChild(document.createTextNode(typeLabels[type] || type));
38
+ var countBadge = document.createElement('span');
39
+ countBadge.className = 'badge bg-secondary ms-2';
40
+ countBadge.textContent = items.length;
41
+ header.appendChild(countBadge);
42
+ container.appendChild(header);
43
+
44
+ var group = document.createElement('div');
45
+ group.className = 'list-group mb-3';
46
+
47
+ items.forEach(function(pattern) {
48
+ var pct = Math.round(pattern.confidence * 100);
49
+ var barColor = pct >= 60 ? '#43e97b' : pct >= 40 ? '#f9c74f' : '#6c757d';
50
+ var badgeClass = pct >= 60 ? 'bg-success' : pct >= 40 ? 'bg-warning text-dark' : 'bg-secondary';
51
+
52
+ var item = document.createElement('div');
53
+ item.className = 'list-group-item';
54
+
55
+ var topRow = document.createElement('div');
56
+ topRow.className = 'd-flex justify-content-between align-items-center';
57
+ var keyEl = document.createElement('strong');
58
+ keyEl.textContent = pattern.key;
59
+ var badge = document.createElement('span');
60
+ badge.className = 'badge ' + badgeClass;
61
+ badge.textContent = pct + '%';
62
+ topRow.appendChild(keyEl);
63
+ topRow.appendChild(badge);
64
+ item.appendChild(topRow);
65
+
66
+ var barContainer = document.createElement('div');
67
+ barContainer.className = 'confidence-bar';
68
+ var barFill = document.createElement('div');
69
+ barFill.className = 'confidence-fill';
70
+ barFill.style.width = pct + '%';
71
+ barFill.style.background = barColor;
72
+ barContainer.appendChild(barFill);
73
+ item.appendChild(barContainer);
74
+
75
+ var valueEl = document.createElement('div');
76
+ valueEl.className = 'mt-1';
77
+ var valueSmall = document.createElement('small');
78
+ valueSmall.className = 'text-muted';
79
+ valueSmall.textContent = typeof pattern.value === 'string' ? pattern.value : JSON.stringify(pattern.value);
80
+ valueEl.appendChild(valueSmall);
81
+ item.appendChild(valueEl);
82
+
83
+ var evidenceEl = document.createElement('small');
84
+ evidenceEl.className = 'text-muted';
85
+ evidenceEl.textContent = 'Evidence: ' + (pattern.evidence_count || '?') + ' memories';
86
+ item.appendChild(evidenceEl);
87
+
88
+ group.appendChild(item);
89
+ });
90
+
91
+ container.appendChild(group);
92
+ }
93
+ }
@@ -0,0 +1,202 @@
1
+ // SuperLocalMemory V2 - Profile Management
2
+ // Depends on: core.js
3
+
4
+ async function loadProfiles() {
5
+ try {
6
+ var response = await fetch('/api/profiles');
7
+ var data = await response.json();
8
+ var select = document.getElementById('profile-select');
9
+ select.textContent = '';
10
+ var profiles = data.profiles || [];
11
+ var active = data.active_profile || 'default';
12
+
13
+ profiles.forEach(function(p) {
14
+ var opt = document.createElement('option');
15
+ opt.value = p.name;
16
+ opt.textContent = p.name + (p.memory_count ? ' (' + p.memory_count + ')' : '');
17
+ if (p.name === active) opt.selected = true;
18
+ select.appendChild(opt);
19
+ });
20
+ } catch (error) {
21
+ console.error('Error loading profiles:', error);
22
+ }
23
+ }
24
+
25
+ async function createProfile(nameOverride) {
26
+ var name = nameOverride || document.getElementById('new-profile-name').value.trim();
27
+ if (!name) {
28
+ name = prompt('Enter new profile name:');
29
+ if (!name || !name.trim()) return;
30
+ name = name.trim();
31
+ }
32
+
33
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
34
+ showToast('Invalid name. Use letters, numbers, dashes, underscores.');
35
+ return;
36
+ }
37
+
38
+ try {
39
+ var response = await fetch('/api/profiles/create', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ profile_name: name })
43
+ });
44
+ var data = await response.json();
45
+ if (response.status === 409) {
46
+ showToast('Profile "' + name + '" already exists');
47
+ return;
48
+ }
49
+ if (!response.ok) {
50
+ showToast(data.detail || 'Failed to create profile');
51
+ return;
52
+ }
53
+ showToast('Profile "' + name + '" created');
54
+ var input = document.getElementById('new-profile-name');
55
+ if (input) input.value = '';
56
+ loadProfiles();
57
+ loadProfilesTable();
58
+ } catch (error) {
59
+ console.error('Error creating profile:', error);
60
+ showToast('Error creating profile');
61
+ }
62
+ }
63
+
64
+ async function deleteProfile(name) {
65
+ if (name === 'default') {
66
+ showToast('Cannot delete the default profile');
67
+ return;
68
+ }
69
+ if (!confirm('Delete profile "' + name + '"?\nIts memories will be moved to the default profile.')) {
70
+ return;
71
+ }
72
+ try {
73
+ var response = await fetch('/api/profiles/' + encodeURIComponent(name), {
74
+ method: 'DELETE'
75
+ });
76
+ var data = await response.json();
77
+ if (!response.ok) {
78
+ showToast(data.detail || 'Failed to delete profile');
79
+ return;
80
+ }
81
+ showToast(data.message || 'Profile deleted');
82
+ loadProfiles();
83
+ loadProfilesTable();
84
+ loadStats();
85
+ } catch (error) {
86
+ console.error('Error deleting profile:', error);
87
+ showToast('Error deleting profile');
88
+ }
89
+ }
90
+
91
+ async function loadProfilesTable() {
92
+ var container = document.getElementById('profiles-table');
93
+ if (!container) return;
94
+ try {
95
+ var response = await fetch('/api/profiles');
96
+ var data = await response.json();
97
+ var profiles = data.profiles || [];
98
+ var active = data.active_profile || 'default';
99
+
100
+ if (profiles.length === 0) {
101
+ showEmpty('profiles-table', 'people', 'No profiles found.');
102
+ return;
103
+ }
104
+
105
+ var table = document.createElement('table');
106
+ table.className = 'table table-sm mb-0';
107
+ var thead = document.createElement('thead');
108
+ var headRow = document.createElement('tr');
109
+ ['Name', 'Memories', 'Status', 'Actions'].forEach(function(h) {
110
+ var th = document.createElement('th');
111
+ th.textContent = h;
112
+ headRow.appendChild(th);
113
+ });
114
+ thead.appendChild(headRow);
115
+ table.appendChild(thead);
116
+
117
+ var tbody = document.createElement('tbody');
118
+ profiles.forEach(function(p) {
119
+ var row = document.createElement('tr');
120
+
121
+ var nameCell = document.createElement('td');
122
+ var nameIcon = document.createElement('i');
123
+ nameIcon.className = 'bi bi-person me-1';
124
+ nameCell.appendChild(nameIcon);
125
+ nameCell.appendChild(document.createTextNode(p.name));
126
+ row.appendChild(nameCell);
127
+
128
+ var countCell = document.createElement('td');
129
+ countCell.textContent = (p.memory_count || 0) + ' memories';
130
+ row.appendChild(countCell);
131
+
132
+ var statusCell = document.createElement('td');
133
+ if (p.name === active) {
134
+ var badge = document.createElement('span');
135
+ badge.className = 'badge bg-success';
136
+ badge.textContent = 'Active';
137
+ statusCell.appendChild(badge);
138
+ } else {
139
+ var switchBtn = document.createElement('button');
140
+ switchBtn.className = 'btn btn-sm btn-outline-primary';
141
+ switchBtn.textContent = 'Switch';
142
+ switchBtn.addEventListener('click', (function(n) {
143
+ return function() { switchProfile(n); };
144
+ })(p.name));
145
+ statusCell.appendChild(switchBtn);
146
+ }
147
+ row.appendChild(statusCell);
148
+
149
+ var actionsCell = document.createElement('td');
150
+ if (p.name !== 'default') {
151
+ var delBtn = document.createElement('button');
152
+ delBtn.className = 'btn btn-sm btn-outline-danger btn-delete-profile';
153
+ delBtn.title = 'Delete profile';
154
+ var delIcon = document.createElement('i');
155
+ delIcon.className = 'bi bi-trash';
156
+ delBtn.appendChild(delIcon);
157
+ delBtn.addEventListener('click', (function(n) {
158
+ return function() { deleteProfile(n); };
159
+ })(p.name));
160
+ actionsCell.appendChild(delBtn);
161
+ } else {
162
+ var protectedBadge = document.createElement('span');
163
+ protectedBadge.className = 'badge bg-secondary';
164
+ protectedBadge.textContent = 'Protected';
165
+ actionsCell.appendChild(protectedBadge);
166
+ }
167
+ row.appendChild(actionsCell);
168
+
169
+ tbody.appendChild(row);
170
+ });
171
+ table.appendChild(tbody);
172
+
173
+ container.textContent = '';
174
+ container.appendChild(table);
175
+ } catch (error) {
176
+ console.error('Error loading profiles table:', error);
177
+ showEmpty('profiles-table', 'exclamation-triangle', 'Failed to load profiles');
178
+ }
179
+ }
180
+
181
+ async function switchProfile(profileName) {
182
+ try {
183
+ var response = await fetch('/api/profiles/' + encodeURIComponent(profileName) + '/switch', {
184
+ method: 'POST'
185
+ });
186
+ var data = await response.json();
187
+ if (data.success || data.active_profile) {
188
+ showToast('Switched to profile: ' + profileName);
189
+ loadProfiles();
190
+ loadStats();
191
+ loadGraph();
192
+ loadProfilesTable();
193
+ var activeTab = document.querySelector('#mainTabs .nav-link.active');
194
+ if (activeTab) activeTab.click();
195
+ } else {
196
+ showToast('Failed to switch profile');
197
+ }
198
+ } catch (error) {
199
+ console.error('Error switching profile:', error);
200
+ showToast('Error switching profile');
201
+ }
202
+ }