footprint-explainable-ui 0.25.5 → 0.26.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/README.md CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  Themeable React components for visualizing [footprintjs](https://github.com/footprintjs/footPrint) pipeline execution — time-travel debugging, flowchart overlays, subflow drill-down, and collapsible detail panels.
12
12
 
13
+ > Part of the **[footprintjs ecosystem](https://footprintjs.github.io/)** — the self-explaining stack.
14
+
13
15
  ## Install
14
16
 
15
17
  ```bash
@@ -52,25 +54,16 @@ const snapshots = toVisualizationSnapshots(
52
54
 
53
55
  ```tsx
54
56
  import { ExplainableShell } from "footprint-explainable-ui";
55
- import { TracedFlowchartView } from "footprint-explainable-ui/flowchart";
56
57
 
57
- function DebugView({ snapshots, spec, narrative, narrativeEntries }) {
58
+ function DebugView({ snapshots, narrativeEntries, traceGraph, runtimeOverlay }) {
58
59
  return (
59
60
  <ExplainableShell
60
61
  snapshots={snapshots}
61
- spec={spec}
62
- narrative={narrative}
63
62
  narrativeEntries={narrativeEntries}
63
+ traceGraph={traceGraph}
64
+ runtimeOverlay={runtimeOverlay}
64
65
  title="My Pipeline"
65
66
  panelLabels={{ topology: "What Ran", details: "What Happened", timeline: "How Long" }}
66
- renderFlowchart={({ spec, snapshots, selectedIndex, onNodeClick }) => (
67
- <TracedFlowchartView
68
- spec={spec}
69
- snapshots={snapshots}
70
- snapshotIndex={selectedIndex}
71
- onNodeClick={onNodeClick}
72
- />
73
- )}
74
67
  />
75
68
  );
76
69
  }
@@ -132,7 +125,8 @@ The all-in-one orchestrator. Handles time-travel, subflow drill-down, memory/nar
132
125
  | Prop | Type | Default | Description |
133
126
  |---|---|---|---|
134
127
  | `snapshots` | `StageSnapshot[]` | required | Visualization snapshots |
135
- | `spec` | `SpecNode \| null` | — | Pipeline spec (enables flowchart + subflow tree) |
128
+ | `traceGraph` | `TraceGraph \| null` | — | Build-time graph drives the flowchart + subflow drill-down |
129
+ | `runtimeOverlay` | `RuntimeOverlay` | — | Per-step execution overlay — lights the executed path |
136
130
  | `title` | `string` | `"Flowchart"` | Breadcrumb root label |
137
131
  | `narrative` | `string[]` | — | Flat narrative lines |
138
132
  | `narrativeEntries` | `NarrativeEntry[]` | — | Structured narrative (rich rendering) |
@@ -164,7 +158,7 @@ Control which panels start open. Desktop default: details panel open (flowchart
164
158
 
165
159
  ```tsx
166
160
  // Desktop (default) — memory panel open
167
- <ExplainableShell snapshots={...} spec={...} />
161
+ <ExplainableShell snapshots={...} traceGraph={...} runtimeOverlay={...} />
168
162
 
169
163
  // Mobile — all collapsed, flowchart fills screen
170
164
  <ExplainableShell
@@ -378,6 +372,20 @@ pipeline and snapshot-asserts the outputs in `test/golden/__snapshots__/`.
378
372
  `footprintjs` is a devDependency used ONLY by the generator — the published
379
373
  library still has zero footprintjs dependency (it consumes plain JSON shapes).
380
374
 
375
+ ## The footprintjs ecosystem
376
+
377
+ The self-explaining stack — from backend pipelines to AI agents. → **[overview](https://footprintjs.github.io/)**
378
+
379
+ | Project | Role |
380
+ |---|---|
381
+ | [footprintjs](https://footprintjs.github.io/footPrint/) | the flowchart pattern (core engine) |
382
+ | [agentfootprint](https://footprintjs.github.io/agentfootprint/) | build self-explaining AI agents |
383
+ | **Explainable UI** ← you are here | visualize a footprintjs run |
384
+ | [Lens](https://github.com/footprintjs/agentfootprint-lens) | debug an agentfootprint run |
385
+ | [Thinking UI](https://footprintjs.github.io/agentThinkingUI/) | replay an agent run for non-devs |
386
+
387
+ ---
388
+
381
389
  ## License
382
390
 
383
391
  MIT
package/dist/index.cjs CHANGED
@@ -5994,42 +5994,19 @@ var DetailsContent = (0, import_react31.memo)(function DetailsContent2({
5994
5994
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: { flex: 1, overflow: "auto" }, children: activeView?.render({ snapshots, selectedIndex }) })
5995
5995
  ] });
5996
5996
  });
5997
- function resolveSubflowLevel(parentSpec, parentSnapshots, subflowNodeName, narrativeEntries) {
5998
- const specNode = findSubflowSpecNode(parentSpec, subflowNodeName);
5999
- if (!specNode?.subflowStructure) return null;
6000
- const parentSnap = parentSnapshots.find(
6001
- (s) => s.stageName === subflowNodeName || s.stageLabel === subflowNodeName
6002
- );
5997
+ function resolveSubflowFromRuntime(parentSnapshots, subflowId, narrativeEntries) {
5998
+ const localId = subflowId.split("/").pop() ?? subflowId;
5999
+ const parentSnap = parentSnapshots.find((s) => {
6000
+ if (!s.subflowResult) return false;
6001
+ const sfStageId = s.runtimeStageId?.split("#")[0]?.split("/").pop();
6002
+ return s.subflowId === subflowId || s.subflowId === localId || s.stageName === subflowId || s.stageLabel === subflowId || sfStageId === subflowId || sfStageId === localId;
6003
+ });
6003
6004
  if (!parentSnap?.subflowResult) return null;
6004
- const sfId = specNode.subflowId ?? subflowNodeName;
6005
- const sfDisplayName = specNode.subflowName ?? specNode.name;
6006
- const sfNarrative = narrativeEntries ? extractSubflowNarrative(narrativeEntries, sfId, sfDisplayName) : void 0;
6005
+ const label = parentSnap.stageLabel ?? parentSnap.stageName ?? localId;
6006
+ const sfNarrative = narrativeEntries ? extractSubflowNarrative(narrativeEntries, subflowId, label) : void 0;
6007
6007
  const sfSnapshots = subflowResultToSnapshots(parentSnap.subflowResult, sfNarrative);
6008
6008
  if (sfSnapshots.length === 0) return null;
6009
- return {
6010
- subflowId: specNode.subflowId ?? subflowNodeName,
6011
- label: specNode.subflowName ?? specNode.name,
6012
- spec: specNode.subflowStructure,
6013
- snapshots: sfSnapshots
6014
- };
6015
- }
6016
- function findSubflowSpecNode(node, name) {
6017
- if ((node.name === name || node.id === name) && node.isSubflowRoot) return node;
6018
- if (node.children) {
6019
- for (const child of node.children) {
6020
- const f = findSubflowSpecNode(child, name);
6021
- if (f) return f;
6022
- }
6023
- }
6024
- if (node.next) return findSubflowSpecNode(node.next, name);
6025
- return null;
6026
- }
6027
- function hasSubflowNodes(node) {
6028
- if (!node) return false;
6029
- if (node.isSubflowRoot) return true;
6030
- if (node.children?.some((c) => c && hasSubflowNodes(c))) return true;
6031
- if (node.next && hasSubflowNodes(node.next)) return true;
6032
- return false;
6009
+ return { subflowId, label, spec: null, snapshots: sfSnapshots, narrative: sfNarrative };
6033
6010
  }
6034
6011
  function buildDataTrace(commitLog, targetRuntimeStageId, maxDepth = 10) {
6035
6012
  const log = commitLog;
@@ -6071,7 +6048,6 @@ var RightPanel = (0, import_react31.memo)(function RightPanel2({
6071
6048
  snapshots,
6072
6049
  selectedIndex,
6073
6050
  runtimeSnapshot,
6074
- spec,
6075
6051
  activeTab,
6076
6052
  allTabs,
6077
6053
  activeNarrativeEntries,
@@ -6117,7 +6093,7 @@ var RightPanel = (0, import_react31.memo)(function RightPanel2({
6117
6093
  id: tab.id,
6118
6094
  name: insightName(tab.name),
6119
6095
  render: () => {
6120
- if (tab.id === "narrative") return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(NarrativePanel, { snapshots, selectedIndex, narrativeEntries: activeNarrativeEntries, runtimeSnapshot, spec, size, style: { height: "100%" } });
6096
+ if (tab.id === "narrative") return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(NarrativePanel, { snapshots, selectedIndex, narrativeEntries: activeNarrativeEntries, runtimeSnapshot, size, style: { height: "100%" } });
6121
6097
  const customView = recorderViews?.find((v2) => v2.id === tab.id);
6122
6098
  if (customView?.render) return customView.render({ snapshots, selectedIndex });
6123
6099
  const autoView = autoRecorderViews.find((v2) => v2.id === tab.id);
@@ -6151,7 +6127,6 @@ function insightName(name) {
6151
6127
  function ExplainableShell({
6152
6128
  snapshots: snapshotsProp,
6153
6129
  runtimeSnapshot,
6154
- spec,
6155
6130
  title,
6156
6131
  resultData: resultDataProp,
6157
6132
  logs = [],
@@ -6189,9 +6164,14 @@ function ExplainableShell({
6189
6164
  const activeRsid = snapshots2[selectedIndex]?.runtimeStageId;
6190
6165
  let overlayIdx = selectedIndex;
6191
6166
  if (activeRsid && runtimeOverlay) {
6192
- const i = runtimeOverlay.executionOrder.findIndex(
6167
+ let i = runtimeOverlay.executionOrder.findIndex(
6193
6168
  (s) => s.runtimeStageId === activeRsid
6194
6169
  );
6170
+ if (i < 0) {
6171
+ i = runtimeOverlay.executionOrder.findIndex(
6172
+ (s) => s.runtimeStageId?.endsWith("/" + activeRsid)
6173
+ );
6174
+ }
6195
6175
  if (i >= 0) overlayIdx = i;
6196
6176
  }
6197
6177
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
@@ -6287,24 +6267,23 @@ function ExplainableShell({
6287
6267
  const currentLevel = (0, import_react31.useMemo)(() => {
6288
6268
  if (drillDownStack.length > 0) {
6289
6269
  const top = drillDownStack[drillDownStack.length - 1];
6290
- return { spec: top.spec, snapshots: top.snapshots };
6270
+ return { spec: top.spec, snapshots: top.snapshots, narrative: top.narrative };
6291
6271
  }
6292
- return { spec: spec ?? null, snapshots };
6293
- }, [drillDownStack, spec, snapshots]);
6272
+ return { spec: null, snapshots, narrative: void 0 };
6273
+ }, [drillDownStack, snapshots]);
6294
6274
  const activeSnapshots = currentLevel.snapshots;
6295
- const activeSpec = currentLevel.spec;
6296
6275
  const safeIdx = activeSnapshots.length > 0 ? Math.max(0, Math.min(snapshotIdx, activeSnapshots.length - 1)) : 0;
6297
- const activeNarrativeEntries = isInSubflow ? void 0 : narrativeEntries;
6276
+ const activeNarrativeEntries = isInSubflow ? currentLevel.narrative : narrativeEntries;
6298
6277
  const breadcrumbs = (0, import_react31.useMemo)(() => {
6299
- const root = { label: title || "Flowchart", spec, description: spec?.description };
6278
+ const root = { label: title || "Flowchart", spec: null, description: void 0 };
6300
6279
  return [root, ...drillDownStack.map((e) => ({ label: e.label, spec: e.spec, description: void 0 }))];
6301
- }, [spec, title, drillDownStack]);
6280
+ }, [title, drillDownStack]);
6302
6281
  const showTreeSidebar = (0, import_react31.useMemo)(() => {
6303
6282
  if (traceGraph?.nodes?.length) {
6304
6283
  return traceGraph.nodes.some((n) => n.data?.isSubflow === true);
6305
6284
  }
6306
- return !!spec && hasSubflowNodes(spec);
6307
- }, [traceGraph, spec]);
6285
+ return false;
6286
+ }, [traceGraph]);
6308
6287
  const rootOverlay = (0, import_react31.useMemo)(() => {
6309
6288
  if (isInSubflow || !snapshots.length) return { activeStage: void 0, doneStages: void 0 };
6310
6289
  const doneStages = new Set(snapshots.slice(0, safeIdx).map((s) => s.stageLabel));
@@ -6320,14 +6299,13 @@ function ExplainableShell({
6320
6299
  }, []);
6321
6300
  const handleDrillDown = (0, import_react31.useCallback)(
6322
6301
  (nodeName) => {
6323
- if (!activeSpec) return;
6324
- const entry = resolveSubflowLevel(activeSpec, activeSnapshots, nodeName, narrativeEntries);
6302
+ const entry = resolveSubflowFromRuntime(activeSnapshots, nodeName, narrativeEntries);
6325
6303
  if (entry) {
6326
6304
  setDrillDownStack((prev) => [...prev, { ...entry, parentSnapshotIdx: snapshotIdx }]);
6327
6305
  setSnapshotIdx(0);
6328
6306
  }
6329
6307
  },
6330
- [activeSpec, activeSnapshots, narrativeEntries, snapshotIdx]
6308
+ [activeSnapshots, narrativeEntries, snapshotIdx]
6331
6309
  );
6332
6310
  const handleBreadcrumbNavigate = (0, import_react31.useCallback)((level) => {
6333
6311
  setDrillDownStack((prev) => {
@@ -6342,23 +6320,21 @@ function ExplainableShell({
6342
6320
  setSnapshotIdx(indexOrId);
6343
6321
  return;
6344
6322
  }
6345
- if (activeSpec) {
6346
- const sfNode = findSubflowSpecNode(activeSpec, indexOrId);
6347
- if (sfNode?.subflowStructure) {
6348
- handleDrillDown(indexOrId);
6349
- return;
6350
- }
6323
+ const drillable = resolveSubflowFromRuntime(activeSnapshots, indexOrId, narrativeEntries);
6324
+ if (drillable) {
6325
+ handleDrillDown(indexOrId);
6326
+ return;
6351
6327
  }
6352
6328
  const idx = activeSnapshots.findIndex((s) => s.stageLabel === indexOrId);
6353
6329
  if (idx >= 0) setSnapshotIdx(idx);
6354
6330
  },
6355
- [activeSpec, activeSnapshots, handleDrillDown]
6331
+ [activeSnapshots, narrativeEntries, handleDrillDown]
6356
6332
  );
6357
6333
  const handleTreeNodeSelect = (0, import_react31.useCallback)(
6358
6334
  (name, isSubflow) => {
6359
- if (isSubflow && spec) {
6335
+ if (isSubflow) {
6360
6336
  setDrillDownStack([]);
6361
- const entry = resolveSubflowLevel(spec, snapshots, name, narrativeEntries);
6337
+ const entry = resolveSubflowFromRuntime(snapshots, name, narrativeEntries);
6362
6338
  if (entry) {
6363
6339
  setDrillDownStack([{ ...entry, parentSnapshotIdx: snapshotIdx }]);
6364
6340
  setSnapshotIdx(0);
@@ -6369,7 +6345,7 @@ function ExplainableShell({
6369
6345
  if (idx >= 0) setSnapshotIdx(idx);
6370
6346
  }
6371
6347
  },
6372
- [spec, snapshots, narrativeEntries, snapshotIdx]
6348
+ [snapshots, narrativeEntries, snapshotIdx]
6373
6349
  );
6374
6350
  const tabLabels = new Map(allTabs.map((t) => [t.id, t.name]));
6375
6351
  if (unstyled) {
@@ -6380,7 +6356,7 @@ function ExplainableShell({
6380
6356
  (activeTab === "explainable" || activeTab === "ai-compatible") && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
6381
6357
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TimeTravelControls, { snapshots: activeSnapshots, selectedIndex: safeIdx, onIndexChange: handleSnapshotChange, unstyled: true }),
6382
6358
  isInSubflow && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SubflowBreadcrumb, { breadcrumbs, onNavigate: handleBreadcrumbNavigate }),
6383
- activeSpec && effectiveRenderFlowchart?.({ spec: activeSpec, snapshots: activeSnapshots, selectedIndex: safeIdx, onNodeClick: handleNodeClick, showStageId }),
6359
+ traceGraph && effectiveRenderFlowchart?.({ spec: null, snapshots: activeSnapshots, selectedIndex: safeIdx, onNodeClick: handleNodeClick, showStageId }),
6384
6360
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(MemoryPanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, unstyled: true }),
6385
6361
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(NarrativePanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, narrativeEntries: activeNarrativeEntries, unstyled: true }),
6386
6362
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, unstyled: true })
@@ -6388,7 +6364,7 @@ function ExplainableShell({
6388
6364
  ] })
6389
6365
  ] });
6390
6366
  }
6391
- const showTopology = !!effectiveRenderFlowchart && !!activeSpec;
6367
+ const showTopology = !!effectiveRenderFlowchart && !!traceGraph;
6392
6368
  const detailsContent = (0, import_react31.useMemo)(() => {
6393
6369
  if (activeTab === "result") {
6394
6370
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ResultPanel, { data: resultData ?? null, logs, hideConsole, size });
@@ -6485,7 +6461,7 @@ function ExplainableShell({
6485
6461
  /* ── Mobile: stacked vertical ── */
6486
6462
  /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
6487
6463
  showTopology && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: { height: 350, flexShrink: 0, overflow: "hidden" }, children: effectiveRenderFlowchart({
6488
- spec: activeSpec,
6464
+ spec: null,
6489
6465
  snapshots: activeSnapshots,
6490
6466
  selectedIndex: safeIdx,
6491
6467
  onNodeClick: handleNodeClick,
@@ -6525,7 +6501,7 @@ function ExplainableShell({
6525
6501
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VLinePill, { label: "Topology", expanded: true, side: "left", onClick: () => toggleLeft(false) })
6526
6502
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VLinePill, { label: "Topology", expanded: false, side: "left", onClick: () => toggleLeft(true) })),
6527
6503
  showTopology ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: { flex: 1, overflow: "hidden", minWidth: 0 }, children: effectiveRenderFlowchart({
6528
- spec: activeSpec,
6504
+ spec: null,
6529
6505
  snapshots: activeSnapshots,
6530
6506
  selectedIndex: safeIdx,
6531
6507
  onNodeClick: handleNodeClick,
@@ -6540,7 +6516,6 @@ function ExplainableShell({
6540
6516
  snapshots: activeSnapshots,
6541
6517
  selectedIndex: safeIdx,
6542
6518
  runtimeSnapshot,
6543
- spec,
6544
6519
  activeTab,
6545
6520
  allTabs,
6546
6521
  activeNarrativeEntries,
@@ -6657,7 +6632,6 @@ function TraceViewer({
6657
6632
  ExplainableShell,
6658
6633
  {
6659
6634
  snapshots,
6660
- spec: parsed.trace.spec,
6661
6635
  narrativeEntries: parsed.trace.narrativeEntries,
6662
6636
  tabs,
6663
6637
  defaultTab,