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.
- package/CHANGELOG.md +167 -1803
- package/README.md +212 -397
- package/bin/slm +179 -3
- package/bin/superlocalmemoryv2:learning +4 -0
- package/bin/superlocalmemoryv2:patterns +4 -0
- package/docs/ACCESSIBILITY.md +291 -0
- package/docs/ARCHITECTURE.md +12 -6
- package/docs/FRAMEWORK-INTEGRATIONS.md +300 -0
- package/docs/MCP-MANUAL-SETUP.md +14 -4
- package/install.sh +99 -3
- package/mcp_server.py +291 -1
- package/package.json +2 -1
- package/requirements-learning.txt +12 -0
- package/scripts/verify-v27.sh +233 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/learning/__init__.py +201 -0
- package/src/learning/adaptive_ranker.py +826 -0
- package/src/learning/cross_project_aggregator.py +866 -0
- package/src/learning/engagement_tracker.py +638 -0
- package/src/learning/feature_extractor.py +461 -0
- package/src/learning/feedback_collector.py +690 -0
- package/src/learning/learning_db.py +842 -0
- package/src/learning/project_context_manager.py +582 -0
- package/src/learning/source_quality_scorer.py +685 -0
- package/src/learning/synthetic_bootstrap.py +1047 -0
- package/src/learning/tests/__init__.py +0 -0
- package/src/learning/tests/test_adaptive_ranker.py +328 -0
- package/src/learning/tests/test_aggregator.py +309 -0
- package/src/learning/tests/test_feedback_collector.py +295 -0
- package/src/learning/tests/test_learning_db.py +606 -0
- package/src/learning/tests/test_project_context.py +296 -0
- package/src/learning/tests/test_source_quality.py +355 -0
- package/src/learning/tests/test_synthetic_bootstrap.py +433 -0
- package/src/learning/tests/test_workflow_miner.py +322 -0
- package/src/learning/workflow_pattern_miner.py +665 -0
- package/ui/index.html +346 -13
- package/ui/js/clusters.js +90 -1
- package/ui/js/graph-core.js +445 -0
- package/ui/js/graph-cytoscape-monolithic-backup.js +1168 -0
- package/ui/js/graph-cytoscape.js +1168 -0
- package/ui/js/graph-d3-backup.js +32 -0
- package/ui/js/graph-filters.js +220 -0
- package/ui/js/graph-interactions.js +354 -0
- package/ui/js/graph-ui.js +214 -0
- package/ui/js/memories.js +52 -0
- 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
|
-
|
|
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
|
|