superlocalmemory 2.6.0 → 2.6.5

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.
@@ -0,0 +1,32 @@
1
+ // SuperLocalMemory V2 - Knowledge Graph (D3.js force-directed)
2
+ // Depends on: core.js, modal.js (openMemoryDetail)
3
+
4
+ var graphData = { nodes: [], links: [] };
5
+
6
+ async function loadGraph() {
7
+ var maxNodes = document.getElementById('graph-max-nodes').value;
8
+ try {
9
+ var response = await fetch('/api/graph?max_nodes=' + maxNodes);
10
+ graphData = await response.json();
11
+ renderGraph(graphData);
12
+ } catch (error) {
13
+ console.error('Error loading graph:', error);
14
+ }
15
+ }
16
+
17
+ function renderGraph(data) {
18
+ var container = document.getElementById('graph-container');
19
+ container.textContent = '';
20
+ var width = container.clientWidth || 1200;
21
+ var height = 600;
22
+ var svg = d3.select('#graph-container').append('svg').attr('width', width).attr('height', height);
23
+ var tooltip = d3.select('body').append('div').attr('class', 'tooltip-custom').style('opacity', 0);
24
+ var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
25
+ var simulation = d3.forceSimulation(data.nodes).force('link', d3.forceLink(data.links).id(function(d) { return d.id; }).distance(100)).force('charge', d3.forceManyBody().strength(-200)).force('center', d3.forceCenter(width / 2, height / 2)).force('collision', d3.forceCollide().radius(20));
26
+ var link = svg.append('g').selectAll('line').data(data.links).enter().append('line').attr('class', 'link').attr('stroke-width', function(d) { return Math.sqrt(d.weight * 2); });
27
+ var node = svg.append('g').selectAll('circle').data(data.nodes).enter().append('circle').attr('class', 'node').attr('r', function(d) { return 5 + (d.importance || 5); }).attr('fill', function(d) { return colorScale(d.cluster_id || 0); }).call(d3.drag().on('start', dragStarted).on('drag', dragged).on('end', dragEnded)).on('mouseover', function(event, d) { tooltip.transition().duration(200).style('opacity', .9); var label = d.category || d.project_name || 'Memory #' + d.id; tooltip.text(label + ': ' + (d.content_preview || d.summary || 'No content')).style('left', (event.pageX + 10) + 'px').style('top', (event.pageY - 28) + 'px'); }).on('mouseout', function() { tooltip.transition().duration(500).style('opacity', 0); }).on('click', function(event, d) { openMemoryDetail(d); });
28
+ simulation.on('tick', function() { link.attr('x1', function(d) { return d.source.x; }).attr('y1', function(d) { return d.source.y; }).attr('x2', function(d) { return d.target.x; }).attr('y2', function(d) { return d.target.y; }); node.attr('cx', function(d) { return d.x; }).attr('cy', function(d) { return d.y; }); });
29
+ function dragStarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }
30
+ function dragged(event, d) { d.fx = event.x; d.fy = event.y; }
31
+ function dragEnded(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }
32
+ }
@@ -0,0 +1,220 @@
1
+ // SuperLocalMemory V2.6.5 - Interactive Knowledge Graph - Filtering 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 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
+ }
@@ -0,0 +1,354 @@
1
+ // SuperLocalMemory V2.6.5 - Interactive Knowledge Graph - Interactions 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
+ // CYTOSCAPE INTERACTIONS
7
+ // ============================================================================
8
+
9
+ function addCytoscapeInteractions() {
10
+ if (!cy) return;
11
+
12
+ // Hover: Show tooltip
13
+ cy.on('mouseover', 'node', function(evt) {
14
+ const node = evt.target;
15
+ const pos = evt.renderedPosition;
16
+ showTooltip(node, pos.x, pos.y);
17
+
18
+ // Highlight connected nodes
19
+ node.addClass('highlighted');
20
+ node.connectedEdges().addClass('highlighted');
21
+ node.neighborhood('node').addClass('highlighted');
22
+
23
+ // Dim others
24
+ cy.nodes().not(node).not(node.neighborhood('node')).addClass('dimmed');
25
+ cy.edges().not(node.connectedEdges()).addClass('dimmed');
26
+ });
27
+
28
+ cy.on('mouseout', 'node', function(evt) {
29
+ hideTooltip();
30
+
31
+ // Remove highlighting
32
+ cy.elements().removeClass('highlighted').removeClass('dimmed');
33
+ });
34
+
35
+ // Single click: Open modal preview
36
+ cy.on('tap', 'node', function(evt) {
37
+ const node = evt.target;
38
+ openMemoryModal(node);
39
+ });
40
+
41
+ // Double click: Navigate to Memories tab
42
+ cy.on('dbltap', 'node', function(evt) {
43
+ const node = evt.target;
44
+ navigateToMemoryTab(node.data('id'));
45
+ });
46
+
47
+ // Enable node dragging
48
+ cy.on('drag', 'node', function(evt) {
49
+ // Node position is automatically updated by Cytoscape
50
+ });
51
+
52
+ // Save layout when drag ends
53
+ cy.on('dragfree', 'node', function(evt) {
54
+ saveLayoutPositions();
55
+ });
56
+
57
+ // Pan & zoom events (for performance monitoring)
58
+ let panZoomTimeout;
59
+ cy.on('pan zoom', function() {
60
+ clearTimeout(panZoomTimeout);
61
+ panZoomTimeout = setTimeout(() => {
62
+ saveLayoutPositions();
63
+ }, 1000);
64
+ });
65
+
66
+ // Add keyboard navigation
67
+ setupKeyboardNavigation();
68
+ }
69
+
70
+ // ============================================================================
71
+ // TOOLTIP
72
+ // ============================================================================
73
+
74
+ let tooltipTimeout;
75
+ function showTooltip(node, x, y) {
76
+ clearTimeout(tooltipTimeout);
77
+ tooltipTimeout = setTimeout(() => {
78
+ let tooltip = document.getElementById('graph-tooltip');
79
+ if (!tooltip) {
80
+ tooltip = document.createElement('div');
81
+ tooltip.id = 'graph-tooltip';
82
+ tooltip.style.cssText = 'position:fixed; background:#333; color:#fff; padding:10px; border-radius:6px; font-size:12px; max-width:300px; z-index:10000; pointer-events:none; box-shadow: 0 4px 12px rgba(0,0,0,0.3);';
83
+ document.body.appendChild(tooltip);
84
+ }
85
+
86
+ // Build tooltip content safely (no innerHTML)
87
+ tooltip.textContent = ''; // Clear
88
+ const data = node.data();
89
+
90
+ const title = document.createElement('strong');
91
+ title.textContent = data.label;
92
+ tooltip.appendChild(title);
93
+
94
+ tooltip.appendChild(document.createElement('br'));
95
+
96
+ const meta = document.createElement('span');
97
+ meta.style.color = '#aaa';
98
+ meta.textContent = `Cluster ${data.cluster_id} • Importance ${data.importance}`;
99
+ tooltip.appendChild(meta);
100
+
101
+ tooltip.appendChild(document.createElement('br'));
102
+
103
+ const preview = document.createElement('span');
104
+ preview.style.cssText = 'font-size:11px; color:#ccc;';
105
+ preview.textContent = data.content_preview;
106
+ tooltip.appendChild(preview);
107
+
108
+ tooltip.style.display = 'block';
109
+ tooltip.style.left = (x + 20) + 'px';
110
+ tooltip.style.top = (y - 20) + 'px';
111
+ }, 200);
112
+ }
113
+
114
+ function hideTooltip() {
115
+ clearTimeout(tooltipTimeout);
116
+ const tooltip = document.getElementById('graph-tooltip');
117
+ if (tooltip) {
118
+ tooltip.style.display = 'none';
119
+ }
120
+ }
121
+
122
+ // ============================================================================
123
+ // MODAL INTEGRATION
124
+ // ============================================================================
125
+
126
+ function openMemoryModal(node) {
127
+ const memoryData = {
128
+ id: node.data('id'),
129
+ content: node.data('content'),
130
+ summary: node.data('summary'),
131
+ category: node.data('category'),
132
+ project_name: node.data('project_name'),
133
+ cluster_id: node.data('cluster_id'),
134
+ importance: node.data('importance'),
135
+ tags: node.data('tags'),
136
+ created_at: node.data('created_at')
137
+ };
138
+
139
+ // Call existing openMemoryDetail function from modal.js
140
+ if (typeof openMemoryDetail === 'function') {
141
+ openMemoryDetail(memoryData);
142
+ } else {
143
+ console.error('openMemoryDetail function not found. Is modal.js loaded?');
144
+ }
145
+ }
146
+
147
+ function navigateToMemoryTab(memoryId) {
148
+ // Switch to Memories tab
149
+ const memoriesTab = document.querySelector('a[href="#memories"]');
150
+ if (memoriesTab) {
151
+ memoriesTab.click();
152
+ }
153
+
154
+ // Scroll to memory after a short delay (for tab to load)
155
+ setTimeout(() => {
156
+ if (typeof scrollToMemory === 'function') {
157
+ scrollToMemory(memoryId);
158
+ } else {
159
+ console.warn('scrollToMemory function not found in memories.js');
160
+ }
161
+ }, 300);
162
+ }
163
+
164
+ // ============================================================================
165
+ // KEYBOARD NAVIGATION
166
+ // ============================================================================
167
+
168
+ function setupKeyboardNavigation() {
169
+ if (!cy) return;
170
+
171
+ const container = document.getElementById('graph-container');
172
+ if (!container) return;
173
+
174
+ // Make container focusable
175
+ container.setAttribute('tabindex', '0');
176
+
177
+ // Focus handler - enable keyboard nav when container is focused
178
+ container.addEventListener('focus', function() {
179
+ keyboardNavigationEnabled = true;
180
+ if (cy.nodes().length > 0) {
181
+ focusNodeAtIndex(0);
182
+ }
183
+ });
184
+
185
+ container.addEventListener('blur', function() {
186
+ keyboardNavigationEnabled = false;
187
+ cy.nodes().removeClass('keyboard-focused');
188
+ });
189
+
190
+ // Keyboard event handler
191
+ container.addEventListener('keydown', function(e) {
192
+ if (!keyboardNavigationEnabled || !cy) return;
193
+
194
+ const nodes = cy.nodes();
195
+ if (nodes.length === 0) return;
196
+
197
+ const currentNode = nodes[focusedNodeIndex];
198
+
199
+ switch(e.key) {
200
+ case 'Tab':
201
+ e.preventDefault();
202
+ if (e.shiftKey) {
203
+ // Shift+Tab: previous node
204
+ focusedNodeIndex = (focusedNodeIndex - 1 + nodes.length) % nodes.length;
205
+ } else {
206
+ // Tab: next node
207
+ focusedNodeIndex = (focusedNodeIndex + 1) % nodes.length;
208
+ }
209
+ focusNodeAtIndex(focusedNodeIndex);
210
+ announceNode(nodes[focusedNodeIndex]);
211
+ break;
212
+
213
+ case 'Enter':
214
+ case ' ':
215
+ e.preventDefault();
216
+ if (currentNode) {
217
+ lastFocusedElement = container;
218
+ openMemoryModal(currentNode);
219
+ }
220
+ break;
221
+
222
+ case 'ArrowRight':
223
+ e.preventDefault();
224
+ moveToAdjacentNode('right', currentNode);
225
+ break;
226
+
227
+ case 'ArrowLeft':
228
+ e.preventDefault();
229
+ moveToAdjacentNode('left', currentNode);
230
+ break;
231
+
232
+ case 'ArrowDown':
233
+ e.preventDefault();
234
+ moveToAdjacentNode('down', currentNode);
235
+ break;
236
+
237
+ case 'ArrowUp':
238
+ e.preventDefault();
239
+ moveToAdjacentNode('up', currentNode);
240
+ break;
241
+
242
+ case 'Escape':
243
+ e.preventDefault();
244
+ if (filterState.cluster_id || filterState.entity) {
245
+ clearGraphFilters();
246
+ updateScreenReaderStatus('Filters cleared, showing all memories');
247
+ } else {
248
+ container.blur();
249
+ keyboardNavigationEnabled = false;
250
+ }
251
+ break;
252
+
253
+ case 'Home':
254
+ e.preventDefault();
255
+ focusedNodeIndex = 0;
256
+ focusNodeAtIndex(0);
257
+ announceNode(nodes[0]);
258
+ break;
259
+
260
+ case 'End':
261
+ e.preventDefault();
262
+ focusedNodeIndex = nodes.length - 1;
263
+ focusNodeAtIndex(focusedNodeIndex);
264
+ announceNode(nodes[focusedNodeIndex]);
265
+ break;
266
+ }
267
+ });
268
+ }
269
+
270
+ function focusNodeAtIndex(index) {
271
+ if (!cy) return;
272
+
273
+ const nodes = cy.nodes();
274
+ if (index < 0 || index >= nodes.length) return;
275
+
276
+ // Remove focus from all nodes
277
+ cy.nodes().removeClass('keyboard-focused');
278
+
279
+ // Add focus to target node
280
+ const node = nodes[index];
281
+ node.addClass('keyboard-focused');
282
+
283
+ // Center node in viewport with smooth animation
284
+ cy.animate({
285
+ center: { eles: node },
286
+ zoom: Math.max(cy.zoom(), 1.0),
287
+ duration: 300,
288
+ easing: 'ease-in-out'
289
+ });
290
+ }
291
+
292
+ function moveToAdjacentNode(direction, currentNode) {
293
+ if (!currentNode) return;
294
+
295
+ const nodes = cy.nodes();
296
+ const currentPos = currentNode.position();
297
+ let bestNode = null;
298
+ let bestScore = Infinity;
299
+
300
+ // Find adjacent nodes based on direction
301
+ nodes.forEach((node, index) => {
302
+ if (node.id() === currentNode.id()) return;
303
+
304
+ const pos = node.position();
305
+ const dx = pos.x - currentPos.x;
306
+ const dy = pos.y - currentPos.y;
307
+ const distance = Math.sqrt(dx * dx + dy * dy);
308
+
309
+ let isCorrectDirection = false;
310
+ let directionScore = 0;
311
+
312
+ switch(direction) {
313
+ case 'right':
314
+ isCorrectDirection = dx > 0;
315
+ directionScore = dx;
316
+ break;
317
+ case 'left':
318
+ isCorrectDirection = dx < 0;
319
+ directionScore = -dx;
320
+ break;
321
+ case 'down':
322
+ isCorrectDirection = dy > 0;
323
+ directionScore = dy;
324
+ break;
325
+ case 'up':
326
+ isCorrectDirection = dy < 0;
327
+ directionScore = -dy;
328
+ break;
329
+ }
330
+
331
+ // Combine distance with direction preference
332
+ if (isCorrectDirection) {
333
+ const score = distance - (directionScore * 0.5);
334
+ if (score < bestScore) {
335
+ bestScore = score;
336
+ bestNode = node;
337
+ focusedNodeIndex = index;
338
+ }
339
+ }
340
+ });
341
+
342
+ if (bestNode) {
343
+ focusNodeAtIndex(focusedNodeIndex);
344
+ announceNode(bestNode);
345
+ }
346
+ }
347
+
348
+ function announceNode(node) {
349
+ if (!node) return;
350
+
351
+ const data = node.data();
352
+ const message = `Memory ${data.id}: ${data.label}, Cluster ${data.cluster_id}, Importance ${data.importance} out of 10`;
353
+ updateScreenReaderStatus(message);
354
+ }