superlocalmemory 3.0.17 → 3.0.19

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.
@@ -128,6 +128,32 @@ class DatabaseManager:
128
128
  )
129
129
  return record.memory_id
130
130
 
131
+ def update_memory_summary(self, memory_id: str, summary: str) -> None:
132
+ """Store a generated summary for a memory record."""
133
+ try:
134
+ self.execute(
135
+ "UPDATE memories SET metadata_json = json_set("
136
+ " COALESCE(metadata_json, '{}'), '$.summary', ?"
137
+ ") WHERE memory_id = ?",
138
+ (summary, memory_id),
139
+ )
140
+ except Exception:
141
+ pass # Non-critical — summary is enhancement only
142
+
143
+ def get_memory_summary(self, memory_id: str) -> str:
144
+ """Retrieve stored summary for a memory, or empty string."""
145
+ try:
146
+ rows = self.execute(
147
+ "SELECT json_extract(metadata_json, '$.summary') as s "
148
+ "FROM memories WHERE memory_id = ?",
149
+ (memory_id,),
150
+ )
151
+ if rows:
152
+ return dict(rows[0]).get("s") or ""
153
+ except Exception:
154
+ pass
155
+ return ""
156
+
131
157
  def store_fact(self, fact: AtomicFact) -> str:
132
158
  """Persist an atomic fact. Returns fact_id."""
133
159
  self.execute(
@@ -300,6 +326,29 @@ class DatabaseManager:
300
326
  for r in rows
301
327
  ]
302
328
 
329
+ def get_memory_content_batch(self, memory_ids: list[str]) -> dict[str, str]:
330
+ """Batch-fetch original memory text. Returns {memory_id: content}."""
331
+ if not memory_ids:
332
+ return {}
333
+ unique_ids = list(set(memory_ids))
334
+ ph = ','.join('?' * len(unique_ids))
335
+ rows = self.execute(
336
+ f"SELECT memory_id, content FROM memories WHERE memory_id IN ({ph})",
337
+ tuple(unique_ids),
338
+ )
339
+ return {dict(r)["memory_id"]: dict(r)["content"] for r in rows}
340
+
341
+ def get_facts_by_memory_id(
342
+ self, memory_id: str, profile_id: str,
343
+ ) -> list[AtomicFact]:
344
+ """Get all atomic facts for a given memory_id."""
345
+ rows = self.execute(
346
+ "SELECT * FROM atomic_facts WHERE memory_id = ? AND profile_id = ? "
347
+ "ORDER BY confidence DESC",
348
+ (memory_id, profile_id),
349
+ )
350
+ return [self._row_to_fact(r) for r in rows]
351
+
303
352
  def store_edge(self, edge: GraphEdge) -> str:
304
353
  """Persist a graph edge. Returns edge_id."""
305
354
  self.execute(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superlocalmemory
3
- Version: 3.0.17
3
+ Version: 3.0.19
4
4
  Summary: Information-geometric agent memory with mathematical guarantees
5
5
  Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
6
6
  License: MIT
@@ -30,13 +30,17 @@ src/superlocalmemory/compliance/retention.py
30
30
  src/superlocalmemory/compliance/scheduler.py
31
31
  src/superlocalmemory/core/__init__.py
32
32
  src/superlocalmemory/core/config.py
33
+ src/superlocalmemory/core/embedding_worker.py
33
34
  src/superlocalmemory/core/embeddings.py
34
35
  src/superlocalmemory/core/engine.py
35
36
  src/superlocalmemory/core/hooks.py
36
37
  src/superlocalmemory/core/maintenance.py
37
38
  src/superlocalmemory/core/modes.py
38
39
  src/superlocalmemory/core/profiles.py
40
+ src/superlocalmemory/core/recall_worker.py
39
41
  src/superlocalmemory/core/registry.py
42
+ src/superlocalmemory/core/summarizer.py
43
+ src/superlocalmemory/core/worker_pool.py
40
44
  src/superlocalmemory/dynamics/__init__.py
41
45
  src/superlocalmemory/dynamics/fisher_langevin_coupling.py
42
46
  src/superlocalmemory/encoding/__init__.py
package/ui/index.html CHANGED
@@ -1569,30 +1569,86 @@
1569
1569
  <h6 class="mb-0"><i class="bi bi-gear-wide-connected"></i> V3 Configuration</h6>
1570
1570
  </div>
1571
1571
  <div class="card-body">
1572
- <div class="row">
1573
- <div class="col-md-6">
1574
- <label class="form-label fw-bold">Operating Mode</label>
1575
- <select id="settings-mode" class="form-select">
1576
- <option value="a">Mode A — Local Guardian (Zero Cloud)</option>
1577
- <option value="b">Mode B — Smart Local (Ollama)</option>
1578
- <option value="c">Mode C — Full Power (Cloud LLM)</option>
1579
- </select>
1580
- <button class="btn btn-sm btn-primary mt-2" id="settings-mode-save">Save Mode</button>
1572
+ <!-- Current Mode Banner -->
1573
+ <div id="settings-current-banner" class="alert alert-info mb-3">
1574
+ <div class="d-flex justify-content-between align-items-center">
1575
+ <span>Active: <strong id="settings-current-mode">Loading...</strong></span>
1576
+ <span id="settings-current-detail" class="small"></span>
1581
1577
  </div>
1582
- <div class="col-md-6">
1583
- <label class="form-label fw-bold">LLM Provider</label>
1584
- <select id="settings-provider" class="form-select">
1585
- <option value="none">None (Mode A)</option>
1586
- <option value="openai">OpenAI</option>
1587
- <option value="anthropic">Anthropic</option>
1588
- <option value="ollama">Ollama (Local)</option>
1589
- <option value="openrouter">OpenRouter</option>
1590
- </select>
1591
- <div class="mt-2">
1592
- <input type="password" id="settings-api-key" class="form-control form-control-sm" placeholder="API Key">
1578
+ </div>
1579
+
1580
+ <!-- Step 1: Mode -->
1581
+ <div class="mb-3">
1582
+ <label class="form-label fw-bold">Step 1: Operating Mode</label>
1583
+ <div class="btn-group w-100" role="group">
1584
+ <input type="radio" class="btn-check" name="settings-mode-radio" id="mode-a-radio" value="a" checked>
1585
+ <label class="btn btn-outline-success" for="mode-a-radio">
1586
+ <strong>Mode A</strong><br><small>Zero Cloud — EU AI Act</small>
1587
+ </label>
1588
+ <input type="radio" class="btn-check" name="settings-mode-radio" id="mode-b-radio" value="b">
1589
+ <label class="btn btn-outline-info" for="mode-b-radio">
1590
+ <strong>Mode B</strong><br><small>Local Ollama LLM</small>
1591
+ </label>
1592
+ <input type="radio" class="btn-check" name="settings-mode-radio" id="mode-c-radio" value="c">
1593
+ <label class="btn btn-outline-warning" for="mode-c-radio">
1594
+ <strong>Mode C</strong><br><small>Cloud LLM (Best Accuracy)</small>
1595
+ </label>
1596
+ </div>
1597
+ </div>
1598
+
1599
+ <!-- Step 2: Provider Config (Mode B/C only) -->
1600
+ <div id="settings-provider-panel" style="display:none;" class="card p-3 mb-3 border-primary">
1601
+ <h6 class="mb-2">Step 2: LLM Configuration</h6>
1602
+
1603
+ <!-- Provider select -->
1604
+ <div class="row mb-2">
1605
+ <div class="col-md-4">
1606
+ <label class="form-label small fw-bold">Provider</label>
1607
+ <select id="settings-provider" class="form-select form-select-sm">
1608
+ <option value="">-- Select --</option>
1609
+ <option value="ollama">Ollama (Local)</option>
1610
+ <option value="openrouter">OpenRouter</option>
1611
+ <option value="openai">OpenAI</option>
1612
+ <option value="anthropic">Anthropic</option>
1613
+ </select>
1614
+ </div>
1615
+ <div class="col-md-4">
1616
+ <label class="form-label small fw-bold">Model <span class="text-danger">*</span></label>
1617
+ <select id="settings-model" class="form-select form-select-sm">
1618
+ <option value="">Select provider first</option>
1619
+ </select>
1620
+ <small id="settings-model-hint" class="text-muted"></small>
1621
+ </div>
1622
+ <div class="col-md-4" id="settings-key-col" style="display:none;">
1623
+ <label class="form-label small fw-bold">API Key <span class="text-danger">*</span></label>
1624
+ <input type="password" id="settings-api-key" class="form-control form-control-sm" placeholder="sk-... or your key">
1625
+ <small class="text-muted">Saved locally in ~/.superlocalmemory/</small>
1626
+ </div>
1627
+ </div>
1628
+
1629
+ <!-- Endpoint (advanced) -->
1630
+ <div class="row mb-2" id="settings-endpoint-row" style="display:none;">
1631
+ <div class="col-md-8">
1632
+ <label class="form-label small">API Endpoint (advanced)</label>
1633
+ <input type="text" id="settings-endpoint" class="form-control form-control-sm" placeholder="https://...">
1593
1634
  </div>
1594
- <button class="btn btn-sm btn-primary mt-2" id="settings-provider-save">Save Provider</button>
1595
1635
  </div>
1636
+
1637
+ <!-- Connection test -->
1638
+ <div>
1639
+ <button class="btn btn-sm btn-outline-primary" id="settings-test-btn">
1640
+ <i class="bi bi-lightning"></i> Test Connection
1641
+ </button>
1642
+ <span id="settings-test-result" class="ms-2 small"></span>
1643
+ </div>
1644
+ </div>
1645
+
1646
+ <!-- Save button -->
1647
+ <div class="mt-2">
1648
+ <button class="btn btn-primary" id="settings-save-all">
1649
+ <i class="bi bi-check-circle"></i> Save Configuration
1650
+ </button>
1651
+ <span id="settings-save-status" class="ms-2" style="display:none;"></span>
1596
1652
  </div>
1597
1653
  <hr>
1598
1654
  <div class="row">
@@ -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', loadAutoSettings);
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