footprint-explainable-ui 0.3.0 → 0.3.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.
- package/README.md +422 -147
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# footprint-explainable-ui
|
|
2
2
|
|
|
3
|
-
Themeable React components for visualizing [FootPrint](https://github.com/
|
|
3
|
+
Themeable React components for visualizing [FootPrint](https://github.com/footprintjs/footPrint) pipeline execution — time-travel debugging, flowchart overlays, subflow drill-down, narrative traces, and scope diffs.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,16 +8,44 @@ Themeable React components for visualizing [FootPrint](https://github.com/sanjay
|
|
|
8
8
|
npm install footprint-explainable-ui
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Peer dependencies
|
|
11
|
+
**Peer dependencies:** `react >= 18`, `react-dom >= 18`
|
|
12
|
+
|
|
13
|
+
For flowchart components, also install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @xyflow/react
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Entry Points
|
|
20
|
+
|
|
21
|
+
| Import path | What it provides |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `footprint-explainable-ui` | Core components, themes, adapters |
|
|
24
|
+
| `footprint-explainable-ui/flowchart` | Flowchart visualization, subflow navigation (requires `@xyflow/react`) |
|
|
12
25
|
|
|
13
26
|
## Quick Start
|
|
14
27
|
|
|
15
|
-
###
|
|
28
|
+
### 1. Convert FootPrint execution data to snapshots
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { FlowChartExecutor } from "footprint";
|
|
32
|
+
import { toVisualizationSnapshots } from "footprint-explainable-ui";
|
|
16
33
|
|
|
17
|
-
|
|
34
|
+
const executor = new FlowChartExecutor(chart);
|
|
35
|
+
await executor.run();
|
|
36
|
+
|
|
37
|
+
// Convert runtime snapshot → visualization snapshots
|
|
38
|
+
const snapshots = toVisualizationSnapshots(executor.getSnapshot());
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Render with the all-in-one shell
|
|
18
42
|
|
|
19
43
|
```tsx
|
|
20
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
ExplainableShell,
|
|
46
|
+
FootprintTheme,
|
|
47
|
+
warmDark,
|
|
48
|
+
} from "footprint-explainable-ui";
|
|
21
49
|
|
|
22
50
|
function App({ snapshots, narrative, result }) {
|
|
23
51
|
return (
|
|
@@ -33,22 +61,21 @@ function App({ snapshots, narrative, result }) {
|
|
|
33
61
|
}
|
|
34
62
|
```
|
|
35
63
|
|
|
36
|
-
###
|
|
37
|
-
|
|
38
|
-
Every component works standalone. Mix and match:
|
|
64
|
+
### 3. Or compose individual components
|
|
39
65
|
|
|
40
66
|
```tsx
|
|
41
67
|
import {
|
|
42
|
-
|
|
68
|
+
TimeTravelControls,
|
|
69
|
+
MemoryInspector,
|
|
43
70
|
ScopeDiff,
|
|
44
71
|
GanttTimeline,
|
|
45
|
-
|
|
46
|
-
TimeTravelControls,
|
|
47
|
-
ResultPanel,
|
|
72
|
+
NarrativeTrace,
|
|
48
73
|
} from "footprint-explainable-ui";
|
|
49
74
|
|
|
50
75
|
function MyDebugger({ snapshots }) {
|
|
51
76
|
const [idx, setIdx] = useState(0);
|
|
77
|
+
const current = snapshots[idx];
|
|
78
|
+
const previous = idx > 0 ? snapshots[idx - 1] : null;
|
|
52
79
|
|
|
53
80
|
return (
|
|
54
81
|
<>
|
|
@@ -59,46 +86,200 @@ function MyDebugger({ snapshots }) {
|
|
|
59
86
|
/>
|
|
60
87
|
<MemoryInspector snapshots={snapshots} selectedIndex={idx} />
|
|
61
88
|
<ScopeDiff
|
|
62
|
-
previous={
|
|
63
|
-
current={
|
|
89
|
+
previous={previous?.memory ?? null}
|
|
90
|
+
current={current.memory}
|
|
64
91
|
hideUnchanged
|
|
65
92
|
/>
|
|
93
|
+
<NarrativeTrace narrative={snapshots.map(s => s.narrative)} />
|
|
66
94
|
<GanttTimeline snapshots={snapshots} selectedIndex={idx} onSelect={setIdx} />
|
|
67
95
|
</>
|
|
68
96
|
);
|
|
69
97
|
}
|
|
70
98
|
```
|
|
71
99
|
|
|
72
|
-
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Flowchart Visualization
|
|
103
|
+
|
|
104
|
+
Import from `footprint-explainable-ui/flowchart`:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import {
|
|
108
|
+
StageNode,
|
|
109
|
+
specToReactFlow,
|
|
110
|
+
useSubflowNavigation,
|
|
111
|
+
SubflowBreadcrumb,
|
|
112
|
+
type SpecNode,
|
|
113
|
+
type ExecutionOverlay,
|
|
114
|
+
} from "footprint-explainable-ui/flowchart";
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Static flowchart from pipeline spec
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { ReactFlow } from "@xyflow/react";
|
|
121
|
+
import "@xyflow/react/dist/style.css";
|
|
122
|
+
import { specToReactFlow, StageNode } from "footprint-explainable-ui/flowchart";
|
|
123
|
+
|
|
124
|
+
const nodeTypes = { stage: StageNode };
|
|
125
|
+
|
|
126
|
+
function PipelineChart({ spec }) {
|
|
127
|
+
const { nodes, edges } = specToReactFlow(spec);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<ReactFlow
|
|
131
|
+
nodes={nodes}
|
|
132
|
+
edges={edges}
|
|
133
|
+
nodeTypes={nodeTypes}
|
|
134
|
+
fitView
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### With execution overlay (time-travel)
|
|
141
|
+
|
|
142
|
+
The overlay highlights which stages have executed, which is active, and the execution path — like a Google Maps route overlay.
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { specToReactFlow, type ExecutionOverlay } from "footprint-explainable-ui/flowchart";
|
|
146
|
+
|
|
147
|
+
// Build overlay from your current time-travel position
|
|
148
|
+
const overlay: ExecutionOverlay = {
|
|
149
|
+
doneStages: new Set(["LoadOrder", "ProcessPayment"]),
|
|
150
|
+
activeStage: "ShipOrder",
|
|
151
|
+
executedStages: new Set(["LoadOrder", "ProcessPayment", "ShipOrder"]),
|
|
152
|
+
executionOrder: ["LoadOrder", "ProcessPayment", "ShipOrder"],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const { nodes, edges } = specToReactFlow(spec, overlay);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Tip:** Compute the overlay from your snapshots array and current index:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
function buildOverlay(snapshots, idx): ExecutionOverlay {
|
|
162
|
+
const executionOrder = snapshots.slice(0, idx + 1).map(s => s.stageLabel);
|
|
163
|
+
const doneStages = new Set(snapshots.slice(0, idx).map(s => s.stageLabel));
|
|
164
|
+
const activeStage = snapshots[idx]?.stageLabel ?? null;
|
|
165
|
+
const executedStages = new Set([...doneStages]);
|
|
166
|
+
if (activeStage) executedStages.add(activeStage);
|
|
167
|
+
return { doneStages, activeStage, executedStages, executionOrder };
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Custom edge colors
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
const { nodes, edges } = specToReactFlow(spec, overlay, {
|
|
175
|
+
edgeExecuted: "#00ff88", // Completed path
|
|
176
|
+
edgeActive: "#ff6b6b", // Currently executing
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Subflow drill-down navigation
|
|
181
|
+
|
|
182
|
+
For pipelines with nested subflows, `useSubflowNavigation` manages a breadcrumb stack. Clicking a subflow node drills into its internal flowchart.
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import {
|
|
186
|
+
useSubflowNavigation,
|
|
187
|
+
SubflowBreadcrumb,
|
|
188
|
+
specToReactFlow,
|
|
189
|
+
StageNode,
|
|
190
|
+
} from "footprint-explainable-ui/flowchart";
|
|
191
|
+
import { ReactFlow } from "@xyflow/react";
|
|
192
|
+
|
|
193
|
+
const nodeTypes = { stage: StageNode };
|
|
194
|
+
|
|
195
|
+
function DrillDownChart({ spec, overlay }) {
|
|
196
|
+
const subflowNav = useSubflowNavigation(spec);
|
|
197
|
+
|
|
198
|
+
// Get the current level's spec from breadcrumbs
|
|
199
|
+
const currentSpec = subflowNav.breadcrumbs[subflowNav.breadcrumbs.length - 1].spec;
|
|
200
|
+
const { nodes, edges } = specToReactFlow(currentSpec, overlay);
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div>
|
|
204
|
+
{/* Breadcrumb bar: Pipeline > PaymentSubflow */}
|
|
205
|
+
{subflowNav.isInSubflow && (
|
|
206
|
+
<SubflowBreadcrumb
|
|
207
|
+
breadcrumbs={subflowNav.breadcrumbs}
|
|
208
|
+
onNavigate={subflowNav.navigateTo}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
<ReactFlow
|
|
213
|
+
nodes={nodes}
|
|
214
|
+
edges={edges}
|
|
215
|
+
nodeTypes={nodeTypes}
|
|
216
|
+
onNodeClick={(_, node) => subflowNav.handleNodeClick(node.id)}
|
|
217
|
+
fitView
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**`useSubflowNavigation` returns:**
|
|
225
|
+
|
|
226
|
+
| Property | Type | Description |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `breadcrumbs` | `BreadcrumbEntry[]` | Stack from root to current level |
|
|
229
|
+
| `nodes` | `Node[]` | ReactFlow nodes for current level |
|
|
230
|
+
| `edges` | `Edge[]` | ReactFlow edges for current level |
|
|
231
|
+
| `handleNodeClick` | `(nodeId) => boolean` | Drills into subflow if applicable |
|
|
232
|
+
| `navigateTo` | `(level) => void` | Jump to breadcrumb level (0 = root) |
|
|
233
|
+
| `isInSubflow` | `boolean` | Whether we're inside a subflow |
|
|
234
|
+
| `currentSubflowNodeName` | `string \| null` | Name of the subflow node drilled into |
|
|
235
|
+
|
|
236
|
+
### Extracting subflow execution data
|
|
237
|
+
|
|
238
|
+
When drilled into a subflow, extract its execution snapshots from the parent's memory:
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
import { toVisualizationSnapshots } from "footprint-explainable-ui";
|
|
242
|
+
|
|
243
|
+
// Find the parent stage that contains the subflow result
|
|
244
|
+
const parentSnap = parentSnapshots.find(s => s.stageLabel === subflowNav.currentSubflowNodeName);
|
|
245
|
+
const sfResult = parentSnap?.memory?.subflowResult;
|
|
246
|
+
const tc = sfResult?.treeContext;
|
|
247
|
+
|
|
248
|
+
if (tc?.stageContexts) {
|
|
249
|
+
const subflowSnapshots = toVisualizationSnapshots({
|
|
250
|
+
sharedState: tc.globalContext,
|
|
251
|
+
executionTree: tc.stageContexts,
|
|
252
|
+
commitLog: tc.history ?? [],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Strip builder's "subflowId/" prefix from stage names
|
|
256
|
+
const prefix = sfResult.subflowId ? `${sfResult.subflowId}/` : null;
|
|
257
|
+
if (prefix) {
|
|
258
|
+
for (const snap of subflowSnapshots) {
|
|
259
|
+
if (snap.stageLabel.startsWith(prefix))
|
|
260
|
+
snap.stageLabel = snap.stageLabel.slice(prefix.length);
|
|
261
|
+
if (snap.stageName.startsWith(prefix))
|
|
262
|
+
snap.stageName = snap.stageName.slice(prefix.length);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
73
269
|
|
|
74
|
-
|
|
270
|
+
## Theming
|
|
75
271
|
|
|
76
|
-
|
|
272
|
+
### ThemeProvider
|
|
77
273
|
|
|
78
274
|
```tsx
|
|
79
275
|
import { FootprintTheme, warmDark, warmLight, coolDark } from "footprint-explainable-ui";
|
|
80
276
|
|
|
81
|
-
// Use a built-in preset
|
|
82
277
|
<FootprintTheme tokens={warmDark}>
|
|
83
278
|
<MyApp />
|
|
84
279
|
</FootprintTheme>
|
|
85
|
-
|
|
86
|
-
// Or customize
|
|
87
|
-
<FootprintTheme tokens={{
|
|
88
|
-
colors: {
|
|
89
|
-
primary: "#e91e63",
|
|
90
|
-
bgPrimary: "#121212",
|
|
91
|
-
textPrimary: "#ffffff",
|
|
92
|
-
},
|
|
93
|
-
radius: "12px",
|
|
94
|
-
}}>
|
|
95
|
-
<MyApp />
|
|
96
|
-
</FootprintTheme>
|
|
97
280
|
```
|
|
98
281
|
|
|
99
|
-
###
|
|
100
|
-
|
|
101
|
-
Set `--fp-*` CSS variables directly — no provider needed:
|
|
282
|
+
### CSS Variables (no provider needed)
|
|
102
283
|
|
|
103
284
|
```css
|
|
104
285
|
:root {
|
|
@@ -115,7 +296,7 @@ Set `--fp-*` CSS variables directly — no provider needed:
|
|
|
115
296
|
### Built-in Presets
|
|
116
297
|
|
|
117
298
|
| Preset | Description |
|
|
118
|
-
|
|
299
|
+
|---|---|
|
|
119
300
|
| `coolDark` | Default — indigo/slate dark theme |
|
|
120
301
|
| `warmDark` | Charcoal-purple with warm text |
|
|
121
302
|
| `warmLight` | Cream/peach light theme |
|
|
@@ -125,55 +306,36 @@ Set `--fp-*` CSS variables directly — no provider needed:
|
|
|
125
306
|
```typescript
|
|
126
307
|
interface ThemeTokens {
|
|
127
308
|
colors?: {
|
|
128
|
-
primary?: string;
|
|
129
|
-
success?: string;
|
|
130
|
-
error?: string;
|
|
131
|
-
warning?: string;
|
|
132
|
-
bgPrimary?: string;
|
|
133
|
-
bgSecondary?: string
|
|
134
|
-
bgTertiary?: string;
|
|
135
|
-
textPrimary?: string
|
|
136
|
-
textSecondary?: string
|
|
137
|
-
textMuted?: string;
|
|
138
|
-
border?: string;
|
|
309
|
+
primary?: string; // Accent (buttons, highlights)
|
|
310
|
+
success?: string; // Completed stages
|
|
311
|
+
error?: string; // Error states
|
|
312
|
+
warning?: string; // Warnings
|
|
313
|
+
bgPrimary?: string; // Main background
|
|
314
|
+
bgSecondary?: string; // Panel/card background
|
|
315
|
+
bgTertiary?: string; // Hover/active background
|
|
316
|
+
textPrimary?: string; // Main text
|
|
317
|
+
textSecondary?: string; // Secondary text
|
|
318
|
+
textMuted?: string; // Dimmed text
|
|
319
|
+
border?: string; // Borders
|
|
139
320
|
};
|
|
140
|
-
radius?: string;
|
|
321
|
+
radius?: string;
|
|
141
322
|
fontFamily?: {
|
|
142
|
-
sans?: string;
|
|
143
|
-
mono?: string;
|
|
323
|
+
sans?: string; // UI text font
|
|
324
|
+
mono?: string; // Code/data font
|
|
144
325
|
};
|
|
145
326
|
}
|
|
146
327
|
```
|
|
147
328
|
|
|
148
|
-
|
|
329
|
+
---
|
|
149
330
|
|
|
150
|
-
|
|
331
|
+
## Components Reference
|
|
151
332
|
|
|
152
|
-
|
|
153
|
-
<GanttTimeline snapshots={snapshots} size="compact" />
|
|
154
|
-
<MemoryInspector snapshots={snapshots} size="detailed" />
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Unstyled Mode
|
|
158
|
-
|
|
159
|
-
Strip all built-in styles and bring your own. Components render semantic `data-fp` attributes for CSS targeting:
|
|
160
|
-
|
|
161
|
-
```tsx
|
|
162
|
-
<NarrativeTrace narrative={lines} unstyled className="my-narrative" />
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
```css
|
|
166
|
-
[data-fp="narrative-header"] { font-weight: bold; }
|
|
167
|
-
[data-fp="narrative-step"] { padding-left: 2rem; }
|
|
168
|
-
[data-fp="narrative-group"][data-latest="true"] { background: highlight; }
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
## Components
|
|
333
|
+
### Core Components
|
|
172
334
|
|
|
173
335
|
| Component | Description |
|
|
174
|
-
|
|
336
|
+
|---|---|
|
|
175
337
|
| `ExplainableShell` | Tabbed container: Result / Explainable / AI-Compatible |
|
|
176
|
-
| `TimeTravelControls` | Play/pause, prev/next,
|
|
338
|
+
| `TimeTravelControls` | Play/pause, prev/next, scrubber timeline |
|
|
177
339
|
| `NarrativeTrace` | Collapsible stage groups with progressive reveal |
|
|
178
340
|
| `NarrativeLog` | Simple timeline-style execution log |
|
|
179
341
|
| `ScopeDiff` | Side-by-side scope changes (added/changed/removed) |
|
|
@@ -182,107 +344,220 @@ Strip all built-in styles and bring your own. Components render semantic `data-f
|
|
|
182
344
|
| `GanttTimeline` | Horizontal duration timeline |
|
|
183
345
|
| `SnapshotPanel` | All-in-one inspector (scrubber + memory + narrative + Gantt) |
|
|
184
346
|
|
|
185
|
-
### Flowchart Components (
|
|
186
|
-
|
|
187
|
-
Requires `@xyflow/react` as a peer dependency. Import from `footprint-explainable-ui/flowchart`:
|
|
188
|
-
|
|
189
|
-
```bash
|
|
190
|
-
npm install @xyflow/react
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
```tsx
|
|
194
|
-
import {
|
|
195
|
-
FlowchartView,
|
|
196
|
-
StageNode,
|
|
197
|
-
specToReactFlow,
|
|
198
|
-
TimeTravelDebugger,
|
|
199
|
-
} from "footprint-explainable-ui/flowchart";
|
|
200
|
-
```
|
|
347
|
+
### Flowchart Components (`footprint-explainable-ui/flowchart`)
|
|
201
348
|
|
|
202
349
|
| Export | Description |
|
|
203
|
-
|
|
350
|
+
|---|---|
|
|
204
351
|
| `FlowchartView` | ReactFlow pipeline visualization with execution overlay |
|
|
205
352
|
| `StageNode` | Custom node with state-aware coloring, step badges, pulse rings |
|
|
206
353
|
| `specToReactFlow` | Convert pipeline spec → ReactFlow nodes/edges with path overlay |
|
|
207
354
|
| `TimeTravelDebugger` | Full debugger with flowchart + all panels |
|
|
355
|
+
| `SubflowBreadcrumb` | Breadcrumb bar for subflow drill-down |
|
|
356
|
+
| `useSubflowNavigation` | Hook managing subflow drill-down navigation stack |
|
|
208
357
|
|
|
209
|
-
###
|
|
358
|
+
### Adapters
|
|
210
359
|
|
|
211
|
-
|
|
360
|
+
| Export | Description |
|
|
361
|
+
|---|---|
|
|
362
|
+
| `toVisualizationSnapshots` | Convert `FlowChartExecutor.getSnapshot()` → `StageSnapshot[]` |
|
|
363
|
+
| `createSnapshots` | Build `StageSnapshot[]` from simple arrays (testing/custom data) |
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Size Variants
|
|
368
|
+
|
|
369
|
+
All components accept a `size` prop: `"compact"`, `"default"`, or `"detailed"`.
|
|
212
370
|
|
|
213
371
|
```tsx
|
|
214
|
-
|
|
215
|
-
|
|
372
|
+
<GanttTimeline snapshots={snapshots} size="compact" />
|
|
373
|
+
<MemoryInspector snapshots={snapshots} size="detailed" />
|
|
374
|
+
```
|
|
216
375
|
|
|
217
|
-
|
|
218
|
-
const { nodes, edges } = specToReactFlow(spec);
|
|
376
|
+
## Unstyled Mode
|
|
219
377
|
|
|
220
|
-
|
|
221
|
-
const overlay: ExecutionOverlay = {
|
|
222
|
-
doneStages: new Set(["ReceiveApp", "PullCredit"]),
|
|
223
|
-
activeStage: "CalculateDTI",
|
|
224
|
-
executedStages: new Set(["ReceiveApp", "PullCredit", "CalculateDTI"]),
|
|
225
|
-
executionOrder: ["ReceiveApp", "PullCredit", "CalculateDTI"],
|
|
226
|
-
};
|
|
227
|
-
const { nodes, edges } = specToReactFlow(spec, overlay);
|
|
378
|
+
Strip all built-in styles for full CSS control. Components render semantic `data-fp` attributes:
|
|
228
379
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
edgeExecuted: "#00ff88",
|
|
232
|
-
edgeActive: "#ff6b6b",
|
|
233
|
-
});
|
|
380
|
+
```tsx
|
|
381
|
+
<NarrativeTrace narrative={lines} unstyled className="my-narrative" />
|
|
234
382
|
```
|
|
235
383
|
|
|
236
|
-
|
|
384
|
+
```css
|
|
385
|
+
[data-fp="narrative-header"] { font-weight: bold; }
|
|
386
|
+
[data-fp="narrative-step"] { padding-left: 2rem; }
|
|
387
|
+
[data-fp="narrative-group"][data-latest="true"] { background: highlight; }
|
|
388
|
+
```
|
|
237
389
|
|
|
238
|
-
|
|
390
|
+
---
|
|
239
391
|
|
|
240
|
-
|
|
241
|
-
- **Default state**: uses `--fp-bg-secondary` background, `--fp-text-primary` text
|
|
242
|
-
- **Active/done/error**: uses `--fp-color-primary` / `success` / `error` background
|
|
243
|
-
- **Step badge**: shows execution order number on executed nodes
|
|
244
|
-
- **Font**: uses `--fp-font-sans` for label text
|
|
392
|
+
## Example: Build a Pipeline Playground
|
|
245
393
|
|
|
246
|
-
|
|
394
|
+
A complete example combining flowchart, time-travel controls, detail panel, and Gantt timeline — the same pattern used by the [FootPrint Playground](https://footprintjs.github.io/footprint-playground/).
|
|
247
395
|
|
|
248
396
|
```tsx
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
397
|
+
import { useState, useMemo } from "react";
|
|
398
|
+
import { ReactFlow } from "@xyflow/react";
|
|
399
|
+
import "@xyflow/react/dist/style.css";
|
|
400
|
+
import {
|
|
401
|
+
toVisualizationSnapshots,
|
|
402
|
+
GanttTimeline,
|
|
403
|
+
ScopeDiff,
|
|
404
|
+
NarrativeTrace,
|
|
405
|
+
MemoryInspector,
|
|
406
|
+
FootprintTheme,
|
|
407
|
+
warmDark,
|
|
408
|
+
} from "footprint-explainable-ui";
|
|
409
|
+
import {
|
|
410
|
+
StageNode,
|
|
411
|
+
specToReactFlow,
|
|
412
|
+
useSubflowNavigation,
|
|
413
|
+
SubflowBreadcrumb,
|
|
414
|
+
type ExecutionOverlay,
|
|
415
|
+
type SpecNode,
|
|
416
|
+
} from "footprint-explainable-ui/flowchart";
|
|
417
|
+
import { FlowChartExecutor } from "footprint";
|
|
418
|
+
|
|
419
|
+
const nodeTypes = { stage: StageNode };
|
|
420
|
+
|
|
421
|
+
// ─── Hook: time-travel + overlay + subflow drill-down ────────────────
|
|
422
|
+
function useFlowchartData(spec: SpecNode | null, vizSnapshots: any[] | null) {
|
|
423
|
+
const [snapshotIdx, setSnapshotIdx] = useState(0);
|
|
424
|
+
const subflowNav = useSubflowNavigation(spec);
|
|
425
|
+
|
|
426
|
+
const activeSnapshots = vizSnapshots; // extend with subflow logic as needed
|
|
427
|
+
|
|
428
|
+
// Compute execution overlay from current scrubber position
|
|
429
|
+
const overlay = useMemo<ExecutionOverlay | undefined>(() => {
|
|
430
|
+
if (!activeSnapshots) return undefined;
|
|
431
|
+
const executionOrder = activeSnapshots
|
|
432
|
+
.slice(0, snapshotIdx + 1)
|
|
433
|
+
.map((s) => s.stageLabel);
|
|
434
|
+
const doneStages = new Set(
|
|
435
|
+
activeSnapshots.slice(0, snapshotIdx).map((s) => s.stageLabel)
|
|
436
|
+
);
|
|
437
|
+
const activeStage = activeSnapshots[snapshotIdx]?.stageLabel ?? null;
|
|
438
|
+
const executedStages = new Set([...doneStages]);
|
|
439
|
+
if (activeStage) executedStages.add(activeStage);
|
|
440
|
+
return { doneStages, activeStage, executedStages, executionOrder };
|
|
441
|
+
}, [activeSnapshots, snapshotIdx]);
|
|
442
|
+
|
|
443
|
+
// Derive ReactFlow nodes/edges with overlay applied
|
|
444
|
+
const currentSpec =
|
|
445
|
+
subflowNav.breadcrumbs[subflowNav.breadcrumbs.length - 1]?.spec ?? null;
|
|
446
|
+
const flowData = useMemo(() => {
|
|
447
|
+
if (!currentSpec || !activeSnapshots) return null;
|
|
448
|
+
return specToReactFlow(currentSpec, overlay);
|
|
449
|
+
}, [currentSpec, activeSnapshots, overlay]);
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
subflowNav,
|
|
453
|
+
activeSnapshots,
|
|
454
|
+
snapshotIdx,
|
|
455
|
+
setSnapshotIdx,
|
|
456
|
+
currentSnap: activeSnapshots?.[snapshotIdx] ?? null,
|
|
457
|
+
flowData,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
255
460
|
|
|
256
|
-
|
|
461
|
+
// ─── Main component ──────────────────────────────────────────────────
|
|
462
|
+
function PipelinePlayground({ chart, spec }: { chart: any; spec: SpecNode }) {
|
|
463
|
+
const [snapshots, setSnapshots] = useState<any[] | null>(null);
|
|
257
464
|
|
|
258
|
-
|
|
259
|
-
|
|
465
|
+
async function run() {
|
|
466
|
+
const executor = new FlowChartExecutor(chart);
|
|
467
|
+
await executor.run();
|
|
468
|
+
setSnapshots(toVisualizationSnapshots(executor.getSnapshot()));
|
|
469
|
+
}
|
|
260
470
|
|
|
261
|
-
const
|
|
262
|
-
|
|
471
|
+
const { subflowNav, activeSnapshots, snapshotIdx, setSnapshotIdx, currentSnap, flowData } =
|
|
472
|
+
useFlowchartData(spec, snapshots);
|
|
263
473
|
|
|
264
|
-
|
|
474
|
+
return (
|
|
475
|
+
<FootprintTheme tokens={warmDark}>
|
|
476
|
+
<button onClick={run}>Run Pipeline</button>
|
|
477
|
+
|
|
478
|
+
{/* Flowchart with execution overlay */}
|
|
479
|
+
<div style={{ height: 400 }}>
|
|
480
|
+
{subflowNav.isInSubflow && (
|
|
481
|
+
<SubflowBreadcrumb
|
|
482
|
+
breadcrumbs={subflowNav.breadcrumbs}
|
|
483
|
+
onNavigate={subflowNav.navigateTo}
|
|
484
|
+
/>
|
|
485
|
+
)}
|
|
486
|
+
{flowData && (
|
|
487
|
+
<ReactFlow
|
|
488
|
+
nodes={flowData.nodes}
|
|
489
|
+
edges={flowData.edges}
|
|
490
|
+
nodeTypes={nodeTypes}
|
|
491
|
+
onNodeClick={(_, node) => subflowNav.handleNodeClick(node.id)}
|
|
492
|
+
fitView
|
|
493
|
+
/>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
{activeSnapshots && (
|
|
498
|
+
<>
|
|
499
|
+
{/* Time-travel scrubber */}
|
|
500
|
+
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
501
|
+
<button
|
|
502
|
+
disabled={snapshotIdx <= 0}
|
|
503
|
+
onClick={() => setSnapshotIdx((i) => i - 1)}
|
|
504
|
+
>
|
|
505
|
+
Prev
|
|
506
|
+
</button>
|
|
507
|
+
<input
|
|
508
|
+
type="range"
|
|
509
|
+
min={0}
|
|
510
|
+
max={activeSnapshots.length - 1}
|
|
511
|
+
value={snapshotIdx}
|
|
512
|
+
onChange={(e) => setSnapshotIdx(Number(e.target.value))}
|
|
513
|
+
/>
|
|
514
|
+
<button
|
|
515
|
+
disabled={snapshotIdx >= activeSnapshots.length - 1}
|
|
516
|
+
onClick={() => setSnapshotIdx((i) => i + 1)}
|
|
517
|
+
>
|
|
518
|
+
Next
|
|
519
|
+
</button>
|
|
520
|
+
<span>
|
|
521
|
+
{currentSnap?.stageLabel} ({snapshotIdx + 1}/{activeSnapshots.length})
|
|
522
|
+
</span>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
{/* Detail panels */}
|
|
526
|
+
<MemoryInspector
|
|
527
|
+
snapshots={activeSnapshots}
|
|
528
|
+
selectedIndex={snapshotIdx}
|
|
529
|
+
/>
|
|
530
|
+
<ScopeDiff
|
|
531
|
+
previous={snapshotIdx > 0 ? activeSnapshots[snapshotIdx - 1].memory : null}
|
|
532
|
+
current={currentSnap?.memory ?? {}}
|
|
533
|
+
hideUnchanged
|
|
534
|
+
/>
|
|
535
|
+
<NarrativeTrace
|
|
536
|
+
narrative={activeSnapshots.map((s) => s.narrative)}
|
|
537
|
+
/>
|
|
538
|
+
<GanttTimeline
|
|
539
|
+
snapshots={activeSnapshots}
|
|
540
|
+
selectedIndex={snapshotIdx}
|
|
541
|
+
onSelect={setSnapshotIdx}
|
|
542
|
+
/>
|
|
543
|
+
</>
|
|
544
|
+
)}
|
|
545
|
+
</FootprintTheme>
|
|
546
|
+
);
|
|
547
|
+
}
|
|
265
548
|
```
|
|
266
549
|
|
|
267
|
-
|
|
550
|
+
This gives you:
|
|
551
|
+
- Flowchart with Google Maps-style execution path overlay
|
|
552
|
+
- Click subflow nodes to drill down (breadcrumb navigation back)
|
|
553
|
+
- Prev/Next scrubber synced with flowchart highlighting
|
|
554
|
+
- Memory inspector, scope diffs, narrative trace, and Gantt timeline
|
|
555
|
+
- All themed via `FootprintTheme`
|
|
268
556
|
|
|
269
|
-
|
|
557
|
+
See the full implementation in the [footprint-playground](https://github.com/footprintjs/footprint-playground) repo.
|
|
270
558
|
|
|
271
|
-
|
|
272
|
-
- **Modal/dialog** — wrap in your modal component
|
|
273
|
-
- **Sidebar panel** — use `size="compact"` for narrow panels
|
|
274
|
-
- **Full-page dashboard** — use `ExplainableShell` with all tabs
|
|
275
|
-
- **Embedded widget** — use `unstyled` mode + custom CSS
|
|
559
|
+
---
|
|
276
560
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
<ExplainableShell snapshots={snapshots} narrative={narrative} size="compact" />
|
|
281
|
-
</Dialog>
|
|
282
|
-
|
|
283
|
-
// In a sidebar
|
|
284
|
-
<aside style={{ width: 320 }}>
|
|
285
|
-
<NarrativeTrace narrative={narrative} size="compact" />
|
|
286
|
-
<GanttTimeline snapshots={snapshots} size="compact" />
|
|
287
|
-
</aside>
|
|
288
|
-
```
|
|
561
|
+
## License
|
|
562
|
+
|
|
563
|
+
MIT
|