@omiron33/omi-neuron-web 0.2.17 → 0.2.19

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 CHANGED
@@ -686,7 +686,7 @@ export interface NeuronWebProps {
686
686
 
687
687
  Props currently used inside `NeuronWeb` (others are reserved for future use):
688
688
 
689
- - Used: `graphData`, `className`, `style`, `fullHeight`, `isFullScreen`, `isLoading`, `error`, `renderEmptyState`, `renderLoadingState`, `ariaLabel`, `theme`, `layout`, `renderNodeHover`, `renderNodeDetail`, `hoverCard`, `clickCard`, `clickZoom`, `cardsMode`, `onNodeHover`, `onNodeClick`, `onNodeDoubleClick`, `onNodeFocused`, `onBackgroundClick`, `performanceMode`, `focusNodeSlug`, `onFocusConsumed`, `visibleNodeSlugs`.
689
+ - Used: `graphData`, `className`, `style`, `fullHeight`, `isFullScreen`, `isLoading`, `error`, `renderEmptyState`, `renderLoadingState`, `ariaLabel`, `theme`, `layout`, `renderNodeHover`, `renderNodeDetail`, `hoverCard`, `clickCard`, `clickZoom`, `cardsMode`, `onNodeHover`, `onNodeClick`, `onNodeDoubleClick`, `onNodeFocused`, `onBackgroundClick`, `performanceMode`, `focusNodeSlug`, `onFocusConsumed`, `visibleNodeSlugs`, `studyPathRequest`, `onStudyPathComplete`.
690
690
  - Used: `cameraFit` (auto-fit bounds to a viewport fraction).
691
691
  - Reserved (declared but not used in the component yet): `selectedNode`, `onEdgeClick`, `onCameraChange`, `studyPathRequest`, `onStudyPathComplete`, `domainColors`, `graphData.storyBeats`.
692
692
 
@@ -847,6 +847,37 @@ Semantics:
847
847
  <NeuronWeb graphData={graphData} visibleNodeSlugs={[]} />;
848
848
  ```
849
849
 
850
+ ### Study path playback (follow the path between nodes)
851
+
852
+ Use `studyPathRequest` to step through an ordered list of nodes. Each step:
853
+ - selects the node
854
+ - tweens the camera to the node (if `clickZoom.enabled`)
855
+ - highlights the edge between the current and next step
856
+
857
+ ```tsx
858
+ <NeuronWeb
859
+ graphData={graphData}
860
+ studyPathRequest={{
861
+ steps: [
862
+ { nodeSlug: 'uap', label: 'Start' },
863
+ { nodeSlug: 'neph', label: 'Next' },
864
+ { nodeSlug: 'jude6', label: 'Finish' },
865
+ ],
866
+ stepDurationMs: 4200,
867
+ }}
868
+ onStudyPathComplete={() => console.log('study path done')}
869
+ />
870
+ ```
871
+
872
+ Fallback form (two-step path):
873
+
874
+ ```tsx
875
+ <NeuronWeb
876
+ graphData={graphData}
877
+ studyPathRequest={{ fromNodeId: 'uap', toNodeId: 'neph' }}
878
+ />
879
+ ```
880
+
850
881
  ### Click cards + click zoom
851
882
 
852
883
  Enable a persistent card on click and optional zoom-to-node behavior:
@@ -6,9 +6,30 @@ interface NeuronStoryBeat {
6
6
  label: string;
7
7
  nodeIds: string[];
8
8
  }
9
+ interface StudyPathStep {
10
+ nodeSlug?: string;
11
+ nodeId?: string;
12
+ label?: string;
13
+ summary?: string;
14
+ }
9
15
  interface StudyPathRequest {
10
- fromNodeId: string;
11
- toNodeId: string;
16
+ /**
17
+ * Ordered steps to follow (slugs or ids). When provided, this takes precedence.
18
+ */
19
+ steps?: StudyPathStep[];
20
+ /**
21
+ * Optional label for the study path (consumer UI usage).
22
+ */
23
+ label?: string;
24
+ /**
25
+ * Time to hold each step before advancing (ms). Defaults to 4200.
26
+ */
27
+ stepDurationMs?: number;
28
+ /**
29
+ * Minimal fallback when steps are not provided.
30
+ */
31
+ fromNodeId?: string;
32
+ toNodeId?: string;
12
33
  }
13
34
  interface NeuronWebTheme {
14
35
  colors: {
@@ -137,6 +158,6 @@ interface NeuronWebProps {
137
158
  ariaLabel?: string;
138
159
  }
139
160
 
140
- declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, layout, cameraFit, cardsMode, clickCard, clickZoom, renderNodeHover, renderNodeDetail, hoverCard, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onBackgroundClick, performanceMode, }: NeuronWebProps): React__default.ReactElement;
161
+ declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, layout, cameraFit, cardsMode, clickCard, clickZoom, studyPathRequest, onStudyPathComplete, renderNodeHover, renderNodeDetail, hoverCard, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onBackgroundClick, performanceMode, }: NeuronWebProps): React__default.ReactElement;
141
162
 
142
163
  export { type CameraFitOptions as C, type HoverCardOptions as H, type NeuronWebTheme as N, type NeuronWebThemeOverride as a, type NeuronLayoutOptions as b, NeuronWeb as c, type NeuronWebProps as d, type NeuronLayoutMode as e, type ClickCardOptions as f, type ClickZoomOptions as g, type CardsMode as h };
@@ -6,9 +6,30 @@ interface NeuronStoryBeat {
6
6
  label: string;
7
7
  nodeIds: string[];
8
8
  }
9
+ interface StudyPathStep {
10
+ nodeSlug?: string;
11
+ nodeId?: string;
12
+ label?: string;
13
+ summary?: string;
14
+ }
9
15
  interface StudyPathRequest {
10
- fromNodeId: string;
11
- toNodeId: string;
16
+ /**
17
+ * Ordered steps to follow (slugs or ids). When provided, this takes precedence.
18
+ */
19
+ steps?: StudyPathStep[];
20
+ /**
21
+ * Optional label for the study path (consumer UI usage).
22
+ */
23
+ label?: string;
24
+ /**
25
+ * Time to hold each step before advancing (ms). Defaults to 4200.
26
+ */
27
+ stepDurationMs?: number;
28
+ /**
29
+ * Minimal fallback when steps are not provided.
30
+ */
31
+ fromNodeId?: string;
32
+ toNodeId?: string;
12
33
  }
13
34
  interface NeuronWebTheme {
14
35
  colors: {
@@ -137,6 +158,6 @@ interface NeuronWebProps {
137
158
  ariaLabel?: string;
138
159
  }
139
160
 
140
- declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, layout, cameraFit, cardsMode, clickCard, clickZoom, renderNodeHover, renderNodeDetail, hoverCard, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onBackgroundClick, performanceMode, }: NeuronWebProps): React__default.ReactElement;
161
+ declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, layout, cameraFit, cardsMode, clickCard, clickZoom, studyPathRequest, onStudyPathComplete, renderNodeHover, renderNodeDetail, hoverCard, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onBackgroundClick, performanceMode, }: NeuronWebProps): React__default.ReactElement;
141
162
 
142
163
  export { type CameraFitOptions as C, type HoverCardOptions as H, type NeuronWebTheme as N, type NeuronWebThemeOverride as a, type NeuronLayoutOptions as b, NeuronWeb as c, type NeuronWebProps as d, type NeuronLayoutMode as e, type ClickCardOptions as f, type ClickZoomOptions as g, type CardsMode as h };
@@ -1061,6 +1061,8 @@ function NeuronWeb({
1061
1061
  cardsMode,
1062
1062
  clickCard,
1063
1063
  clickZoom,
1064
+ studyPathRequest,
1065
+ onStudyPathComplete,
1064
1066
  renderNodeHover,
1065
1067
  renderNodeDetail,
1066
1068
  hoverCard,
@@ -1076,9 +1078,12 @@ function NeuronWeb({
1076
1078
  const clickCardRef = react.useRef(null);
1077
1079
  const [hoveredNodeId, setHoveredNodeId] = react.useState(null);
1078
1080
  const [selectedNodeId, setSelectedNodeId] = react.useState(null);
1081
+ const [studyPathPlayer, setStudyPathPlayer] = react.useState(null);
1079
1082
  const fitStateRef = react.useRef({ hasFit: false, signature: "" });
1080
1083
  const firstFilterChangeRef = react.useRef(true);
1081
1084
  const [filterTransitioning, setFilterTransitioning] = react.useState(false);
1085
+ const pathEdgeIdsRef = react.useRef([]);
1086
+ const focusEdgesRef = react.useRef(null);
1082
1087
  const filteredGraphData = react.useMemo(() => {
1083
1088
  if (visibleNodeSlugs === null || visibleNodeSlugs === void 0) {
1084
1089
  return graphData;
@@ -1250,6 +1255,43 @@ function NeuronWeb({
1250
1255
  });
1251
1256
  return map;
1252
1257
  }, [workingGraph.edges]);
1258
+ const applyFocusEdges = react.useCallback(
1259
+ (edgeIds) => {
1260
+ if (!edgeRenderer) return;
1261
+ focusEdgesRef.current = edgeIds;
1262
+ const merged = new Set(edgeIds ?? []);
1263
+ pathEdgeIdsRef.current.forEach((id) => merged.add(id));
1264
+ edgeRenderer.setFocusEdges(merged.size ? Array.from(merged) : null);
1265
+ },
1266
+ [edgeRenderer]
1267
+ );
1268
+ const resolvedStudyPathSteps = react.useMemo(() => {
1269
+ if (!studyPathRequest) return null;
1270
+ if (studyPathRequest.steps && studyPathRequest.steps.length) {
1271
+ return studyPathRequest.steps;
1272
+ }
1273
+ if (studyPathRequest.fromNodeId && studyPathRequest.toNodeId) {
1274
+ return [
1275
+ { nodeId: studyPathRequest.fromNodeId },
1276
+ { nodeId: studyPathRequest.toNodeId }
1277
+ ];
1278
+ }
1279
+ return null;
1280
+ }, [studyPathRequest]);
1281
+ react.useEffect(() => {
1282
+ if (!resolvedStudyPathSteps || resolvedStudyPathSteps.length === 0) {
1283
+ setStudyPathPlayer(null);
1284
+ pathEdgeIdsRef.current = [];
1285
+ applyFocusEdges(focusEdgesRef.current);
1286
+ return;
1287
+ }
1288
+ setStudyPathPlayer({
1289
+ steps: resolvedStudyPathSteps,
1290
+ index: 0,
1291
+ playing: true,
1292
+ stepDurationMs: studyPathRequest?.stepDurationMs ?? 4200
1293
+ });
1294
+ }, [resolvedStudyPathSteps, studyPathRequest?.stepDurationMs, applyFocusEdges]);
1253
1295
  const nodeByIdentifier = react.useMemo(() => {
1254
1296
  const map = /* @__PURE__ */ new Map();
1255
1297
  resolvedNodes.forEach((node) => {
@@ -1266,6 +1308,70 @@ function NeuronWeb({
1266
1308
  const clickCardOffset = clickCard?.offset ?? [24, 24];
1267
1309
  const clickCardWidth = clickCard?.width ?? 320;
1268
1310
  const clickZoomEnabled = clickZoom?.enabled ?? true;
1311
+ react.useEffect(() => {
1312
+ if (!studyPathPlayer) return;
1313
+ const step = studyPathPlayer.steps[studyPathPlayer.index];
1314
+ const stepKey = step?.nodeSlug ?? step?.nodeId ?? null;
1315
+ const node = stepKey ? nodeByIdentifier.get(stepKey) ?? null : null;
1316
+ if (node && nodeRenderer && edgeRenderer) {
1317
+ setSelectedNodeId(node.id);
1318
+ nodeRenderer.setSelectedNode(node.id);
1319
+ nodeRenderer.pulseNode(node.id);
1320
+ if (clickZoomEnabled) {
1321
+ const nodePosition = nodeRenderer.getNodePosition(node.id);
1322
+ if (nodePosition) {
1323
+ animationController?.focusOnNode(nodePosition, () => {
1324
+ if (onNodeFocused) onNodeFocused(node);
1325
+ });
1326
+ }
1327
+ }
1328
+ const slug = node.slug;
1329
+ applyFocusEdges(slug ? edgesBySlug.get(slug) ?? [] : []);
1330
+ }
1331
+ const nextStep = studyPathPlayer.index < studyPathPlayer.steps.length - 1 ? studyPathPlayer.steps[studyPathPlayer.index + 1] : null;
1332
+ const currentSlug = node?.slug ?? (step?.nodeSlug ?? null);
1333
+ const nextKey = nextStep?.nodeSlug ?? nextStep?.nodeId ?? null;
1334
+ const nextNode = nextKey ? nodeByIdentifier.get(nextKey) ?? null : null;
1335
+ const nextSlug = nextNode?.slug ?? (nextStep?.nodeSlug ?? null);
1336
+ if (currentSlug && nextSlug) {
1337
+ pathEdgeIdsRef.current = workingGraph.edges.filter(
1338
+ (edge) => edge.from === currentSlug && edge.to === nextSlug || edge.to === currentSlug && edge.from === nextSlug
1339
+ ).map((edge) => edge.id);
1340
+ } else {
1341
+ pathEdgeIdsRef.current = [];
1342
+ }
1343
+ applyFocusEdges(focusEdgesRef.current);
1344
+ }, [
1345
+ studyPathPlayer,
1346
+ nodeByIdentifier,
1347
+ nodeRenderer,
1348
+ edgeRenderer,
1349
+ edgesBySlug,
1350
+ workingGraph.edges,
1351
+ animationController,
1352
+ clickZoomEnabled,
1353
+ onNodeFocused,
1354
+ applyFocusEdges
1355
+ ]);
1356
+ react.useEffect(() => {
1357
+ if (!studyPathPlayer || !studyPathPlayer.playing) return;
1358
+ const timer = window.setTimeout(() => {
1359
+ setStudyPathPlayer((prev) => {
1360
+ if (!prev) return prev;
1361
+ if (prev.index >= prev.steps.length - 1) {
1362
+ if (onStudyPathComplete) onStudyPathComplete();
1363
+ return { ...prev, playing: false };
1364
+ }
1365
+ return { ...prev, index: prev.index + 1 };
1366
+ });
1367
+ }, studyPathPlayer.stepDurationMs);
1368
+ return () => window.clearTimeout(timer);
1369
+ }, [studyPathPlayer, onStudyPathComplete]);
1370
+ react.useEffect(() => {
1371
+ if (!studyPathPlayer || studyPathPlayer.playing) return;
1372
+ pathEdgeIdsRef.current = [];
1373
+ applyFocusEdges(focusEdgesRef.current);
1374
+ }, [studyPathPlayer, applyFocusEdges]);
1269
1375
  react.useEffect(() => {
1270
1376
  if (!sceneManager || !nodeRenderer || !edgeRenderer) return;
1271
1377
  nodeRenderer.renderNodes(resolvedNodes);
@@ -1442,9 +1548,9 @@ function NeuronWeb({
1442
1548
  if (selectedNodeId && !nodeMap.has(selectedNodeId)) {
1443
1549
  setSelectedNodeId(null);
1444
1550
  nodeRenderer.setSelectedNode(null);
1445
- edgeRenderer.setFocusEdges(null);
1551
+ applyFocusEdges(null);
1446
1552
  }
1447
- }, [selectedNodeId, nodeMap, nodeRenderer, edgeRenderer]);
1553
+ }, [selectedNodeId, nodeMap, nodeRenderer, edgeRenderer, applyFocusEdges]);
1448
1554
  react.useEffect(() => {
1449
1555
  if (!focusNodeSlug || !nodeRenderer || !edgeRenderer) return;
1450
1556
  const node = nodeByIdentifier.get(focusNodeSlug);
@@ -1455,7 +1561,7 @@ function NeuronWeb({
1455
1561
  setSelectedNodeId(node.id);
1456
1562
  nodeRenderer.setSelectedNode(node.id);
1457
1563
  nodeRenderer.pulseNode(node.id);
1458
- edgeRenderer.setFocusEdges(node.slug ? edgesBySlug.get(node.slug) ?? [] : []);
1564
+ applyFocusEdges(node.slug ? edgesBySlug.get(node.slug) ?? [] : []);
1459
1565
  if (clickZoomEnabled) {
1460
1566
  const nodePosition = nodeRenderer.getNodePosition(node.id);
1461
1567
  if (nodePosition) {
@@ -1478,7 +1584,8 @@ function NeuronWeb({
1478
1584
  clickZoomEnabled,
1479
1585
  animationController,
1480
1586
  onNodeFocused,
1481
- onFocusConsumed
1587
+ onFocusConsumed,
1588
+ applyFocusEdges
1482
1589
  ]);
1483
1590
  react.useEffect(() => {
1484
1591
  if (!interactionManager || !nodeRenderer || !edgeRenderer) return;
@@ -1488,13 +1595,13 @@ function NeuronWeb({
1488
1595
  nodeRenderer.setHoveredNode(nodeId);
1489
1596
  if (nodeId) {
1490
1597
  const slug = nodeSlugById.get(nodeId);
1491
- edgeRenderer.setFocusEdges(slug ? edgesBySlug.get(slug) ?? [] : []);
1598
+ applyFocusEdges(slug ? edgesBySlug.get(slug) ?? [] : []);
1492
1599
  } else {
1493
1600
  const selectedSlug = selectedNodeId ? nodeSlugById.get(selectedNodeId) : null;
1494
1601
  if (selectedSlug) {
1495
- edgeRenderer.setFocusEdges(edgesBySlug.get(selectedSlug) ?? []);
1602
+ applyFocusEdges(edgesBySlug.get(selectedSlug) ?? []);
1496
1603
  } else {
1497
- edgeRenderer.setFocusEdges(null);
1604
+ applyFocusEdges(null);
1498
1605
  }
1499
1606
  }
1500
1607
  if (onNodeHover) {
@@ -1514,7 +1621,7 @@ function NeuronWeb({
1514
1621
  }
1515
1622
  }
1516
1623
  const slug = nodeSlugById.get(node.id);
1517
- edgeRenderer.setFocusEdges(slug ? edgesBySlug.get(slug) ?? [] : []);
1624
+ applyFocusEdges(slug ? edgesBySlug.get(slug) ?? [] : []);
1518
1625
  if (onNodeClick) {
1519
1626
  onNodeClick(node);
1520
1627
  }
@@ -1534,7 +1641,7 @@ function NeuronWeb({
1534
1641
  setSelectedNodeId(null);
1535
1642
  nodeRenderer.setSelectedNode(null);
1536
1643
  if (!hoveredNodeId) {
1537
- edgeRenderer.setFocusEdges(null);
1644
+ applyFocusEdges(null);
1538
1645
  }
1539
1646
  if (onBackgroundClick) onBackgroundClick();
1540
1647
  };
@@ -1552,7 +1659,8 @@ function NeuronWeb({
1552
1659
  onNodeClick,
1553
1660
  onNodeDoubleClick,
1554
1661
  onNodeFocused,
1555
- onBackgroundClick
1662
+ onBackgroundClick,
1663
+ applyFocusEdges
1556
1664
  ]);
1557
1665
  react.useEffect(() => {
1558
1666
  if (!sceneManager || !interactionManager) return;
@@ -1753,5 +1861,5 @@ exports.NeuronWeb = NeuronWeb;
1753
1861
  exports.SceneManager = SceneManager;
1754
1862
  exports.ThemeEngine = ThemeEngine;
1755
1863
  exports.applyFuzzyLayout = applyFuzzyLayout;
1756
- //# sourceMappingURL=chunk-CKFZJZLN.cjs.map
1757
- //# sourceMappingURL=chunk-CKFZJZLN.cjs.map
1864
+ //# sourceMappingURL=chunk-OUO3CKBM.cjs.map
1865
+ //# sourceMappingURL=chunk-OUO3CKBM.cjs.map