superlocalmemory 3.0.16 → 3.0.18
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/bin/slm-npm +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/cli/commands.py +29 -0
- package/src/superlocalmemory/cli/main.py +94 -30
- package/src/superlocalmemory/core/embedding_worker.py +120 -0
- package/src/superlocalmemory/core/embeddings.py +156 -240
- package/src/superlocalmemory/core/recall_worker.py +193 -0
- package/src/superlocalmemory/core/summarizer.py +182 -0
- package/src/superlocalmemory/core/worker_pool.py +209 -0
- package/src/superlocalmemory/mcp/server.py +9 -0
- package/src/superlocalmemory/mcp/tools_core.py +21 -8
- package/src/superlocalmemory/mcp/tools_v3.py +21 -0
- package/src/superlocalmemory/server/routes/helpers.py +21 -0
- package/src/superlocalmemory/server/routes/memories.py +100 -42
- package/src/superlocalmemory/server/routes/stats.py +11 -0
- package/src/superlocalmemory/server/routes/v3_api.py +195 -43
- package/src/superlocalmemory/server/ui.py +15 -14
- package/src/superlocalmemory/storage/database.py +23 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +1 -1
- package/src/superlocalmemory.egg-info/SOURCES.txt +4 -0
- package/ui/index.html +113 -29
- package/ui/js/auto-settings.js +330 -1
- package/ui/js/clusters.js +138 -101
- package/ui/js/graph-core.js +3 -1
- package/ui/js/graph-interactions.js +2 -5
- package/ui/js/memories.js +65 -2
- package/ui/js/modal.js +79 -42
- package/ui/js/recall-lab.js +206 -60
package/ui/js/clusters.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// SuperLocalMemory
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Security: All dynamic values escaped via escapeHtml(). Data from local DB only.
|
|
1
|
+
// SuperLocalMemory V3 - Clusters View
|
|
2
|
+
// Part of Qualixar | https://superlocalmemory.com
|
|
5
3
|
|
|
6
4
|
async function loadClusters() {
|
|
7
5
|
showLoading('clusters-list', 'Loading clusters...');
|
|
@@ -18,152 +16,191 @@ async function loadClusters() {
|
|
|
18
16
|
function renderClusters(clusters) {
|
|
19
17
|
var container = document.getElementById('clusters-list');
|
|
20
18
|
if (!clusters || clusters.length === 0) {
|
|
21
|
-
showEmpty('clusters-list', 'collection', 'No clusters found.
|
|
19
|
+
showEmpty('clusters-list', 'collection', 'No clusters found yet. Clusters form automatically as you store related memories.');
|
|
22
20
|
return;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
var colors = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a'];
|
|
23
|
+
var colors = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a', '#30cfd0', '#764ba2', '#f5576c'];
|
|
26
24
|
container.textContent = '';
|
|
27
25
|
|
|
28
26
|
clusters.forEach(function(cluster, idx) {
|
|
29
27
|
var color = colors[idx % colors.length];
|
|
30
28
|
|
|
31
29
|
var card = document.createElement('div');
|
|
32
|
-
card.className = 'card
|
|
33
|
-
card.style.
|
|
34
|
-
card.setAttribute('data-cluster-id', cluster.cluster_id);
|
|
35
|
-
card.title = 'Click to filter graph to this cluster';
|
|
30
|
+
card.className = 'card mb-2';
|
|
31
|
+
card.style.borderLeft = '4px solid ' + color;
|
|
36
32
|
|
|
37
33
|
var body = document.createElement('div');
|
|
38
|
-
body.className = 'card-body';
|
|
34
|
+
body.className = 'card-body py-2 px-3';
|
|
35
|
+
body.style.cursor = 'pointer';
|
|
36
|
+
|
|
37
|
+
// Header row
|
|
38
|
+
var headerRow = document.createElement('div');
|
|
39
|
+
headerRow.className = 'd-flex justify-content-between align-items-center';
|
|
39
40
|
|
|
40
41
|
var title = document.createElement('h6');
|
|
41
|
-
title.className = '
|
|
42
|
-
title.textContent = 'Cluster ' + cluster.cluster_id
|
|
42
|
+
title.className = 'mb-0';
|
|
43
|
+
title.textContent = 'Cluster ' + cluster.cluster_id;
|
|
44
|
+
|
|
45
|
+
var badges = document.createElement('div');
|
|
43
46
|
var countBadge = document.createElement('span');
|
|
44
|
-
countBadge.className = 'badge bg-secondary
|
|
47
|
+
countBadge.className = 'badge bg-secondary me-1';
|
|
45
48
|
countBadge.textContent = cluster.member_count + ' memories';
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
72
|
-
} else {
|
|
73
|
-
var none = document.createElement('span');
|
|
74
|
-
none.className = 'text-muted';
|
|
75
|
-
none.textContent = 'No entities';
|
|
76
|
-
body.appendChild(none);
|
|
49
|
+
badges.appendChild(countBadge);
|
|
50
|
+
|
|
51
|
+
if (cluster.avg_importance) {
|
|
52
|
+
var impBadge = document.createElement('span');
|
|
53
|
+
impBadge.className = 'badge bg-outline-primary';
|
|
54
|
+
impBadge.style.cssText = 'border:1px solid #667eea; color:#667eea;';
|
|
55
|
+
impBadge.textContent = 'imp: ' + parseFloat(cluster.avg_importance).toFixed(1);
|
|
56
|
+
badges.appendChild(impBadge);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
var expandIcon = document.createElement('i');
|
|
60
|
+
expandIcon.className = 'bi bi-chevron-down ms-2';
|
|
61
|
+
expandIcon.style.transition = 'transform 0.2s';
|
|
62
|
+
badges.appendChild(expandIcon);
|
|
63
|
+
|
|
64
|
+
headerRow.appendChild(title);
|
|
65
|
+
headerRow.appendChild(badges);
|
|
66
|
+
body.appendChild(headerRow);
|
|
67
|
+
|
|
68
|
+
// Summary line (categories if available)
|
|
69
|
+
if (cluster.categories) {
|
|
70
|
+
var catLine = document.createElement('small');
|
|
71
|
+
catLine.className = 'text-muted';
|
|
72
|
+
catLine.textContent = cluster.categories;
|
|
73
|
+
body.appendChild(catLine);
|
|
77
74
|
}
|
|
78
75
|
|
|
76
|
+
// Expandable member area (hidden by default)
|
|
77
|
+
var memberArea = document.createElement('div');
|
|
78
|
+
memberArea.className = 'mt-2';
|
|
79
|
+
memberArea.style.display = 'none';
|
|
80
|
+
memberArea.id = 'cluster-members-' + cluster.cluster_id;
|
|
81
|
+
|
|
82
|
+
var loadingText = document.createElement('div');
|
|
83
|
+
loadingText.className = 'text-center text-muted small py-2';
|
|
84
|
+
loadingText.textContent = 'Loading members...';
|
|
85
|
+
memberArea.appendChild(loadingText);
|
|
86
|
+
|
|
87
|
+
body.appendChild(memberArea);
|
|
79
88
|
card.appendChild(body);
|
|
80
89
|
container.appendChild(card);
|
|
81
90
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
// Click to expand/collapse
|
|
92
|
+
var expanded = false;
|
|
93
|
+
body.addEventListener('click', function(e) {
|
|
94
|
+
expanded = !expanded;
|
|
95
|
+
memberArea.style.display = expanded ? 'block' : 'none';
|
|
96
|
+
expandIcon.style.transform = expanded ? 'rotate(180deg)' : 'rotate(0)';
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
if (expanded && memberArea.children.length === 1 && memberArea.children[0] === loadingText) {
|
|
99
|
+
loadClusterMembers(cluster.cluster_id, memberArea);
|
|
100
|
+
}
|
|
91
101
|
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
92
104
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
async function loadClusterMembers(clusterId, container) {
|
|
106
|
+
try {
|
|
107
|
+
var response = await fetch('/api/clusters/' + clusterId + '?limit=10');
|
|
108
|
+
var data = await response.json();
|
|
109
|
+
container.textContent = '';
|
|
110
|
+
|
|
111
|
+
// Show cluster summary if available
|
|
112
|
+
if (data.summary) {
|
|
113
|
+
var summaryDiv = document.createElement('div');
|
|
114
|
+
summaryDiv.className = 'alert alert-light border-start border-3 border-primary py-2 px-3 mb-2 small';
|
|
115
|
+
var summaryLabel = document.createElement('strong');
|
|
116
|
+
summaryLabel.textContent = 'Summary: ';
|
|
117
|
+
summaryDiv.appendChild(summaryLabel);
|
|
118
|
+
summaryDiv.appendChild(document.createTextNode(data.summary));
|
|
119
|
+
container.appendChild(summaryDiv);
|
|
105
120
|
}
|
|
106
121
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
if (!data.members || data.members.length === 0) {
|
|
123
|
+
var empty = document.createElement('div');
|
|
124
|
+
empty.className = 'text-muted small';
|
|
125
|
+
empty.textContent = 'No members found.';
|
|
126
|
+
container.appendChild(empty);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
data.members.forEach(function(m, i) {
|
|
131
|
+
var row = document.createElement('div');
|
|
132
|
+
row.className = 'border-bottom py-1';
|
|
133
|
+
if (i === data.members.length - 1) row.className = 'py-1';
|
|
134
|
+
|
|
135
|
+
var content = document.createElement('div');
|
|
136
|
+
content.className = 'small';
|
|
137
|
+
var text = m.content || m.summary || '';
|
|
138
|
+
content.textContent = (i + 1) + '. ' + (text.length > 150 ? text.substring(0, 150) + '...' : text);
|
|
139
|
+
row.appendChild(content);
|
|
140
|
+
|
|
141
|
+
var meta = document.createElement('div');
|
|
142
|
+
meta.className = 'text-muted';
|
|
143
|
+
meta.style.fontSize = '0.7rem';
|
|
144
|
+
var parts = [];
|
|
145
|
+
if (m.category) parts.push(m.category);
|
|
146
|
+
if (m.importance) parts.push('imp: ' + m.importance);
|
|
147
|
+
if (m.created_at) parts.push(m.created_at.substring(0, 10));
|
|
148
|
+
meta.textContent = parts.join(' | ');
|
|
149
|
+
row.appendChild(meta);
|
|
150
|
+
|
|
151
|
+
container.appendChild(row);
|
|
113
152
|
});
|
|
114
|
-
|
|
153
|
+
|
|
154
|
+
// View in graph button
|
|
155
|
+
var graphBtn = document.createElement('button');
|
|
156
|
+
graphBtn.className = 'btn btn-sm btn-outline-primary mt-2';
|
|
157
|
+
graphBtn.textContent = 'View in Knowledge Graph';
|
|
158
|
+
graphBtn.addEventListener('click', function(e) {
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
filterGraphToCluster(clusterId);
|
|
161
|
+
});
|
|
162
|
+
container.appendChild(graphBtn);
|
|
163
|
+
|
|
164
|
+
} catch (error) {
|
|
165
|
+
container.textContent = '';
|
|
166
|
+
var errDiv = document.createElement('div');
|
|
167
|
+
errDiv.className = 'text-danger small';
|
|
168
|
+
errDiv.textContent = 'Failed to load: ' + error.message;
|
|
169
|
+
container.appendChild(errDiv);
|
|
170
|
+
}
|
|
115
171
|
}
|
|
116
172
|
|
|
117
|
-
// v2.6.5: Filter graph to a specific cluster
|
|
118
173
|
function filterGraphToCluster(clusterId) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (graphTab) {
|
|
122
|
-
graphTab.click();
|
|
123
|
-
}
|
|
174
|
+
var graphTab = document.querySelector('a[href="#graph"]');
|
|
175
|
+
if (graphTab) graphTab.click();
|
|
124
176
|
|
|
125
|
-
// Apply filter after a delay (for tab to load)
|
|
126
177
|
setTimeout(function() {
|
|
127
178
|
if (typeof filterState !== 'undefined' && typeof filterByCluster === 'function' && typeof renderGraph === 'function') {
|
|
128
179
|
filterState.cluster_id = clusterId;
|
|
129
|
-
|
|
180
|
+
var filtered = filterByCluster(originalGraphData, clusterId);
|
|
130
181
|
renderGraph(filtered);
|
|
131
|
-
|
|
132
|
-
// Update URL
|
|
133
|
-
const url = new URL(window.location);
|
|
182
|
+
var url = new URL(window.location);
|
|
134
183
|
url.searchParams.set('cluster_id', clusterId);
|
|
135
184
|
window.history.replaceState({}, '', url);
|
|
136
185
|
}
|
|
137
186
|
}, 300);
|
|
138
187
|
}
|
|
139
188
|
|
|
140
|
-
// v2.6.5: Filter graph by entity
|
|
141
189
|
function filterGraphByEntity(entity) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (graphTab) {
|
|
145
|
-
graphTab.click();
|
|
146
|
-
}
|
|
190
|
+
var graphTab = document.querySelector('a[href="#graph"]');
|
|
191
|
+
if (graphTab) graphTab.click();
|
|
147
192
|
|
|
148
|
-
// Apply filter after a delay
|
|
149
193
|
setTimeout(function() {
|
|
150
194
|
if (typeof filterState !== 'undefined' && typeof filterByEntity === 'function' && typeof renderGraph === 'function') {
|
|
151
195
|
filterState.entity = entity;
|
|
152
|
-
|
|
196
|
+
var filtered = filterByEntity(originalGraphData, entity);
|
|
153
197
|
renderGraph(filtered);
|
|
154
198
|
}
|
|
155
199
|
}, 300);
|
|
156
200
|
}
|
|
157
201
|
|
|
158
|
-
// v2.6.5: Show memories in a cluster (future: sidebar list)
|
|
159
202
|
function showClusterMemories(clusterId) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
memoriesTab.click();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// TODO: Implement sidebar memory list view
|
|
167
|
-
console.log('Show memories for cluster', clusterId);
|
|
168
|
-
showToast('Filtering memories for cluster ' + clusterId);
|
|
203
|
+
var memoriesTab = document.querySelector('a[href="#memories"]');
|
|
204
|
+
if (memoriesTab) memoriesTab.click();
|
|
205
|
+
if (typeof showToast === 'function') showToast('Filtering memories for cluster ' + clusterId);
|
|
169
206
|
}
|
package/ui/js/graph-core.js
CHANGED
|
@@ -98,8 +98,10 @@ function transformDataForCytoscape(data) {
|
|
|
98
98
|
|
|
99
99
|
// Add nodes
|
|
100
100
|
data.nodes.forEach(node => {
|
|
101
|
-
const label = node.category || node.project_name || `Memory #${node.id}`;
|
|
102
101
|
const contentPreview = node.content_preview || node.summary || node.content || '';
|
|
102
|
+
// Label: first 4 words of content (readable on node), fallback to category
|
|
103
|
+
const contentWords = contentPreview.split(/\s+/).slice(0, 4).join(' ');
|
|
104
|
+
const label = contentWords || node.category || `Memory #${node.id}`;
|
|
103
105
|
const preview = contentPreview.substring(0, 50) + (contentPreview.length > 50 ? '...' : '');
|
|
104
106
|
|
|
105
107
|
elements.push({
|
|
@@ -32,7 +32,7 @@ function addCytoscapeInteractions() {
|
|
|
32
32
|
cy.elements().removeClass('highlighted').removeClass('dimmed');
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
// Single click: Open modal preview
|
|
35
|
+
// Single click: Open modal preview (source='graph' for context-aware buttons)
|
|
36
36
|
cy.on('tap', 'node', function(evt) {
|
|
37
37
|
const node = evt.target;
|
|
38
38
|
openMemoryModal(node);
|
|
@@ -136,11 +136,8 @@ function openMemoryModal(node) {
|
|
|
136
136
|
created_at: node.data('created_at')
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
-
// Call existing openMemoryDetail function from modal.js
|
|
140
139
|
if (typeof openMemoryDetail === 'function') {
|
|
141
|
-
openMemoryDetail(memoryData);
|
|
142
|
-
} else {
|
|
143
|
-
console.error('openMemoryDetail function not found. Is modal.js loaded?');
|
|
140
|
+
openMemoryDetail(memoryData, 'graph'); // source='graph': show Expand Neighbors, hide View Original
|
|
144
141
|
}
|
|
145
142
|
}
|
|
146
143
|
|
package/ui/js/memories.js
CHANGED
|
@@ -55,11 +55,14 @@ function renderMemoriesTable(memories, showScores) {
|
|
|
55
55
|
+ '</div></div></td>';
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
var memId = mem.memory_id || mem.id;
|
|
59
|
+
var expandBtnHtml = '<button class="btn btn-sm btn-outline-secondary expand-facts-btn ms-1" data-memory-id="' + escapeHtml(String(memId)) + '" title="View atomic facts">▼</button>';
|
|
60
|
+
|
|
58
61
|
rows += '<tr data-mem-idx="' + idx + '">'
|
|
59
62
|
+ '<td>' + escapeHtml(String(mem.id)) + '</td>'
|
|
60
63
|
+ '<td><span class="badge bg-primary">' + escapeHtml(mem.category || 'None') + '</span></td>'
|
|
61
64
|
+ '<td><small>' + escapeHtml(mem.project_name || '-') + '</small></td>'
|
|
62
|
-
+ '<td class="memory-content" title="' + escapeHtml(content) + '">' + escapeHtml(contentPreview) + '</td>'
|
|
65
|
+
+ '<td class="memory-content" title="' + escapeHtml(content) + '">' + escapeHtml(contentPreview) + expandBtnHtml + '</td>'
|
|
63
66
|
+ scoreCell
|
|
64
67
|
+ '<td><span class="badge bg-' + importanceClass + ' badge-importance">' + escapeHtml(String(importance)) + '</span></td>'
|
|
65
68
|
+ '<td>' + escapeHtml(String(mem.cluster_id || '-')) + '</td>'
|
|
@@ -87,8 +90,18 @@ function renderMemoriesTable(memories, showScores) {
|
|
|
87
90
|
table.addEventListener('click', function(e) {
|
|
88
91
|
var th = e.target.closest('th.sortable');
|
|
89
92
|
if (th) { handleSort(th); return; }
|
|
93
|
+
|
|
94
|
+
// Expand facts button
|
|
95
|
+
var expandBtn = e.target.closest('.expand-facts-btn');
|
|
96
|
+
if (expandBtn) {
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
var memId = expandBtn.getAttribute('data-memory-id');
|
|
99
|
+
toggleFactsExpansion(expandBtn, memId);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
var row = e.target.closest('tr[data-mem-idx]');
|
|
91
|
-
if (row) {
|
|
104
|
+
if (row && !e.target.closest('.expand-facts-btn')) {
|
|
92
105
|
var idx = parseInt(row.getAttribute('data-mem-idx'), 10);
|
|
93
106
|
if (window._slmMemories && window._slmMemories[idx]) {
|
|
94
107
|
openMemoryDetail(window._slmMemories[idx]);
|
|
@@ -169,6 +182,56 @@ function scrollToMemory(memoryId) {
|
|
|
169
182
|
scrollToMemoryInTable(memoryId);
|
|
170
183
|
}
|
|
171
184
|
|
|
185
|
+
async function toggleFactsExpansion(btn, memoryId) {
|
|
186
|
+
var row = btn.closest('tr');
|
|
187
|
+
if (!row) return;
|
|
188
|
+
var existingExpansion = row.nextElementSibling;
|
|
189
|
+
if (existingExpansion && existingExpansion.classList.contains('facts-expansion-row')) {
|
|
190
|
+
existingExpansion.remove();
|
|
191
|
+
btn.innerHTML = '▼';
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
btn.innerHTML = '⌛';
|
|
196
|
+
try {
|
|
197
|
+
var resp = await fetch('/api/memories/' + encodeURIComponent(memoryId) + '/facts');
|
|
198
|
+
var data = await resp.json();
|
|
199
|
+
var expRow = document.createElement('tr');
|
|
200
|
+
expRow.className = 'facts-expansion-row';
|
|
201
|
+
var expCell = document.createElement('td');
|
|
202
|
+
expCell.colSpan = 8;
|
|
203
|
+
expCell.style.cssText = 'background:#f8f9fa; padding:8px 16px;';
|
|
204
|
+
|
|
205
|
+
if (data.facts && data.facts.length > 0) {
|
|
206
|
+
var label = document.createElement('small');
|
|
207
|
+
label.className = 'text-muted';
|
|
208
|
+
label.textContent = data.fact_count + ' atomic facts:';
|
|
209
|
+
expCell.appendChild(label);
|
|
210
|
+
|
|
211
|
+
data.facts.forEach(function(f, i) {
|
|
212
|
+
var fDiv = document.createElement('div');
|
|
213
|
+
fDiv.className = 'small py-1' + (i < data.facts.length - 1 ? ' border-bottom' : '');
|
|
214
|
+
var badge = document.createElement('span');
|
|
215
|
+
badge.className = 'badge bg-secondary me-1';
|
|
216
|
+
badge.style.fontSize = '0.65rem';
|
|
217
|
+
badge.textContent = f.fact_type;
|
|
218
|
+
fDiv.appendChild(badge);
|
|
219
|
+
fDiv.appendChild(document.createTextNode(f.content.substring(0, 200)));
|
|
220
|
+
expCell.appendChild(fDiv);
|
|
221
|
+
});
|
|
222
|
+
} else {
|
|
223
|
+
expCell.textContent = 'No atomic facts found for this memory.';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
expRow.appendChild(expCell);
|
|
227
|
+
row.parentNode.insertBefore(expRow, row.nextSibling);
|
|
228
|
+
btn.innerHTML = '▲';
|
|
229
|
+
} catch (err) {
|
|
230
|
+
btn.innerHTML = '▼';
|
|
231
|
+
console.error('Failed to load facts:', err);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
172
235
|
function scrollToMemoryInTable(memoryId) {
|
|
173
236
|
const memId = String(memoryId);
|
|
174
237
|
|
package/ui/js/modal.js
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
var currentMemoryDetail = null;
|
|
8
8
|
|
|
9
|
-
function openMemoryDetail(mem) {
|
|
9
|
+
function openMemoryDetail(mem, source) {
|
|
10
|
+
// source: 'graph', 'recall', 'memories', or undefined
|
|
10
11
|
currentMemoryDetail = mem;
|
|
12
|
+
var fromGraph = source === 'graph';
|
|
13
|
+
var fromRecall = source === 'recall';
|
|
11
14
|
var body = document.getElementById('memory-detail-body');
|
|
12
15
|
if (!mem) {
|
|
13
16
|
body.textContent = 'No memory data';
|
|
@@ -62,55 +65,89 @@ function openMemoryDetail(mem) {
|
|
|
62
65
|
|
|
63
66
|
body.appendChild(dl);
|
|
64
67
|
|
|
65
|
-
//
|
|
66
|
-
if (mem.
|
|
68
|
+
// Context-aware action buttons
|
|
69
|
+
if (mem.id) {
|
|
67
70
|
body.appendChild(document.createElement('hr'));
|
|
68
71
|
|
|
69
72
|
var actionsDiv = document.createElement('div');
|
|
70
73
|
actionsDiv.className = 'memory-detail-graph-actions';
|
|
71
74
|
actionsDiv.style.cssText = 'display:flex; gap:10px; flex-wrap:wrap;';
|
|
72
75
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
76
|
+
// "View Original Memory" — shown on Recall Lab + Memories, hidden on Graph
|
|
77
|
+
// (On Graph the node IS the memory; on Recall Lab we have a fact, not the original)
|
|
78
|
+
if (!fromGraph) {
|
|
79
|
+
var viewBtn = document.createElement('button');
|
|
80
|
+
viewBtn.className = 'btn btn-primary btn-sm';
|
|
81
|
+
viewBtn.innerHTML = '<i class="bi bi-journal-text"></i> View Original Memory';
|
|
82
|
+
viewBtn.onclick = function() {
|
|
83
|
+
var mid = mem.memory_id || mem.id;
|
|
84
|
+
viewBtn.disabled = true;
|
|
85
|
+
viewBtn.textContent = 'Loading...';
|
|
86
|
+
fetch('/api/memories/' + encodeURIComponent(mid) + '/facts')
|
|
87
|
+
.then(function(r) { return r.json(); })
|
|
88
|
+
.then(function(data) {
|
|
89
|
+
if (data.ok && data.original_content) {
|
|
90
|
+
contentDiv.textContent = '';
|
|
91
|
+
var origLabel = document.createElement('small');
|
|
92
|
+
origLabel.className = 'text-muted d-block mb-1';
|
|
93
|
+
origLabel.textContent = 'Original memory (' + (data.fact_count || 0) + ' atomic facts extracted):';
|
|
94
|
+
contentDiv.appendChild(origLabel);
|
|
95
|
+
var origText = document.createElement('div');
|
|
96
|
+
origText.style.cssText = 'white-space:pre-wrap;background:#f8f9fa;padding:10px;border-radius:6px;margin-bottom:8px;';
|
|
97
|
+
origText.textContent = data.original_content;
|
|
98
|
+
contentDiv.appendChild(origText);
|
|
99
|
+
if (data.facts && data.facts.length > 0) {
|
|
100
|
+
var toggle = document.createElement('button');
|
|
101
|
+
toggle.className = 'btn btn-sm btn-outline-secondary mb-2';
|
|
102
|
+
toggle.textContent = 'Show atomic facts (' + data.facts.length + ')';
|
|
103
|
+
var factsDiv = document.createElement('div');
|
|
104
|
+
factsDiv.style.display = 'none';
|
|
105
|
+
data.facts.forEach(function(f) {
|
|
106
|
+
var fDiv = document.createElement('div');
|
|
107
|
+
fDiv.className = 'small py-1 border-bottom';
|
|
108
|
+
var badge = document.createElement('span');
|
|
109
|
+
badge.className = 'badge bg-secondary me-1';
|
|
110
|
+
badge.style.fontSize = '0.6rem';
|
|
111
|
+
badge.textContent = f.fact_type;
|
|
112
|
+
fDiv.appendChild(badge);
|
|
113
|
+
fDiv.appendChild(document.createTextNode(f.content));
|
|
114
|
+
factsDiv.appendChild(fDiv);
|
|
115
|
+
});
|
|
116
|
+
toggle.onclick = function() {
|
|
117
|
+
var hidden = factsDiv.style.display === 'none';
|
|
118
|
+
factsDiv.style.display = hidden ? 'block' : 'none';
|
|
119
|
+
toggle.textContent = hidden ? 'Hide atomic facts' : 'Show atomic facts (' + data.facts.length + ')';
|
|
120
|
+
};
|
|
121
|
+
contentDiv.appendChild(toggle);
|
|
122
|
+
contentDiv.appendChild(factsDiv);
|
|
123
|
+
}
|
|
124
|
+
viewBtn.textContent = 'Showing original';
|
|
125
|
+
} else {
|
|
126
|
+
viewBtn.textContent = 'Not available';
|
|
127
|
+
}
|
|
128
|
+
}).catch(function() {
|
|
129
|
+
viewBtn.textContent = 'Failed to load';
|
|
130
|
+
viewBtn.disabled = false;
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
actionsDiv.appendChild(viewBtn);
|
|
134
|
+
}
|
|
91
135
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
setTimeout(function() {
|
|
106
|
-
if (typeof expandNeighbors === 'function') {
|
|
107
|
-
expandNeighbors(mem.id);
|
|
108
|
-
}
|
|
109
|
-
}, 500);
|
|
110
|
-
};
|
|
111
|
-
actionsDiv.appendChild(expandBtn);
|
|
136
|
+
// "Expand Neighbors" — shown on Graph, hidden elsewhere (no graph context)
|
|
137
|
+
if (fromGraph) {
|
|
138
|
+
var expandBtn = document.createElement('button');
|
|
139
|
+
expandBtn.className = 'btn btn-outline-secondary btn-sm';
|
|
140
|
+
expandBtn.innerHTML = '<i class="bi bi-diagram-3"></i> Expand Neighbors';
|
|
141
|
+
expandBtn.onclick = function() {
|
|
142
|
+
modal.hide();
|
|
143
|
+
setTimeout(function() {
|
|
144
|
+
if (typeof expandNeighbors === 'function') expandNeighbors(mem.id);
|
|
145
|
+
}, 300);
|
|
146
|
+
};
|
|
147
|
+
actionsDiv.appendChild(expandBtn);
|
|
148
|
+
}
|
|
112
149
|
|
|
113
|
-
//
|
|
150
|
+
// "Filter to Cluster" — always available if cluster exists
|
|
114
151
|
if (mem.cluster_id) {
|
|
115
152
|
var filterBtn = document.createElement('button');
|
|
116
153
|
filterBtn.className = 'btn btn-outline-info btn-sm';
|