footprint-explainable-ui 0.26.2 → 0.28.0

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/dist/index.cjs CHANGED
@@ -2300,6 +2300,67 @@ function TimeTravelControls({
2300
2300
  // src/components/ExplainableShell/ExplainableShell.tsx
2301
2301
  var import_react31 = require("react");
2302
2302
 
2303
+ // src/components/ExplainableShell/_internal/dataTrace.ts
2304
+ function readsByStep(tree) {
2305
+ const byStep = /* @__PURE__ */ new Map();
2306
+ if (!tree) return byStep;
2307
+ const visited = /* @__PURE__ */ new Set();
2308
+ const stack = [tree];
2309
+ while (stack.length > 0) {
2310
+ const node = stack.pop();
2311
+ if (visited.has(node)) continue;
2312
+ visited.add(node);
2313
+ if (node.runtimeStageId && node.stageReads) {
2314
+ const keys = Object.keys(node.stageReads);
2315
+ if (keys.length > 0) byStep.set(node.runtimeStageId, keys);
2316
+ }
2317
+ if (node.next) stack.push(node.next);
2318
+ if (node.children) for (const c of node.children) stack.push(c);
2319
+ }
2320
+ return byStep;
2321
+ }
2322
+ function buildDataTrace(commitLog, executionTree, targetRuntimeStageId, maxDepth = 10, maxFrames = 50) {
2323
+ const log = commitLog ?? [];
2324
+ const reads = readsByStep(executionTree);
2325
+ const readsAvailable = reads.size > 0;
2326
+ if (!log.length) return { frames: [], readsAvailable };
2327
+ const idxOf = /* @__PURE__ */ new Map();
2328
+ for (let i = 0; i < log.length; i++) idxOf.set(log[i].runtimeStageId, i);
2329
+ const startIdx = idxOf.get(targetRuntimeStageId);
2330
+ if (startIdx === void 0) return { frames: [], readsAvailable };
2331
+ const findLastWriter = (key, beforeIdx) => {
2332
+ for (let i = beforeIdx - 1; i >= 0; i--) {
2333
+ if (log[i].trace.some((t) => t.path === key)) return i;
2334
+ }
2335
+ return -1;
2336
+ };
2337
+ const frames = [];
2338
+ const visited = /* @__PURE__ */ new Set();
2339
+ const queue = [[startIdx, 0, ""]];
2340
+ while (queue.length > 0 && frames.length < maxFrames) {
2341
+ const [idx, depth, linkedBy] = queue.shift();
2342
+ const commit = log[idx];
2343
+ if (visited.has(commit.runtimeStageId)) continue;
2344
+ visited.add(commit.runtimeStageId);
2345
+ frames.push({
2346
+ runtimeStageId: commit.runtimeStageId,
2347
+ stageId: commit.stageId,
2348
+ stageName: commit.stage,
2349
+ keysWritten: commit.trace.map((t) => t.path),
2350
+ linkedBy,
2351
+ depth
2352
+ });
2353
+ if (depth >= maxDepth) continue;
2354
+ for (const key of reads.get(commit.runtimeStageId) ?? []) {
2355
+ const writerIdx = findLastWriter(key, idx);
2356
+ if (writerIdx >= 0 && !visited.has(log[writerIdx].runtimeStageId)) {
2357
+ queue.push([writerIdx, depth + 1, key]);
2358
+ }
2359
+ }
2360
+ }
2361
+ return { frames, readsAvailable };
2362
+ }
2363
+
2303
2364
  // src/utils/narrativeSync.ts
2304
2365
  function buildEntryRangeIndex(entries) {
2305
2366
  const ranges = /* @__PURE__ */ new Map();
@@ -5031,6 +5092,7 @@ function TracedFlow({
5031
5092
  nodeTypes: userNodeTypes,
5032
5093
  edgeTypes: userEdgeTypes,
5033
5094
  coActiveStageIds,
5095
+ sliceCone,
5034
5096
  children,
5035
5097
  className,
5036
5098
  style
@@ -5132,6 +5194,38 @@ function TracedFlow({
5132
5194
  ),
5133
5195
  [positioned.edges, slice, colors]
5134
5196
  );
5197
+ const [coneRevealed, setConeRevealed] = (0, import_react25.useState)(false);
5198
+ (0, import_react25.useEffect)(() => {
5199
+ if (!sliceCone) return;
5200
+ setConeRevealed(false);
5201
+ const raf = requestAnimationFrame(() => setConeRevealed(true));
5202
+ return () => cancelAnimationFrame(raf);
5203
+ }, [sliceCone]);
5204
+ const conedNodes = (0, import_react25.useMemo)(() => {
5205
+ if (!sliceCone || sliceCone.size === 0) return reactFlowNodes;
5206
+ return reactFlowNodes.map((n) => {
5207
+ const depth = sliceCone.get(n.id);
5208
+ if (depth === void 0) {
5209
+ return { ...n, style: { ...n.style, opacity: 0.22, transition: "opacity 260ms ease" } };
5210
+ }
5211
+ return {
5212
+ ...n,
5213
+ style: {
5214
+ ...n.style,
5215
+ opacity: coneRevealed ? 1 : 0.22,
5216
+ transition: "opacity 320ms ease",
5217
+ transitionDelay: `${depth * 90}ms`
5218
+ }
5219
+ };
5220
+ });
5221
+ }, [reactFlowNodes, sliceCone, coneRevealed]);
5222
+ const conedEdges = (0, import_react25.useMemo)(() => {
5223
+ if (!sliceCone || sliceCone.size === 0) return reactFlowEdges;
5224
+ return reactFlowEdges.map((e) => {
5225
+ const inCone = sliceCone.has(e.source) && sliceCone.has(e.target);
5226
+ return inCone ? e : { ...e, style: { ...e.style, opacity: 0.12, transition: "opacity 260ms ease" } };
5227
+ });
5228
+ }, [reactFlowEdges, sliceCone]);
5135
5229
  const handleNodeClick = (0, import_react25.useCallback)(
5136
5230
  (_, node) => {
5137
5231
  const data = node.data ?? {};
@@ -5173,8 +5267,8 @@ function TracedFlow({
5173
5267
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { style: { flex: 1, minHeight: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
5174
5268
  import_react26.ReactFlow,
5175
5269
  {
5176
- nodes: reactFlowNodes,
5177
- edges: reactFlowEdges,
5270
+ nodes: conedNodes,
5271
+ edges: conedEdges,
5178
5272
  nodeTypes: mergedNodeTypes,
5179
5273
  edgeTypes: mergedEdgeTypes,
5180
5274
  onNodeClick: handleNodeClick,
@@ -5205,8 +5299,10 @@ var DataTracePanel = (0, import_react27.memo)(function DataTracePanel2({
5205
5299
  frames,
5206
5300
  selectedStageId,
5207
5301
  onFrameClick,
5208
- fromStageName
5302
+ fromStageName,
5303
+ note
5209
5304
  }) {
5305
+ const noteLine = note ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { style: { color: theme.textMuted, fontSize: 11, fontStyle: "italic", marginBottom: 8 }, children: note }) : null;
5210
5306
  if (frames.length === 0) {
5211
5307
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { padding: "14px 14px 12px", fontSize: 13, lineHeight: 1.55 }, children: [
5212
5308
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
@@ -5224,10 +5320,12 @@ var DataTracePanel = (0, import_react27.memo)(function DataTracePanel2({
5224
5320
  }
5225
5321
  ),
5226
5322
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { style: { color: theme.textSecondary, marginBottom: 10 }, children: "Trace any value back to the stage that created it \u2014 and everything upstream that influenced it." }),
5323
+ noteLine,
5227
5324
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { style: { color: theme.textMuted, fontSize: 12 }, children: "Select a stage above to see its dependency chain." })
5228
5325
  ] });
5229
5326
  }
5230
5327
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { padding: "8px 0", fontSize: 13 }, children: [
5328
+ note && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { style: { padding: "4px 12px 0", fontSize: 11, color: theme.textMuted, fontStyle: "italic" }, children: note }),
5231
5329
  fromStageName && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { padding: "4px 12px 8px" }, children: [
5232
5330
  /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
5233
5331
  "div",
@@ -5361,10 +5459,16 @@ var InspectorPanel = (0, import_react28.memo)(function InspectorPanel2({
5361
5459
  snapshots,
5362
5460
  selectedIndex,
5363
5461
  dataTraceFrames,
5462
+ dataTraceNote,
5364
5463
  selectedStageId,
5365
- onNavigateToStage
5464
+ onNavigateToStage,
5465
+ onTabChange
5366
5466
  }) {
5367
- const [tab, setTab] = (0, import_react28.useState)("state");
5467
+ const [tab, setTabState] = (0, import_react28.useState)("state");
5468
+ const setTab = (t) => {
5469
+ setTabState(t);
5470
+ onTabChange?.(t);
5471
+ };
5368
5472
  const currentSnapshot = snapshots[selectedIndex];
5369
5473
  return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
5370
5474
  "div",
@@ -5417,6 +5521,7 @@ var InspectorPanel = (0, import_react28.memo)(function InspectorPanel2({
5417
5521
  DataTracePanel,
5418
5522
  {
5419
5523
  frames: dataTraceFrames,
5524
+ note: dataTraceNote,
5420
5525
  selectedStageId,
5421
5526
  onFrameClick: onNavigateToStage,
5422
5527
  fromStageName: currentSnapshot?.stageName
@@ -6008,40 +6113,6 @@ function resolveSubflowFromRuntime(parentSnapshots, subflowId, narrativeEntries)
6008
6113
  if (sfSnapshots.length === 0) return null;
6009
6114
  return { subflowId, label, spec: null, snapshots: sfSnapshots, narrative: sfNarrative };
6010
6115
  }
6011
- function buildDataTrace(commitLog, targetRuntimeStageId, maxDepth = 10) {
6012
- const log = commitLog;
6013
- if (!log?.length) return [];
6014
- const idxMap = /* @__PURE__ */ new Map();
6015
- for (let i = 0; i < log.length; i++) idxMap.set(log[i].runtimeStageId, i);
6016
- const startIdx = idxMap.get(targetRuntimeStageId);
6017
- if (startIdx === void 0) return [];
6018
- const startCommit = log[startIdx];
6019
- const frames = [];
6020
- const visited = /* @__PURE__ */ new Set();
6021
- let current = startCommit;
6022
- let currentIdx = startIdx;
6023
- let depth = 0;
6024
- while (current && depth <= maxDepth) {
6025
- if (visited.has(current.runtimeStageId)) break;
6026
- visited.add(current.runtimeStageId);
6027
- frames.push({
6028
- runtimeStageId: current.runtimeStageId,
6029
- stageId: current.stageId,
6030
- stageName: current.stage,
6031
- keysWritten: current.trace.map((t) => t.path),
6032
- linkedBy: depth === 0 ? "" : current.trace[0]?.path ?? "",
6033
- depth
6034
- });
6035
- if (currentIdx > 0) {
6036
- currentIdx--;
6037
- current = log[currentIdx];
6038
- depth++;
6039
- } else {
6040
- break;
6041
- }
6042
- }
6043
- return frames;
6044
- }
6045
6116
  var RightPanel = (0, import_react31.memo)(function RightPanel2({
6046
6117
  mode,
6047
6118
  onModeChange,
@@ -6054,7 +6125,9 @@ var RightPanel = (0, import_react31.memo)(function RightPanel2({
6054
6125
  recorderViews,
6055
6126
  autoRecorderViews,
6056
6127
  size,
6057
- onNavigateToStage
6128
+ onNavigateToStage,
6129
+ dataTrace,
6130
+ onInspectorTabChange
6058
6131
  }) {
6059
6132
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
6060
6133
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: {
@@ -6107,7 +6180,9 @@ var RightPanel = (0, import_react31.memo)(function RightPanel2({
6107
6180
  {
6108
6181
  snapshots,
6109
6182
  selectedIndex,
6110
- dataTraceFrames: runtimeSnapshot?.commitLog ? buildDataTrace(runtimeSnapshot.commitLog, snapshots[selectedIndex]?.runtimeStageId ?? "") : [],
6183
+ dataTraceFrames: dataTrace.frames,
6184
+ dataTraceNote: dataTrace.readsAvailable ? void 0 : "\u26A0 reads were not recorded (readTracking off) \u2014 dependencies are unknowable, not absent.",
6185
+ onTabChange: onInspectorTabChange,
6111
6186
  selectedStageId: snapshots[selectedIndex]?.runtimeStageId,
6112
6187
  onNavigateToStage
6113
6188
  }
@@ -6161,7 +6236,7 @@ function ExplainableShell({
6161
6236
  const resultData = resultDataProp ?? derivedFromRuntime?.resultData ?? null;
6162
6237
  const tracedFlowRenderer = (0, import_react31.useMemo)(() => {
6163
6238
  if (!traceGraph) return void 0;
6164
- return ({ selectedIndex, snapshots: snapshots2, onNodeClick }) => {
6239
+ return ({ selectedIndex, snapshots: snapshots2, onNodeClick, sliceCone: sliceCone2 }) => {
6165
6240
  const activeRsid = snapshots2[selectedIndex]?.runtimeStageId;
6166
6241
  let overlayIdx = selectedIndex;
6167
6242
  if (activeRsid && runtimeOverlay) {
@@ -6185,6 +6260,7 @@ function ExplainableShell({
6185
6260
  {
6186
6261
  graph: traceGraph,
6187
6262
  overlay: runtimeOverlay ?? void 0,
6263
+ sliceCone: sliceCone2 ?? void 0,
6188
6264
  colors: traceColors || void 0,
6189
6265
  scrubIndex: overlayIdx,
6190
6266
  onNodeClick: (stageId) => onNodeClick?.(stageId),
@@ -6245,6 +6321,7 @@ function ExplainableShell({
6245
6321
  const [drillDownStack, setDrillDownStack] = (0, import_react31.useState)([]);
6246
6322
  const [rightExpanded, setRightExpanded] = (0, import_react31.useState)(defaultExpanded?.details ?? true);
6247
6323
  const [rightPanelMode, setRightPanelMode] = (0, import_react31.useState)("insights");
6324
+ const [inspectorTab, setInspectorTab] = (0, import_react31.useState)("state");
6248
6325
  const [leftExpanded, setLeftExpanded] = (0, import_react31.useState)(defaultExpanded?.topology ?? false);
6249
6326
  const [timelineExpanded, setTimelineExpanded] = (0, import_react31.useState)(defaultExpanded?.timeline ?? false);
6250
6327
  (0, import_react31.useEffect)(() => {
@@ -6280,6 +6357,21 @@ function ExplainableShell({
6280
6357
  }, [drillDownStack, snapshots]);
6281
6358
  const activeSnapshots = currentLevel.snapshots;
6282
6359
  const safeIdx = activeSnapshots.length > 0 ? Math.max(0, Math.min(snapshotIdx, activeSnapshots.length - 1)) : 0;
6360
+ const shellDataTrace = (0, import_react31.useMemo)(
6361
+ () => runtimeSnapshot?.commitLog ? buildDataTrace(runtimeSnapshot.commitLog, runtimeSnapshot.executionTree, activeSnapshots[safeIdx]?.runtimeStageId ?? "") : { frames: [], readsAvailable: true },
6362
+ [runtimeSnapshot, activeSnapshots, safeIdx]
6363
+ );
6364
+ const sliceCone = (0, import_react31.useMemo)(() => {
6365
+ if (rightPanelMode !== "what" || inspectorTab !== "trace") return void 0;
6366
+ if (shellDataTrace.frames.length < 2) return void 0;
6367
+ const cone = /* @__PURE__ */ new Map();
6368
+ for (const f of shellDataTrace.frames) {
6369
+ const stagePart = f.runtimeStageId.split("#")[0];
6370
+ const prev = cone.get(stagePart);
6371
+ if (prev === void 0 || f.depth < prev) cone.set(stagePart, f.depth);
6372
+ }
6373
+ return cone;
6374
+ }, [rightPanelMode, inspectorTab, shellDataTrace]);
6283
6375
  const activeNarrativeEntries = isInSubflow ? currentLevel.narrative : narrativeEntries;
6284
6376
  const breadcrumbs = (0, import_react31.useMemo)(() => {
6285
6377
  const root = { label: title || "Flowchart", spec: null, description: void 0 };
@@ -6363,7 +6455,7 @@ function ExplainableShell({
6363
6455
  (activeTab === "explainable" || activeTab === "ai-compatible") && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
6364
6456
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TimeTravelControls, { snapshots: activeSnapshots, selectedIndex: safeIdx, onIndexChange: handleSnapshotChange, unstyled: true }),
6365
6457
  isInSubflow && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SubflowBreadcrumb, { breadcrumbs, onNavigate: handleBreadcrumbNavigate }),
6366
- traceGraph && effectiveRenderFlowchart?.({ spec: null, snapshots: activeSnapshots, selectedIndex: safeIdx, onNodeClick: handleNodeClick, showStageId }),
6458
+ traceGraph && effectiveRenderFlowchart?.({ spec: null, snapshots: activeSnapshots, selectedIndex: safeIdx, onNodeClick: handleNodeClick, showStageId, ...sliceCone && { sliceCone } }),
6367
6459
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(MemoryPanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, unstyled: true }),
6368
6460
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(NarrativePanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, narrativeEntries: activeNarrativeEntries, unstyled: true }),
6369
6461
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, unstyled: true })
@@ -6436,12 +6528,22 @@ function ExplainableShell({
6436
6528
  }) }),
6437
6529
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: { flex: 1, overflow: "auto" }, children: detailsContent })
6438
6530
  ] });
6531
+ const shellThemeVars = (0, import_react31.useMemo)(() => {
6532
+ if (!traceTheme) return {};
6533
+ const base = traceTheme.mode ? tokensToCSSVars(traceTheme.mode === "light" ? coolLight : coolDark) : {};
6534
+ return {
6535
+ ...base,
6536
+ ...traceTheme.visited !== void 0 && { ["--fp-node-visited"]: traceTheme.visited },
6537
+ ...traceTheme.current !== void 0 && { ["--fp-node-cursor"]: traceTheme.current }
6538
+ };
6539
+ }, [traceTheme]);
6439
6540
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
6440
6541
  "div",
6441
6542
  {
6442
6543
  ref: shellRef,
6443
6544
  className,
6444
6545
  style: {
6546
+ ...shellThemeVars,
6445
6547
  height: "100%",
6446
6548
  display: "flex",
6447
6549
  flexDirection: "column",
@@ -6472,7 +6574,8 @@ function ExplainableShell({
6472
6574
  snapshots: activeSnapshots,
6473
6575
  selectedIndex: safeIdx,
6474
6576
  onNodeClick: handleNodeClick,
6475
- showStageId
6577
+ showStageId,
6578
+ ...sliceCone && { sliceCone }
6476
6579
  }) }),
6477
6580
  showTreeSidebar && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
6478
6581
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(HLinePill, { label: leftLabel, expanded: leftExpanded, onClick: () => toggleLeft(!leftExpanded) }),
@@ -6512,7 +6615,8 @@ function ExplainableShell({
6512
6615
  snapshots: activeSnapshots,
6513
6616
  selectedIndex: safeIdx,
6514
6617
  onNodeClick: handleNodeClick,
6515
- showStageId
6618
+ showStageId,
6619
+ ...sliceCone && { sliceCone }
6516
6620
  }) }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: { flex: 1 } }),
6517
6621
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VLinePill, { label: "Details", expanded: rightExpanded, onClick: () => toggleRight(!rightExpanded) }),
6518
6622
  rightExpanded && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: { width: "42%", minWidth: 320, maxWidth: 550, display: "flex", flexDirection: "column", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
@@ -6520,6 +6624,8 @@ function ExplainableShell({
6520
6624
  {
6521
6625
  mode: rightPanelMode,
6522
6626
  onModeChange: setRightPanelMode,
6627
+ dataTrace: shellDataTrace,
6628
+ onInspectorTabChange: setInspectorTab,
6523
6629
  snapshots: activeSnapshots,
6524
6630
  selectedIndex: safeIdx,
6525
6631
  runtimeSnapshot,