superlocalmemory 3.3.29 → 3.4.1

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 (69) hide show
  1. package/ATTRIBUTION.md +1 -1
  2. package/CHANGELOG.md +3 -0
  3. package/LICENSE +633 -70
  4. package/README.md +14 -11
  5. package/docs/screenshots/01-dashboard-main.png +0 -0
  6. package/docs/screenshots/02-knowledge-graph.png +0 -0
  7. package/docs/screenshots/03-patterns-learning.png +0 -0
  8. package/docs/screenshots/04-learning-dashboard.png +0 -0
  9. package/docs/screenshots/05-behavioral-analysis.png +0 -0
  10. package/docs/screenshots/06-graph-communities.png +0 -0
  11. package/docs/v2-archive/ACCESSIBILITY.md +1 -1
  12. package/docs/v2-archive/FRAMEWORK-INTEGRATIONS.md +1 -1
  13. package/docs/v2-archive/MCP-MANUAL-SETUP.md +1 -1
  14. package/docs/v2-archive/SEARCH-ENGINE-V2.2.0.md +2 -2
  15. package/docs/v2-archive/SEARCH-INTEGRATION-GUIDE.md +1 -1
  16. package/docs/v2-archive/UNIVERSAL-INTEGRATION.md +1 -1
  17. package/docs/v2-archive/V2.2.0-OPTIONAL-SEARCH.md +1 -1
  18. package/docs/v2-archive/example_graph_usage.py +1 -1
  19. package/ide/configs/codex-mcp.toml +1 -1
  20. package/ide/integrations/langchain/README.md +1 -1
  21. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +1 -1
  22. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +1 -1
  23. package/ide/integrations/langchain/pyproject.toml +2 -2
  24. package/ide/integrations/langchain/tests/__init__.py +1 -1
  25. package/ide/integrations/langchain/tests/test_chat_message_history.py +1 -1
  26. package/ide/integrations/langchain/tests/test_security.py +1 -1
  27. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +1 -1
  28. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +1 -1
  29. package/ide/integrations/llamaindex/pyproject.toml +2 -2
  30. package/ide/integrations/llamaindex/tests/__init__.py +1 -1
  31. package/ide/integrations/llamaindex/tests/test_chat_store.py +1 -1
  32. package/ide/integrations/llamaindex/tests/test_security.py +1 -1
  33. package/ide/skills/slm-build-graph/SKILL.md +3 -3
  34. package/ide/skills/slm-list-recent/SKILL.md +3 -3
  35. package/ide/skills/slm-recall/SKILL.md +3 -3
  36. package/ide/skills/slm-remember/SKILL.md +3 -3
  37. package/ide/skills/slm-show-patterns/SKILL.md +3 -3
  38. package/ide/skills/slm-status/SKILL.md +3 -3
  39. package/ide/skills/slm-switch-profile/SKILL.md +3 -3
  40. package/package.json +3 -3
  41. package/pyproject.toml +3 -3
  42. package/src/superlocalmemory/core/engine_wiring.py +5 -1
  43. package/src/superlocalmemory/core/graph_analyzer.py +254 -12
  44. package/src/superlocalmemory/learning/consolidation_worker.py +240 -52
  45. package/src/superlocalmemory/retrieval/entity_channel.py +135 -4
  46. package/src/superlocalmemory/retrieval/spreading_activation.py +45 -0
  47. package/src/superlocalmemory/server/api.py +9 -1
  48. package/src/superlocalmemory/server/routes/behavioral.py +8 -4
  49. package/src/superlocalmemory/server/routes/chat.py +320 -0
  50. package/src/superlocalmemory/server/routes/insights.py +368 -0
  51. package/src/superlocalmemory/server/routes/learning.py +106 -6
  52. package/src/superlocalmemory/server/routes/memories.py +20 -9
  53. package/src/superlocalmemory/server/routes/stats.py +25 -3
  54. package/src/superlocalmemory/server/routes/timeline.py +252 -0
  55. package/src/superlocalmemory/server/routes/v3_api.py +161 -0
  56. package/src/superlocalmemory/server/ui.py +8 -0
  57. package/src/superlocalmemory/ui/index.html +168 -58
  58. package/src/superlocalmemory/ui/js/graph-event-bus.js +83 -0
  59. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  60. package/src/superlocalmemory/ui/js/knowledge-graph.js +942 -0
  61. package/src/superlocalmemory/ui/js/memory-chat.js +344 -0
  62. package/src/superlocalmemory/ui/js/memory-timeline.js +265 -0
  63. package/src/superlocalmemory/ui/js/quick-actions.js +334 -0
  64. package/src/superlocalmemory.egg-info/PKG-INFO +597 -0
  65. package/src/superlocalmemory.egg-info/SOURCES.txt +287 -0
  66. package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
  67. package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
  68. package/src/superlocalmemory.egg-info/requires.txt +47 -0
  69. package/src/superlocalmemory.egg-info/top_level.txt +1 -0
@@ -0,0 +1,344 @@
1
+ // SuperLocalMemory v3.4.1 — Ask My Memory Chat Interface
2
+ // Copyright (c) 2026 Varun Pratap Bhardwaj — AGPL-3.0-or-later
3
+ // SSE streaming chat grounded in 6-channel memory retrieval
4
+
5
+ // ============================================================================
6
+ // STATE
7
+ // ============================================================================
8
+
9
+ var chatState = {
10
+ messages: [], // {role: 'user'|'assistant', content: '', citations: []}
11
+ streaming: false,
12
+ abortController: null,
13
+ mode: 'a', // a=raw results, b=ollama, c=cloud
14
+ };
15
+
16
+ // Quick action presets
17
+ var QUICK_ACTIONS = [
18
+ { label: 'What changed this week?', query: 'What changed or was decided in the last 7 days?' },
19
+ { label: 'Key decisions', query: 'What are the most important decisions that were made?' },
20
+ { label: 'Find contradictions', query: 'Are there any contradicting or conflicting memories?' },
21
+ { label: 'Summarize', query: 'Give me a high-level summary of what you know about me and my work.' },
22
+ ];
23
+
24
+ // ============================================================================
25
+ // INIT — Build chat UI in the right panel
26
+ // ============================================================================
27
+
28
+ function initMemoryChat() {
29
+ var rightPanel = document.getElementById('graph-right-panel');
30
+ if (!rightPanel) return;
31
+
32
+ // Replace the detail panel with chat + detail toggle
33
+ rightPanel.innerHTML = ''
34
+ + '<div class="card" style="height:550px; display:flex; flex-direction:column;">'
35
+ + ' <div class="card-header py-2 d-flex align-items-center justify-content-between">'
36
+ + ' <div>'
37
+ + ' <button class="btn btn-sm btn-outline-primary active me-1" id="chat-tab-btn" onclick="showChatPanel()"><i class="bi bi-chat-dots"></i> Ask Memory</button>'
38
+ + ' <button class="btn btn-sm btn-outline-secondary" id="detail-tab-btn" onclick="showDetailPanel()"><i class="bi bi-info-circle"></i> Detail</button>'
39
+ + ' </div>'
40
+ + ' <span class="badge bg-secondary" id="chat-mode-badge" style="font-size:0.7rem;">Mode A</span>'
41
+ + ' </div>'
42
+
43
+ // Chat panel
44
+ + ' <div id="chat-panel" style="flex:1; display:flex; flex-direction:column; overflow:hidden;">'
45
+ + ' <div id="chat-messages" style="flex:1; overflow-y:auto; padding:8px; font-size:0.85rem;"></div>'
46
+
47
+ // Quick actions
48
+ + ' <div id="chat-quick-actions" class="px-2 py-1 border-top" style="font-size:0.75rem;">'
49
+ + ' ' + QUICK_ACTIONS.map(function(a) {
50
+ return '<button class="btn btn-sm btn-outline-secondary me-1 mb-1" onclick="sendChatQuery(\'' + a.query.replace(/'/g, "\\'") + '\')">' + a.label + '</button>';
51
+ }).join('')
52
+ + ' </div>'
53
+
54
+ // Input
55
+ + ' <div class="p-2 border-top">'
56
+ + ' <div class="input-group input-group-sm">'
57
+ + ' <input type="text" class="form-control" id="chat-input" placeholder="Ask your memory..."'
58
+ + ' onkeydown="if(event.key===\'Enter\')sendChatFromInput()">'
59
+ + ' <button class="btn btn-primary" onclick="sendChatFromInput()" id="chat-send-btn">'
60
+ + ' <i class="bi bi-send"></i>'
61
+ + ' </button>'
62
+ + ' <button class="btn btn-outline-danger d-none" onclick="cancelChat()" id="chat-cancel-btn">'
63
+ + ' <i class="bi bi-stop-circle"></i>'
64
+ + ' </button>'
65
+ + ' </div>'
66
+ + ' </div>'
67
+ + ' </div>'
68
+
69
+ // Detail panel (hidden by default)
70
+ + ' <div id="detail-panel" style="flex:1; overflow-y:auto; padding:8px; font-size:0.85rem; display:none;">'
71
+ + ' <div id="sigma-detail-content" class="text-muted">Click a node to see its details.</div>'
72
+ + ' </div>'
73
+
74
+ + '</div>';
75
+
76
+ // Load chat mode from config
77
+ _loadChatMode();
78
+ }
79
+
80
+ // ============================================================================
81
+ // PANEL TOGGLE (Chat vs Detail)
82
+ // ============================================================================
83
+
84
+ function showChatPanel() {
85
+ var chatPanel = document.getElementById('chat-panel');
86
+ var detailPanel = document.getElementById('detail-panel');
87
+ var chatBtn = document.getElementById('chat-tab-btn');
88
+ var detailBtn = document.getElementById('detail-tab-btn');
89
+ if (chatPanel) chatPanel.style.display = 'flex';
90
+ if (detailPanel) detailPanel.style.display = 'none';
91
+ if (chatBtn) { chatBtn.classList.add('active'); chatBtn.classList.replace('btn-outline-secondary', 'btn-outline-primary'); }
92
+ if (detailBtn) { detailBtn.classList.remove('active'); detailBtn.classList.replace('btn-outline-primary', 'btn-outline-secondary'); }
93
+ }
94
+
95
+ function showDetailPanel() {
96
+ var chatPanel = document.getElementById('chat-panel');
97
+ var detailPanel = document.getElementById('detail-panel');
98
+ var chatBtn = document.getElementById('chat-tab-btn');
99
+ var detailBtn = document.getElementById('detail-tab-btn');
100
+ if (chatPanel) chatPanel.style.display = 'none';
101
+ if (detailPanel) detailPanel.style.display = 'flex';
102
+ if (detailBtn) { detailBtn.classList.add('active'); detailBtn.classList.replace('btn-outline-secondary', 'btn-outline-primary'); }
103
+ if (chatBtn) { chatBtn.classList.remove('active'); chatBtn.classList.replace('btn-outline-primary', 'btn-outline-secondary'); }
104
+ }
105
+
106
+ // ============================================================================
107
+ // SEND MESSAGE
108
+ // ============================================================================
109
+
110
+ function sendChatFromInput() {
111
+ var input = document.getElementById('chat-input');
112
+ if (!input || !input.value.trim()) return;
113
+ sendChatQuery(input.value.trim());
114
+ input.value = '';
115
+ }
116
+
117
+ function sendChatQuery(query) {
118
+ if (chatState.streaming) return; // Don't allow concurrent
119
+
120
+ // Add user message
121
+ _addMessage('user', query);
122
+ _renderMessages();
123
+
124
+ // Start streaming
125
+ chatState.streaming = true;
126
+ _toggleStreamUI(true);
127
+
128
+ var assistantMsg = { role: 'assistant', content: '', citations: [] };
129
+ chatState.messages.push(assistantMsg);
130
+
131
+ chatState.abortController = new AbortController();
132
+
133
+ fetch('/api/v3/chat/stream', {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify({
137
+ query: query,
138
+ mode: chatState.mode,
139
+ limit: 10,
140
+ }),
141
+ signal: chatState.abortController.signal,
142
+ }).then(function(response) {
143
+ if (!response.ok) throw new Error('HTTP ' + response.status);
144
+ var reader = response.body.getReader();
145
+ var decoder = new TextDecoder();
146
+ var buffer = '';
147
+
148
+ function pump() {
149
+ return reader.read().then(function(result) {
150
+ if (result.done) {
151
+ _onStreamEnd();
152
+ return;
153
+ }
154
+ buffer += decoder.decode(result.value, { stream: true });
155
+ var lines = buffer.split('\n');
156
+ buffer = lines.pop(); // Keep incomplete line
157
+
158
+ var currentEvent = '';
159
+ for (var i = 0; i < lines.length; i++) {
160
+ var line = lines[i];
161
+ if (line.startsWith('event: ')) {
162
+ currentEvent = line.substring(7).trim();
163
+ } else if (line.startsWith('data: ')) {
164
+ var data = line.substring(6);
165
+ _handleSSEEvent(currentEvent, data, assistantMsg);
166
+ }
167
+ }
168
+ _renderMessages();
169
+ _scrollToBottom();
170
+ return pump();
171
+ });
172
+ }
173
+ return pump();
174
+ }).catch(function(err) {
175
+ if (err.name === 'AbortError') {
176
+ assistantMsg.content += '\n\n[Cancelled]';
177
+ } else {
178
+ assistantMsg.content += '\n\n[Error: ' + err.message + ']';
179
+ }
180
+ _onStreamEnd();
181
+ });
182
+ }
183
+
184
+ function cancelChat() {
185
+ if (chatState.abortController) {
186
+ chatState.abortController.abort();
187
+ }
188
+ }
189
+
190
+ // ============================================================================
191
+ // SSE EVENT HANDLING
192
+ // ============================================================================
193
+
194
+ function _handleSSEEvent(eventType, data, assistantMsg) {
195
+ if (eventType === 'token') {
196
+ assistantMsg.content += data;
197
+ } else if (eventType === 'citation') {
198
+ try {
199
+ var citation = JSON.parse(data);
200
+ assistantMsg.citations.push(citation);
201
+ } catch (e) { /* skip */ }
202
+ } else if (eventType === 'error') {
203
+ try {
204
+ var err = JSON.parse(data);
205
+ assistantMsg.content += '\n[Error: ' + (err.message || 'Unknown') + ']';
206
+ } catch (e) {
207
+ assistantMsg.content += '\n[Error]';
208
+ }
209
+ }
210
+ // 'done' event handled by stream end
211
+ }
212
+
213
+ // ============================================================================
214
+ // MESSAGE RENDERING
215
+ // ============================================================================
216
+
217
+ function _addMessage(role, content) {
218
+ chatState.messages.push({ role: role, content: content, citations: [] });
219
+ }
220
+
221
+ function _renderMessages() {
222
+ var container = document.getElementById('chat-messages');
223
+ if (!container) return;
224
+ container.innerHTML = '';
225
+
226
+ chatState.messages.forEach(function(msg) {
227
+ var div = document.createElement('div');
228
+ div.className = 'mb-2 p-2 rounded ' + (msg.role === 'user'
229
+ ? 'bg-primary bg-opacity-10 text-end'
230
+ : 'bg-light');
231
+
232
+ // Render content with basic markdown (bold, newlines)
233
+ var html = (msg.content || '')
234
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
235
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
236
+ .replace(/\n/g, '<br>');
237
+
238
+ // Make [MEM-N] citations clickable
239
+ html = html.replace(/\[MEM-(\d+)\]/g, function(match, num) {
240
+ var idx = parseInt(num) - 1;
241
+ var citation = msg.citations[idx];
242
+ if (citation) {
243
+ return '<a href="#" class="badge bg-primary text-decoration-none" '
244
+ + 'onclick="event.preventDefault(); _onCitationClick(\'' + citation.fact_id + '\')" '
245
+ + 'title="' + (citation.content_preview || '').replace(/"/g, '&quot;') + '">'
246
+ + match + '</a>';
247
+ }
248
+ return '<span class="badge bg-secondary">' + match + '</span>';
249
+ });
250
+
251
+ div.innerHTML = '<small class="text-muted">' + (msg.role === 'user' ? 'You' : 'Memory') + '</small><br>' + html;
252
+ container.appendChild(div);
253
+ });
254
+ }
255
+
256
+ function _scrollToBottom() {
257
+ var container = document.getElementById('chat-messages');
258
+ if (container) container.scrollTop = container.scrollHeight;
259
+ }
260
+
261
+ // ============================================================================
262
+ // CITATION CLICK → HIGHLIGHT IN GRAPH
263
+ // ============================================================================
264
+
265
+ function _onCitationClick(factId) {
266
+ // Use event bus for graph ↔ chat linking
267
+ if (window.SLMEventBus) {
268
+ SLMEventBus.publishDebounced('slm:chat:citationClicked', { factId: factId }, 100);
269
+ }
270
+ // Also show detail panel
271
+ if (typeof openSigmaNodeDetail === 'function') {
272
+ openSigmaNodeDetail(factId);
273
+ showDetailPanel();
274
+ }
275
+ }
276
+
277
+ // ============================================================================
278
+ // UI HELPERS
279
+ // ============================================================================
280
+
281
+ function _toggleStreamUI(streaming) {
282
+ var sendBtn = document.getElementById('chat-send-btn');
283
+ var cancelBtn = document.getElementById('chat-cancel-btn');
284
+ var input = document.getElementById('chat-input');
285
+ if (sendBtn) sendBtn.classList.toggle('d-none', streaming);
286
+ if (cancelBtn) cancelBtn.classList.toggle('d-none', !streaming);
287
+ if (input) input.disabled = streaming;
288
+ }
289
+
290
+ function _onStreamEnd() {
291
+ chatState.streaming = false;
292
+ chatState.abortController = null;
293
+ _toggleStreamUI(false);
294
+ _renderMessages();
295
+ _scrollToBottom();
296
+ }
297
+
298
+ function _loadChatMode() {
299
+ // Auto-detect mode from Settings — no user dropdown needed
300
+ fetch('/api/v3/mode').then(function(r) { return r.json(); }).then(function(data) {
301
+ var mode = data.mode || 'a';
302
+ chatState.mode = mode;
303
+
304
+ var modeNames = { 'a': 'Mode A · Raw Results', 'b': 'Mode B · Ollama', 'c': 'Mode C · Cloud LLM' };
305
+ var modeColors = { 'a': 'bg-secondary', 'b': 'bg-success', 'c': 'bg-primary' };
306
+
307
+ var badge = document.getElementById('chat-mode-badge');
308
+ if (badge) {
309
+ badge.textContent = modeNames[mode] || 'Mode ' + mode.toUpperCase();
310
+ badge.className = 'badge ' + (modeColors[mode] || 'bg-secondary');
311
+ badge.style.fontSize = '0.7rem';
312
+ badge.title = 'Change mode in Settings tab';
313
+ }
314
+
315
+ // Show guidance for Mode A users
316
+ if (mode === 'a') {
317
+ var msgs = document.getElementById('chat-messages');
318
+ if (msgs && msgs.children.length === 0) {
319
+ msgs.innerHTML = '<div class="text-muted small p-3 text-center">'
320
+ + '<i class="bi bi-info-circle"></i> <strong>Mode A</strong> — No LLM connected.<br>'
321
+ + 'Chat returns raw memory retrieval results.<br>'
322
+ + 'For AI-powered conversation, switch to <strong>Mode B</strong> (Ollama) or <strong>Mode C</strong> (Cloud) in the <strong>Settings</strong> tab.<br>'
323
+ + '<br>You can also use the <strong>Recall Lab</strong> tab for full 6-channel search.'
324
+ + '</div>';
325
+ }
326
+ }
327
+ }).catch(function() { /* keep default 'a' */ });
328
+ }
329
+
330
+ // ============================================================================
331
+ // INIT ON DOM READY
332
+ // ============================================================================
333
+
334
+ document.addEventListener('DOMContentLoaded', function() {
335
+ // Delay init until graph tab is shown (panel must exist in DOM)
336
+ var graphTab = document.getElementById('graph-tab');
337
+ if (graphTab) {
338
+ graphTab.addEventListener('shown.bs.tab', function() {
339
+ if (!document.getElementById('chat-panel')) {
340
+ initMemoryChat();
341
+ }
342
+ });
343
+ }
344
+ });
@@ -0,0 +1,265 @@
1
+ // SuperLocalMemory v3.4.1 — Memory Timeline (D3.js v7)
2
+ // Copyright (c) 2026 Varun Pratap Bhardwaj — AGPL-3.0-or-later
3
+ // Horizontal timeline: facts + temporal events + consolidation over time.
4
+
5
+ var memoryTimelineState = {
6
+ range: '7d',
7
+ groupBy: 'category',
8
+ events: [],
9
+ svg: null,
10
+ zoom: null,
11
+ };
12
+
13
+ var TRUST_COLORS = { high: '#198754', medium: '#ffc107', low: '#dc3545', unknown: '#6c757d' };
14
+
15
+ function trustColor(score) {
16
+ if (score === null || score === undefined) return TRUST_COLORS.unknown;
17
+ if (score >= 0.7) return TRUST_COLORS.high;
18
+ if (score >= 0.4) return TRUST_COLORS.medium;
19
+ return TRUST_COLORS.low;
20
+ }
21
+
22
+ // ============================================================================
23
+ // INIT
24
+ // ============================================================================
25
+
26
+ function initMemoryTimeline() {
27
+ // Zoom buttons
28
+ document.querySelectorAll('#timeline-zoom-group button').forEach(function(btn) {
29
+ btn.addEventListener('click', function() {
30
+ setZoomLevel(btn.dataset.zoom);
31
+ });
32
+ });
33
+ // Group-by buttons
34
+ document.querySelectorAll('#timeline-group-by-group button').forEach(function(btn) {
35
+ btn.addEventListener('click', function() {
36
+ setGroupBy(btn.dataset.groupby);
37
+ });
38
+ });
39
+ // Listen for refresh events (fire-and-forget from other components)
40
+ window.addEventListener('slm:timeline:refresh', function() {
41
+ fetchTimelineData(memoryTimelineState.range, memoryTimelineState.groupBy);
42
+ });
43
+ // Initial load
44
+ fetchTimelineData('7d', 'category');
45
+ }
46
+
47
+ // ============================================================================
48
+ // DATA FETCH
49
+ // ============================================================================
50
+
51
+ function fetchTimelineData(range, groupBy) {
52
+ memoryTimelineState.range = range;
53
+ memoryTimelineState.groupBy = groupBy;
54
+
55
+ var container = document.getElementById('memory-timeline-chart');
56
+ if (!container) return;
57
+
58
+ fetch('/api/v3/timeline/?range=' + range + '&group_by=' + groupBy + '&limit=1000')
59
+ .then(function(r) { return r.json(); })
60
+ .then(function(data) {
61
+ memoryTimelineState.events = data.events || [];
62
+ renderMemoryTimeline(memoryTimelineState.events, container, groupBy);
63
+ })
64
+ .catch(function(e) {
65
+ container.innerHTML = '';
66
+ var msg = document.createElement('div');
67
+ msg.className = 'text-muted small text-center py-4';
68
+ msg.textContent = 'Timeline unavailable: ' + e.message;
69
+ container.appendChild(msg);
70
+ });
71
+ }
72
+
73
+ // ============================================================================
74
+ // D3 RENDERING
75
+ // ============================================================================
76
+
77
+ function renderMemoryTimeline(events, container, groupBy) {
78
+ container.innerHTML = '';
79
+ if (!events || events.length === 0) {
80
+ var msg = document.createElement('div');
81
+ msg.className = 'text-muted small text-center py-4';
82
+ msg.textContent = 'No memory events in this time range.';
83
+ container.appendChild(msg);
84
+ return;
85
+ }
86
+
87
+ if (typeof d3 === 'undefined') {
88
+ container.textContent = 'D3.js not loaded.';
89
+ return;
90
+ }
91
+
92
+ var width = container.clientWidth || 800;
93
+ var height = 260;
94
+ var margin = { top: 20, right: 20, bottom: 30, left: 100 };
95
+
96
+ // Parse timestamps
97
+ events.forEach(function(e) {
98
+ e._date = new Date(e.timestamp);
99
+ if (groupBy === 'community') {
100
+ e._category = e.community_id !== null ? ('C' + e.community_id) : 'Unknown';
101
+ } else {
102
+ e._category = e.category || 'semantic';
103
+ }
104
+ });
105
+
106
+ // Build categories
107
+ var categories;
108
+ if (groupBy === 'community') {
109
+ var counts = {};
110
+ events.forEach(function(e) { counts[e._category] = (counts[e._category] || 0) + 1; });
111
+ var sorted = Object.entries(counts).sort(function(a, b) { return b[1] - a[1]; });
112
+ var top10 = sorted.slice(0, 10).map(function(pair) { return pair[0]; });
113
+ if (sorted.length > 10) {
114
+ categories = top10.concat(['Other']);
115
+ events.forEach(function(e) {
116
+ if (top10.indexOf(e._category) === -1) e._category = 'Other';
117
+ });
118
+ } else {
119
+ categories = sorted.map(function(pair) { return pair[0]; });
120
+ }
121
+ } else {
122
+ categories = ['semantic', 'episodic', 'opinion', 'temporal', 'consolidation'];
123
+ }
124
+
125
+ // Scales
126
+ var xDomain = d3.extent(events, function(e) { return e._date; });
127
+ var xScale = d3.scaleTime()
128
+ .domain(xDomain)
129
+ .range([margin.left, width - margin.right]);
130
+
131
+ var yScale = d3.scaleBand()
132
+ .domain(categories)
133
+ .range([margin.top, height - margin.bottom])
134
+ .padding(0.3);
135
+
136
+ // SVG
137
+ var svg = d3.select(container).append('svg')
138
+ .attr('width', width)
139
+ .attr('height', height)
140
+ .style('font-size', '11px');
141
+
142
+ // Axes
143
+ var xAxisG = svg.append('g')
144
+ .attr('transform', 'translate(0,' + (height - margin.bottom) + ')')
145
+ .call(d3.axisBottom(xScale).ticks(6).tickFormat(d3.timeFormat('%b %d')));
146
+
147
+ svg.append('g')
148
+ .attr('transform', 'translate(' + margin.left + ',0)')
149
+ .call(d3.axisLeft(yScale));
150
+
151
+ // Data points
152
+ var circles = svg.selectAll('circle')
153
+ .data(events)
154
+ .join('circle')
155
+ .attr('cx', function(d) { return xScale(d._date); })
156
+ .attr('cy', function(d) { return yScale(d._category) + yScale.bandwidth() / 2; })
157
+ .attr('r', 4)
158
+ .attr('fill', function(d) { return trustColor(d.trust_score); })
159
+ .attr('stroke', '#fff')
160
+ .attr('stroke-width', 0.5)
161
+ .attr('cursor', 'pointer')
162
+ .attr('opacity', 0.8)
163
+ .on('click', function(event, d) { onTimelinePointClick(d); })
164
+ .on('mouseover', function(event, d) { showTimelineTooltip(event, d); })
165
+ .on('mouseout', hideTimelineTooltip);
166
+
167
+ // Zoom (x-axis only)
168
+ var zoom = d3.zoom()
169
+ .scaleExtent([0.5, 20])
170
+ .translateExtent([[margin.left, 0], [width - margin.right, height]])
171
+ .extent([[margin.left, 0], [width - margin.right, height]])
172
+ .on('zoom', function(event) {
173
+ var newX = event.transform.rescaleX(xScale);
174
+ xAxisG.call(d3.axisBottom(newX).ticks(6).tickFormat(d3.timeFormat('%b %d')));
175
+ circles.attr('cx', function(d) { return newX(d._date); });
176
+ });
177
+
178
+ svg.call(zoom);
179
+ memoryTimelineState.svg = svg;
180
+ memoryTimelineState.zoom = zoom;
181
+ }
182
+
183
+ // ============================================================================
184
+ // INTERACTIONS
185
+ // ============================================================================
186
+
187
+ function onTimelinePointClick(d) {
188
+ // Fire event bus (fire-and-forget — Phase 3 adds listener)
189
+ if (d.id) {
190
+ window.dispatchEvent(new CustomEvent('slm:graph:highlight', {
191
+ detail: { nodeIds: [d.id] },
192
+ }));
193
+ }
194
+ // Open detail modal if available
195
+ if (typeof openMemoryDetail === 'function' && d.id) {
196
+ openMemoryDetail(d.id);
197
+ }
198
+ }
199
+
200
+ function showTimelineTooltip(event, d) {
201
+ var tooltip = document.getElementById('timeline-tooltip');
202
+ if (!tooltip) {
203
+ tooltip = document.createElement('div');
204
+ tooltip.id = 'timeline-tooltip';
205
+ tooltip.style.cssText = 'position:fixed;background:#333;color:#fff;padding:6px 10px;border-radius:6px;font-size:0.75rem;z-index:9999;pointer-events:none;max-width:300px;';
206
+ document.body.appendChild(tooltip);
207
+ }
208
+ tooltip.textContent = (d.content_preview || 'No preview') + ' | ' + (d.category || '') + ' | Trust: ' + (d.trust_score !== null ? (d.trust_score * 100).toFixed(0) + '%' : 'N/A');
209
+ tooltip.style.left = (event.clientX + 10) + 'px';
210
+ tooltip.style.top = (event.clientY - 30) + 'px';
211
+ tooltip.style.display = 'block';
212
+ }
213
+
214
+ function hideTimelineTooltip() {
215
+ var tooltip = document.getElementById('timeline-tooltip');
216
+ if (tooltip) tooltip.style.display = 'none';
217
+ }
218
+
219
+ // ============================================================================
220
+ // CONTROLS
221
+ // ============================================================================
222
+
223
+ function setZoomLevel(level) {
224
+ var rangeMap = { day: '1d', week: '7d', month: '30d' };
225
+ var range = rangeMap[level] || '7d';
226
+
227
+ // Update button active state
228
+ document.querySelectorAll('#timeline-zoom-group button').forEach(function(b) {
229
+ b.classList.toggle('active', b.dataset.zoom === level);
230
+ });
231
+ fetchTimelineData(range, memoryTimelineState.groupBy);
232
+ }
233
+
234
+ function setGroupBy(mode) {
235
+ document.querySelectorAll('#timeline-group-by-group button').forEach(function(b) {
236
+ b.classList.toggle('active', b.dataset.groupby === mode);
237
+ });
238
+ fetchTimelineData(memoryTimelineState.range, mode);
239
+ }
240
+
241
+ // ============================================================================
242
+ // AUTO-INIT (when Knowledge Graph tab opens)
243
+ // ============================================================================
244
+
245
+ // Deferred init — only load when the graph tab becomes visible
246
+ var _timelineInitDone = false;
247
+ function tryInitTimeline() {
248
+ if (_timelineInitDone) return;
249
+ var panel = document.getElementById('memory-timeline-panel');
250
+ if (panel && panel.offsetParent !== null) {
251
+ _timelineInitDone = true;
252
+ initMemoryTimeline();
253
+ }
254
+ }
255
+
256
+ // Check on tab switch
257
+ document.addEventListener('shown.bs.tab', function() {
258
+ setTimeout(tryInitTimeline, 100);
259
+ });
260
+ // Also check on DOMContentLoaded
261
+ if (document.readyState === 'loading') {
262
+ document.addEventListener('DOMContentLoaded', function() { setTimeout(tryInitTimeline, 500); });
263
+ } else {
264
+ setTimeout(tryInitTimeline, 500);
265
+ }