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