superlocalmemory 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +167 -1803
  2. package/README.md +212 -397
  3. package/bin/slm +179 -3
  4. package/bin/superlocalmemoryv2:learning +4 -0
  5. package/bin/superlocalmemoryv2:patterns +4 -0
  6. package/docs/ACCESSIBILITY.md +291 -0
  7. package/docs/ARCHITECTURE.md +12 -6
  8. package/docs/FRAMEWORK-INTEGRATIONS.md +300 -0
  9. package/docs/MCP-MANUAL-SETUP.md +14 -4
  10. package/install.sh +99 -3
  11. package/mcp_server.py +291 -1
  12. package/package.json +2 -1
  13. package/requirements-learning.txt +12 -0
  14. package/scripts/verify-v27.sh +233 -0
  15. package/skills/slm-show-patterns/SKILL.md +224 -0
  16. package/src/learning/__init__.py +201 -0
  17. package/src/learning/adaptive_ranker.py +826 -0
  18. package/src/learning/cross_project_aggregator.py +866 -0
  19. package/src/learning/engagement_tracker.py +638 -0
  20. package/src/learning/feature_extractor.py +461 -0
  21. package/src/learning/feedback_collector.py +690 -0
  22. package/src/learning/learning_db.py +842 -0
  23. package/src/learning/project_context_manager.py +582 -0
  24. package/src/learning/source_quality_scorer.py +685 -0
  25. package/src/learning/synthetic_bootstrap.py +1047 -0
  26. package/src/learning/tests/__init__.py +0 -0
  27. package/src/learning/tests/test_adaptive_ranker.py +328 -0
  28. package/src/learning/tests/test_aggregator.py +309 -0
  29. package/src/learning/tests/test_feedback_collector.py +295 -0
  30. package/src/learning/tests/test_learning_db.py +606 -0
  31. package/src/learning/tests/test_project_context.py +296 -0
  32. package/src/learning/tests/test_source_quality.py +355 -0
  33. package/src/learning/tests/test_synthetic_bootstrap.py +433 -0
  34. package/src/learning/tests/test_workflow_miner.py +322 -0
  35. package/src/learning/workflow_pattern_miner.py +665 -0
  36. package/ui/index.html +346 -13
  37. package/ui/js/clusters.js +90 -1
  38. package/ui/js/graph-core.js +445 -0
  39. package/ui/js/graph-cytoscape-monolithic-backup.js +1168 -0
  40. package/ui/js/graph-cytoscape.js +1168 -0
  41. package/ui/js/graph-d3-backup.js +32 -0
  42. package/ui/js/graph-filters.js +220 -0
  43. package/ui/js/graph-interactions.js +354 -0
  44. package/ui/js/graph-ui.js +214 -0
  45. package/ui/js/memories.js +52 -0
  46. package/ui/js/modal.js +104 -1
@@ -0,0 +1,214 @@
1
+ // SuperLocalMemory V2.6.5 - Interactive Knowledge Graph - UI Elements Module
2
+ // Copyright (c) 2026 Varun Pratap Bhardwaj — MIT License
3
+ // Part of modular graph visualization system (split from monolithic graph-cytoscape.js)
4
+
5
+ // ============================================================================
6
+ // FILTER BADGE UI
7
+ // ============================================================================
8
+
9
+ function updateFilterBadge() {
10
+ const statusFull = document.getElementById('graph-status-full');
11
+ const statusFiltered = document.getElementById('graph-status-filtered');
12
+ const filterDescription = document.getElementById('graph-filter-description');
13
+ const filterCount = document.getElementById('graph-filter-count');
14
+ const statusFullText = document.getElementById('graph-status-full-text');
15
+
16
+ const hasFilter = filterState.cluster_id || filterState.entity;
17
+
18
+ if (hasFilter) {
19
+ // FILTERED STATE - Show prominent alert with "Show All Memories" button
20
+ if (statusFull) statusFull.style.display = 'none';
21
+ if (statusFiltered) statusFiltered.style.display = 'block';
22
+
23
+ // Update filter description
24
+ if (filterDescription) {
25
+ const text = filterState.cluster_id
26
+ ? `Viewing Cluster ${filterState.cluster_id}`
27
+ : `Viewing: ${filterState.entity}`;
28
+ filterDescription.textContent = text;
29
+ }
30
+
31
+ // Update count (will be set after graph renders)
32
+ if (filterCount && graphData && graphData.nodes) {
33
+ filterCount.textContent = `(${graphData.nodes.length} ${graphData.nodes.length === 1 ? 'memory' : 'memories'})`;
34
+ }
35
+
36
+ console.log('[updateFilterBadge] Showing FILTERED state');
37
+ } else {
38
+ // FULL GRAPH STATE - Show normal status with refresh button
39
+ if (statusFull) statusFull.style.display = 'block';
40
+ if (statusFiltered) statusFiltered.style.display = 'none';
41
+
42
+ // Update count
43
+ if (statusFullText && graphData && graphData.nodes) {
44
+ const maxNodes = document.getElementById('graph-max-nodes')?.value || 50;
45
+ statusFullText.textContent = `Showing ${graphData.nodes.length} of ${maxNodes} memories`;
46
+ }
47
+
48
+ console.log('[updateFilterBadge] Showing FULL state');
49
+ }
50
+ }
51
+
52
+ // ============================================================================
53
+ // GRAPH STATS
54
+ // ============================================================================
55
+
56
+ function updateGraphStats(data) {
57
+ const statsEl = document.getElementById('graph-stats');
58
+ if (statsEl) {
59
+ // Clear and rebuild safely
60
+ statsEl.textContent = '';
61
+
62
+ const nodeBadge = document.createElement('span');
63
+ nodeBadge.className = 'badge bg-primary';
64
+ nodeBadge.textContent = `${data.nodes.length} nodes`;
65
+ statsEl.appendChild(nodeBadge);
66
+
67
+ const edgeBadge = document.createElement('span');
68
+ edgeBadge.className = 'badge bg-secondary';
69
+ edgeBadge.textContent = `${data.links.length} edges`;
70
+ statsEl.appendChild(document.createTextNode(' '));
71
+ statsEl.appendChild(edgeBadge);
72
+
73
+ const clusterBadge = document.createElement('span');
74
+ clusterBadge.className = 'badge bg-info';
75
+ clusterBadge.textContent = `${data.clusters?.length || 0} clusters`;
76
+ statsEl.appendChild(document.createTextNode(' '));
77
+ statsEl.appendChild(clusterBadge);
78
+ }
79
+ }
80
+
81
+ // ============================================================================
82
+ // LOADING SPINNER
83
+ // ============================================================================
84
+
85
+ function showLoadingSpinner() {
86
+ const container = document.getElementById('graph-container');
87
+ if (container) {
88
+ container.textContent = ''; // Clear safely
89
+
90
+ const wrapper = document.createElement('div');
91
+ wrapper.style.cssText = 'text-align:center; padding:100px;';
92
+
93
+ const spinner = document.createElement('div');
94
+ spinner.className = 'spinner-border text-primary';
95
+ spinner.setAttribute('role', 'status');
96
+ wrapper.appendChild(spinner);
97
+
98
+ const text = document.createElement('p');
99
+ text.style.marginTop = '20px';
100
+ text.textContent = 'Loading graph...';
101
+ wrapper.appendChild(text);
102
+
103
+ container.appendChild(wrapper);
104
+ }
105
+ }
106
+
107
+ function hideLoadingSpinner() {
108
+ // Do nothing - renderGraph() already cleared the spinner
109
+ // If we clear here, we destroy the Cytoscape canvas!
110
+ console.log('[hideLoadingSpinner] Graph already rendered, spinner cleared by renderGraph()');
111
+ }
112
+
113
+ function showError(message) {
114
+ const container = document.getElementById('graph-container');
115
+ if (container) {
116
+ container.textContent = ''; // Clear safely
117
+
118
+ const alert = document.createElement('div');
119
+ alert.className = 'alert alert-danger';
120
+ alert.setAttribute('role', 'alert');
121
+ alert.style.margin = '50px';
122
+ alert.textContent = message;
123
+ container.appendChild(alert);
124
+ }
125
+ }
126
+
127
+ // ============================================================================
128
+ // LAYOUT MANAGEMENT
129
+ // ============================================================================
130
+
131
+ function saveLayoutPositions() {
132
+ if (!cy) return;
133
+
134
+ const positions = {};
135
+ cy.nodes().forEach(node => {
136
+ positions[node.id()] = node.position();
137
+ });
138
+
139
+ try {
140
+ localStorage.setItem('slm_graph_layout', JSON.stringify(positions));
141
+ } catch (e) {
142
+ console.warn('Failed to save graph layout:', e);
143
+ }
144
+ }
145
+
146
+ function restoreSavedLayout() {
147
+ if (!cy) return;
148
+
149
+ try {
150
+ const saved = localStorage.getItem('slm_graph_layout');
151
+ if (saved) {
152
+ const positions = JSON.parse(saved);
153
+ cy.nodes().forEach(node => {
154
+ const pos = positions[node.id()];
155
+ if (pos) {
156
+ node.position(pos);
157
+ }
158
+ });
159
+ }
160
+ } catch (e) {
161
+ console.warn('Failed to restore graph layout:', e);
162
+ }
163
+ }
164
+
165
+ function changeGraphLayout(layoutName) {
166
+ if (!cy) return;
167
+
168
+ currentLayout = layoutName;
169
+ const layout = cy.layout(getLayoutConfig(layoutName));
170
+ layout.run();
171
+
172
+ // Save preference
173
+ localStorage.setItem('slm_graph_layout_preference', layoutName);
174
+ }
175
+
176
+ // ============================================================================
177
+ // EXPAND NEIGHBORS
178
+ // ============================================================================
179
+
180
+ function expandNeighbors(memoryId) {
181
+ if (!cy) return;
182
+
183
+ const node = cy.getElementById(String(memoryId));
184
+ if (!node || node.length === 0) return;
185
+
186
+ // Hide all nodes and edges
187
+ cy.elements().addClass('dimmed');
188
+
189
+ // Show target node + neighbors + connecting edges
190
+ node.removeClass('dimmed');
191
+ node.neighborhood().removeClass('dimmed');
192
+ node.connectedEdges().removeClass('dimmed');
193
+
194
+ // Fit view to visible elements
195
+ cy.fit(node.neighborhood().union(node), 50);
196
+ }
197
+
198
+ // ============================================================================
199
+ // SCREEN READER STATUS
200
+ // ============================================================================
201
+
202
+ function updateScreenReaderStatus(message) {
203
+ let statusRegion = document.getElementById('graph-sr-status');
204
+ if (!statusRegion) {
205
+ statusRegion = document.createElement('div');
206
+ statusRegion.id = 'graph-sr-status';
207
+ statusRegion.setAttribute('role', 'status');
208
+ statusRegion.setAttribute('aria-live', 'polite');
209
+ statusRegion.setAttribute('aria-atomic', 'true');
210
+ statusRegion.style.cssText = 'position:absolute; left:-10000px; width:1px; height:1px; overflow:hidden;';
211
+ document.body.appendChild(statusRegion);
212
+ }
213
+ statusRegion.textContent = message;
214
+ }
package/ui/js/memories.js CHANGED
@@ -147,3 +147,55 @@ function handleSort(th) {
147
147
  var showScores = memories.length > 0 && typeof memories[0].score === 'number';
148
148
  renderMemoriesTable(memories, showScores);
149
149
  }
150
+
151
+ // ============================================================================
152
+ // Navigation from Graph (v2.6.5)
153
+ // ============================================================================
154
+
155
+ // Scroll to a specific memory by ID (called from graph double-click)
156
+ function scrollToMemory(memoryId) {
157
+ const container = document.getElementById('memories-list');
158
+ if (!container) return;
159
+
160
+ // Find the memory in current list
161
+ if (!window._slmMemories) {
162
+ console.warn('No memories loaded yet. Loading all memories...');
163
+ loadMemories().then(() => {
164
+ setTimeout(() => scrollToMemoryInTable(memoryId), 500);
165
+ });
166
+ return;
167
+ }
168
+
169
+ scrollToMemoryInTable(memoryId);
170
+ }
171
+
172
+ function scrollToMemoryInTable(memoryId) {
173
+ const memId = String(memoryId);
174
+
175
+ // Find the memory index
176
+ let idx = -1;
177
+ if (window._slmMemories) {
178
+ idx = window._slmMemories.findIndex(m => String(m.id) === memId);
179
+ }
180
+
181
+ if (idx === -1) {
182
+ console.warn(`Memory #${memoryId} not found in current list`);
183
+ return;
184
+ }
185
+
186
+ // Find the table row
187
+ const row = document.querySelector(`tr[data-mem-idx="${idx}"]`);
188
+ if (!row) {
189
+ console.warn(`Row for memory #${memoryId} not found in DOM`);
190
+ return;
191
+ }
192
+
193
+ // Scroll to row
194
+ row.scrollIntoView({ behavior: 'smooth', block: 'center' });
195
+
196
+ // Highlight temporarily
197
+ row.style.backgroundColor = '#fff3cd';
198
+ setTimeout(() => {
199
+ row.style.backgroundColor = '';
200
+ }, 2000);
201
+ }
package/ui/js/modal.js CHANGED
@@ -14,6 +14,11 @@ function openMemoryDetail(mem) {
14
14
  return;
15
15
  }
16
16
 
17
+ // Store last focused element (for keyboard nav return)
18
+ if (!window.lastFocusedElement) {
19
+ window.lastFocusedElement = document.activeElement;
20
+ }
21
+
17
22
  var content = mem.content || mem.summary || '(no content)';
18
23
  var tags = mem.tags || '';
19
24
  var importance = mem.importance || 5;
@@ -57,7 +62,105 @@ function openMemoryDetail(mem) {
57
62
 
58
63
  body.appendChild(dl);
59
64
 
60
- var modal = new bootstrap.Modal(document.getElementById('memoryDetailModal'));
65
+ // Graph action buttons (v2.6.5)
66
+ if (mem.cluster_id || mem.id) {
67
+ body.appendChild(document.createElement('hr'));
68
+
69
+ var actionsDiv = document.createElement('div');
70
+ actionsDiv.className = 'memory-detail-graph-actions';
71
+ actionsDiv.style.cssText = 'display:flex; gap:10px; flex-wrap:wrap;';
72
+
73
+ // Button 1: View Full Memory (navigate to Memories tab)
74
+ var viewBtn = document.createElement('button');
75
+ viewBtn.className = 'btn btn-primary btn-sm';
76
+ var viewIcon = document.createElement('i');
77
+ viewIcon.className = 'bi bi-journal-text';
78
+ viewBtn.appendChild(viewIcon);
79
+ viewBtn.appendChild(document.createTextNode(' View Full Memory'));
80
+ viewBtn.onclick = function() {
81
+ modal.hide();
82
+ if (typeof navigateToMemoryTab === 'function') {
83
+ navigateToMemoryTab(mem.id);
84
+ } else {
85
+ // Fallback: just switch tab
86
+ const memoriesTab = document.querySelector('a[href="#memories"]');
87
+ if (memoriesTab) memoriesTab.click();
88
+ }
89
+ };
90
+ actionsDiv.appendChild(viewBtn);
91
+
92
+ // Button 2: Expand Neighbors (show connected nodes in graph)
93
+ var expandBtn = document.createElement('button');
94
+ expandBtn.className = 'btn btn-outline-secondary btn-sm';
95
+ var expandIcon = document.createElement('i');
96
+ expandIcon.className = 'bi bi-diagram-3';
97
+ expandBtn.appendChild(expandIcon);
98
+ expandBtn.appendChild(document.createTextNode(' Expand Neighbors'));
99
+ expandBtn.onclick = function() {
100
+ modal.hide();
101
+ // Switch to Graph tab
102
+ const graphTab = document.querySelector('a[href="#graph"]');
103
+ if (graphTab) graphTab.click();
104
+ // Expand neighbors after a delay
105
+ setTimeout(function() {
106
+ if (typeof expandNeighbors === 'function') {
107
+ expandNeighbors(mem.id);
108
+ }
109
+ }, 500);
110
+ };
111
+ actionsDiv.appendChild(expandBtn);
112
+
113
+ // Button 3: Filter to Cluster (show only this cluster in graph)
114
+ if (mem.cluster_id) {
115
+ var filterBtn = document.createElement('button');
116
+ filterBtn.className = 'btn btn-outline-info btn-sm';
117
+ var filterIcon = document.createElement('i');
118
+ filterIcon.className = 'bi bi-funnel';
119
+ filterBtn.appendChild(filterIcon);
120
+ filterBtn.appendChild(document.createTextNode(' Filter to Cluster ' + mem.cluster_id));
121
+ filterBtn.onclick = function() {
122
+ modal.hide();
123
+ // Switch to Graph tab
124
+ const graphTab = document.querySelector('a[href="#graph"]');
125
+ if (graphTab) graphTab.click();
126
+ // Apply cluster filter after a delay
127
+ setTimeout(function() {
128
+ if (typeof filterState !== 'undefined' && typeof filterByCluster === 'function' && typeof renderGraph === 'function') {
129
+ filterState.cluster_id = mem.cluster_id;
130
+ const filtered = filterByCluster(originalGraphData, mem.cluster_id);
131
+ renderGraph(filtered);
132
+ // Update URL
133
+ const url = new URL(window.location);
134
+ url.searchParams.set('cluster_id', mem.cluster_id);
135
+ window.history.replaceState({}, '', url);
136
+ }
137
+ }, 500);
138
+ };
139
+ actionsDiv.appendChild(filterBtn);
140
+ }
141
+
142
+ body.appendChild(actionsDiv);
143
+ }
144
+
145
+ var modalEl = document.getElementById('memoryDetailModal');
146
+ var modal = new bootstrap.Modal(modalEl);
147
+
148
+ // Focus first interactive element when modal opens
149
+ modalEl.addEventListener('shown.bs.modal', function() {
150
+ const firstButton = modalEl.querySelector('button, a[href]');
151
+ if (firstButton) {
152
+ firstButton.focus();
153
+ }
154
+ }, { once: true });
155
+
156
+ // Return focus when modal closes
157
+ modalEl.addEventListener('hidden.bs.modal', function() {
158
+ if (window.lastFocusedElement && typeof window.lastFocusedElement.focus === 'function') {
159
+ window.lastFocusedElement.focus();
160
+ window.lastFocusedElement = null;
161
+ }
162
+ }, { once: true });
163
+
61
164
  modal.show();
62
165
  }
63
166