superlocalmemory 3.4.17 → 3.4.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/CHANGELOG.md +8 -0
- package/package.json +1 -3
- package/pyproject.toml +10 -1
- package/src/superlocalmemory/cli/setup_wizard.py +30 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +4 -1
- package/src/superlocalmemory.egg-info/requires.txt +3 -0
- package/docs/ARCHITECTURE.md +0 -149
- package/docs/api-reference.md +0 -284
- package/docs/auto-memory.md +0 -150
- package/docs/cli-reference.md +0 -327
- package/docs/cloud-backup.md +0 -174
- package/docs/compliance.md +0 -191
- package/docs/configuration.md +0 -182
- package/docs/getting-started.md +0 -102
- package/docs/ide-setup.md +0 -261
- package/docs/mcp-tools.md +0 -220
- package/docs/migration-from-v2.md +0 -170
- package/docs/profiles.md +0 -173
- package/docs/screenshots/01-dashboard-main.png +0 -0
- package/docs/screenshots/02-knowledge-graph.png +0 -0
- package/docs/screenshots/03-math-health.png +0 -0
- package/docs/screenshots/03-patterns-learning.png +0 -0
- package/docs/screenshots/04-learning-dashboard.png +0 -0
- package/docs/screenshots/04-recall-lab.png +0 -0
- package/docs/screenshots/05-behavioral-analysis.png +0 -0
- package/docs/screenshots/05-trust-dashboard.png +0 -0
- package/docs/screenshots/06-graph-communities.png +0 -0
- package/docs/screenshots/06-settings.png +0 -0
- package/docs/screenshots/07-memories-blurred.png +0 -0
- package/docs/skill-evolution.md +0 -256
- package/docs/troubleshooting.md +0 -310
- package/docs/v2-archive/ACCESSIBILITY.md +0 -291
- package/docs/v2-archive/ARCHITECTURE.md +0 -886
- package/docs/v2-archive/CLI-COMMANDS-REFERENCE.md +0 -425
- package/docs/v2-archive/COMPRESSION-README.md +0 -390
- package/docs/v2-archive/FRAMEWORK-INTEGRATIONS.md +0 -300
- package/docs/v2-archive/MCP-MANUAL-SETUP.md +0 -775
- package/docs/v2-archive/MCP-TROUBLESHOOTING.md +0 -787
- package/docs/v2-archive/PATTERN-LEARNING.md +0 -228
- package/docs/v2-archive/PROFILES-GUIDE.md +0 -453
- package/docs/v2-archive/RESET-GUIDE.md +0 -353
- package/docs/v2-archive/SEARCH-ENGINE-V2.2.0.md +0 -749
- package/docs/v2-archive/SEARCH-INTEGRATION-GUIDE.md +0 -502
- package/docs/v2-archive/UI-SERVER.md +0 -262
- package/docs/v2-archive/UNIVERSAL-INTEGRATION.md +0 -488
- package/docs/v2-archive/V2.2.0-OPTIONAL-SEARCH.md +0 -666
- package/docs/v2-archive/WINDOWS-INSTALL-README.txt +0 -34
- package/docs/v2-archive/WINDOWS-POST-INSTALL.txt +0 -45
- package/docs/v2-archive/example_graph_usage.py +0 -146
- package/ui/index.html +0 -1879
- package/ui/js/agents.js +0 -192
- package/ui/js/auto-settings.js +0 -399
- package/ui/js/behavioral.js +0 -276
- package/ui/js/clusters.js +0 -206
- package/ui/js/compliance.js +0 -252
- package/ui/js/core.js +0 -246
- package/ui/js/dashboard.js +0 -110
- package/ui/js/events.js +0 -178
- package/ui/js/fact-detail.js +0 -92
- package/ui/js/feedback.js +0 -333
- package/ui/js/graph-core.js +0 -447
- package/ui/js/graph-filters.js +0 -220
- package/ui/js/graph-interactions.js +0 -351
- package/ui/js/graph-ui.js +0 -214
- package/ui/js/ide-status.js +0 -102
- package/ui/js/init.js +0 -45
- package/ui/js/learning.js +0 -435
- package/ui/js/lifecycle.js +0 -298
- package/ui/js/math-health.js +0 -98
- package/ui/js/memories.js +0 -264
- package/ui/js/modal.js +0 -357
- package/ui/js/patterns.js +0 -93
- package/ui/js/profiles.js +0 -236
- package/ui/js/recall-lab.js +0 -292
- package/ui/js/search.js +0 -59
- package/ui/js/settings.js +0 -224
- package/ui/js/timeline.js +0 -32
- package/ui/js/trust-dashboard.js +0 -73
package/ui/js/graph-core.js
DELETED
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
// SuperLocalMemory V2.6.5 - Interactive Knowledge Graph - Core Rendering Module
|
|
2
|
-
// Copyright (c) 2026 Varun Pratap Bhardwaj — Elastic License 2.0
|
|
3
|
-
// Part of modular graph visualization system (split from monolithic graph-cytoscape.js)
|
|
4
|
-
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// GLOBAL STATE
|
|
7
|
-
// ============================================================================
|
|
8
|
-
|
|
9
|
-
var cy = null; // Cytoscape.js instance (global)
|
|
10
|
-
var graphData = { nodes: [], links: [] }; // Raw data from API
|
|
11
|
-
var originalGraphData = { nodes: [], links: [] }; // Unfiltered data (for reset)
|
|
12
|
-
var currentLayout = 'fcose'; // Default layout
|
|
13
|
-
var filterState = { cluster_id: null, entity: null }; // Current filters
|
|
14
|
-
var isInitialLoad = true; // Track if this is the first graph load
|
|
15
|
-
var focusedNodeIndex = 0; // Keyboard navigation: currently focused node
|
|
16
|
-
var keyboardNavigationEnabled = false; // Track if keyboard nav is active
|
|
17
|
-
var lastFocusedElement = null; // Store last focused element for modal return
|
|
18
|
-
|
|
19
|
-
// ============================================================================
|
|
20
|
-
// CLUSTER COLORS
|
|
21
|
-
// ============================================================================
|
|
22
|
-
|
|
23
|
-
const CLUSTER_COLORS = [
|
|
24
|
-
'#667eea', '#764ba2', '#43e97b', '#38f9d7',
|
|
25
|
-
'#4facfe', '#00f2fe', '#f093fb', '#f5576c',
|
|
26
|
-
'#fa709a', '#fee140', '#30cfd0', '#330867'
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
function getClusterColor(cluster_id) {
|
|
30
|
-
if (!cluster_id) return '#999';
|
|
31
|
-
return CLUSTER_COLORS[cluster_id % CLUSTER_COLORS.length];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ============================================================================
|
|
35
|
-
// HTML ESCAPE UTILITY
|
|
36
|
-
// ============================================================================
|
|
37
|
-
|
|
38
|
-
function escapeHtml(text) {
|
|
39
|
-
const div = document.createElement('div');
|
|
40
|
-
div.textContent = text;
|
|
41
|
-
return div.innerHTML;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ============================================================================
|
|
45
|
-
// GRAPH LOADING
|
|
46
|
-
// ============================================================================
|
|
47
|
-
|
|
48
|
-
async function loadGraph() {
|
|
49
|
-
var maxNodes = document.getElementById('graph-max-nodes').value;
|
|
50
|
-
var minImportance = document.getElementById('graph-min-importance')?.value || 1;
|
|
51
|
-
|
|
52
|
-
// Get cluster filter from URL params ONLY on initial load
|
|
53
|
-
// After that, use filterState (which tab event handler controls)
|
|
54
|
-
if (isInitialLoad) {
|
|
55
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
56
|
-
const clusterIdParam = urlParams.get('cluster_id');
|
|
57
|
-
if (clusterIdParam) {
|
|
58
|
-
filterState.cluster_id = parseInt(clusterIdParam);
|
|
59
|
-
console.log('[loadGraph] Initial load with cluster filter from URL:', filterState.cluster_id);
|
|
60
|
-
}
|
|
61
|
-
isInitialLoad = false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// CRITICAL FIX: When filtering by cluster, fetch MORE nodes to ensure all cluster members are included
|
|
65
|
-
// Otherwise only top N memories are fetched and cluster filter fails
|
|
66
|
-
const fetchLimit = filterState.cluster_id ? 200 : maxNodes;
|
|
67
|
-
console.log('[loadGraph] Fetching with limit:', fetchLimit, 'Cluster filter:', filterState.cluster_id);
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
showLoadingSpinner();
|
|
71
|
-
const response = await fetch(`/api/graph?max_nodes=${fetchLimit}&min_importance=${minImportance}`);
|
|
72
|
-
graphData = await response.json();
|
|
73
|
-
originalGraphData = JSON.parse(JSON.stringify(graphData)); // Deep copy
|
|
74
|
-
|
|
75
|
-
// Apply filters if set
|
|
76
|
-
if (filterState.cluster_id) {
|
|
77
|
-
graphData = filterByCluster(originalGraphData, filterState.cluster_id);
|
|
78
|
-
}
|
|
79
|
-
if (filterState.entity) {
|
|
80
|
-
graphData = filterByEntity(originalGraphData, filterState.entity);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
renderGraph(graphData);
|
|
84
|
-
hideLoadingSpinner();
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error('Error loading graph:', error);
|
|
87
|
-
showError('Failed to load graph. Please try again.');
|
|
88
|
-
hideLoadingSpinner();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ============================================================================
|
|
93
|
-
// DATA TRANSFORMATION
|
|
94
|
-
// ============================================================================
|
|
95
|
-
|
|
96
|
-
function transformDataForCytoscape(data) {
|
|
97
|
-
const elements = [];
|
|
98
|
-
|
|
99
|
-
// Add nodes
|
|
100
|
-
data.nodes.forEach(node => {
|
|
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}`;
|
|
105
|
-
const preview = contentPreview.substring(0, 50) + (contentPreview.length > 50 ? '...' : '');
|
|
106
|
-
|
|
107
|
-
elements.push({
|
|
108
|
-
group: 'nodes',
|
|
109
|
-
data: {
|
|
110
|
-
id: String(node.id),
|
|
111
|
-
label: label,
|
|
112
|
-
content: node.content || '',
|
|
113
|
-
summary: node.summary || '',
|
|
114
|
-
content_preview: preview,
|
|
115
|
-
category: node.category || '',
|
|
116
|
-
project_name: node.project_name || '',
|
|
117
|
-
cluster_id: node.cluster_id || 0,
|
|
118
|
-
importance: node.importance || 5,
|
|
119
|
-
tags: node.tags || '',
|
|
120
|
-
entities: node.entities || [],
|
|
121
|
-
created_at: node.created_at || '',
|
|
122
|
-
// For rendering
|
|
123
|
-
weight: (node.importance || 5) * 5 // Size multiplier
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Add edges
|
|
129
|
-
data.links.forEach(link => {
|
|
130
|
-
const sourceId = String(typeof link.source === 'object' ? link.source.id : link.source);
|
|
131
|
-
const targetId = String(typeof link.target === 'object' ? link.target.id : link.target);
|
|
132
|
-
|
|
133
|
-
elements.push({
|
|
134
|
-
group: 'edges',
|
|
135
|
-
data: {
|
|
136
|
-
id: `${sourceId}-${targetId}`,
|
|
137
|
-
source: sourceId,
|
|
138
|
-
target: targetId,
|
|
139
|
-
weight: link.weight || 0.5,
|
|
140
|
-
relationship_type: link.relationship_type || '',
|
|
141
|
-
shared_entities: link.shared_entities || []
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
return elements;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ============================================================================
|
|
150
|
-
// GRAPH RENDERING
|
|
151
|
-
// ============================================================================
|
|
152
|
-
|
|
153
|
-
function renderGraph(data) {
|
|
154
|
-
const container = document.getElementById('graph-container');
|
|
155
|
-
if (!container) {
|
|
156
|
-
console.error('Graph container not found');
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Clear existing graph
|
|
161
|
-
if (cy) {
|
|
162
|
-
cy.destroy();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Check node count for performance tier
|
|
166
|
-
const nodeCount = data.nodes.length;
|
|
167
|
-
let layout = determineBestLayout(nodeCount);
|
|
168
|
-
|
|
169
|
-
// Transform data
|
|
170
|
-
const elements = transformDataForCytoscape(data);
|
|
171
|
-
|
|
172
|
-
if (elements.length === 0) {
|
|
173
|
-
const emptyMsg = document.createElement('div');
|
|
174
|
-
emptyMsg.style.cssText = 'text-align:center; padding:50px; color:#666;';
|
|
175
|
-
emptyMsg.textContent = 'No memories found. Try adjusting filters.';
|
|
176
|
-
container.textContent = ''; // Clear safely
|
|
177
|
-
container.appendChild(emptyMsg);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Clear container (remove spinner/old content) - safe approach
|
|
182
|
-
container.textContent = '';
|
|
183
|
-
console.log('[renderGraph] Container cleared, initializing Cytoscape with', elements.length, 'elements');
|
|
184
|
-
|
|
185
|
-
// Initialize Cytoscape WITHOUT running layout yet
|
|
186
|
-
try {
|
|
187
|
-
cy = cytoscape({
|
|
188
|
-
container: container,
|
|
189
|
-
elements: elements,
|
|
190
|
-
style: getCytoscapeStyles(),
|
|
191
|
-
layout: { name: 'preset' }, // Don't run layout yet
|
|
192
|
-
minZoom: 0.2,
|
|
193
|
-
maxZoom: 3,
|
|
194
|
-
wheelSensitivity: 0.05, // Reduced from 0.2 - less sensitive zoom
|
|
195
|
-
autoungrabify: false,
|
|
196
|
-
autounselectify: false,
|
|
197
|
-
// Mobile touch support
|
|
198
|
-
touchTapThreshold: 8, // Pixels of movement allowed for tap (vs drag)
|
|
199
|
-
desktopTapThreshold: 4, // Desktop click threshold
|
|
200
|
-
pixelRatio: 'auto' // Retina/HiDPI support
|
|
201
|
-
});
|
|
202
|
-
console.log('[renderGraph] Cytoscape initialized successfully, nodes:', cy.nodes().length);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error('[renderGraph] Cytoscape initialization failed:', error);
|
|
205
|
-
showError('Failed to render graph: ' + error.message);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Add interactions
|
|
210
|
-
addCytoscapeInteractions();
|
|
211
|
-
|
|
212
|
-
// Add navigator (mini-map) if available
|
|
213
|
-
if (cy.navigator && nodeCount > 50) {
|
|
214
|
-
cy.navigator({
|
|
215
|
-
container: false, // Will be added to DOM separately if needed
|
|
216
|
-
viewLiveFramerate: 0,
|
|
217
|
-
dblClickDelay: 200,
|
|
218
|
-
removeCustomContainer: false,
|
|
219
|
-
rerenderDelay: 100
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Update UI
|
|
224
|
-
updateFilterBadge();
|
|
225
|
-
updateGraphStats(data);
|
|
226
|
-
|
|
227
|
-
// Announce graph load to screen readers
|
|
228
|
-
updateScreenReaderStatus(`Graph loaded with ${data.nodes.length} memories and ${data.links.length} connections`);
|
|
229
|
-
|
|
230
|
-
// Check if we should restore saved layout or run fresh layout
|
|
231
|
-
const hasSavedLayout = localStorage.getItem('slm_graph_layout');
|
|
232
|
-
|
|
233
|
-
if (hasSavedLayout) {
|
|
234
|
-
// Restore saved positions, then fit
|
|
235
|
-
restoreSavedLayout();
|
|
236
|
-
console.log('[renderGraph] Restored saved layout positions');
|
|
237
|
-
cy.fit(null, 80);
|
|
238
|
-
} else {
|
|
239
|
-
// No saved layout - run layout algorithm with fit
|
|
240
|
-
console.log('[renderGraph] Running fresh layout:', layout);
|
|
241
|
-
const layoutConfig = getLayoutConfig(layout);
|
|
242
|
-
const graphLayout = cy.layout(layoutConfig);
|
|
243
|
-
|
|
244
|
-
// CRITICAL: Wait for layout to finish, THEN force fit
|
|
245
|
-
graphLayout.on('layoutstop', function() {
|
|
246
|
-
console.log('[renderGraph] Layout completed, forcing fit to viewport');
|
|
247
|
-
cy.fit(null, 80); // Force center with 80px padding
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
graphLayout.run();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ============================================================================
|
|
255
|
-
// LAYOUT ALGORITHMS
|
|
256
|
-
// ============================================================================
|
|
257
|
-
|
|
258
|
-
function determineBestLayout(nodeCount) {
|
|
259
|
-
if (nodeCount <= 500) {
|
|
260
|
-
return 'fcose'; // Full interactive graph
|
|
261
|
-
} else if (nodeCount <= 2000) {
|
|
262
|
-
return 'cose'; // Faster force-directed
|
|
263
|
-
} else {
|
|
264
|
-
return 'circle'; // Focus mode (circular for large graphs)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function getLayoutConfig(layoutName) {
|
|
269
|
-
const configs = {
|
|
270
|
-
'fcose': {
|
|
271
|
-
name: 'fcose',
|
|
272
|
-
quality: 'default',
|
|
273
|
-
randomize: false,
|
|
274
|
-
animate: false, // Disabled for stability
|
|
275
|
-
fit: true,
|
|
276
|
-
padding: 80, // Increased padding to keep within bounds
|
|
277
|
-
nodeSeparation: 100,
|
|
278
|
-
idealEdgeLength: 100,
|
|
279
|
-
edgeElasticity: 0.45,
|
|
280
|
-
nestingFactor: 0.1,
|
|
281
|
-
gravity: 0.25,
|
|
282
|
-
numIter: 2500,
|
|
283
|
-
tile: true,
|
|
284
|
-
tilingPaddingVertical: 10,
|
|
285
|
-
tilingPaddingHorizontal: 10,
|
|
286
|
-
gravityRangeCompound: 1.5,
|
|
287
|
-
gravityCompound: 1.0,
|
|
288
|
-
gravityRange: 3.8
|
|
289
|
-
},
|
|
290
|
-
'cose': {
|
|
291
|
-
name: 'cose',
|
|
292
|
-
animate: false, // Disabled for stability
|
|
293
|
-
fit: true,
|
|
294
|
-
padding: 80, // Increased padding
|
|
295
|
-
nodeRepulsion: 8000,
|
|
296
|
-
idealEdgeLength: 100,
|
|
297
|
-
edgeElasticity: 100,
|
|
298
|
-
nestingFactor: 5,
|
|
299
|
-
gravity: 80,
|
|
300
|
-
numIter: 1000,
|
|
301
|
-
randomize: false
|
|
302
|
-
},
|
|
303
|
-
'circle': {
|
|
304
|
-
name: 'circle',
|
|
305
|
-
animate: true,
|
|
306
|
-
animationDuration: 1000,
|
|
307
|
-
fit: true,
|
|
308
|
-
padding: 50,
|
|
309
|
-
sort: function(a, b) {
|
|
310
|
-
return b.data('importance') - a.data('importance');
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
'grid': {
|
|
314
|
-
name: 'grid',
|
|
315
|
-
animate: true,
|
|
316
|
-
animationDuration: 1000,
|
|
317
|
-
fit: true,
|
|
318
|
-
padding: 50,
|
|
319
|
-
sort: function(a, b) {
|
|
320
|
-
return b.data('importance') - a.data('importance');
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
'breadthfirst': {
|
|
324
|
-
name: 'breadthfirst',
|
|
325
|
-
animate: true,
|
|
326
|
-
animationDuration: 1000,
|
|
327
|
-
fit: true,
|
|
328
|
-
padding: 50,
|
|
329
|
-
directed: false,
|
|
330
|
-
circle: false,
|
|
331
|
-
spacingFactor: 1.5,
|
|
332
|
-
sort: function(a, b) {
|
|
333
|
-
return b.data('importance') - a.data('importance');
|
|
334
|
-
}
|
|
335
|
-
},
|
|
336
|
-
'concentric': {
|
|
337
|
-
name: 'concentric',
|
|
338
|
-
animate: true,
|
|
339
|
-
animationDuration: 1000,
|
|
340
|
-
fit: true,
|
|
341
|
-
padding: 50,
|
|
342
|
-
concentric: function(node) {
|
|
343
|
-
return node.data('importance');
|
|
344
|
-
},
|
|
345
|
-
levelWidth: function() {
|
|
346
|
-
return 2;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
return configs[layoutName] || configs['fcose'];
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ============================================================================
|
|
355
|
-
// CYTOSCAPE STYLES
|
|
356
|
-
// ============================================================================
|
|
357
|
-
|
|
358
|
-
function getCytoscapeStyles() {
|
|
359
|
-
return [
|
|
360
|
-
{
|
|
361
|
-
selector: 'node',
|
|
362
|
-
style: {
|
|
363
|
-
'background-color': function(ele) {
|
|
364
|
-
return getClusterColor(ele.data('cluster_id'));
|
|
365
|
-
},
|
|
366
|
-
'width': function(ele) {
|
|
367
|
-
return Math.max(20, ele.data('weight'));
|
|
368
|
-
},
|
|
369
|
-
'height': function(ele) {
|
|
370
|
-
return Math.max(20, ele.data('weight'));
|
|
371
|
-
},
|
|
372
|
-
'label': 'data(label)',
|
|
373
|
-
'font-size': '10px',
|
|
374
|
-
'text-valign': 'center',
|
|
375
|
-
'text-halign': 'center',
|
|
376
|
-
'color': '#333',
|
|
377
|
-
'text-outline-width': 2,
|
|
378
|
-
'text-outline-color': '#fff',
|
|
379
|
-
'border-width': function(ele) {
|
|
380
|
-
// Trust score → border thickness (if available)
|
|
381
|
-
return 2;
|
|
382
|
-
},
|
|
383
|
-
'border-color': '#555'
|
|
384
|
-
}
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
selector: 'node:selected',
|
|
388
|
-
style: {
|
|
389
|
-
'border-width': 4,
|
|
390
|
-
'border-color': '#667eea',
|
|
391
|
-
'background-color': '#667eea'
|
|
392
|
-
}
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
selector: 'node.highlighted',
|
|
396
|
-
style: {
|
|
397
|
-
'border-width': 4,
|
|
398
|
-
'border-color': '#ff6b6b',
|
|
399
|
-
'box-shadow': '0 0 20px #ff6b6b'
|
|
400
|
-
}
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
selector: 'node.dimmed',
|
|
404
|
-
style: {
|
|
405
|
-
'opacity': 0.3
|
|
406
|
-
}
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
selector: 'node.keyboard-focused',
|
|
410
|
-
style: {
|
|
411
|
-
'border-width': 5,
|
|
412
|
-
'border-color': '#0066ff',
|
|
413
|
-
'border-style': 'solid',
|
|
414
|
-
'box-shadow': '0 0 15px #0066ff'
|
|
415
|
-
}
|
|
416
|
-
},
|
|
417
|
-
{
|
|
418
|
-
selector: 'edge',
|
|
419
|
-
style: {
|
|
420
|
-
'width': function(ele) {
|
|
421
|
-
return Math.max(1, ele.data('weight') * 3);
|
|
422
|
-
},
|
|
423
|
-
'line-color': '#ccc',
|
|
424
|
-
'line-style': function(ele) {
|
|
425
|
-
return ele.data('weight') > 0.3 ? 'solid' : 'dashed';
|
|
426
|
-
},
|
|
427
|
-
'curve-style': 'bezier',
|
|
428
|
-
'target-arrow-shape': 'none',
|
|
429
|
-
'opacity': 0.6
|
|
430
|
-
}
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
selector: 'edge.highlighted',
|
|
434
|
-
style: {
|
|
435
|
-
'line-color': '#667eea',
|
|
436
|
-
'width': 3,
|
|
437
|
-
'opacity': 1
|
|
438
|
-
}
|
|
439
|
-
},
|
|
440
|
-
{
|
|
441
|
-
selector: 'edge.dimmed',
|
|
442
|
-
style: {
|
|
443
|
-
'opacity': 0.1
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
];
|
|
447
|
-
}
|
package/ui/js/graph-filters.js
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
// SuperLocalMemory V2.6.5 - Interactive Knowledge Graph - Filtering Module
|
|
2
|
-
// Copyright (c) 2026 Varun Pratap Bhardwaj — Elastic License 2.0
|
|
3
|
-
// Part of modular graph visualization system (split from monolithic graph-cytoscape.js)
|
|
4
|
-
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// FILTER LOGIC
|
|
7
|
-
// ============================================================================
|
|
8
|
-
|
|
9
|
-
function filterByCluster(data, cluster_id) {
|
|
10
|
-
console.log('[filterByCluster] Filtering for cluster_id:', cluster_id, 'Type:', typeof cluster_id);
|
|
11
|
-
console.log('[filterByCluster] Total nodes in data:', data.nodes.length);
|
|
12
|
-
|
|
13
|
-
// Convert to integer for comparison
|
|
14
|
-
const targetClusterId = parseInt(cluster_id);
|
|
15
|
-
let debugCount = 0;
|
|
16
|
-
|
|
17
|
-
const filteredNodes = data.nodes.filter(n => {
|
|
18
|
-
// Convert node cluster_id to integer for comparison
|
|
19
|
-
const nodeClusterId = n.cluster_id ? parseInt(n.cluster_id) : null;
|
|
20
|
-
|
|
21
|
-
// Log first 3 nodes to debug
|
|
22
|
-
if (debugCount < 3) {
|
|
23
|
-
console.log('[filterByCluster] Sample node:', {
|
|
24
|
-
id: n.id,
|
|
25
|
-
cluster_id: n.cluster_id,
|
|
26
|
-
type: typeof n.cluster_id,
|
|
27
|
-
parsed: nodeClusterId,
|
|
28
|
-
match: nodeClusterId === targetClusterId
|
|
29
|
-
});
|
|
30
|
-
debugCount++;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Use strict equality after type conversion
|
|
34
|
-
return nodeClusterId === targetClusterId;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
console.log('[filterByCluster] Filtered to', filteredNodes.length, 'nodes');
|
|
38
|
-
|
|
39
|
-
const nodeIds = new Set(filteredNodes.map(n => n.id));
|
|
40
|
-
const filteredLinks = data.links.filter(l =>
|
|
41
|
-
nodeIds.has(l.source) && nodeIds.has(l.target)
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
console.log('[filterByCluster] Found', filteredLinks.length, 'edges between filtered nodes');
|
|
45
|
-
|
|
46
|
-
return { nodes: filteredNodes, links: filteredLinks, clusters: data.clusters };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function filterByEntity(data, entity) {
|
|
50
|
-
const filteredNodes = data.nodes.filter(n => {
|
|
51
|
-
if (n.entities && Array.isArray(n.entities)) {
|
|
52
|
-
return n.entities.includes(entity);
|
|
53
|
-
}
|
|
54
|
-
if (n.tags && n.tags.includes(entity)) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
58
|
-
});
|
|
59
|
-
const nodeIds = new Set(filteredNodes.map(n => n.id));
|
|
60
|
-
const filteredLinks = data.links.filter(l =>
|
|
61
|
-
nodeIds.has(l.source) && nodeIds.has(l.target)
|
|
62
|
-
);
|
|
63
|
-
return { nodes: filteredNodes, links: filteredLinks, clusters: data.clusters };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function clearGraphFilters() {
|
|
67
|
-
console.log('[clearGraphFilters] Clearing all filters and reloading full graph');
|
|
68
|
-
|
|
69
|
-
// Clear filter state
|
|
70
|
-
filterState = { cluster_id: null, entity: null };
|
|
71
|
-
|
|
72
|
-
// CRITICAL: Clear URL completely - remove ?cluster_id=X from address bar
|
|
73
|
-
const cleanUrl = window.location.origin + window.location.pathname;
|
|
74
|
-
window.history.replaceState({}, '', cleanUrl);
|
|
75
|
-
console.log('[clearGraphFilters] URL cleaned to:', cleanUrl);
|
|
76
|
-
|
|
77
|
-
// CRITICAL: Clear saved layout positions so nodes don't stay in corner
|
|
78
|
-
// When switching from filtered to full graph, we want fresh layout
|
|
79
|
-
localStorage.removeItem('slm_graph_layout');
|
|
80
|
-
console.log('[clearGraphFilters] Cleared saved layout positions');
|
|
81
|
-
|
|
82
|
-
// Reload full graph from API (respects dropdown settings)
|
|
83
|
-
loadGraph();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ============================================================================
|
|
87
|
-
// EVENT HANDLERS
|
|
88
|
-
// ============================================================================
|
|
89
|
-
|
|
90
|
-
function setupGraphEventListeners() {
|
|
91
|
-
// Load graph when Graph tab is clicked
|
|
92
|
-
const graphTab = document.querySelector('a[href="#graph"]');
|
|
93
|
-
if (graphTab) {
|
|
94
|
-
// CRITICAL FIX: Add click handler to clear filter even when already on KG tab
|
|
95
|
-
// Bootstrap's shown.bs.tab only fires when SWITCHING TO tab, not when clicking same tab!
|
|
96
|
-
graphTab.addEventListener('click', function(event) {
|
|
97
|
-
console.log('[Event] Knowledge Graph tab CLICKED');
|
|
98
|
-
|
|
99
|
-
// Check if we're viewing a filtered graph
|
|
100
|
-
const hasFilter = filterState.cluster_id || filterState.entity;
|
|
101
|
-
|
|
102
|
-
if (hasFilter && cy) {
|
|
103
|
-
console.log('[Event] Click detected on KG tab while filter active - clearing filter');
|
|
104
|
-
|
|
105
|
-
// Clear filter state
|
|
106
|
-
filterState = { cluster_id: null, entity: null };
|
|
107
|
-
|
|
108
|
-
// Clear URL completely
|
|
109
|
-
const cleanUrl = window.location.origin + window.location.pathname;
|
|
110
|
-
window.history.replaceState({}, '', cleanUrl);
|
|
111
|
-
console.log('[Event] URL cleaned to:', cleanUrl);
|
|
112
|
-
|
|
113
|
-
// CRITICAL: Clear saved layout positions so nodes don't stay in corner
|
|
114
|
-
localStorage.removeItem('slm_graph_layout');
|
|
115
|
-
console.log('[Event] Cleared saved layout positions');
|
|
116
|
-
|
|
117
|
-
// Reload with full graph
|
|
118
|
-
loadGraph();
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Also keep shown.bs.tab for when switching FROM another tab TO KG tab
|
|
123
|
-
graphTab.addEventListener('shown.bs.tab', function(event) {
|
|
124
|
-
console.log('[Event] Knowledge Graph tab SHOWN (tab switch)');
|
|
125
|
-
|
|
126
|
-
if (cy) {
|
|
127
|
-
// Graph already exists - user is returning to KG tab from another tab
|
|
128
|
-
// Clear filter and reload to show full graph
|
|
129
|
-
console.log('[Event] Returning to KG tab from another tab - clearing filter');
|
|
130
|
-
|
|
131
|
-
// Clear filter state
|
|
132
|
-
filterState = { cluster_id: null, entity: null };
|
|
133
|
-
|
|
134
|
-
// Clear URL completely
|
|
135
|
-
const cleanUrl = window.location.origin + window.location.pathname;
|
|
136
|
-
window.history.replaceState({}, '', cleanUrl);
|
|
137
|
-
console.log('[Event] URL cleaned to:', cleanUrl);
|
|
138
|
-
|
|
139
|
-
// CRITICAL: Clear saved layout positions so nodes don't stay in corner
|
|
140
|
-
localStorage.removeItem('slm_graph_layout');
|
|
141
|
-
console.log('[Event] Cleared saved layout positions');
|
|
142
|
-
|
|
143
|
-
// Reload with full graph (respects dropdown settings)
|
|
144
|
-
loadGraph();
|
|
145
|
-
} else {
|
|
146
|
-
// First load - check if cluster filter is in URL (from cluster badge click)
|
|
147
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
148
|
-
const clusterIdParam = urlParams.get('cluster_id');
|
|
149
|
-
|
|
150
|
-
if (clusterIdParam) {
|
|
151
|
-
console.log('[Event] First load with cluster filter:', clusterIdParam);
|
|
152
|
-
// Load with filter (will be applied in loadGraph)
|
|
153
|
-
} else {
|
|
154
|
-
console.log('[Event] First load, no filter');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
loadGraph();
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Reload graph when dropdown settings change
|
|
163
|
-
const maxNodesSelect = document.getElementById('graph-max-nodes');
|
|
164
|
-
if (maxNodesSelect) {
|
|
165
|
-
maxNodesSelect.addEventListener('change', function() {
|
|
166
|
-
console.log('[Event] Max nodes changed to:', this.value);
|
|
167
|
-
|
|
168
|
-
// Clear any active filter when user manually changes settings
|
|
169
|
-
if (filterState.cluster_id || filterState.entity) {
|
|
170
|
-
console.log('[Event] Clearing filter due to dropdown change');
|
|
171
|
-
filterState = { cluster_id: null, entity: null };
|
|
172
|
-
|
|
173
|
-
// Clear URL
|
|
174
|
-
const cleanUrl = window.location.origin + window.location.pathname;
|
|
175
|
-
window.history.replaceState({}, '', cleanUrl);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Clear saved layout when changing node count (different # of nodes = different layout)
|
|
179
|
-
localStorage.removeItem('slm_graph_layout');
|
|
180
|
-
console.log('[Event] Cleared saved layout due to settings change');
|
|
181
|
-
|
|
182
|
-
loadGraph();
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const minImportanceSelect = document.getElementById('graph-min-importance');
|
|
187
|
-
if (minImportanceSelect) {
|
|
188
|
-
minImportanceSelect.addEventListener('change', function() {
|
|
189
|
-
console.log('[Event] Min importance changed to:', this.value);
|
|
190
|
-
|
|
191
|
-
// Clear any active filter when user manually changes settings
|
|
192
|
-
if (filterState.cluster_id || filterState.entity) {
|
|
193
|
-
console.log('[Event] Clearing filter due to dropdown change');
|
|
194
|
-
filterState = { cluster_id: null, entity: null };
|
|
195
|
-
|
|
196
|
-
// Clear URL
|
|
197
|
-
const cleanUrl = window.location.origin + window.location.pathname;
|
|
198
|
-
window.history.replaceState({}, '', cleanUrl);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Clear saved layout when changing importance (different nodes = different layout)
|
|
202
|
-
localStorage.removeItem('slm_graph_layout');
|
|
203
|
-
console.log('[Event] Cleared saved layout due to settings change');
|
|
204
|
-
|
|
205
|
-
loadGraph();
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
console.log('[Init] Graph event listeners setup complete');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ============================================================================
|
|
213
|
-
// INITIALIZATION
|
|
214
|
-
// ============================================================================
|
|
215
|
-
|
|
216
|
-
if (document.readyState === 'loading') {
|
|
217
|
-
document.addEventListener('DOMContentLoaded', setupGraphEventListeners);
|
|
218
|
-
} else {
|
|
219
|
-
setupGraphEventListeners();
|
|
220
|
-
}
|