superlocalmemory 3.0.17 → 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/main.py +10 -0
- 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/server/routes/helpers.py +21 -0
- package/src/superlocalmemory/server/routes/memories.py +49 -33
- 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 +77 -21
- package/ui/js/auto-settings.js +330 -1
- package/ui/js/clusters.js +11 -0
- 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 +98 -46
package/ui/js/auto-settings.js
CHANGED
|
@@ -66,5 +66,334 @@ document.querySelectorAll('#auto-recall-toggle, #auto-recall-session').forEach(f
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Mode / Provider / Model Configuration (Professional Settings)
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
var PROVIDER_CONFIG = {
|
|
74
|
+
'ollama': {
|
|
75
|
+
name: 'Ollama',
|
|
76
|
+
needsKey: false,
|
|
77
|
+
endpoint: 'http://localhost:11434',
|
|
78
|
+
endpointEditable: true,
|
|
79
|
+
detectModels: true, // auto-detect via /api/v3/ollama/status
|
|
80
|
+
},
|
|
81
|
+
'openrouter': {
|
|
82
|
+
name: 'OpenRouter',
|
|
83
|
+
needsKey: true,
|
|
84
|
+
endpoint: 'https://openrouter.ai/api/v1',
|
|
85
|
+
endpointEditable: false,
|
|
86
|
+
},
|
|
87
|
+
'openai': {
|
|
88
|
+
name: 'OpenAI',
|
|
89
|
+
needsKey: true,
|
|
90
|
+
endpoint: 'https://api.openai.com/v1',
|
|
91
|
+
endpointEditable: true, // editable for Azure OpenAI
|
|
92
|
+
},
|
|
93
|
+
'anthropic': {
|
|
94
|
+
name: 'Anthropic',
|
|
95
|
+
needsKey: true,
|
|
96
|
+
endpoint: 'https://api.anthropic.com',
|
|
97
|
+
endpointEditable: false,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
var MODEL_OPTIONS = {
|
|
102
|
+
'none': [],
|
|
103
|
+
'ollama': [
|
|
104
|
+
{value: 'llama3.1:8b', label: 'Llama 3.1 8B'},
|
|
105
|
+
{value: 'llama3.2:latest', label: 'Llama 3.2'},
|
|
106
|
+
{value: 'qwen3-vl:8b', label: 'Qwen3 VL 8B'},
|
|
107
|
+
{value: 'mistral:latest', label: 'Mistral'},
|
|
108
|
+
],
|
|
109
|
+
'openrouter': [
|
|
110
|
+
{value: 'meta-llama/llama-3.1-8b-instruct:free', label: 'Llama 3.1 8B (Free)'},
|
|
111
|
+
{value: 'google/gemini-2.0-flash-001', label: 'Gemini 2.0 Flash'},
|
|
112
|
+
{value: 'anthropic/claude-3.5-haiku', label: 'Claude 3.5 Haiku'},
|
|
113
|
+
{value: 'openai/gpt-4o-mini', label: 'GPT-4o Mini'},
|
|
114
|
+
{value: 'deepseek/deepseek-chat-v3-0324:free', label: 'DeepSeek V3 (Free)'},
|
|
115
|
+
],
|
|
116
|
+
'openai': [
|
|
117
|
+
{value: 'gpt-4o-mini', label: 'GPT-4o Mini'},
|
|
118
|
+
{value: 'gpt-4o', label: 'GPT-4o'},
|
|
119
|
+
{value: 'gpt-4-turbo', label: 'GPT-4 Turbo'},
|
|
120
|
+
],
|
|
121
|
+
'anthropic': [
|
|
122
|
+
{value: 'claude-3-5-haiku-latest', label: 'Claude 3.5 Haiku'},
|
|
123
|
+
{value: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet'},
|
|
124
|
+
{value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6'},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
async function loadModeSettings() {
|
|
129
|
+
try {
|
|
130
|
+
var resp = await fetch('/api/v3/mode');
|
|
131
|
+
if (!resp.ok) return;
|
|
132
|
+
var data = await resp.json();
|
|
133
|
+
var mode = data.mode || 'a';
|
|
134
|
+
var provider = data.provider || 'none';
|
|
135
|
+
var model = data.model || '';
|
|
136
|
+
|
|
137
|
+
// Set radio button
|
|
138
|
+
var radio = document.getElementById('mode-' + mode + '-radio');
|
|
139
|
+
if (radio) radio.checked = true;
|
|
140
|
+
|
|
141
|
+
// Set provider dropdown
|
|
142
|
+
var provEl = document.getElementById('settings-provider');
|
|
143
|
+
if (provEl && provider !== 'none') provEl.value = provider;
|
|
144
|
+
|
|
145
|
+
// Update banner
|
|
146
|
+
var modeNames = {a: 'Mode A — Local Guardian', b: 'Mode B — Smart Local', c: 'Mode C — Full Power'};
|
|
147
|
+
var bannerMode = document.getElementById('settings-current-mode');
|
|
148
|
+
if (bannerMode) {
|
|
149
|
+
var label = modeNames[mode] || mode;
|
|
150
|
+
if (provider && provider !== 'none') label += ' | ' + provider;
|
|
151
|
+
if (model) label += ' | ' + model;
|
|
152
|
+
bannerMode.textContent = label;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
var bannerDetail = document.getElementById('settings-current-detail');
|
|
156
|
+
if (bannerDetail) {
|
|
157
|
+
if (mode === 'a') bannerDetail.textContent = 'Zero cloud — EU AI Act compliant';
|
|
158
|
+
else if (data.has_key) bannerDetail.textContent = 'API key configured';
|
|
159
|
+
else if (provider === 'ollama') bannerDetail.textContent = 'No API key needed';
|
|
160
|
+
else bannerDetail.textContent = 'API key not set';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var banner = document.getElementById('settings-current-banner');
|
|
164
|
+
if (banner) {
|
|
165
|
+
banner.className = mode === 'a' ? 'alert alert-success mb-3' :
|
|
166
|
+
mode === 'b' ? 'alert alert-info mb-3' :
|
|
167
|
+
'alert alert-warning mb-3';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Show provider panel and populate model dropdown
|
|
171
|
+
updateModeUI();
|
|
172
|
+
|
|
173
|
+
// After provider UI updates, set the saved model value
|
|
174
|
+
if (model) {
|
|
175
|
+
setTimeout(function() {
|
|
176
|
+
var modelEl = document.getElementById('settings-model');
|
|
177
|
+
if (modelEl) {
|
|
178
|
+
// Check if option exists, if not add it
|
|
179
|
+
var found = false;
|
|
180
|
+
for (var i = 0; i < modelEl.options.length; i++) {
|
|
181
|
+
if (modelEl.options[i].value === model) { found = true; break; }
|
|
182
|
+
}
|
|
183
|
+
if (!found) {
|
|
184
|
+
var opt = document.createElement('option');
|
|
185
|
+
opt.value = model;
|
|
186
|
+
opt.textContent = model + ' (current)';
|
|
187
|
+
modelEl.insertBefore(opt, modelEl.firstChild);
|
|
188
|
+
}
|
|
189
|
+
modelEl.value = model;
|
|
190
|
+
}
|
|
191
|
+
}, 500);
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.log('Load mode settings error:', e);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function updateModeUI() {
|
|
199
|
+
var mode = document.querySelector('input[name="settings-mode-radio"]:checked')?.value || 'a';
|
|
200
|
+
var panel = document.getElementById('settings-provider-panel');
|
|
201
|
+
if (panel) {
|
|
202
|
+
panel.style.display = (mode === 'a') ? 'none' : 'block';
|
|
203
|
+
}
|
|
204
|
+
// Only set provider if it's currently empty (first load or Mode A→B/C)
|
|
205
|
+
var providerEl = document.getElementById('settings-provider');
|
|
206
|
+
if (providerEl && !providerEl.value) {
|
|
207
|
+
if (mode === 'b') providerEl.value = 'ollama';
|
|
208
|
+
}
|
|
209
|
+
if (mode !== 'a') updateProviderUI();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function updateProviderUI() {
|
|
213
|
+
var provider = document.getElementById('settings-provider')?.value || 'none';
|
|
214
|
+
var modelSelect = document.getElementById('settings-model');
|
|
215
|
+
var modelHint = document.getElementById('settings-model-hint');
|
|
216
|
+
|
|
217
|
+
// Preserve current model before rebuilding dropdown
|
|
218
|
+
var currentModel = modelSelect ? modelSelect.value : '';
|
|
219
|
+
|
|
220
|
+
var cfg = PROVIDER_CONFIG[provider] || {};
|
|
221
|
+
|
|
222
|
+
// Show/hide API key column
|
|
223
|
+
var keyCol = document.getElementById('settings-key-col');
|
|
224
|
+
if (keyCol) keyCol.style.display = cfg.needsKey ? 'block' : 'none';
|
|
225
|
+
|
|
226
|
+
// Show/hide endpoint row
|
|
227
|
+
var endpointRow = document.getElementById('settings-endpoint-row');
|
|
228
|
+
var endpointInput = document.getElementById('settings-endpoint');
|
|
229
|
+
if (endpointRow) {
|
|
230
|
+
endpointRow.style.display = cfg.endpointEditable ? 'block' : 'none';
|
|
231
|
+
if (endpointInput && cfg.endpoint) endpointInput.value = cfg.endpoint;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// For Ollama: check live status and populate real models
|
|
235
|
+
if (provider === 'ollama') {
|
|
236
|
+
if (modelHint) modelHint.textContent = 'Checking Ollama...';
|
|
237
|
+
fetch('/api/v3/ollama/status').then(function(r) { return r.json(); }).then(function(data) {
|
|
238
|
+
if (modelSelect) {
|
|
239
|
+
modelSelect.textContent = '';
|
|
240
|
+
if (data.running && data.models.length > 0) {
|
|
241
|
+
data.models.forEach(function(m) {
|
|
242
|
+
var opt = document.createElement('option');
|
|
243
|
+
opt.value = m.name;
|
|
244
|
+
opt.textContent = m.name;
|
|
245
|
+
modelSelect.appendChild(opt);
|
|
246
|
+
});
|
|
247
|
+
if (modelHint) modelHint.textContent = 'Ollama running (' + data.count + ' models)';
|
|
248
|
+
if (modelHint) modelHint.className = 'text-success small';
|
|
249
|
+
} else {
|
|
250
|
+
var opt = document.createElement('option');
|
|
251
|
+
opt.value = '';
|
|
252
|
+
opt.textContent = 'Ollama not running!';
|
|
253
|
+
modelSelect.appendChild(opt);
|
|
254
|
+
if (modelHint) modelHint.textContent = 'Ollama not detected. Run: ollama serve';
|
|
255
|
+
if (modelHint) modelHint.className = 'text-danger small';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}).catch(function() {
|
|
259
|
+
if (modelHint) { modelHint.textContent = 'Ollama not reachable'; modelHint.className = 'text-danger small'; }
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// For other providers: use static model list
|
|
265
|
+
if (modelSelect) {
|
|
266
|
+
modelSelect.textContent = '';
|
|
267
|
+
var options = MODEL_OPTIONS[provider] || [];
|
|
268
|
+
if (options.length === 0) {
|
|
269
|
+
var opt = document.createElement('option');
|
|
270
|
+
opt.value = '';
|
|
271
|
+
opt.textContent = 'N/A (Mode A)';
|
|
272
|
+
modelSelect.appendChild(opt);
|
|
273
|
+
} else {
|
|
274
|
+
options.forEach(function(o) {
|
|
275
|
+
var opt = document.createElement('option');
|
|
276
|
+
opt.value = o.value;
|
|
277
|
+
opt.textContent = o.label;
|
|
278
|
+
modelSelect.appendChild(opt);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// Restore previous model selection if it exists in the new list
|
|
282
|
+
if (currentModel) {
|
|
283
|
+
var found = false;
|
|
284
|
+
for (var i = 0; i < modelSelect.options.length; i++) {
|
|
285
|
+
if (modelSelect.options[i].value === currentModel) { found = true; break; }
|
|
286
|
+
}
|
|
287
|
+
if (found) {
|
|
288
|
+
modelSelect.value = currentModel;
|
|
289
|
+
} else if (currentModel) {
|
|
290
|
+
// Model not in list — add it as custom option so user doesn't lose their choice
|
|
291
|
+
var custom = document.createElement('option');
|
|
292
|
+
custom.value = currentModel;
|
|
293
|
+
custom.textContent = currentModel + ' (saved)';
|
|
294
|
+
modelSelect.insertBefore(custom, modelSelect.firstChild);
|
|
295
|
+
modelSelect.value = currentModel;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Update hint
|
|
301
|
+
if (modelHint) {
|
|
302
|
+
var hints = {
|
|
303
|
+
'none': 'No LLM needed in Mode A',
|
|
304
|
+
'openrouter': '200+ models via OpenRouter API',
|
|
305
|
+
'openai': 'OpenAI models (requires API key)',
|
|
306
|
+
'anthropic': 'Anthropic models (requires API key)',
|
|
307
|
+
};
|
|
308
|
+
modelHint.textContent = hints[provider] || '';
|
|
309
|
+
modelHint.className = 'text-muted small';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function testConnection() {
|
|
314
|
+
var provider = document.getElementById('settings-provider')?.value || '';
|
|
315
|
+
var model = document.getElementById('settings-model')?.value || '';
|
|
316
|
+
var apiKey = document.getElementById('settings-api-key')?.value || '';
|
|
317
|
+
var resultEl = document.getElementById('settings-test-result');
|
|
318
|
+
|
|
319
|
+
if (!provider) {
|
|
320
|
+
if (resultEl) { resultEl.textContent = 'Select a provider first'; resultEl.className = 'ms-2 small text-danger'; }
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (resultEl) { resultEl.textContent = 'Testing...'; resultEl.className = 'ms-2 small text-muted'; }
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
var resp = await fetch('/api/v3/provider/test', {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: {'Content-Type': 'application/json'},
|
|
330
|
+
body: JSON.stringify({provider: provider, model: model, api_key: apiKey})
|
|
331
|
+
});
|
|
332
|
+
var data = await resp.json();
|
|
333
|
+
if (data.success) {
|
|
334
|
+
if (resultEl) { resultEl.textContent = 'Connected! ' + (data.message || ''); resultEl.className = 'ms-2 small text-success fw-bold'; }
|
|
335
|
+
} else {
|
|
336
|
+
if (resultEl) { resultEl.textContent = 'Failed: ' + (data.error || 'Unknown'); resultEl.className = 'ms-2 small text-danger'; }
|
|
337
|
+
}
|
|
338
|
+
} catch (e) {
|
|
339
|
+
if (resultEl) { resultEl.textContent = 'Error: ' + e.message; resultEl.className = 'ms-2 small text-danger'; }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function saveAllSettings() {
|
|
344
|
+
var mode = document.querySelector('input[name="settings-mode-radio"]:checked')?.value || 'a';
|
|
345
|
+
var provider = document.getElementById('settings-provider')?.value || 'none';
|
|
346
|
+
if (mode === 'a') provider = 'none';
|
|
347
|
+
var model = document.getElementById('settings-model')?.value || '';
|
|
348
|
+
var apiKey = document.getElementById('settings-api-key')?.value || '';
|
|
349
|
+
|
|
350
|
+
var statusEl = document.getElementById('settings-save-status');
|
|
351
|
+
var saveBtn = document.getElementById('settings-save-all');
|
|
352
|
+
if (saveBtn) saveBtn.disabled = true;
|
|
353
|
+
if (statusEl) { statusEl.textContent = 'Saving...'; statusEl.style.display = 'inline'; statusEl.className = 'ms-2 text-muted'; }
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
// Save mode
|
|
357
|
+
var modeResp = await fetch('/api/v3/mode/set', {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
headers: {'Content-Type': 'application/json'},
|
|
360
|
+
body: JSON.stringify({mode: mode, provider: provider, model: model, api_key: apiKey})
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (modeResp.ok) {
|
|
364
|
+
if (statusEl) {
|
|
365
|
+
statusEl.textContent = 'Configuration saved! Mode: ' + mode.toUpperCase() +
|
|
366
|
+
(provider !== 'none' ? ' | Provider: ' + provider : '');
|
|
367
|
+
statusEl.className = 'ms-2 text-success fw-bold';
|
|
368
|
+
}
|
|
369
|
+
loadModeSettings();
|
|
370
|
+
} else {
|
|
371
|
+
if (statusEl) { statusEl.textContent = 'Save failed'; statusEl.className = 'ms-2 text-danger'; }
|
|
372
|
+
}
|
|
373
|
+
} catch (e) {
|
|
374
|
+
if (statusEl) { statusEl.textContent = 'Error: ' + e.message; statusEl.className = 'ms-2 text-danger'; }
|
|
375
|
+
}
|
|
376
|
+
if (saveBtn) saveBtn.disabled = false;
|
|
377
|
+
|
|
378
|
+
// Auto-hide status after 5 seconds
|
|
379
|
+
setTimeout(function() {
|
|
380
|
+
if (statusEl) statusEl.style.display = 'none';
|
|
381
|
+
}, 5000);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Bind events
|
|
385
|
+
document.getElementById('settings-provider')?.addEventListener('change', updateProviderUI);
|
|
386
|
+
document.getElementById('settings-save-all')?.addEventListener('click', saveAllSettings);
|
|
387
|
+
document.getElementById('settings-test-btn')?.addEventListener('click', testConnection);
|
|
388
|
+
|
|
389
|
+
// Mode radio buttons
|
|
390
|
+
document.querySelectorAll('input[name="settings-mode-radio"]').forEach(function(radio) {
|
|
391
|
+
radio.addEventListener('change', updateModeUI);
|
|
392
|
+
});
|
|
393
|
+
|
|
69
394
|
// Load settings when the settings tab is shown
|
|
70
|
-
document.getElementById('settings-tab')?.addEventListener('shown.bs.tab',
|
|
395
|
+
document.getElementById('settings-tab')?.addEventListener('shown.bs.tab', function() {
|
|
396
|
+
loadAutoSettings();
|
|
397
|
+
loadModeSettings();
|
|
398
|
+
updateModeUI();
|
|
399
|
+
});
|
package/ui/js/clusters.js
CHANGED
|
@@ -108,6 +108,17 @@ async function loadClusterMembers(clusterId, container) {
|
|
|
108
108
|
var data = await response.json();
|
|
109
109
|
container.textContent = '';
|
|
110
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);
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
if (!data.members || data.members.length === 0) {
|
|
112
123
|
var empty = document.createElement('div');
|
|
113
124
|
empty.className = 'text-muted small';
|
|
@@ -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';
|