footprint-explainable-ui 0.3.1 → 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 +169 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -389,6 +389,175 @@ Strip all built-in styles for full CSS control. Components render semantic `data
|
|
|
389
389
|
|
|
390
390
|
---
|
|
391
391
|
|
|
392
|
+
## Example: Build a Pipeline Playground
|
|
393
|
+
|
|
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/).
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
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
|
+
}
|
|
460
|
+
|
|
461
|
+
// ─── Main component ──────────────────────────────────────────────────
|
|
462
|
+
function PipelinePlayground({ chart, spec }: { chart: any; spec: SpecNode }) {
|
|
463
|
+
const [snapshots, setSnapshots] = useState<any[] | null>(null);
|
|
464
|
+
|
|
465
|
+
async function run() {
|
|
466
|
+
const executor = new FlowChartExecutor(chart);
|
|
467
|
+
await executor.run();
|
|
468
|
+
setSnapshots(toVisualizationSnapshots(executor.getSnapshot()));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const { subflowNav, activeSnapshots, snapshotIdx, setSnapshotIdx, currentSnap, flowData } =
|
|
472
|
+
useFlowchartData(spec, snapshots);
|
|
473
|
+
|
|
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
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
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`
|
|
556
|
+
|
|
557
|
+
See the full implementation in the [footprint-playground](https://github.com/footprintjs/footprint-playground) repo.
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
392
561
|
## License
|
|
393
562
|
|
|
394
563
|
MIT
|