@omiron33/omi-neuron-web 0.1.0 → 0.1.2

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,1194 @@
1
+ import { useRef, useState, useMemo, useEffect } from 'react';
2
+ import * as THREE from 'three';
3
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4
+ import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ // src/visualization/constants.ts
8
+ var DEFAULT_THEME = {
9
+ colors: {
10
+ background: "#020314",
11
+ domainColors: {},
12
+ defaultDomainColor: "#c0c5ff",
13
+ edgeDefault: "#4d4d55",
14
+ edgeActive: "#c6d4ff",
15
+ edgeSelected: "#ffffff",
16
+ labelText: "#ffffff",
17
+ labelBackground: "rgba(0, 0, 0, 0.8)"
18
+ },
19
+ typography: {
20
+ labelFontFamily: "system-ui, sans-serif",
21
+ labelFontSize: 12,
22
+ labelFontWeight: "500"
23
+ },
24
+ effects: {
25
+ starfieldEnabled: true,
26
+ starfieldColor: "#ffffff",
27
+ glowEnabled: true,
28
+ glowIntensity: 0.6,
29
+ ambientMotionEnabled: true,
30
+ ambientMotionSpeed: 0.6,
31
+ ambientMotionAmplitude: 0.25,
32
+ edgeFlowEnabled: true,
33
+ edgeFlowSpeed: 1.2,
34
+ fogEnabled: true,
35
+ fogColor: "#020314",
36
+ fogNear: 32,
37
+ fogFar: 180
38
+ },
39
+ animation: {
40
+ focusDuration: 800,
41
+ transitionDuration: 650,
42
+ easing: "easeInOut",
43
+ hoverScale: 1.12,
44
+ selectedScale: 1.4,
45
+ selectionPulseScale: 0.35,
46
+ selectionPulseDuration: 520,
47
+ hoverCardFadeDuration: 160
48
+ }
49
+ };
50
+ var SceneManager = class {
51
+ constructor(container, config) {
52
+ this.container = container;
53
+ this.config = config;
54
+ this.scene = new THREE.Scene();
55
+ this.camera = new THREE.PerspectiveCamera(60, 1, 0.1, 2e3);
56
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
57
+ this.labelRenderer = new CSS2DRenderer();
58
+ this.controls = new OrbitControls(this.camera, this.renderer.domElement);
59
+ }
60
+ scene;
61
+ camera;
62
+ renderer;
63
+ labelRenderer;
64
+ controls;
65
+ animationId = null;
66
+ lastFrameTime = 0;
67
+ elapsedTime = 0;
68
+ frameListeners = /* @__PURE__ */ new Set();
69
+ starfield = null;
70
+ ambientLight = null;
71
+ keyLight = null;
72
+ fillLight = null;
73
+ initialize() {
74
+ const { cameraPosition, cameraTarget, backgroundColor } = this.config;
75
+ this.scene.background = new THREE.Color(backgroundColor);
76
+ if (this.config.fogEnabled) {
77
+ const fogColor = this.config.fogColor ?? backgroundColor;
78
+ this.scene.fog = new THREE.Fog(fogColor, this.config.fogNear ?? 32, this.config.fogFar ?? 200);
79
+ }
80
+ this.camera.position.set(...cameraPosition);
81
+ this.controls.target.set(...cameraTarget);
82
+ this.controls.minDistance = this.config.minZoom;
83
+ this.controls.maxDistance = this.config.maxZoom;
84
+ this.controls.update();
85
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, this.config.pixelRatioCap));
86
+ this.renderer.outputColorSpace = THREE.SRGBColorSpace;
87
+ this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
88
+ this.renderer.toneMappingExposure = 1.05;
89
+ this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
90
+ this.labelRenderer.setSize(this.container.clientWidth, this.container.clientHeight);
91
+ this.labelRenderer.domElement.style.position = "absolute";
92
+ this.labelRenderer.domElement.style.top = "0";
93
+ this.labelRenderer.domElement.style.left = "0";
94
+ this.labelRenderer.domElement.style.pointerEvents = "none";
95
+ this.labelRenderer.domElement.style.width = "100%";
96
+ this.labelRenderer.domElement.style.height = "100%";
97
+ this.container.appendChild(this.renderer.domElement);
98
+ this.container.appendChild(this.labelRenderer.domElement);
99
+ this.initLights();
100
+ if (this.config.enableStarfield) {
101
+ this.initStarfield();
102
+ }
103
+ window.addEventListener("resize", this.resize);
104
+ this.startAnimationLoop();
105
+ }
106
+ dispose() {
107
+ this.stopAnimationLoop();
108
+ window.removeEventListener("resize", this.resize);
109
+ this.renderer.dispose();
110
+ this.scene.clear();
111
+ this.container.innerHTML = "";
112
+ this.frameListeners.clear();
113
+ this.starfield = null;
114
+ this.ambientLight = null;
115
+ this.keyLight = null;
116
+ this.fillLight = null;
117
+ }
118
+ startAnimationLoop() {
119
+ const loop = (time = performance.now()) => {
120
+ if (!this.lastFrameTime) {
121
+ this.lastFrameTime = time;
122
+ }
123
+ const delta = (time - this.lastFrameTime) / 1e3;
124
+ this.lastFrameTime = time;
125
+ this.elapsedTime += delta;
126
+ this.frameListeners.forEach((listener) => listener(delta, this.elapsedTime));
127
+ this.render();
128
+ this.animationId = requestAnimationFrame(loop);
129
+ };
130
+ loop();
131
+ }
132
+ stopAnimationLoop() {
133
+ if (this.animationId) {
134
+ cancelAnimationFrame(this.animationId);
135
+ this.animationId = null;
136
+ }
137
+ }
138
+ render() {
139
+ this.controls.update();
140
+ this.renderer.render(this.scene, this.camera);
141
+ this.labelRenderer.render(this.scene, this.camera);
142
+ }
143
+ resize = () => {
144
+ const width = this.container.clientWidth;
145
+ const height = this.container.clientHeight;
146
+ this.camera.aspect = width / height;
147
+ this.camera.updateProjectionMatrix();
148
+ this.renderer.setSize(width, height);
149
+ this.labelRenderer.setSize(width, height);
150
+ };
151
+ updateBackground(color) {
152
+ this.scene.background = new THREE.Color(color);
153
+ if (this.scene.fog && this.config.fogEnabled) {
154
+ this.scene.fog.color = new THREE.Color(this.config.fogColor ?? color);
155
+ }
156
+ if (this.starfield) {
157
+ this.starfield.material.color = new THREE.Color(
158
+ this.config.starfieldColor
159
+ );
160
+ }
161
+ }
162
+ updateCamera(position) {
163
+ this.camera.position.set(...position);
164
+ }
165
+ addFrameListener(listener) {
166
+ this.frameListeners.add(listener);
167
+ return () => this.frameListeners.delete(listener);
168
+ }
169
+ getWorldPosition(screenX, screenY) {
170
+ return this.screenToWorld(screenX, screenY);
171
+ }
172
+ screenToWorld(x, y) {
173
+ const rect = this.container.getBoundingClientRect();
174
+ const ndc = new THREE.Vector3(
175
+ (x - rect.left) / rect.width * 2 - 1,
176
+ -((y - rect.top) / rect.height) * 2 + 1,
177
+ 0.5
178
+ );
179
+ ndc.unproject(this.camera);
180
+ return ndc;
181
+ }
182
+ worldToScreen(position) {
183
+ const vector = position.clone().project(this.camera);
184
+ const rect = this.container.getBoundingClientRect();
185
+ return {
186
+ x: (vector.x * 0.5 + 0.5) * rect.width + rect.left,
187
+ y: (-vector.y * 0.5 + 0.5) * rect.height + rect.top
188
+ };
189
+ }
190
+ onContextLost = () => {
191
+ };
192
+ onContextRestored = () => {
193
+ };
194
+ initStarfield() {
195
+ const count = Math.max(0, this.config.starfieldCount);
196
+ if (!count) return;
197
+ const geometry = new THREE.BufferGeometry();
198
+ const positions = new Float32Array(count * 3);
199
+ for (let i = 0; i < count; i += 1) {
200
+ const radius = 180 + Math.random() * 120;
201
+ const theta = Math.random() * Math.PI * 2;
202
+ const phi = Math.acos(2 * Math.random() - 1);
203
+ positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
204
+ positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
205
+ positions[i * 3 + 2] = radius * Math.cos(phi);
206
+ }
207
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
208
+ const material = new THREE.PointsMaterial({
209
+ color: this.config.starfieldColor,
210
+ size: 0.6,
211
+ sizeAttenuation: true,
212
+ transparent: true,
213
+ opacity: 0.45,
214
+ depthWrite: false
215
+ });
216
+ this.starfield = new THREE.Points(geometry, material);
217
+ this.starfield.name = "neuron-starfield";
218
+ this.scene.add(this.starfield);
219
+ }
220
+ initLights() {
221
+ const ambientIntensity = this.config.ambientLightIntensity ?? 0.65;
222
+ const keyIntensity = this.config.keyLightIntensity ?? 1;
223
+ const fillIntensity = this.config.fillLightIntensity ?? 0.45;
224
+ this.ambientLight = new THREE.AmbientLight("#ffffff", ambientIntensity);
225
+ this.keyLight = new THREE.DirectionalLight("#b6c6ff", keyIntensity);
226
+ this.keyLight.position.set(12, 18, 16);
227
+ this.fillLight = new THREE.PointLight("#5c7aff", fillIntensity, 120, 2.2);
228
+ this.fillLight.position.set(-18, -6, 10);
229
+ this.scene.add(this.ambientLight, this.keyLight, this.fillLight);
230
+ }
231
+ };
232
+
233
+ // src/visualization/hooks/useSceneManager.ts
234
+ function useSceneManager(containerRef, config) {
235
+ const [manager, setManager] = useState(null);
236
+ useEffect(() => {
237
+ const container = containerRef.current;
238
+ if (!container) return;
239
+ const sceneManager = new SceneManager(container, config);
240
+ sceneManager.initialize();
241
+ setManager(sceneManager);
242
+ return () => {
243
+ sceneManager.dispose();
244
+ setManager(null);
245
+ };
246
+ }, [containerRef, config.backgroundColor]);
247
+ return manager;
248
+ }
249
+ var NodeRenderer = class {
250
+ constructor(scene, config) {
251
+ this.scene = scene;
252
+ this.config = config;
253
+ this.scene.add(this.group);
254
+ }
255
+ group = new THREE.Group();
256
+ nodeStates = /* @__PURE__ */ new Map();
257
+ hoveredNodeId = null;
258
+ selectedNodeId = null;
259
+ renderNodes(nodes) {
260
+ this.clear();
261
+ nodes.forEach((node) => {
262
+ const color = new THREE.Color(
263
+ this.config.domainColors[node.domain] ?? this.config.defaultColor
264
+ );
265
+ const geometry = new THREE.SphereGeometry(this.config.baseScale, 18, 18);
266
+ const material = new THREE.MeshStandardMaterial({
267
+ color,
268
+ roughness: 0.45,
269
+ metalness: 0.1,
270
+ emissive: color.clone().multiplyScalar(this.config.glowIntensity * 0.4),
271
+ emissiveIntensity: 1
272
+ });
273
+ const mesh = new THREE.Mesh(geometry, material);
274
+ const position = new THREE.Vector3();
275
+ if (node.position) {
276
+ position.set(...node.position);
277
+ }
278
+ mesh.position.copy(position);
279
+ const tierScale = node.tier ? this.config.tierScales[node.tier] ?? 1 : 1;
280
+ const baseScale = this.config.baseScale * tierScale;
281
+ mesh.scale.setScalar(baseScale);
282
+ mesh.userData = { nodeId: node.id, nodeSlug: node.slug };
283
+ this.group.add(mesh);
284
+ this.nodeStates.set(node.id, {
285
+ mesh,
286
+ basePosition: position.clone(),
287
+ baseScale,
288
+ phase: Math.random() * Math.PI * 2,
289
+ baseColor: color,
290
+ hovered: false,
291
+ selected: false,
292
+ pulseStart: null
293
+ });
294
+ });
295
+ }
296
+ updateNode(nodeId, updates) {
297
+ const state = this.nodeStates.get(nodeId);
298
+ if (!state) return;
299
+ if (updates.position) {
300
+ state.basePosition.set(...updates.position);
301
+ state.mesh.position.set(...updates.position);
302
+ }
303
+ if (updates.tier) {
304
+ const tierScale = this.config.tierScales[updates.tier] ?? 1;
305
+ state.baseScale = this.config.baseScale * tierScale;
306
+ }
307
+ if (updates.domain) {
308
+ const color = new THREE.Color(
309
+ this.config.domainColors[updates.domain] ?? this.config.defaultColor
310
+ );
311
+ state.baseColor = color;
312
+ const material = state.mesh.material;
313
+ material.color = color;
314
+ material.emissive = color.clone().multiplyScalar(this.config.glowIntensity * 0.4);
315
+ }
316
+ }
317
+ removeNode(nodeId) {
318
+ const state = this.nodeStates.get(nodeId);
319
+ if (!state) return;
320
+ this.group.remove(state.mesh);
321
+ this.nodeStates.delete(nodeId);
322
+ }
323
+ clear() {
324
+ this.group.clear();
325
+ this.nodeStates.clear();
326
+ this.hoveredNodeId = null;
327
+ this.selectedNodeId = null;
328
+ }
329
+ showNodes(nodeIds) {
330
+ nodeIds.forEach((id) => {
331
+ const obj = this.nodeObjects.get(id);
332
+ if (obj) obj.visible = true;
333
+ });
334
+ }
335
+ hideNodes(nodeIds) {
336
+ nodeIds.forEach((id) => {
337
+ const obj = this.nodeObjects.get(id);
338
+ if (obj) obj.visible = false;
339
+ });
340
+ }
341
+ setVisibleNodes(nodeIds) {
342
+ if (!nodeIds) {
343
+ this.nodeStates.forEach((state) => {
344
+ state.mesh.visible = true;
345
+ });
346
+ return;
347
+ }
348
+ const visibleSet = new Set(nodeIds);
349
+ this.nodeStates.forEach((state, id) => {
350
+ state.mesh.visible = visibleSet.has(id);
351
+ });
352
+ }
353
+ updateLabelVisibility() {
354
+ }
355
+ highlightNode(nodeId) {
356
+ this.setHoveredNode(nodeId);
357
+ }
358
+ unhighlightNode(nodeId) {
359
+ if (this.hoveredNodeId === nodeId) {
360
+ this.setHoveredNode(null);
361
+ }
362
+ }
363
+ clearHighlights() {
364
+ this.setHoveredNode(null);
365
+ }
366
+ getNodePosition(nodeId) {
367
+ const state = this.nodeStates.get(nodeId);
368
+ return state ? state.mesh.position.clone() : null;
369
+ }
370
+ getNodeObject(nodeId) {
371
+ const state = this.nodeStates.get(nodeId);
372
+ return state?.mesh ?? null;
373
+ }
374
+ getNodeObjects() {
375
+ return Array.from(this.nodeStates.values()).map((state) => state.mesh);
376
+ }
377
+ setHoveredNode(nodeId) {
378
+ if (this.hoveredNodeId === nodeId) return;
379
+ if (this.hoveredNodeId) {
380
+ const prev = this.nodeStates.get(this.hoveredNodeId);
381
+ if (prev) prev.hovered = false;
382
+ }
383
+ this.hoveredNodeId = nodeId;
384
+ if (nodeId) {
385
+ const next = this.nodeStates.get(nodeId);
386
+ if (next) next.hovered = true;
387
+ }
388
+ }
389
+ setSelectedNode(nodeId) {
390
+ if (this.selectedNodeId === nodeId) return;
391
+ if (this.selectedNodeId) {
392
+ const prev = this.nodeStates.get(this.selectedNodeId);
393
+ if (prev) prev.selected = false;
394
+ }
395
+ this.selectedNodeId = nodeId;
396
+ if (nodeId) {
397
+ const next = this.nodeStates.get(nodeId);
398
+ if (next) next.selected = true;
399
+ }
400
+ }
401
+ pulseNode(nodeId) {
402
+ const state = this.nodeStates.get(nodeId);
403
+ if (!state) return;
404
+ state.pulseStart = performance.now() / 1e3;
405
+ }
406
+ update(_, elapsed) {
407
+ const now = performance.now() / 1e3;
408
+ this.nodeStates.forEach((state) => {
409
+ if (this.config.ambientMotionEnabled) {
410
+ const drift = Math.sin(elapsed * this.config.ambientMotionSpeed + state.phase) * this.config.ambientMotionAmplitude;
411
+ const driftX = Math.cos(elapsed * this.config.ambientMotionSpeed * 0.6 + state.phase) * this.config.ambientMotionAmplitude * 0.45;
412
+ const driftZ = Math.sin(elapsed * this.config.ambientMotionSpeed * 0.4 + state.phase) * this.config.ambientMotionAmplitude * 0.35;
413
+ state.mesh.position.set(
414
+ state.basePosition.x + driftX,
415
+ state.basePosition.y + drift,
416
+ state.basePosition.z + driftZ
417
+ );
418
+ } else {
419
+ state.mesh.position.copy(state.basePosition);
420
+ }
421
+ const hoverScale = state.hovered ? this.config.hoverScale : 1;
422
+ const selectedScale = state.selected ? this.config.selectedScale : 1;
423
+ let pulseScale = 0;
424
+ if (state.pulseStart !== null) {
425
+ const progress = (now - state.pulseStart) / this.config.pulseDuration;
426
+ if (progress >= 1) {
427
+ state.pulseStart = null;
428
+ } else {
429
+ pulseScale = Math.sin(progress * Math.PI) * this.config.pulseScale;
430
+ }
431
+ }
432
+ const targetScale = state.baseScale * hoverScale * selectedScale * (1 + pulseScale);
433
+ const currentScale = state.mesh.scale.x;
434
+ const nextScale = currentScale + (targetScale - currentScale) * 0.18;
435
+ state.mesh.scale.setScalar(nextScale);
436
+ const material = state.mesh.material;
437
+ const emissiveBoost = state.selected ? 0.65 : state.hovered ? 0.45 : 0.25;
438
+ material.emissive = state.baseColor.clone().multiplyScalar(this.config.glowIntensity * emissiveBoost);
439
+ });
440
+ }
441
+ dispose() {
442
+ this.clear();
443
+ this.scene.remove(this.group);
444
+ }
445
+ };
446
+ var EdgeRenderer = class {
447
+ constructor(scene, config) {
448
+ this.scene = scene;
449
+ this.config = config;
450
+ this.scene.add(this.group);
451
+ }
452
+ group = new THREE.Group();
453
+ edgeStates = /* @__PURE__ */ new Map();
454
+ focusEdges = /* @__PURE__ */ new Set();
455
+ renderEdges(edges, nodePositions) {
456
+ this.clear();
457
+ edges.forEach((edge) => {
458
+ const from = nodePositions.get(edge.from);
459
+ const to = nodePositions.get(edge.to);
460
+ if (!from || !to) return;
461
+ const geometry = new THREE.BufferGeometry().setFromPoints([from, to]);
462
+ const opacity = this.config.strengthOpacityScale ? edge.strength : this.config.baseOpacity;
463
+ const material = new THREE.LineBasicMaterial({
464
+ color: this.config.defaultColor,
465
+ transparent: true,
466
+ opacity,
467
+ depthWrite: false
468
+ });
469
+ const line = new THREE.Line(geometry, material);
470
+ line.userData = { edgeId: edge.id };
471
+ this.group.add(line);
472
+ this.edgeStates.set(edge.id, {
473
+ line,
474
+ baseOpacity: opacity,
475
+ phase: Math.random() * Math.PI * 2
476
+ });
477
+ });
478
+ }
479
+ updateEdge(edgeId, updates) {
480
+ const state = this.edgeStates.get(edgeId);
481
+ if (!state) return;
482
+ if (updates.strength !== void 0) {
483
+ const opacity = this.config.strengthOpacityScale ? updates.strength : this.config.baseOpacity;
484
+ state.baseOpacity = opacity;
485
+ state.line.material.opacity = opacity;
486
+ }
487
+ }
488
+ removeEdge(edgeId) {
489
+ const state = this.edgeStates.get(edgeId);
490
+ if (!state) return;
491
+ this.group.remove(state.line);
492
+ this.edgeStates.delete(edgeId);
493
+ }
494
+ clear() {
495
+ this.group.clear();
496
+ this.edgeStates.clear();
497
+ this.focusEdges.clear();
498
+ }
499
+ filterByStrength(minStrength) {
500
+ this.edgeStates.forEach((state) => {
501
+ state.line.visible = state.line.material.opacity >= minStrength;
502
+ });
503
+ }
504
+ filterByType() {
505
+ }
506
+ highlightEdge(edgeId) {
507
+ this.setFocusEdges([edgeId]);
508
+ }
509
+ highlightEdgesForNode() {
510
+ }
511
+ unhighlightEdge(edgeId) {
512
+ if (this.focusEdges.has(edgeId)) {
513
+ this.focusEdges.delete(edgeId);
514
+ }
515
+ }
516
+ clearHighlights() {
517
+ this.focusEdges.clear();
518
+ }
519
+ showEdges(edgeIds) {
520
+ edgeIds.forEach((id) => {
521
+ const state = this.edgeStates.get(id);
522
+ if (state) state.line.visible = true;
523
+ });
524
+ }
525
+ hideEdges(edgeIds) {
526
+ edgeIds.forEach((id) => {
527
+ const state = this.edgeStates.get(id);
528
+ if (state) state.line.visible = false;
529
+ });
530
+ }
531
+ setFocusEdges(edgeIds) {
532
+ this.focusEdges = new Set(edgeIds ?? []);
533
+ }
534
+ update(_, elapsed) {
535
+ const hasFocus = this.focusEdges.size > 0;
536
+ this.edgeStates.forEach((state, id) => {
537
+ const material = state.line.material;
538
+ const isFocused = this.focusEdges.has(id);
539
+ let opacity = state.baseOpacity;
540
+ if (hasFocus) {
541
+ if (isFocused) {
542
+ const pulse = this.config.edgeFlowEnabled ? Math.sin(elapsed * this.config.edgeFlowSpeed + state.phase) * 0.25 + 0.75 : 1;
543
+ opacity = Math.min(1, state.baseOpacity + pulse * 0.4);
544
+ material.color = new THREE.Color(this.config.activeColor);
545
+ } else {
546
+ opacity = Math.max(0.05, state.baseOpacity * 0.2);
547
+ material.color = new THREE.Color(this.config.defaultColor);
548
+ }
549
+ } else {
550
+ if (this.config.edgeFlowEnabled) {
551
+ const pulse = Math.sin(elapsed * this.config.edgeFlowSpeed + state.phase) * 0.15 + 0.85;
552
+ opacity = Math.min(1, state.baseOpacity * pulse);
553
+ }
554
+ material.color = new THREE.Color(this.config.defaultColor);
555
+ }
556
+ material.opacity = opacity;
557
+ });
558
+ }
559
+ dispose() {
560
+ this.clear();
561
+ this.scene.remove(this.group);
562
+ }
563
+ };
564
+
565
+ // src/visualization/layouts/fuzzy-layout.ts
566
+ var GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
567
+ function hashString(input) {
568
+ let hash = 2166136261;
569
+ for (let i = 0; i < input.length; i += 1) {
570
+ hash ^= input.charCodeAt(i);
571
+ hash = Math.imul(hash, 16777619);
572
+ }
573
+ return hash >>> 0;
574
+ }
575
+ function mulberry32(seed) {
576
+ let t = seed;
577
+ return () => {
578
+ t += 1831565813;
579
+ let r = Math.imul(t ^ t >>> 15, t | 1);
580
+ r ^= r + Math.imul(r ^ r >>> 7, r | 61);
581
+ return ((r ^ r >>> 14) >>> 0) / 4294967296;
582
+ };
583
+ }
584
+ function buildSeed(baseSeed, nodeKey) {
585
+ return mulberry32(hashString(`${baseSeed}:${nodeKey}`));
586
+ }
587
+ function applyFuzzyLayout(nodes, options = {}) {
588
+ const mode = options.mode ?? "auto";
589
+ if (mode === "positioned") {
590
+ return nodes;
591
+ }
592
+ const needsLayout = mode === "fuzzy" || nodes.some((node) => !node.position);
593
+ if (!needsLayout) {
594
+ return nodes;
595
+ }
596
+ const baseSeed = options.seed ?? "omi-neuron-web";
597
+ const count = Math.max(nodes.length, 1);
598
+ const spread = options.spread ?? 1.2;
599
+ const baseRadius = (options.radius ?? Math.max(4, Math.sqrt(count) * 2.4)) * spread;
600
+ const jitter = (options.jitter ?? baseRadius * 0.12) * spread;
601
+ const zSpread = (options.zSpread ?? baseRadius * 0.6) * spread;
602
+ return nodes.map((node, index) => {
603
+ const shouldApply = mode === "fuzzy" || !node.position;
604
+ if (!shouldApply) {
605
+ return node;
606
+ }
607
+ const nodeKey = node.slug || node.id || String(index);
608
+ const rand = buildSeed(baseSeed, nodeKey);
609
+ const angle = rand() * Math.PI * 2 + index * GOLDEN_ANGLE * 0.05;
610
+ const radius = baseRadius * Math.sqrt(rand());
611
+ const jitterOffset = (rand() - 0.5) * jitter;
612
+ const r = Math.max(0.6, radius + jitterOffset);
613
+ const x = Math.cos(angle) * r;
614
+ const y = Math.sin(angle) * r;
615
+ const z = (rand() - 0.5) * zSpread;
616
+ return { ...node, position: [x, y, z] };
617
+ });
618
+ }
619
+ var InteractionManager = class {
620
+ constructor(scene, camera, renderer, config) {
621
+ this.scene = scene;
622
+ this.camera = camera;
623
+ this.renderer = renderer;
624
+ this.config = config;
625
+ }
626
+ raycaster = new THREE.Raycaster();
627
+ pointer = new THREE.Vector2();
628
+ nodeObjects = [];
629
+ edgeObjects = [];
630
+ nodeLookup = /* @__PURE__ */ new Map();
631
+ edgeLookup = /* @__PURE__ */ new Map();
632
+ hoverTimeout = null;
633
+ lastHoverId = null;
634
+ lastClickTime = 0;
635
+ lastClickId = null;
636
+ onNodeHover = () => {
637
+ };
638
+ onNodeClick = () => {
639
+ };
640
+ onNodeDoubleClick = () => {
641
+ };
642
+ onEdgeClick = () => {
643
+ };
644
+ onBackgroundClick = () => {
645
+ };
646
+ onPointerMove(event) {
647
+ if (!this.config.enableHover) return;
648
+ this.updatePointer(event);
649
+ const node = this.getIntersectedNode(this.pointer);
650
+ if (this.hoverTimeout) {
651
+ window.clearTimeout(this.hoverTimeout);
652
+ }
653
+ this.hoverTimeout = window.setTimeout(() => {
654
+ if (node?.id !== this.lastHoverId) {
655
+ this.lastHoverId = node?.id ?? null;
656
+ this.onNodeHover(node);
657
+ }
658
+ }, this.config.hoverDelay);
659
+ }
660
+ onPointerDown() {
661
+ }
662
+ onPointerUp(event) {
663
+ if (!this.config.enableClick) return;
664
+ this.updatePointer(event);
665
+ const node = this.getIntersectedNode(this.pointer);
666
+ if (node) {
667
+ const now = performance.now();
668
+ const isDouble = this.config.enableDoubleClick && now - this.lastClickTime < this.config.doubleClickDelay && this.lastClickId === node.id;
669
+ this.lastClickTime = now;
670
+ this.lastClickId = node.id;
671
+ if (isDouble) {
672
+ this.onNodeDoubleClick(node);
673
+ } else {
674
+ this.onNodeClick(node);
675
+ }
676
+ } else {
677
+ this.onBackgroundClick();
678
+ }
679
+ }
680
+ onKeyDown() {
681
+ }
682
+ onPointerLeave() {
683
+ if (!this.config.enableHover) return;
684
+ if (this.hoverTimeout) {
685
+ window.clearTimeout(this.hoverTimeout);
686
+ }
687
+ this.hoverTimeout = window.setTimeout(() => {
688
+ if (this.lastHoverId !== null) {
689
+ this.lastHoverId = null;
690
+ this.onNodeHover(null);
691
+ }
692
+ }, Math.max(0, this.config.hoverDelay));
693
+ }
694
+ getIntersectedNode(point) {
695
+ this.raycaster.setFromCamera(point, this.camera);
696
+ const intersects = this.raycaster.intersectObjects(this.nodeObjects, true);
697
+ if (!intersects.length) return null;
698
+ const hit = intersects[0].object;
699
+ if (!hit.userData?.nodeId) return null;
700
+ return this.nodeLookup.get(hit.userData.nodeId) ?? null;
701
+ }
702
+ getIntersectedEdge() {
703
+ return null;
704
+ }
705
+ dispose() {
706
+ if (this.hoverTimeout) {
707
+ window.clearTimeout(this.hoverTimeout);
708
+ this.hoverTimeout = null;
709
+ }
710
+ }
711
+ setTargets(nodeObjects, nodeLookup, edgeObjects, edgeLookup) {
712
+ this.nodeObjects = nodeObjects;
713
+ this.nodeLookup = nodeLookup;
714
+ this.edgeObjects = edgeObjects ?? [];
715
+ this.edgeLookup = edgeLookup ?? /* @__PURE__ */ new Map();
716
+ }
717
+ updatePointer(event) {
718
+ const rect = this.renderer.domElement.getBoundingClientRect();
719
+ this.pointer.x = (event.clientX - rect.left) / rect.width * 2 - 1;
720
+ this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
721
+ }
722
+ };
723
+
724
+ // src/visualization/animations/animation-controller.ts
725
+ var AnimationController = class {
726
+ constructor(camera, controls, config) {
727
+ this.camera = camera;
728
+ this.controls = controls;
729
+ this.config = config;
730
+ }
731
+ focusTween = null;
732
+ focusOnNode(position, callback) {
733
+ const direction = this.camera.position.clone().sub(this.controls.target).normalize();
734
+ const distance = this.camera.position.distanceTo(this.controls.target);
735
+ const targetPosition = position.clone().add(direction.multiplyScalar(distance));
736
+ this.focusOnPosition(targetPosition, position, callback);
737
+ }
738
+ focusOnPosition(position, target, callback) {
739
+ const now = performance.now();
740
+ this.focusTween = {
741
+ startTime: now,
742
+ duration: this.config.focusDuration,
743
+ startPosition: this.camera.position.clone(),
744
+ startTarget: this.controls.target.clone(),
745
+ endPosition: position.clone(),
746
+ endTarget: target.clone(),
747
+ onComplete: callback
748
+ };
749
+ }
750
+ resetCamera() {
751
+ this.controls.reset();
752
+ }
753
+ animateNodeAppear() {
754
+ }
755
+ animateNodeDisappear() {
756
+ }
757
+ animateNodesFilter() {
758
+ }
759
+ animatePath() {
760
+ }
761
+ stopPathAnimation() {
762
+ }
763
+ pause() {
764
+ }
765
+ resume() {
766
+ }
767
+ update() {
768
+ if (!this.focusTween) return;
769
+ const now = performance.now();
770
+ const elapsed = Math.min(1, (now - this.focusTween.startTime) / this.focusTween.duration);
771
+ const eased = this.applyEasing(elapsed);
772
+ this.camera.position.lerpVectors(
773
+ this.focusTween.startPosition,
774
+ this.focusTween.endPosition,
775
+ eased
776
+ );
777
+ this.controls.target.lerpVectors(
778
+ this.focusTween.startTarget,
779
+ this.focusTween.endTarget,
780
+ eased
781
+ );
782
+ this.controls.update();
783
+ if (elapsed >= 1) {
784
+ const completion = this.focusTween.onComplete;
785
+ this.focusTween = null;
786
+ if (completion) completion();
787
+ }
788
+ }
789
+ dispose() {
790
+ }
791
+ applyEasing(value) {
792
+ if (this.config.easing === "linear") return value;
793
+ if (this.config.easing === "easeOut") return 1 - Math.pow(1 - value, 2);
794
+ return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 2) / 2;
795
+ }
796
+ };
797
+ function NeuronWeb({
798
+ graphData,
799
+ className,
800
+ style,
801
+ isLoading,
802
+ error,
803
+ renderEmptyState,
804
+ renderLoadingState,
805
+ ariaLabel,
806
+ theme,
807
+ layout,
808
+ renderNodeHover,
809
+ hoverCard,
810
+ onNodeHover,
811
+ onNodeClick,
812
+ onNodeDoubleClick,
813
+ onNodeFocused,
814
+ onBackgroundClick,
815
+ performanceMode
816
+ }) {
817
+ const containerRef = useRef(null);
818
+ const hoverCardRef = useRef(null);
819
+ const [hoveredNodeId, setHoveredNodeId] = useState(null);
820
+ const [selectedNodeId, setSelectedNodeId] = useState(null);
821
+ const resolvedTheme = useMemo(
822
+ () => ({
823
+ ...DEFAULT_THEME,
824
+ colors: { ...DEFAULT_THEME.colors, ...theme?.colors ?? {} },
825
+ typography: { ...DEFAULT_THEME.typography, ...theme?.typography ?? {} },
826
+ effects: { ...DEFAULT_THEME.effects, ...theme?.effects ?? {} },
827
+ animation: { ...DEFAULT_THEME.animation, ...theme?.animation ?? {} }
828
+ }),
829
+ [theme]
830
+ );
831
+ const resolvedPerformanceMode = useMemo(() => {
832
+ if (performanceMode && performanceMode !== "auto") return performanceMode;
833
+ const count = graphData.nodes.length;
834
+ if (count > 360) return "fallback";
835
+ if (count > 180) return "degraded";
836
+ return "normal";
837
+ }, [performanceMode, graphData.nodes.length]);
838
+ const sceneManager = useSceneManager(containerRef, {
839
+ backgroundColor: resolvedTheme.colors.background,
840
+ cameraPosition: [4, 8, 20],
841
+ cameraTarget: [0, 0, 0],
842
+ minZoom: 4,
843
+ maxZoom: 42,
844
+ enableStarfield: resolvedTheme.effects.starfieldEnabled,
845
+ starfieldCount: 1200,
846
+ starfieldColor: resolvedTheme.effects.starfieldColor,
847
+ pixelRatioCap: 2,
848
+ ambientLightIntensity: 0.7,
849
+ keyLightIntensity: 1.1,
850
+ fillLightIntensity: 0.6,
851
+ fogEnabled: resolvedTheme.effects.fogEnabled,
852
+ fogColor: resolvedTheme.effects.fogColor,
853
+ fogNear: resolvedTheme.effects.fogNear,
854
+ fogFar: resolvedTheme.effects.fogFar
855
+ });
856
+ const nodeRenderer = useMemo(() => {
857
+ if (!sceneManager) return null;
858
+ return new NodeRenderer(sceneManager.scene, {
859
+ domainColors: resolvedTheme.colors.domainColors,
860
+ defaultColor: resolvedTheme.colors.defaultDomainColor,
861
+ baseScale: 0.4,
862
+ tierScales: {
863
+ primary: 1.6,
864
+ secondary: 1.2,
865
+ tertiary: 1,
866
+ insight: 1
867
+ },
868
+ glowIntensity: resolvedTheme.effects.glowEnabled ? resolvedTheme.effects.glowIntensity : 0,
869
+ labelDistance: 20,
870
+ maxVisibleLabels: 50,
871
+ ambientMotionEnabled: resolvedTheme.effects.ambientMotionEnabled && resolvedPerformanceMode === "normal",
872
+ ambientMotionAmplitude: resolvedTheme.effects.ambientMotionAmplitude,
873
+ ambientMotionSpeed: resolvedTheme.effects.ambientMotionSpeed,
874
+ hoverScale: resolvedTheme.animation.hoverScale,
875
+ selectedScale: resolvedTheme.animation.selectedScale,
876
+ pulseScale: resolvedTheme.animation.selectionPulseScale,
877
+ pulseDuration: resolvedTheme.animation.selectionPulseDuration / 1e3
878
+ });
879
+ }, [sceneManager, resolvedTheme, resolvedPerformanceMode]);
880
+ const edgeRenderer = useMemo(() => {
881
+ if (!sceneManager) return null;
882
+ return new EdgeRenderer(sceneManager.scene, {
883
+ defaultColor: resolvedTheme.colors.edgeDefault,
884
+ activeColor: resolvedTheme.colors.edgeActive,
885
+ selectedColor: resolvedTheme.colors.edgeSelected,
886
+ baseOpacity: 0.5,
887
+ strengthOpacityScale: true,
888
+ edgeFlowEnabled: resolvedTheme.effects.edgeFlowEnabled && resolvedPerformanceMode === "normal",
889
+ edgeFlowSpeed: resolvedTheme.effects.edgeFlowSpeed
890
+ });
891
+ }, [sceneManager, resolvedTheme, resolvedPerformanceMode]);
892
+ const interactionManager = useMemo(() => {
893
+ if (!sceneManager) return null;
894
+ return new InteractionManager(sceneManager.scene, sceneManager.camera, sceneManager.renderer, {
895
+ enableHover: true,
896
+ enableClick: true,
897
+ enableDoubleClick: true,
898
+ hoverDelay: Math.max(40, resolvedTheme.animation.hoverCardFadeDuration * 0.6),
899
+ doubleClickDelay: 280
900
+ });
901
+ }, [sceneManager, resolvedTheme.animation.hoverCardFadeDuration]);
902
+ const animationController = useMemo(() => {
903
+ if (!sceneManager) return null;
904
+ return new AnimationController(sceneManager.camera, sceneManager.controls, {
905
+ focusDuration: resolvedTheme.animation.focusDuration,
906
+ transitionDuration: resolvedTheme.animation.transitionDuration,
907
+ easing: resolvedTheme.animation.easing
908
+ });
909
+ }, [sceneManager, resolvedTheme]);
910
+ const resolvedNodes = useMemo(
911
+ () => applyFuzzyLayout(graphData.nodes, layout),
912
+ [graphData.nodes, layout]
913
+ );
914
+ const nodeMap = useMemo(
915
+ () => new Map(resolvedNodes.map((node) => [node.id, node])),
916
+ [resolvedNodes]
917
+ );
918
+ const nodeSlugById = useMemo(() => {
919
+ const map = /* @__PURE__ */ new Map();
920
+ resolvedNodes.forEach((node) => map.set(node.id, node.slug));
921
+ return map;
922
+ }, [resolvedNodes]);
923
+ const edgesBySlug = useMemo(() => {
924
+ const map = /* @__PURE__ */ new Map();
925
+ graphData.edges.forEach((edge) => {
926
+ const add = (slug) => {
927
+ const list = map.get(slug);
928
+ if (list) list.push(edge.id);
929
+ else map.set(slug, [edge.id]);
930
+ };
931
+ add(edge.from);
932
+ add(edge.to);
933
+ });
934
+ return map;
935
+ }, [graphData.edges]);
936
+ const hoveredNode = hoveredNodeId ? nodeMap.get(hoveredNodeId) ?? null : null;
937
+ const hoverCardEnabled = (hoverCard?.enabled ?? true) && resolvedPerformanceMode !== "fallback";
938
+ const hoverCardOffset = hoverCard?.offset ?? [18, 18];
939
+ const hoverCardWidth = hoverCard?.width ?? 240;
940
+ useEffect(() => {
941
+ if (!sceneManager || !nodeRenderer || !edgeRenderer) return;
942
+ nodeRenderer.renderNodes(resolvedNodes);
943
+ const positions = /* @__PURE__ */ new Map();
944
+ resolvedNodes.forEach((node) => {
945
+ if (!node.position) return;
946
+ positions.set(node.slug, new THREE.Vector3(...node.position));
947
+ });
948
+ edgeRenderer.renderEdges(graphData.edges, positions);
949
+ }, [resolvedNodes, graphData.edges, sceneManager, nodeRenderer, edgeRenderer]);
950
+ useEffect(() => {
951
+ if (!sceneManager) return;
952
+ sceneManager.updateBackground(resolvedTheme.colors.background);
953
+ }, [sceneManager, resolvedTheme.colors.background]);
954
+ useEffect(() => {
955
+ if (!sceneManager) return;
956
+ sceneManager.controls.enableDamping = true;
957
+ sceneManager.controls.dampingFactor = 0.08;
958
+ }, [sceneManager]);
959
+ useEffect(() => {
960
+ if (!sceneManager) return;
961
+ sceneManager.renderer.domElement.style.cursor = hoveredNodeId ? "pointer" : "grab";
962
+ }, [sceneManager, hoveredNodeId]);
963
+ useEffect(() => {
964
+ if (!sceneManager || !nodeRenderer || !edgeRenderer) return;
965
+ return sceneManager.addFrameListener((delta, elapsed) => {
966
+ nodeRenderer.update(delta, elapsed);
967
+ edgeRenderer.update(delta, elapsed);
968
+ animationController?.update();
969
+ if (hoverCardRef.current && hoveredNodeId) {
970
+ const position = nodeRenderer.getNodePosition(hoveredNodeId);
971
+ const rect = containerRef.current?.getBoundingClientRect();
972
+ if (position && rect) {
973
+ const screen = sceneManager.worldToScreen(position);
974
+ const cardWidth = hoverCardRef.current.offsetWidth;
975
+ const cardHeight = hoverCardRef.current.offsetHeight;
976
+ const rawX = screen.x - rect.left + hoverCardOffset[0];
977
+ const rawY = screen.y - rect.top + hoverCardOffset[1];
978
+ const maxX = Math.max(8, rect.width - cardWidth - 8);
979
+ const maxY = Math.max(8, rect.height - cardHeight - 8);
980
+ const x = Math.min(Math.max(rawX, 8), maxX);
981
+ const y = Math.min(Math.max(rawY, 8), maxY);
982
+ hoverCardRef.current.style.transform = `translate(${x}px, ${y}px)`;
983
+ }
984
+ }
985
+ });
986
+ }, [
987
+ sceneManager,
988
+ nodeRenderer,
989
+ edgeRenderer,
990
+ animationController,
991
+ hoveredNodeId,
992
+ hoverCardOffset
993
+ ]);
994
+ useEffect(() => {
995
+ if (!interactionManager || !nodeRenderer) return;
996
+ interactionManager.setTargets(nodeRenderer.getNodeObjects(), nodeMap);
997
+ }, [interactionManager, nodeRenderer, nodeMap]);
998
+ useEffect(() => {
999
+ if (!interactionManager || !nodeRenderer || !edgeRenderer) return;
1000
+ interactionManager.onNodeHover = (node) => {
1001
+ const nodeId = node?.id ?? null;
1002
+ setHoveredNodeId(nodeId);
1003
+ nodeRenderer.setHoveredNode(nodeId);
1004
+ if (nodeId) {
1005
+ const slug = nodeSlugById.get(nodeId);
1006
+ edgeRenderer.setFocusEdges(slug ? edgesBySlug.get(slug) ?? [] : []);
1007
+ } else {
1008
+ const selectedSlug = selectedNodeId ? nodeSlugById.get(selectedNodeId) : null;
1009
+ if (selectedSlug) {
1010
+ edgeRenderer.setFocusEdges(edgesBySlug.get(selectedSlug) ?? []);
1011
+ } else {
1012
+ edgeRenderer.setFocusEdges(null);
1013
+ }
1014
+ }
1015
+ if (onNodeHover) {
1016
+ onNodeHover(node ? node : null);
1017
+ }
1018
+ };
1019
+ interactionManager.onNodeClick = (node) => {
1020
+ setSelectedNodeId(node.id);
1021
+ nodeRenderer.setSelectedNode(node.id);
1022
+ nodeRenderer.pulseNode(node.id);
1023
+ const slug = nodeSlugById.get(node.id);
1024
+ edgeRenderer.setFocusEdges(slug ? edgesBySlug.get(slug) ?? [] : []);
1025
+ if (onNodeClick) {
1026
+ onNodeClick(node);
1027
+ }
1028
+ };
1029
+ interactionManager.onNodeDoubleClick = (node) => {
1030
+ const nodePosition = nodeRenderer.getNodePosition(node.id);
1031
+ if (nodePosition) {
1032
+ animationController?.focusOnNode(nodePosition, () => {
1033
+ if (onNodeFocused) onNodeFocused(node);
1034
+ });
1035
+ }
1036
+ if (onNodeDoubleClick) {
1037
+ onNodeDoubleClick(node);
1038
+ }
1039
+ };
1040
+ interactionManager.onBackgroundClick = () => {
1041
+ setSelectedNodeId(null);
1042
+ nodeRenderer.setSelectedNode(null);
1043
+ if (!hoveredNodeId) {
1044
+ edgeRenderer.setFocusEdges(null);
1045
+ }
1046
+ if (onBackgroundClick) onBackgroundClick();
1047
+ };
1048
+ }, [
1049
+ interactionManager,
1050
+ nodeRenderer,
1051
+ edgeRenderer,
1052
+ nodeSlugById,
1053
+ edgesBySlug,
1054
+ animationController,
1055
+ hoveredNodeId,
1056
+ selectedNodeId,
1057
+ onNodeHover,
1058
+ onNodeClick,
1059
+ onNodeDoubleClick,
1060
+ onNodeFocused,
1061
+ onBackgroundClick
1062
+ ]);
1063
+ useEffect(() => {
1064
+ if (!sceneManager || !interactionManager) return;
1065
+ const element = sceneManager.renderer.domElement;
1066
+ const handleMove = (event) => interactionManager.onPointerMove(event);
1067
+ const handleUp = (event) => interactionManager.onPointerUp(event);
1068
+ const handleLeave = () => interactionManager.onPointerLeave();
1069
+ element.addEventListener("pointermove", handleMove);
1070
+ element.addEventListener("pointerup", handleUp);
1071
+ element.addEventListener("pointerleave", handleLeave);
1072
+ return () => {
1073
+ element.removeEventListener("pointermove", handleMove);
1074
+ element.removeEventListener("pointerup", handleUp);
1075
+ element.removeEventListener("pointerleave", handleLeave);
1076
+ };
1077
+ }, [sceneManager, interactionManager]);
1078
+ if (isLoading) {
1079
+ return /* @__PURE__ */ jsx("div", { className, style, "aria-label": ariaLabel, children: renderLoadingState ? renderLoadingState() : /* @__PURE__ */ jsx("div", { children: "Loading\u2026" }) });
1080
+ }
1081
+ if (error) {
1082
+ return /* @__PURE__ */ jsx("div", { className, style, "aria-label": ariaLabel, children: renderEmptyState ? renderEmptyState() : /* @__PURE__ */ jsx("div", { children: error }) });
1083
+ }
1084
+ if (!resolvedNodes.length) {
1085
+ return /* @__PURE__ */ jsx("div", { className, style, "aria-label": ariaLabel, children: renderEmptyState ? renderEmptyState() : /* @__PURE__ */ jsx("div", { children: "No data" }) });
1086
+ }
1087
+ return /* @__PURE__ */ jsxs(
1088
+ "div",
1089
+ {
1090
+ className,
1091
+ style: { position: "relative", width: "100%", height: "100%", overflow: "hidden", ...style },
1092
+ "aria-label": ariaLabel,
1093
+ children: [
1094
+ /* @__PURE__ */ jsx("div", { ref: containerRef, style: { width: "100%", height: "100%" } }),
1095
+ hoverCardEnabled && hoveredNode && /* @__PURE__ */ jsx(
1096
+ "div",
1097
+ {
1098
+ ref: hoverCardRef,
1099
+ style: {
1100
+ position: "absolute",
1101
+ width: hoverCardWidth,
1102
+ pointerEvents: "none",
1103
+ padding: "12px 14px",
1104
+ borderRadius: 12,
1105
+ background: "linear-gradient(135deg, rgba(11, 15, 35, 0.95) 0%, rgba(20, 24, 52, 0.9) 100%)",
1106
+ border: "1px solid rgba(120, 140, 255, 0.35)",
1107
+ boxShadow: "0 18px 45px rgba(5, 10, 30, 0.55)",
1108
+ color: resolvedTheme.colors.labelText,
1109
+ fontFamily: resolvedTheme.typography.labelFontFamily,
1110
+ fontSize: 12,
1111
+ zIndex: 4,
1112
+ opacity: 0.98,
1113
+ transition: `opacity ${resolvedTheme.animation.hoverCardFadeDuration}ms ease`,
1114
+ transform: `translate(${hoverCardOffset[0]}px, ${hoverCardOffset[1]}px)`
1115
+ },
1116
+ children: renderNodeHover ? renderNodeHover(hoveredNode) : /* @__PURE__ */ jsxs("div", { children: [
1117
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 600, marginBottom: 6 }, children: hoveredNode.label }),
1118
+ /* @__PURE__ */ jsx("div", { style: { opacity: 0.75 }, children: typeof hoveredNode.metadata?.summary === "string" ? hoveredNode.metadata.summary : "Click to focus this node and explore connections." })
1119
+ ] })
1120
+ }
1121
+ )
1122
+ ]
1123
+ }
1124
+ );
1125
+ }
1126
+
1127
+ // src/visualization/themes/theme-engine.ts
1128
+ var ThemeEngine = class {
1129
+ theme;
1130
+ onThemeChange = () => {
1131
+ };
1132
+ constructor(initialTheme) {
1133
+ this.theme = this.mergeTheme(DEFAULT_THEME, initialTheme);
1134
+ }
1135
+ getTheme() {
1136
+ return this.theme;
1137
+ }
1138
+ setTheme(theme) {
1139
+ this.theme = this.mergeTheme(this.theme, theme);
1140
+ this.onThemeChange(this.theme);
1141
+ }
1142
+ resetTheme() {
1143
+ this.theme = { ...DEFAULT_THEME };
1144
+ this.onThemeChange(this.theme);
1145
+ }
1146
+ setDomainColor(domain, color) {
1147
+ this.theme.colors.domainColors[domain] = color;
1148
+ this.onThemeChange(this.theme);
1149
+ }
1150
+ setBackground(color) {
1151
+ this.theme.colors.background = color;
1152
+ this.onThemeChange(this.theme);
1153
+ }
1154
+ setLabelStyle(style) {
1155
+ this.theme.typography = { ...this.theme.typography, ...style };
1156
+ this.onThemeChange(this.theme);
1157
+ }
1158
+ applyPreset(preset) {
1159
+ if (preset === "light") {
1160
+ this.theme = this.mergeTheme(DEFAULT_THEME, {
1161
+ colors: { background: "#f7f7fb", labelText: "#111" }
1162
+ });
1163
+ } else if (preset === "dark") {
1164
+ this.theme = { ...DEFAULT_THEME };
1165
+ }
1166
+ this.onThemeChange(this.theme);
1167
+ }
1168
+ saveToStorage() {
1169
+ if (typeof window === "undefined") return;
1170
+ window.localStorage.setItem("omi-neuron-theme", JSON.stringify(this.theme));
1171
+ }
1172
+ loadFromStorage() {
1173
+ if (typeof window === "undefined") return;
1174
+ const stored = window.localStorage.getItem("omi-neuron-theme");
1175
+ if (stored) {
1176
+ this.theme = this.mergeTheme(this.theme, JSON.parse(stored));
1177
+ }
1178
+ }
1179
+ mergeTheme(base, next) {
1180
+ if (!next) return { ...base };
1181
+ return {
1182
+ ...base,
1183
+ ...next,
1184
+ colors: { ...base.colors, ...next.colors ?? {} },
1185
+ typography: { ...base.typography, ...next.typography ?? {} },
1186
+ effects: { ...base.effects, ...next.effects ?? {} },
1187
+ animation: { ...base.animation, ...next.animation ?? {} }
1188
+ };
1189
+ }
1190
+ };
1191
+
1192
+ export { DEFAULT_THEME, NeuronWeb, SceneManager, ThemeEngine, applyFuzzyLayout };
1193
+ //# sourceMappingURL=chunk-GPDX3O37.js.map
1194
+ //# sourceMappingURL=chunk-GPDX3O37.js.map