superlocalmemory 3.4.3 → 3.4.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.
- package/README.md +7 -17
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/cli/commands.py +29 -0
- package/src/superlocalmemory/cli/daemon.py +128 -68
- package/src/superlocalmemory/cli/main.py +15 -2
- package/src/superlocalmemory/cli/service_installer.py +367 -0
- package/src/superlocalmemory/cli/setup_wizard.py +13 -0
- package/src/superlocalmemory/mcp/server.py +32 -3
- package/src/superlocalmemory/mcp/tools_mesh.py +249 -0
- package/src/superlocalmemory/server/routes/adapters.py +63 -0
- package/src/superlocalmemory/server/routes/entity.py +56 -0
- package/src/superlocalmemory/server/unified_daemon.py +2 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +1588 -0
- package/src/superlocalmemory/ui/index.html +134 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +28 -1
- package/src/superlocalmemory/ui/js/ng-entities.js +272 -0
- package/src/superlocalmemory/ui/js/ng-health.js +208 -0
- package/src/superlocalmemory/ui/js/ng-ingestion.js +203 -0
- package/src/superlocalmemory/ui/js/ng-mesh.js +311 -0
- package/src/superlocalmemory/ui/js/ng-shell.js +471 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +601 -0
- package/src/superlocalmemory.egg-info/SOURCES.txt +313 -0
- package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
- package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
- package/src/superlocalmemory.egg-info/requires.txt +55 -0
- package/src/superlocalmemory.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// Neural Glass Shell — Dashboard V2 "Neural Glass"
|
|
2
|
+
// Injects sidebar, restructures DOM, handles navigation
|
|
3
|
+
// Progressive enhancement: if this fails, Bootstrap tabs still work
|
|
4
|
+
|
|
5
|
+
(function() {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// ── Sidebar Navigation Config ──────────────────────────────
|
|
9
|
+
var NAV_SECTIONS = [
|
|
10
|
+
{
|
|
11
|
+
label: 'Memory',
|
|
12
|
+
items: [
|
|
13
|
+
{ id: 'dashboard-pane', icon: 'bi-speedometer2', text: 'Dashboard' },
|
|
14
|
+
{ id: 'graph-pane', icon: 'bi-diagram-3', text: 'Knowledge Graph' },
|
|
15
|
+
{ id: 'memories-pane', icon: 'bi-list-ul', text: 'Memories' },
|
|
16
|
+
{ id: 'recall-lab-pane', icon: 'bi-search-heart', text: 'Recall Lab' },
|
|
17
|
+
{ id: 'timeline-pane', icon: 'bi-clock-history', text: 'Timeline' }
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
label: 'Intelligence',
|
|
22
|
+
items: [
|
|
23
|
+
{ id: 'clusters-pane', icon: 'bi-collection', text: 'Clusters' },
|
|
24
|
+
{ id: 'patterns-pane', icon: 'bi-puzzle', text: 'Patterns' },
|
|
25
|
+
{ id: 'learning-pane', icon: 'bi-mortarboard', text: 'Learning' },
|
|
26
|
+
{ id: 'behavioral-pane', icon: 'bi-lightbulb', text: 'Behavioral' }
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: 'System',
|
|
31
|
+
items: [
|
|
32
|
+
{ id: 'events-pane', icon: 'bi-broadcast', text: 'Live Events' },
|
|
33
|
+
{ id: 'agents-pane', icon: 'bi-robot', text: 'Agents' },
|
|
34
|
+
{ id: 'trust-pane', icon: 'bi-shield-check', text: 'Trust' },
|
|
35
|
+
{ id: 'lifecycle-pane', icon: 'bi-hourglass-split', text: 'Lifecycle' },
|
|
36
|
+
{ id: 'compliance-pane', icon: 'bi-shield-lock', text: 'Compliance' },
|
|
37
|
+
{ id: 'math-health-pane', icon: 'bi-calculator', text: 'Math Health' },
|
|
38
|
+
{ id: 'ide-pane', icon: 'bi-plug', text: 'IDEs' }
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'v3.4.3',
|
|
43
|
+
items: [
|
|
44
|
+
{ id: 'health-pane', icon: 'bi-heart-pulse', text: 'Health Monitor', badge: 'NEW' },
|
|
45
|
+
{ id: 'ingestion-pane', icon: 'bi-cloud-download', text: 'Ingestion', badge: 'NEW' },
|
|
46
|
+
{ id: 'entities-pane', icon: 'bi-person-badge', text: 'Entity Explorer', badge: 'NEW' },
|
|
47
|
+
{ id: 'mesh-pane', icon: 'bi-share', text: 'Mesh Peers', badge: 'NEW' }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: 'Config',
|
|
52
|
+
items: [
|
|
53
|
+
{ id: 'settings-pane', icon: 'bi-gear', text: 'Settings' }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// ── Build Sidebar HTML ─────────────────────────────────────
|
|
59
|
+
function buildSidebar() {
|
|
60
|
+
var sidebar = document.createElement('aside');
|
|
61
|
+
sidebar.className = 'ng-sidebar';
|
|
62
|
+
sidebar.id = 'ng-sidebar';
|
|
63
|
+
sidebar.setAttribute('role', 'navigation');
|
|
64
|
+
sidebar.setAttribute('aria-label', 'Main navigation');
|
|
65
|
+
|
|
66
|
+
// Header
|
|
67
|
+
var header = document.createElement('div');
|
|
68
|
+
header.className = 'ng-sidebar-header';
|
|
69
|
+
header.innerHTML =
|
|
70
|
+
'<div class="ng-sidebar-brand">' +
|
|
71
|
+
'<div class="ng-sidebar-brand-icon">' +
|
|
72
|
+
'<i class="bi bi-diamond-fill" style="font-size:0.875rem"></i>' +
|
|
73
|
+
'</div>' +
|
|
74
|
+
'<div>' +
|
|
75
|
+
'<div class="ng-sidebar-brand-text">SuperLocalMemory</div>' +
|
|
76
|
+
'<div class="ng-sidebar-brand-version" id="ng-version">v3.4.4</div>' +
|
|
77
|
+
'</div>' +
|
|
78
|
+
'</div>';
|
|
79
|
+
sidebar.appendChild(header);
|
|
80
|
+
|
|
81
|
+
// Nav
|
|
82
|
+
var nav = document.createElement('nav');
|
|
83
|
+
nav.className = 'ng-sidebar-nav';
|
|
84
|
+
|
|
85
|
+
NAV_SECTIONS.forEach(function(section) {
|
|
86
|
+
var sDiv = document.createElement('div');
|
|
87
|
+
sDiv.className = 'ng-sidebar-section';
|
|
88
|
+
|
|
89
|
+
var label = document.createElement('div');
|
|
90
|
+
label.className = 'ng-sidebar-section-label';
|
|
91
|
+
label.textContent = section.label;
|
|
92
|
+
sDiv.appendChild(label);
|
|
93
|
+
|
|
94
|
+
section.items.forEach(function(item) {
|
|
95
|
+
var a = document.createElement('a');
|
|
96
|
+
a.className = 'ng-sidebar-item';
|
|
97
|
+
a.setAttribute('data-target', item.id);
|
|
98
|
+
a.setAttribute('role', 'tab');
|
|
99
|
+
a.setAttribute('aria-controls', item.id);
|
|
100
|
+
a.setAttribute('tabindex', '0');
|
|
101
|
+
if (item.id === 'dashboard-pane') a.classList.add('active');
|
|
102
|
+
|
|
103
|
+
a.innerHTML =
|
|
104
|
+
'<i class="bi ' + item.icon + ' ng-sidebar-icon"></i>' +
|
|
105
|
+
'<span>' + item.text + '</span>' +
|
|
106
|
+
(item.badge ? '<span class="ng-sidebar-badge">' + item.badge + '</span>' : '');
|
|
107
|
+
|
|
108
|
+
a.addEventListener('click', function(e) {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
activateTab(item.id);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
a.addEventListener('keydown', function(e) {
|
|
114
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
activateTab(item.id);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
sDiv.appendChild(a);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
nav.appendChild(sDiv);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
sidebar.appendChild(nav);
|
|
127
|
+
|
|
128
|
+
// Footer
|
|
129
|
+
var footer = document.createElement('div');
|
|
130
|
+
footer.className = 'ng-sidebar-footer';
|
|
131
|
+
|
|
132
|
+
// Move profile selector from navbar to sidebar footer
|
|
133
|
+
var profileSelect = document.getElementById('profile-select');
|
|
134
|
+
var addProfileBtn = document.getElementById('add-profile-btn');
|
|
135
|
+
|
|
136
|
+
footer.innerHTML =
|
|
137
|
+
'<div style="margin-bottom:8px">' +
|
|
138
|
+
'<div class="ng-sidebar-section-label" style="padding:0 0 4px">Profile</div>' +
|
|
139
|
+
'<div style="display:flex;gap:4px;align-items:center" id="ng-profile-container"></div>' +
|
|
140
|
+
'</div>' +
|
|
141
|
+
'<div style="display:flex;gap:4px;align-items:center">' +
|
|
142
|
+
'<button class="ng-btn" id="ng-refresh-btn" title="Refresh" style="flex:1;justify-content:center">' +
|
|
143
|
+
'<i class="bi bi-arrow-clockwise"></i>' +
|
|
144
|
+
'</button>' +
|
|
145
|
+
'<button class="ng-btn" id="ng-theme-toggle" title="Toggle theme" onclick="toggleDarkMode()" style="flex:1;justify-content:center">' +
|
|
146
|
+
'<i class="bi bi-sun-fill" id="ng-theme-icon"></i>' +
|
|
147
|
+
'</button>' +
|
|
148
|
+
'<button class="ng-btn" id="ng-privacy-btn" title="Privacy blur (for screen recording)" onclick="togglePrivacyBlur()" style="flex:1;justify-content:center">' +
|
|
149
|
+
'<i class="bi bi-eye-slash" id="ng-privacy-icon"></i>' +
|
|
150
|
+
'</button>' +
|
|
151
|
+
'<a href="https://github.com/qualixar/superlocalmemory" target="_blank" class="ng-btn" title="Star on GitHub" style="flex:1;justify-content:center">' +
|
|
152
|
+
'<i class="bi bi-github"></i>' +
|
|
153
|
+
'</a>' +
|
|
154
|
+
'</div>';
|
|
155
|
+
|
|
156
|
+
sidebar.appendChild(footer);
|
|
157
|
+
|
|
158
|
+
return sidebar;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Restructure DOM ────────────────────────────────────────
|
|
162
|
+
function restructureDOM() {
|
|
163
|
+
var body = document.body;
|
|
164
|
+
// Target the MAIN container-fluid (direct child of body), not the one inside navbar
|
|
165
|
+
var containers = document.querySelectorAll('body > .container-fluid');
|
|
166
|
+
var container = containers.length > 0 ? containers[containers.length - 1] : null;
|
|
167
|
+
if (!container) {
|
|
168
|
+
// Fallback: find the container that holds the tab-content
|
|
169
|
+
container = document.querySelector('.tab-content')?.closest('.container-fluid');
|
|
170
|
+
}
|
|
171
|
+
if (!container) return;
|
|
172
|
+
|
|
173
|
+
// Create shell wrapper
|
|
174
|
+
var shell = document.createElement('div');
|
|
175
|
+
shell.className = 'ng-shell';
|
|
176
|
+
|
|
177
|
+
// Build and insert sidebar
|
|
178
|
+
var sidebar = buildSidebar();
|
|
179
|
+
shell.appendChild(sidebar);
|
|
180
|
+
|
|
181
|
+
// Create content wrapper
|
|
182
|
+
var content = document.createElement('main');
|
|
183
|
+
content.className = 'ng-content';
|
|
184
|
+
var inner = document.createElement('div');
|
|
185
|
+
inner.className = 'ng-content-inner';
|
|
186
|
+
|
|
187
|
+
// Move container content into inner
|
|
188
|
+
while (container.firstChild) {
|
|
189
|
+
inner.appendChild(container.firstChild);
|
|
190
|
+
}
|
|
191
|
+
content.appendChild(inner);
|
|
192
|
+
shell.appendChild(content);
|
|
193
|
+
|
|
194
|
+
// Move footer into content
|
|
195
|
+
var existingFooter = document.querySelector('footer');
|
|
196
|
+
if (existingFooter) {
|
|
197
|
+
content.appendChild(existingFooter);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Replace container with shell
|
|
201
|
+
container.parentNode.replaceChild(shell, container);
|
|
202
|
+
|
|
203
|
+
// Move dashboard-only elements INTO the dashboard-pane so they scroll with it
|
|
204
|
+
var dashboardPane = document.getElementById('dashboard-pane');
|
|
205
|
+
if (dashboardPane) {
|
|
206
|
+
['stats-container', 'privacy-notice', 'feedback-progress'].forEach(function(id) {
|
|
207
|
+
var el = document.getElementById(id);
|
|
208
|
+
if (el) dashboardPane.insertBefore(el, dashboardPane.firstChild);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Move profile selector to sidebar footer
|
|
213
|
+
var profileContainer = document.getElementById('ng-profile-container');
|
|
214
|
+
var profileSelect = document.getElementById('profile-select');
|
|
215
|
+
var addProfileBtn = document.getElementById('add-profile-btn');
|
|
216
|
+
if (profileContainer && profileSelect) {
|
|
217
|
+
profileSelect.classList.remove('profile-select');
|
|
218
|
+
profileSelect.style.cssText = 'flex:1;font-size:0.8125rem;';
|
|
219
|
+
profileContainer.appendChild(profileSelect);
|
|
220
|
+
if (addProfileBtn) {
|
|
221
|
+
addProfileBtn.className = 'ng-btn';
|
|
222
|
+
addProfileBtn.style.cssText = 'padding:4px 8px;';
|
|
223
|
+
profileContainer.appendChild(addProfileBtn);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Wire refresh button
|
|
228
|
+
var refreshBtn = document.getElementById('ng-refresh-btn');
|
|
229
|
+
if (refreshBtn && typeof refreshDashboard === 'function') {
|
|
230
|
+
refreshBtn.addEventListener('click', refreshDashboard);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Sync version from dashboard
|
|
234
|
+
setTimeout(function() {
|
|
235
|
+
var dashVer = document.getElementById('dashboard-version');
|
|
236
|
+
var ngVer = document.getElementById('ng-version');
|
|
237
|
+
if (dashVer && ngVer && dashVer.textContent !== '...') {
|
|
238
|
+
ngVer.textContent = 'v' + dashVer.textContent;
|
|
239
|
+
}
|
|
240
|
+
}, 1500);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Tab Activation ─────────────────────────────────────────
|
|
244
|
+
function activateTab(targetId) {
|
|
245
|
+
// Deactivate all sidebar items
|
|
246
|
+
document.querySelectorAll('.ng-sidebar-item').forEach(function(item) {
|
|
247
|
+
item.classList.remove('active');
|
|
248
|
+
item.setAttribute('aria-selected', 'false');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Activate clicked sidebar item
|
|
252
|
+
var activeItem = document.querySelector('.ng-sidebar-item[data-target="' + targetId + '"]');
|
|
253
|
+
if (activeItem) {
|
|
254
|
+
activeItem.classList.add('active');
|
|
255
|
+
activeItem.setAttribute('aria-selected', 'true');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Deactivate all tab panes
|
|
259
|
+
document.querySelectorAll('.tab-pane').forEach(function(pane) {
|
|
260
|
+
pane.classList.remove('show', 'active');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Activate target pane
|
|
264
|
+
var targetPane = document.getElementById(targetId);
|
|
265
|
+
if (targetPane) {
|
|
266
|
+
targetPane.classList.add('show', 'active');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Dispatch Bootstrap tab event for backward compat
|
|
270
|
+
var tabButton = document.getElementById(targetId.replace('-pane', '-tab'));
|
|
271
|
+
if (tabButton) {
|
|
272
|
+
try {
|
|
273
|
+
var event = new Event('shown.bs.tab', { bubbles: true });
|
|
274
|
+
event.target = tabButton;
|
|
275
|
+
event.relatedTarget = null;
|
|
276
|
+
tabButton.dispatchEvent(event);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
// Ignore if event dispatch fails
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Update URL hash via replaceState (avoids auto-scroll to hash target)
|
|
283
|
+
history.replaceState(null, '', '#' + targetId);
|
|
284
|
+
|
|
285
|
+
// Scroll content to top on tab switch
|
|
286
|
+
window.scrollTo({ top: 0, behavior: 'instant' });
|
|
287
|
+
var contentEl = document.querySelector('.ng-content');
|
|
288
|
+
if (contentEl) contentEl.scrollTo({ top: 0, behavior: 'instant' });
|
|
289
|
+
|
|
290
|
+
// Dashboard-only elements are inside dashboard-pane, so they auto-hide/show with the tab
|
|
291
|
+
|
|
292
|
+
// Trigger data loading for lazy-loaded tabs (immediate + deferred for async-heavy tabs)
|
|
293
|
+
triggerTabLoad(targetId);
|
|
294
|
+
// Deferred retry for tabs that need API data to populate
|
|
295
|
+
setTimeout(function() { triggerTabLoad(targetId); }, 500);
|
|
296
|
+
|
|
297
|
+
// Scroll sidebar item into view
|
|
298
|
+
if (activeItem) {
|
|
299
|
+
activeItem.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Lazy Load Tab Data ─────────────────────────────────────
|
|
304
|
+
function triggerTabLoad(tabId) {
|
|
305
|
+
switch(tabId) {
|
|
306
|
+
case 'graph-pane':
|
|
307
|
+
if (typeof loadGraph === 'function') loadGraph();
|
|
308
|
+
// v3.4.4: Initialize chat panel if not already present
|
|
309
|
+
if (typeof initMemoryChat === 'function' && !document.getElementById('chat-panel')) {
|
|
310
|
+
initMemoryChat();
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
case 'memories-pane':
|
|
314
|
+
if (typeof loadMemories === 'function') loadMemories();
|
|
315
|
+
break;
|
|
316
|
+
case 'clusters-pane':
|
|
317
|
+
if (typeof loadClusters === 'function') loadClusters();
|
|
318
|
+
break;
|
|
319
|
+
case 'patterns-pane':
|
|
320
|
+
if (typeof loadPatterns === 'function') loadPatterns();
|
|
321
|
+
break;
|
|
322
|
+
case 'timeline-pane':
|
|
323
|
+
if (typeof loadTimeline === 'function') loadTimeline();
|
|
324
|
+
break;
|
|
325
|
+
case 'events-pane':
|
|
326
|
+
if (typeof initEventStream === 'function') initEventStream();
|
|
327
|
+
if (typeof loadEventStats === 'function') loadEventStats();
|
|
328
|
+
break;
|
|
329
|
+
case 'agents-pane':
|
|
330
|
+
if (typeof loadAgents === 'function') loadAgents();
|
|
331
|
+
break;
|
|
332
|
+
case 'learning-pane':
|
|
333
|
+
if (typeof loadLearning === 'function') loadLearning();
|
|
334
|
+
break;
|
|
335
|
+
case 'trust-pane':
|
|
336
|
+
if (typeof loadTrustDashboard === 'function') loadTrustDashboard();
|
|
337
|
+
break;
|
|
338
|
+
case 'lifecycle-pane':
|
|
339
|
+
if (typeof loadLifecycle === 'function') loadLifecycle();
|
|
340
|
+
break;
|
|
341
|
+
case 'behavioral-pane':
|
|
342
|
+
if (typeof loadBehavioral === 'function') loadBehavioral();
|
|
343
|
+
break;
|
|
344
|
+
case 'compliance-pane':
|
|
345
|
+
if (typeof loadCompliance === 'function') loadCompliance();
|
|
346
|
+
break;
|
|
347
|
+
case 'math-health-pane':
|
|
348
|
+
if (typeof loadMathHealth === 'function') loadMathHealth();
|
|
349
|
+
break;
|
|
350
|
+
case 'ide-pane':
|
|
351
|
+
if (typeof loadIDEStatus === 'function') loadIDEStatus();
|
|
352
|
+
break;
|
|
353
|
+
case 'health-pane':
|
|
354
|
+
if (typeof loadHealthMonitor === 'function') loadHealthMonitor();
|
|
355
|
+
break;
|
|
356
|
+
case 'ingestion-pane':
|
|
357
|
+
if (typeof loadIngestionStatus === 'function') loadIngestionStatus();
|
|
358
|
+
break;
|
|
359
|
+
case 'entities-pane':
|
|
360
|
+
if (typeof loadEntityExplorer === 'function') loadEntityExplorer();
|
|
361
|
+
break;
|
|
362
|
+
case 'mesh-pane':
|
|
363
|
+
if (typeof loadMeshPeers === 'function') loadMeshPeers();
|
|
364
|
+
break;
|
|
365
|
+
case 'settings-pane':
|
|
366
|
+
if (typeof loadSettings === 'function') loadSettings();
|
|
367
|
+
if (typeof loadModeSettings === 'function') loadModeSettings();
|
|
368
|
+
if (typeof loadAutoSettings === 'function') loadAutoSettings();
|
|
369
|
+
if (typeof updateModeUI === 'function') updateModeUI();
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── Hash-based Routing ─────────────────────────────────────
|
|
375
|
+
function handleHash() {
|
|
376
|
+
var hash = window.location.hash.replace('#', '');
|
|
377
|
+
if (hash && document.getElementById(hash)) {
|
|
378
|
+
activateTab(hash);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Theme Application ───────────────────────────────────────
|
|
383
|
+
function applyNgTheme(theme) {
|
|
384
|
+
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
385
|
+
// ng-dark class ONLY in dark mode — light mode uses Bootstrap defaults
|
|
386
|
+
if (theme === 'dark') {
|
|
387
|
+
document.body.classList.add('ng-dark');
|
|
388
|
+
} else {
|
|
389
|
+
document.body.classList.remove('ng-dark');
|
|
390
|
+
}
|
|
391
|
+
// Update both icons (original + sidebar)
|
|
392
|
+
var icon = document.getElementById('theme-icon');
|
|
393
|
+
if (icon) icon.className = theme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
|
|
394
|
+
var ngIcon = document.getElementById('ng-theme-icon');
|
|
395
|
+
if (ngIcon) ngIcon.className = theme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
|
|
396
|
+
|
|
397
|
+
// Fix inline styles that conflict with dark/light mode (setProperty needed to override inline styles)
|
|
398
|
+
var themedElements = ['graph-container', 'memory-timeline-chart'];
|
|
399
|
+
themedElements.forEach(function(id) {
|
|
400
|
+
var el = document.getElementById(id);
|
|
401
|
+
if (!el) return;
|
|
402
|
+
if (theme === 'dark') {
|
|
403
|
+
el.style.setProperty('background', '#0f1012', 'important');
|
|
404
|
+
el.style.setProperty('background-color', '#0f1012', 'important');
|
|
405
|
+
el.style.setProperty('border-color', 'rgba(255,255,255,0.06)', 'important');
|
|
406
|
+
} else {
|
|
407
|
+
el.style.setProperty('background', '#ffffff', 'important');
|
|
408
|
+
el.style.setProperty('background-color', '#ffffff', 'important');
|
|
409
|
+
el.style.setProperty('border-color', '#e5e7eb', 'important');
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Also fix graph label colors for readability
|
|
414
|
+
var graphLabels = document.querySelectorAll('.node-label');
|
|
415
|
+
graphLabels.forEach(function(l) {
|
|
416
|
+
l.style.fill = theme === 'dark' ? '#ccc' : '#333';
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Initialize ─────────────────────────────────────────────
|
|
421
|
+
function init() {
|
|
422
|
+
// Respect saved theme or auto-detect
|
|
423
|
+
var savedTheme = localStorage.getItem('slm-theme');
|
|
424
|
+
if (!savedTheme) {
|
|
425
|
+
savedTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
426
|
+
}
|
|
427
|
+
applyNgTheme(savedTheme);
|
|
428
|
+
|
|
429
|
+
// Override toggleDarkMode — syncs Bootstrap theme + ng-dark class
|
|
430
|
+
window.toggleDarkMode = function() {
|
|
431
|
+
var current = document.documentElement.getAttribute('data-bs-theme');
|
|
432
|
+
var next = current === 'dark' ? 'light' : 'dark';
|
|
433
|
+
localStorage.setItem('slm-theme', next);
|
|
434
|
+
applyNgTheme(next);
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// Restructure DOM
|
|
438
|
+
restructureDOM();
|
|
439
|
+
|
|
440
|
+
// Handle initial hash — run synchronously, no timeout race
|
|
441
|
+
if (window.location.hash) {
|
|
442
|
+
handleHash();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Listen for hash changes
|
|
446
|
+
window.addEventListener('hashchange', handleHash);
|
|
447
|
+
|
|
448
|
+
// toggleDarkMode already overridden above with theme toggle support
|
|
449
|
+
|
|
450
|
+
// Privacy blur toggle for screen recording
|
|
451
|
+
window.togglePrivacyBlur = function() {
|
|
452
|
+
document.body.classList.toggle('ng-privacy-blur');
|
|
453
|
+
var icon = document.getElementById('ng-privacy-icon');
|
|
454
|
+
var isBlurred = document.body.classList.contains('ng-privacy-blur');
|
|
455
|
+
if (icon) icon.className = isBlurred ? 'bi bi-eye' : 'bi bi-eye-slash';
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// Auto-enable blur if URL has ?blur=1
|
|
459
|
+
if (window.location.search.indexOf('blur=1') >= 0) {
|
|
460
|
+
document.body.classList.add('ng-privacy-blur');
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Run when DOM is ready
|
|
465
|
+
if (document.readyState === 'loading') {
|
|
466
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
467
|
+
} else {
|
|
468
|
+
init();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
})();
|