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.
Files changed (78) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +1 -3
  3. package/pyproject.toml +10 -1
  4. package/src/superlocalmemory/cli/setup_wizard.py +30 -0
  5. package/src/superlocalmemory.egg-info/PKG-INFO +4 -1
  6. package/src/superlocalmemory.egg-info/requires.txt +3 -0
  7. package/docs/ARCHITECTURE.md +0 -149
  8. package/docs/api-reference.md +0 -284
  9. package/docs/auto-memory.md +0 -150
  10. package/docs/cli-reference.md +0 -327
  11. package/docs/cloud-backup.md +0 -174
  12. package/docs/compliance.md +0 -191
  13. package/docs/configuration.md +0 -182
  14. package/docs/getting-started.md +0 -102
  15. package/docs/ide-setup.md +0 -261
  16. package/docs/mcp-tools.md +0 -220
  17. package/docs/migration-from-v2.md +0 -170
  18. package/docs/profiles.md +0 -173
  19. package/docs/screenshots/01-dashboard-main.png +0 -0
  20. package/docs/screenshots/02-knowledge-graph.png +0 -0
  21. package/docs/screenshots/03-math-health.png +0 -0
  22. package/docs/screenshots/03-patterns-learning.png +0 -0
  23. package/docs/screenshots/04-learning-dashboard.png +0 -0
  24. package/docs/screenshots/04-recall-lab.png +0 -0
  25. package/docs/screenshots/05-behavioral-analysis.png +0 -0
  26. package/docs/screenshots/05-trust-dashboard.png +0 -0
  27. package/docs/screenshots/06-graph-communities.png +0 -0
  28. package/docs/screenshots/06-settings.png +0 -0
  29. package/docs/screenshots/07-memories-blurred.png +0 -0
  30. package/docs/skill-evolution.md +0 -256
  31. package/docs/troubleshooting.md +0 -310
  32. package/docs/v2-archive/ACCESSIBILITY.md +0 -291
  33. package/docs/v2-archive/ARCHITECTURE.md +0 -886
  34. package/docs/v2-archive/CLI-COMMANDS-REFERENCE.md +0 -425
  35. package/docs/v2-archive/COMPRESSION-README.md +0 -390
  36. package/docs/v2-archive/FRAMEWORK-INTEGRATIONS.md +0 -300
  37. package/docs/v2-archive/MCP-MANUAL-SETUP.md +0 -775
  38. package/docs/v2-archive/MCP-TROUBLESHOOTING.md +0 -787
  39. package/docs/v2-archive/PATTERN-LEARNING.md +0 -228
  40. package/docs/v2-archive/PROFILES-GUIDE.md +0 -453
  41. package/docs/v2-archive/RESET-GUIDE.md +0 -353
  42. package/docs/v2-archive/SEARCH-ENGINE-V2.2.0.md +0 -749
  43. package/docs/v2-archive/SEARCH-INTEGRATION-GUIDE.md +0 -502
  44. package/docs/v2-archive/UI-SERVER.md +0 -262
  45. package/docs/v2-archive/UNIVERSAL-INTEGRATION.md +0 -488
  46. package/docs/v2-archive/V2.2.0-OPTIONAL-SEARCH.md +0 -666
  47. package/docs/v2-archive/WINDOWS-INSTALL-README.txt +0 -34
  48. package/docs/v2-archive/WINDOWS-POST-INSTALL.txt +0 -45
  49. package/docs/v2-archive/example_graph_usage.py +0 -146
  50. package/ui/index.html +0 -1879
  51. package/ui/js/agents.js +0 -192
  52. package/ui/js/auto-settings.js +0 -399
  53. package/ui/js/behavioral.js +0 -276
  54. package/ui/js/clusters.js +0 -206
  55. package/ui/js/compliance.js +0 -252
  56. package/ui/js/core.js +0 -246
  57. package/ui/js/dashboard.js +0 -110
  58. package/ui/js/events.js +0 -178
  59. package/ui/js/fact-detail.js +0 -92
  60. package/ui/js/feedback.js +0 -333
  61. package/ui/js/graph-core.js +0 -447
  62. package/ui/js/graph-filters.js +0 -220
  63. package/ui/js/graph-interactions.js +0 -351
  64. package/ui/js/graph-ui.js +0 -214
  65. package/ui/js/ide-status.js +0 -102
  66. package/ui/js/init.js +0 -45
  67. package/ui/js/learning.js +0 -435
  68. package/ui/js/lifecycle.js +0 -298
  69. package/ui/js/math-health.js +0 -98
  70. package/ui/js/memories.js +0 -264
  71. package/ui/js/modal.js +0 -357
  72. package/ui/js/patterns.js +0 -93
  73. package/ui/js/profiles.js +0 -236
  74. package/ui/js/recall-lab.js +0 -292
  75. package/ui/js/search.js +0 -59
  76. package/ui/js/settings.js +0 -224
  77. package/ui/js/timeline.js +0 -32
  78. package/ui/js/trust-dashboard.js +0 -73
@@ -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
- }
@@ -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
- }