footprint-explainable-ui 0.18.1 → 0.20.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/adapters/fromRuntimeSnapshot.d.ts +33 -4
- package/dist/adapters/fromRuntimeSnapshot.d.ts.map +1 -1
- package/dist/adapters/fromRuntimeSnapshot.js +162 -33
- package/dist/adapters/fromRuntimeSnapshot.js.map +1 -1
- package/dist/components/ExplainableShell/ExplainableShell.d.ts +157 -14
- package/dist/components/ExplainableShell/ExplainableShell.d.ts.map +1 -1
- package/dist/components/ExplainableShell/ExplainableShell.js +676 -68
- package/dist/components/ExplainableShell/ExplainableShell.js.map +1 -1
- package/dist/components/ExplainableShell/index.d.ts +1 -1
- package/dist/components/ExplainableShell/index.d.ts.map +1 -1
- package/dist/components/FlowchartView/SubflowTree.d.ts +7 -14
- package/dist/components/FlowchartView/SubflowTree.d.ts.map +1 -1
- package/dist/components/FlowchartView/SubflowTree.js +56 -46
- package/dist/components/FlowchartView/SubflowTree.js.map +1 -1
- package/dist/components/FlowchartView/index.d.ts +32 -4
- package/dist/components/FlowchartView/index.d.ts.map +1 -1
- package/dist/components/FlowchartView/index.js +22 -2
- package/dist/components/FlowchartView/index.js.map +1 -1
- package/dist/components/FlowchartView/useSubflowNavigation.d.ts +41 -16
- package/dist/components/FlowchartView/useSubflowNavigation.d.ts.map +1 -1
- package/dist/components/FlowchartView/useSubflowNavigation.js +69 -50
- package/dist/components/FlowchartView/useSubflowNavigation.js.map +1 -1
- package/dist/components/GanttTimeline/GanttTimeline.d.ts.map +1 -1
- package/dist/components/GanttTimeline/GanttTimeline.js +5 -5
- package/dist/components/GanttTimeline/GanttTimeline.js.map +1 -1
- package/dist/components/MemoryInspector/MemoryInspector.d.ts.map +1 -1
- package/dist/components/MemoryInspector/MemoryInspector.js +36 -13
- package/dist/components/MemoryInspector/MemoryInspector.js.map +1 -1
- package/dist/components/NarrativeTrace/NarrativeTrace.d.ts.map +1 -1
- package/dist/components/NarrativeTrace/NarrativeTrace.js +24 -17
- package/dist/components/NarrativeTrace/NarrativeTrace.js.map +1 -1
- package/dist/components/ScopeDiff/ScopeDiff.js +3 -3
- package/dist/components/ScopeDiff/ScopeDiff.js.map +1 -1
- package/dist/components/StageNode/StageNode.d.ts +21 -0
- package/dist/components/StageNode/StageNode.d.ts.map +1 -1
- package/dist/components/StageNode/StageNode.js +189 -9
- package/dist/components/StageNode/StageNode.js.map +1 -1
- package/dist/components/TimeTravelControls/TimeTravelControls.d.ts.map +1 -1
- package/dist/components/TimeTravelControls/TimeTravelControls.js +19 -3
- package/dist/components/TimeTravelControls/TimeTravelControls.js.map +1 -1
- package/dist/components/TimeTravelDebugger/TimeTravelDebugger.d.ts +19 -8
- package/dist/components/TimeTravelDebugger/TimeTravelDebugger.d.ts.map +1 -1
- package/dist/components/TimeTravelDebugger/TimeTravelDebugger.js +23 -8
- package/dist/components/TimeTravelDebugger/TimeTravelDebugger.js.map +1 -1
- package/dist/flowchart.cjs +3512 -1341
- package/dist/flowchart.cjs.map +1 -1
- package/dist/flowchart.d.cts +1682 -176
- package/dist/flowchart.d.ts +1682 -176
- package/dist/flowchart.d.ts.map +1 -1
- package/dist/flowchart.js +3553 -1404
- package/dist/flowchart.js.map +1 -1
- package/dist/index.cjs +755 -556
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +277 -29
- package/dist/index.d.ts +277 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +748 -546
- package/dist/index.js.map +1 -1
- package/dist/theme/ThemeProvider.d.ts +14 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -1
- package/dist/theme/ThemeProvider.js +15 -1
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/index.d.ts +4 -2
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +3 -2
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/presets.d.ts +3 -0
- package/dist/theme/presets.d.ts.map +1 -1
- package/dist/theme/presets.js +22 -0
- package/dist/theme/presets.js.map +1 -1
- package/dist/theme/tokens.d.ts +22 -1
- package/dist/theme/tokens.d.ts.map +1 -1
- package/dist/theme/tokens.js +23 -2
- package/dist/theme/tokens.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/components/FlowchartView/FlowchartView.d.ts +0 -20
- package/dist/components/FlowchartView/FlowchartView.d.ts.map +0 -1
- package/dist/components/FlowchartView/FlowchartView.js +0 -80
- package/dist/components/FlowchartView/FlowchartView.js.map +0 -1
- package/dist/components/FlowchartView/TracedFlowchartView.d.ts +0 -20
- package/dist/components/FlowchartView/TracedFlowchartView.d.ts.map +0 -1
- package/dist/components/FlowchartView/TracedFlowchartView.js +0 -101
- package/dist/components/FlowchartView/TracedFlowchartView.js.map +0 -1
- package/dist/components/FlowchartView/specToReactFlow.d.ts +0 -56
- package/dist/components/FlowchartView/specToReactFlow.d.ts.map +0 -1
- package/dist/components/FlowchartView/specToReactFlow.js +0 -202
- package/dist/components/FlowchartView/specToReactFlow.js.map +0 -1
|
@@ -1,75 +1,660 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ExplainableShell — Pure orchestrator for explainable pipeline visualization.
|
|
4
|
+
*
|
|
5
|
+
* Collapsible sections use the **line + centered pill** pattern:
|
|
6
|
+
* - Collapsed = thin divider line with a pill button sitting on it
|
|
7
|
+
* - Expanded = full content with a pill at the closing edge
|
|
8
|
+
*
|
|
9
|
+
* Sub-components are memo'd to minimize re-renders when scrubbing the
|
|
10
|
+
* time-travel slider. Only components that depend on snapshotIdx re-render.
|
|
11
|
+
*
|
|
12
|
+
* Consumer controls theme via --fp-* CSS custom properties.
|
|
13
|
+
*/
|
|
14
|
+
import { memo, useState, useCallback, useMemo, useRef, useEffect } from "react";
|
|
15
|
+
import { theme } from "../../theme";
|
|
16
|
+
import { extractSubflowNarrative } from "../../utils/narrativeSync";
|
|
17
|
+
import { toVisualizationSnapshots, subflowResultToSnapshots } from "../../adapters/fromRuntimeSnapshot";
|
|
4
18
|
import { ResultPanel } from "../ResultPanel";
|
|
5
19
|
import { GanttTimeline } from "../GanttTimeline";
|
|
6
|
-
import { MemoryInspector } from "../MemoryInspector";
|
|
7
|
-
import { NarrativeTrace } from "../NarrativeTrace";
|
|
8
|
-
import { ScopeDiff } from "../ScopeDiff";
|
|
9
20
|
import { TimeTravelControls } from "../TimeTravelControls";
|
|
21
|
+
import { MemoryPanel } from "../MemoryPanel";
|
|
22
|
+
import { NarrativePanel } from "../NarrativePanel";
|
|
23
|
+
import { SubflowTree } from "../FlowchartView/SubflowTree";
|
|
24
|
+
import { SubflowBreadcrumb } from "../FlowchartView/SubflowBreadcrumb";
|
|
25
|
+
import { TracedFlow } from "../FlowchartView/TracedFlow";
|
|
26
|
+
import { InspectorPanel } from "../InspectorPanel/InspectorPanel";
|
|
27
|
+
import { InsightPanel } from "../InsightPanel/InsightPanel";
|
|
28
|
+
import { CompactTimeline } from "../CompactTimeline/CompactTimeline";
|
|
10
29
|
// ---------------------------------------------------------------------------
|
|
11
|
-
//
|
|
30
|
+
// Line + Pill — collapsed state is just a line with a pill centered on it
|
|
12
31
|
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
/** Horizontal line with centered pill (for top/bottom edges) */
|
|
33
|
+
const HLinePill = memo(function HLinePill({ label, detail, expanded, onClick, }) {
|
|
34
|
+
return (_jsxs("div", { style: {
|
|
35
|
+
display: "flex",
|
|
36
|
+
alignItems: "center",
|
|
37
|
+
gap: 0,
|
|
38
|
+
padding: "0",
|
|
39
|
+
}, children: [_jsx("div", { style: { flex: 1, height: 1, background: theme.border } }), _jsxs("button", { onClick: onClick, style: {
|
|
40
|
+
display: "flex",
|
|
41
|
+
alignItems: "center",
|
|
42
|
+
gap: 5,
|
|
43
|
+
padding: "3px 12px",
|
|
44
|
+
margin: "4px 0",
|
|
45
|
+
fontSize: 10,
|
|
46
|
+
fontWeight: 600,
|
|
47
|
+
fontFamily: "inherit",
|
|
48
|
+
color: theme.textMuted,
|
|
49
|
+
background: theme.bgSecondary,
|
|
50
|
+
border: `1px solid ${theme.border}`,
|
|
51
|
+
borderRadius: 10,
|
|
52
|
+
cursor: "pointer",
|
|
53
|
+
whiteSpace: "nowrap",
|
|
54
|
+
letterSpacing: "0.04em",
|
|
55
|
+
textTransform: "uppercase",
|
|
56
|
+
transition: "color 0.15s ease",
|
|
57
|
+
}, children: [_jsx("span", { style: { fontSize: 7 }, children: expanded ? "▼" : "▶" }), label, detail && _jsx("span", { style: { fontWeight: 400, opacity: 0.5, fontSize: 9 }, children: detail })] }), _jsx("div", { style: { flex: 1, height: 1, background: theme.border } })] }));
|
|
58
|
+
});
|
|
59
|
+
/** Vertical line with centered pill (for left/right edges).
|
|
60
|
+
* `side` controls arrow direction:
|
|
61
|
+
* - "right": expanded=▶ collapsed=◀ (panel is on right, collapses right)
|
|
62
|
+
* - "left": expanded=◀ collapsed=▶ (panel is on left, collapses left)
|
|
63
|
+
*/
|
|
64
|
+
const VLinePill = memo(function VLinePill({ label, expanded, side = "right", onClick, }) {
|
|
65
|
+
const arrow = side === "right"
|
|
66
|
+
? (expanded ? "▶" : "◀")
|
|
67
|
+
: (expanded ? "◀" : "▶");
|
|
68
|
+
return (_jsxs("div", { style: {
|
|
69
|
+
display: "flex",
|
|
70
|
+
flexDirection: "column",
|
|
71
|
+
alignItems: "center",
|
|
72
|
+
gap: 0,
|
|
73
|
+
padding: "0",
|
|
74
|
+
}, children: [_jsx("div", { style: { flex: 1, width: 1, background: theme.border } }), _jsxs("button", { onClick: onClick, style: {
|
|
75
|
+
display: "flex",
|
|
76
|
+
alignItems: "center",
|
|
77
|
+
gap: 4,
|
|
78
|
+
padding: "10px 4px",
|
|
79
|
+
margin: "0 3px",
|
|
80
|
+
fontSize: 10,
|
|
81
|
+
fontWeight: 600,
|
|
82
|
+
fontFamily: "inherit",
|
|
83
|
+
color: theme.textMuted,
|
|
84
|
+
background: theme.bgSecondary,
|
|
85
|
+
border: `1px solid ${theme.border}`,
|
|
86
|
+
borderRadius: 10,
|
|
87
|
+
cursor: "pointer",
|
|
88
|
+
whiteSpace: "nowrap",
|
|
89
|
+
letterSpacing: "0.04em",
|
|
90
|
+
textTransform: "uppercase",
|
|
91
|
+
writingMode: "vertical-lr",
|
|
92
|
+
transition: "color 0.15s ease",
|
|
93
|
+
}, children: [_jsx("span", { style: { fontSize: 7, writingMode: "horizontal-tb" }, children: arrow }), label] }), _jsx("div", { style: { flex: 1, width: 1, background: theme.border } })] }));
|
|
94
|
+
});
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// KeyedRecorderView — Time-travel aware renderer for auto-detected recorders
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
/**
|
|
99
|
+
* Detects if data has a keyed-recorder shape: an object property whose values
|
|
100
|
+
* are objects with at least one numeric field. Returns { steps, keyType }.
|
|
101
|
+
* keyType: 'runtimeStageId' (keys contain '#') or 'stageName' (plain names).
|
|
102
|
+
*/
|
|
103
|
+
function detectKeyedSteps(data) {
|
|
104
|
+
if (!data || typeof data !== "object")
|
|
105
|
+
return null;
|
|
106
|
+
const obj = data;
|
|
107
|
+
for (const val of Object.values(obj)) {
|
|
108
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
109
|
+
const entries = Object.entries(val);
|
|
110
|
+
if (entries.length === 0)
|
|
111
|
+
continue;
|
|
112
|
+
// Check: values must be objects with at least one numeric field
|
|
113
|
+
const allObjectsWithNumbers = entries.every(([, v]) => {
|
|
114
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
115
|
+
return false;
|
|
116
|
+
return Object.values(v).some((f) => typeof f === "number");
|
|
117
|
+
});
|
|
118
|
+
if (allObjectsWithNumbers) {
|
|
119
|
+
const keyType = entries.some(([k]) => k.includes("#")) ? "runtimeStageId" : "stageName";
|
|
120
|
+
return { steps: val, keyType };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/** Extract render hints from recorder data: numericField name + grandTotal. */
|
|
127
|
+
function extractRenderHints(data) {
|
|
128
|
+
if (!data || typeof data !== "object")
|
|
129
|
+
return null;
|
|
130
|
+
const obj = data;
|
|
131
|
+
if (typeof obj.numericField === "string" && typeof obj.grandTotal === "number") {
|
|
132
|
+
return { numericField: obj.numericField, grandTotal: obj.grandTotal };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function KeyedRecorderView({ data, description, preferredOperation = "accumulate", snapshots, selectedIndex, }) {
|
|
137
|
+
const [showAggregate, setShowAggregate] = useState(false);
|
|
138
|
+
const detected = useMemo(() => detectKeyedSteps(data), [data]);
|
|
139
|
+
// Visible keys up to slider position — match by runtimeStageId or stageName
|
|
140
|
+
const visibleKeys = useMemo(() => {
|
|
141
|
+
const keys = new Set();
|
|
142
|
+
for (let i = 0; i <= selectedIndex && i < snapshots.length; i++) {
|
|
143
|
+
const snap = snapshots[i];
|
|
144
|
+
if (detected?.keyType === "runtimeStageId") {
|
|
145
|
+
if (snap.runtimeStageId)
|
|
146
|
+
keys.add(snap.runtimeStageId);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Match by stageName or stageLabel
|
|
150
|
+
if (snap.stageName)
|
|
151
|
+
keys.add(snap.stageName);
|
|
152
|
+
if (snap.stageLabel)
|
|
153
|
+
keys.add(snap.stageLabel);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return keys;
|
|
157
|
+
}, [snapshots, selectedIndex, detected?.keyType]);
|
|
158
|
+
const isAtEnd = selectedIndex >= snapshots.length - 1;
|
|
159
|
+
if (!detected) {
|
|
160
|
+
// Fallback: raw JSON for non-keyed data
|
|
161
|
+
return (_jsx("div", { style: { padding: 12, fontFamily: theme.fontMono, fontSize: 11, whiteSpace: "pre-wrap", overflow: "auto", height: "100%" }, children: typeof data === "string" ? data : JSON.stringify(data, null, 2) }));
|
|
162
|
+
}
|
|
163
|
+
const steps = detected.steps;
|
|
164
|
+
const hints = extractRenderHints(data);
|
|
165
|
+
const numFieldKey = hints?.numericField ?? "";
|
|
166
|
+
// Progressive entries (accumulate)
|
|
167
|
+
const allKeys = Object.keys(steps);
|
|
168
|
+
const visibleEntries = allKeys.filter((k) => visibleKeys.has(k));
|
|
169
|
+
// Running total — computed from visible entries using the declared numeric field
|
|
170
|
+
let runningTotal = 0;
|
|
171
|
+
if (numFieldKey) {
|
|
172
|
+
for (const k of visibleEntries) {
|
|
173
|
+
runningTotal += steps[k][numFieldKey] ?? 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Grand total — provided by the recorder, not recomputed
|
|
177
|
+
const grandTotal = hints?.grandTotal ?? 0;
|
|
178
|
+
return (_jsxs("div", { style: { overflow: "auto", height: "100%", display: "flex", flexDirection: "column" }, children: [description && (_jsx("div", { style: { padding: "6px 12px", fontSize: 11, color: theme.textMuted, fontStyle: "italic", borderBottom: `1px solid ${theme.border}`, flexShrink: 0 }, children: description })), _jsxs("div", { style: { padding: 12, flex: 1, overflow: "auto" }, children: [preferredOperation === "aggregate" ? (
|
|
179
|
+
/* AGGREGATE: collect silently during scrub, button at end to reveal total */
|
|
180
|
+
_jsxs(_Fragment, { children: [isAtEnd ? (_jsx("div", { style: { marginBottom: 16 }, children: !showAggregate ? (_jsx("button", { onClick: () => setShowAggregate(true), style: {
|
|
181
|
+
background: theme.primary, color: "#fff", border: "none", borderRadius: 8,
|
|
182
|
+
padding: "12px 20px", fontSize: 13, fontWeight: 600, cursor: "pointer",
|
|
183
|
+
fontFamily: "inherit", width: "100%",
|
|
184
|
+
}, children: "Aggregate \u2014 Show Grand Total" })) : (_jsxs("div", { style: { padding: "14px 16px", background: `color-mix(in srgb, ${theme.success} 12%, transparent)`, borderRadius: 8, border: `1px solid ${theme.success}44` }, children: [_jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6, fontWeight: 600 }, children: "Aggregate \u2014 grand total" }), numFieldKey && (_jsxs("div", { style: { fontSize: 26, fontWeight: 700, color: theme.success }, children: [grandTotal < 1 ? grandTotal.toFixed(3) : grandTotal.toFixed(1), _jsxs("span", { style: { fontSize: 11, color: theme.textMuted, fontWeight: 400, marginLeft: 8 }, children: [numFieldKey, " \u00B7 ", allKeys.length, " steps"] })] }))] })) })) : (_jsxs("div", { style: { padding: "10px 14px", background: `color-mix(in srgb, ${theme.textMuted} 6%, transparent)`, borderRadius: 6, marginBottom: 16, border: `1px dashed ${theme.border}` }, children: [_jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", fontWeight: 600 }, children: "Collecting data..." }), _jsxs("div", { style: { fontSize: 11, color: theme.textMuted, marginTop: 4 }, children: [visibleEntries.length, " of ", allKeys.length, " steps collected. Scrub to end to aggregate."] })] })), _jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6, fontWeight: 600 }, children: "Per-step detail" })] })) : preferredOperation === "accumulate" ? (
|
|
185
|
+
/* ACCUMULATE: running total grows with slider — IS the total at end, no button */
|
|
186
|
+
_jsxs(_Fragment, { children: [numFieldKey && visibleEntries.length > 0 && (_jsxs("div", { style: { padding: "10px 14px", background: `color-mix(in srgb, ${theme.primary} 8%, transparent)`, borderRadius: 6, marginBottom: 16 }, children: [_jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 4, fontWeight: 600 }, children: "Accumulate \u2014 running total up to this step" }), _jsx("span", { style: { fontWeight: 700, fontSize: 18, color: theme.primary }, children: runningTotal < 1 ? runningTotal.toFixed(3) : runningTotal.toFixed(1) }), _jsxs("span", { style: { color: theme.textMuted, marginLeft: 8, fontSize: 10 }, children: [numFieldKey, " \u00B7 ", visibleEntries.length, " of ", allKeys.length, " steps"] })] })), _jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6, fontWeight: 600 }, children: "Per-step detail" })] })) : (
|
|
187
|
+
/* TRANSLATE: per-step entries prominent, no totals */
|
|
188
|
+
_jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6, fontWeight: 600 }, children: "Translate \u2014 per-step detail" })), visibleEntries.map((key) => {
|
|
189
|
+
const entry = steps[key];
|
|
190
|
+
const label = entry.stageName ?? key;
|
|
191
|
+
const numVal = numFieldKey ? entry[numFieldKey] : undefined;
|
|
192
|
+
return (_jsxs("div", { style: { display: "flex", alignItems: "center", padding: "4px 0", fontSize: 12, fontFamily: theme.fontMono, borderBottom: `1px solid ${theme.border}22` }, children: [_jsx("span", { style: { color: theme.textMuted, width: 140, flexShrink: 0, fontSize: 10 }, children: key }), _jsx("span", { style: { fontWeight: 600, flex: 1 }, children: label }), numVal !== undefined && (_jsx("span", { style: { color: theme.primary, fontWeight: 700, marginLeft: 8 }, children: numVal < 1 ? numVal.toFixed(3) : numVal.toFixed(1) }))] }, key));
|
|
193
|
+
}), visibleEntries.length === 0 && (_jsx("div", { style: { color: theme.textMuted, fontSize: 11, fontStyle: "italic", padding: "8px 0" }, children: "Scrub the slider to reveal entries..." }))] })] }));
|
|
194
|
+
}
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// DetailsContent — Recorder-driven tab switcher (Memory + Narrative are defaults)
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
const DetailsContent = memo(function DetailsContent({ snapshots, selectedIndex, narrativeEntries, size, fillHeight, extraViews, }) {
|
|
199
|
+
// Built-in views (always available)
|
|
200
|
+
const builtInViews = [
|
|
201
|
+
{
|
|
202
|
+
id: "memory",
|
|
203
|
+
name: "Memory",
|
|
204
|
+
render: ({ snapshots: snaps, selectedIndex: idx }) => (_jsx(MemoryPanel, { snapshots: snaps, selectedIndex: idx, size: size, style: fillHeight ? { height: "100%" } : undefined })),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "narrative",
|
|
208
|
+
name: "Narrative",
|
|
209
|
+
render: ({ snapshots: snaps, selectedIndex: idx }) => (_jsx(NarrativePanel, { snapshots: snaps, selectedIndex: idx, narrativeEntries: narrativeEntries, size: size, style: fillHeight ? { height: "100%" } : undefined })),
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
const allViews = [...builtInViews, ...(extraViews ?? [])];
|
|
213
|
+
const [activeViewId, setActiveViewId] = useState(allViews[0]?.id ?? "memory");
|
|
214
|
+
// Reset tab when available views change (e.g., recorder toggled on/off)
|
|
215
|
+
const viewIds = allViews.map((v) => v.id).join(",");
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (!allViews.find((v) => v.id === activeViewId)) {
|
|
218
|
+
setActiveViewId(allViews[0]?.id ?? "memory");
|
|
219
|
+
}
|
|
220
|
+
}, [viewIds]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
221
|
+
const activeView = allViews.find((v) => v.id === activeViewId) ?? allViews[0];
|
|
222
|
+
return (_jsxs("div", { style: { flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }, children: [_jsx("div", { style: { display: "flex", borderBottom: `1px solid ${theme.border}`, flexShrink: 0, overflowX: "auto" }, children: allViews.map((view) => {
|
|
223
|
+
const active = view.id === activeViewId;
|
|
224
|
+
return (_jsx("button", { onClick: () => setActiveViewId(view.id), style: {
|
|
225
|
+
flex: allViews.length <= 3 ? 1 : undefined,
|
|
226
|
+
padding: "6px 8px", fontSize: 11,
|
|
227
|
+
fontWeight: active ? 600 : 400,
|
|
228
|
+
color: active ? theme.primary : theme.textMuted,
|
|
229
|
+
background: active ? `color-mix(in srgb, ${theme.primary} 8%, transparent)` : "transparent",
|
|
230
|
+
border: "none",
|
|
231
|
+
borderBottom: active ? `2px solid ${theme.primary}` : "2px solid transparent",
|
|
232
|
+
cursor: "pointer", textTransform: "uppercase", letterSpacing: "0.06em", fontFamily: "inherit",
|
|
233
|
+
whiteSpace: "nowrap",
|
|
234
|
+
}, children: view.name }, view.id));
|
|
235
|
+
}) }), _jsx("div", { style: { flex: 1, overflow: "auto" }, children: activeView?.render({ snapshots, selectedIndex }) })] }));
|
|
236
|
+
});
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Subflow resolution helpers
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
function resolveSubflowLevel(parentSpec, parentSnapshots, subflowNodeName, narrativeEntries) {
|
|
241
|
+
const specNode = findSubflowSpecNode(parentSpec, subflowNodeName);
|
|
242
|
+
if (!specNode?.subflowStructure)
|
|
243
|
+
return null;
|
|
244
|
+
const parentSnap = parentSnapshots.find((s) => s.stageName === subflowNodeName || s.stageLabel === subflowNodeName);
|
|
245
|
+
if (!parentSnap?.subflowResult)
|
|
246
|
+
return null;
|
|
247
|
+
// Extract subflow narrative: prefer subflowId (structured), fall back to display name (text scan)
|
|
248
|
+
const sfId = specNode.subflowId ?? subflowNodeName;
|
|
249
|
+
const sfDisplayName = specNode.subflowName ?? specNode.name;
|
|
250
|
+
const sfNarrative = narrativeEntries
|
|
251
|
+
? extractSubflowNarrative(narrativeEntries, sfId, sfDisplayName)
|
|
252
|
+
: undefined;
|
|
253
|
+
const sfSnapshots = subflowResultToSnapshots(parentSnap.subflowResult, sfNarrative);
|
|
254
|
+
if (sfSnapshots.length === 0)
|
|
255
|
+
return null;
|
|
256
|
+
return {
|
|
257
|
+
subflowId: specNode.subflowId ?? subflowNodeName,
|
|
258
|
+
label: specNode.subflowName ?? specNode.name,
|
|
259
|
+
spec: specNode.subflowStructure,
|
|
260
|
+
snapshots: sfSnapshots,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function findSubflowSpecNode(node, name) {
|
|
264
|
+
if ((node.name === name || node.id === name) && node.isSubflowRoot)
|
|
265
|
+
return node;
|
|
266
|
+
if (node.children) {
|
|
267
|
+
for (const child of node.children) {
|
|
268
|
+
const f = findSubflowSpecNode(child, name);
|
|
269
|
+
if (f)
|
|
270
|
+
return f;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (node.next)
|
|
274
|
+
return findSubflowSpecNode(node.next, name);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function hasSubflowNodes(node) {
|
|
278
|
+
if (!node)
|
|
279
|
+
return false;
|
|
280
|
+
if (node.isSubflowRoot)
|
|
281
|
+
return true;
|
|
282
|
+
if (node.children?.some((c) => c && hasSubflowNodes(c)))
|
|
283
|
+
return true;
|
|
284
|
+
if (node.next && hasSubflowNodes(node.next))
|
|
285
|
+
return true;
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
function buildDataTrace(commitLog, targetRuntimeStageId, maxDepth = 10) {
|
|
289
|
+
const log = commitLog;
|
|
290
|
+
if (!log?.length)
|
|
291
|
+
return [];
|
|
292
|
+
const idxMap = new Map();
|
|
293
|
+
for (let i = 0; i < log.length; i++)
|
|
294
|
+
idxMap.set(log[i].runtimeStageId, i);
|
|
295
|
+
const startIdx = idxMap.get(targetRuntimeStageId);
|
|
296
|
+
if (startIdx === undefined)
|
|
297
|
+
return [];
|
|
298
|
+
const startCommit = log[startIdx];
|
|
299
|
+
const frames = [];
|
|
300
|
+
const visited = new Set();
|
|
301
|
+
// BFS backward: for each commit, find what keys existed before it that it might have read
|
|
302
|
+
// Simplified: trace the write chain backward — each commit's written keys link to whoever wrote the keys it implicitly depends on
|
|
303
|
+
let current = startCommit;
|
|
304
|
+
let currentIdx = startIdx;
|
|
305
|
+
let depth = 0;
|
|
306
|
+
while (current && depth <= maxDepth) {
|
|
307
|
+
if (visited.has(current.runtimeStageId))
|
|
308
|
+
break;
|
|
309
|
+
visited.add(current.runtimeStageId);
|
|
310
|
+
frames.push({
|
|
311
|
+
runtimeStageId: current.runtimeStageId,
|
|
312
|
+
stageId: current.stageId,
|
|
313
|
+
stageName: current.stage,
|
|
314
|
+
keysWritten: current.trace.map((t) => t.path),
|
|
315
|
+
linkedBy: depth === 0 ? "" : current.trace[0]?.path ?? "",
|
|
316
|
+
depth,
|
|
317
|
+
});
|
|
318
|
+
// Find the previous commit (the one right before this one)
|
|
319
|
+
if (currentIdx > 0) {
|
|
320
|
+
currentIdx--;
|
|
321
|
+
current = log[currentIdx];
|
|
322
|
+
depth++;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return frames;
|
|
329
|
+
}
|
|
330
|
+
const RightPanel = memo(function RightPanel({ mode, onModeChange, snapshots, selectedIndex, runtimeSnapshot, spec, activeTab, allTabs, activeNarrativeEntries, recorderViews, autoRecorderViews, size, onNavigateToStage, }) {
|
|
331
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
332
|
+
display: "flex",
|
|
333
|
+
borderBottom: `1px solid ${theme.border}`,
|
|
334
|
+
flexShrink: 0,
|
|
335
|
+
background: theme.bgSecondary,
|
|
336
|
+
}, children: ["insights", "what"].map((m) => (_jsx("button", { onClick: () => onModeChange(m), style: {
|
|
337
|
+
flex: 1,
|
|
338
|
+
padding: "7px 12px",
|
|
339
|
+
fontSize: 11,
|
|
340
|
+
fontWeight: mode === m ? 700 : 500,
|
|
341
|
+
textTransform: "uppercase",
|
|
342
|
+
letterSpacing: "0.06em",
|
|
343
|
+
color: mode === m ? theme.primary : theme.textMuted,
|
|
344
|
+
background: "transparent",
|
|
345
|
+
border: "none",
|
|
346
|
+
borderBottom: mode === m ? `2px solid ${theme.primary}` : "2px solid transparent",
|
|
347
|
+
cursor: "pointer",
|
|
348
|
+
fontFamily: "inherit",
|
|
349
|
+
}, children: m === "insights" ? "Insights" : "Inspector" }, m))) }), _jsx("div", { style: { flex: 1, overflow: "hidden" }, children: mode === "insights" ? (_jsx(InsightPanel, { mode: "tabs", expandedId: activeTab, insights: allTabs.filter((t) => t.id !== "result" && t.id !== "memory").map((tab) => ({
|
|
350
|
+
id: tab.id,
|
|
351
|
+
name: insightName(tab.name),
|
|
352
|
+
render: () => {
|
|
353
|
+
if (tab.id === "narrative")
|
|
354
|
+
return _jsx(NarrativePanel, { snapshots: snapshots, selectedIndex: selectedIndex, narrativeEntries: activeNarrativeEntries, runtimeSnapshot: runtimeSnapshot, spec: spec, size: size, style: { height: "100%" } });
|
|
355
|
+
const customView = recorderViews?.find((v) => v.id === tab.id);
|
|
356
|
+
if (customView?.render)
|
|
357
|
+
return customView.render({ snapshots, selectedIndex });
|
|
358
|
+
const autoView = autoRecorderViews.find((v) => v.id === tab.id);
|
|
359
|
+
if (autoView)
|
|
360
|
+
return _jsx(KeyedRecorderView, { data: autoView.data, description: autoView.description, preferredOperation: autoView.preferredOperation, snapshots: snapshots, selectedIndex: selectedIndex });
|
|
361
|
+
return null;
|
|
362
|
+
},
|
|
363
|
+
})) })) : (_jsx(InspectorPanel, { snapshots: snapshots, selectedIndex: selectedIndex, dataTraceFrames: runtimeSnapshot?.commitLog ? buildDataTrace(runtimeSnapshot.commitLog, snapshots[selectedIndex]?.runtimeStageId ?? '') : [], selectedStageId: snapshots[selectedIndex]?.runtimeStageId, onNavigateToStage: onNavigateToStage })) })] }));
|
|
364
|
+
});
|
|
365
|
+
/** Map internal recorder names to user-facing Insight names. */
|
|
366
|
+
function insightName(name) {
|
|
367
|
+
const map = {
|
|
368
|
+
"Narrative": "Story",
|
|
369
|
+
"Memory": "State",
|
|
370
|
+
"Metrics": "Performance",
|
|
371
|
+
"Quality": "Quality",
|
|
372
|
+
"Cost": "Cost",
|
|
373
|
+
};
|
|
374
|
+
return map[name] ?? name;
|
|
375
|
+
}
|
|
376
|
+
export function ExplainableShell({ snapshots: snapshotsProp, runtimeSnapshot, spec, title, resultData: resultDataProp, logs = [], narrativeEntries, tabs = ["result", "explainable"], defaultTab, hideConsole = false, hideTabs: hideTabsProp, panelLabels, defaultExpanded, recorderViews, renderFlowchart, showStageId = false, traceGraph, runtimeOverlay, size = "default", unstyled = false, className, style, }) {
|
|
377
|
+
// Convert runtimeSnapshot → visualization snapshots (zero-boilerplate mode)
|
|
378
|
+
const derivedFromRuntime = useMemo(() => {
|
|
379
|
+
if (!runtimeSnapshot)
|
|
380
|
+
return null;
|
|
381
|
+
try {
|
|
382
|
+
const snaps = toVisualizationSnapshots(runtimeSnapshot, narrativeEntries);
|
|
383
|
+
return { snapshots: snaps, resultData: runtimeSnapshot.sharedState };
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}, [runtimeSnapshot, narrativeEntries]);
|
|
389
|
+
// Use derived data when runtimeSnapshot is provided, otherwise use explicit props
|
|
390
|
+
const snapshots = snapshotsProp ?? derivedFromRuntime?.snapshots ?? [];
|
|
391
|
+
const resultData = resultDataProp ?? derivedFromRuntime?.resultData ?? null;
|
|
392
|
+
// Flowchart renderer selection (v6+ — recorder-driven only):
|
|
393
|
+
// - explicit `renderFlowchart` always wins (consumer override)
|
|
394
|
+
// - `traceGraph` → render via `<TracedFlow>` (event-driven graph
|
|
395
|
+
// + optional runtime overlay, no spec-tree post-walk)
|
|
396
|
+
//
|
|
397
|
+
// Consumers MUST pass `traceGraph` for chart visualization. The
|
|
398
|
+
// legacy `spec={...}` → post-walk fallback was removed when the
|
|
399
|
+
// recorder gained convergence-edge expansion (post-fork `next`
|
|
400
|
+
// fires N edges, one per branch child) — the recorder graph is now
|
|
401
|
+
// the single source of truth.
|
|
402
|
+
const tracedFlowRenderer = useMemo(() => {
|
|
403
|
+
if (!traceGraph)
|
|
404
|
+
return undefined;
|
|
405
|
+
return ({ selectedIndex, snapshots, onNodeClick }) => {
|
|
406
|
+
// The shell's `selectedIndex` indexes into `snapshots[]` (which
|
|
407
|
+
// may be filtered to a drill-down subset). The overlay's
|
|
408
|
+
// `executionOrder` is the FULL execution timeline (all stages
|
|
409
|
+
// including subflow internals). When the two arrays have
|
|
410
|
+
// different lengths, passing selectedIndex straight through
|
|
411
|
+
// misaligns the chart's active highlight.
|
|
412
|
+
//
|
|
413
|
+
// Translate: take the runtimeStageId at snapshots[selectedIndex]
|
|
414
|
+
// and find the matching position in overlay.executionOrder.
|
|
415
|
+
// Fall back to selectedIndex when no overlay or no match
|
|
416
|
+
// (charts without subflows have aligned indexes anyway).
|
|
417
|
+
const activeRsid = snapshots[selectedIndex]?.runtimeStageId;
|
|
418
|
+
let overlayIdx = selectedIndex;
|
|
419
|
+
if (activeRsid && runtimeOverlay) {
|
|
420
|
+
const i = runtimeOverlay.executionOrder.findIndex((s) => s.runtimeStageId === activeRsid);
|
|
421
|
+
if (i >= 0)
|
|
422
|
+
overlayIdx = i;
|
|
423
|
+
}
|
|
424
|
+
return (_jsx(TracedFlow, { graph: traceGraph, overlay: runtimeOverlay ?? undefined, scrubIndex: overlayIdx, onNodeClick: (stageId) => onNodeClick?.(stageId), onSubflowChange: (mountId) => {
|
|
425
|
+
// Forward chart's drill state to the shell's drill-down
|
|
426
|
+
// stack so memory/narrative/timeline panels follow the
|
|
427
|
+
// chart into/out of subflows. We route through the same
|
|
428
|
+
// onNodeClick channel — it already triggers drill-down
|
|
429
|
+
// for subflow mount nodes via the shell's handleNodeClick
|
|
430
|
+
// → handleDrillDown path.
|
|
431
|
+
//
|
|
432
|
+
// The `mountId === null` case (popping back to top) is
|
|
433
|
+
// intentionally NOT auto-triggered here: the shell's
|
|
434
|
+
// breadcrumb-back button is the right user gesture for
|
|
435
|
+
// navigating UP from a subflow. Auto-popping on scrub
|
|
436
|
+
// would surprise users who manually drilled in.
|
|
437
|
+
if (mountId !== null)
|
|
438
|
+
onNodeClick?.(mountId);
|
|
439
|
+
} }));
|
|
440
|
+
};
|
|
441
|
+
}, [traceGraph, runtimeOverlay]);
|
|
442
|
+
const effectiveRenderFlowchart = renderFlowchart ?? tracedFlowRenderer;
|
|
443
|
+
const leftLabel = panelLabels?.topology ?? "Topology";
|
|
444
|
+
const rightLabel = panelLabels?.details ?? "Details";
|
|
445
|
+
const bottomLabel = panelLabels?.timeline ?? "Timeline";
|
|
446
|
+
// Responsive: detect narrow container + notify children of size changes
|
|
447
|
+
const shellRef = useRef(null);
|
|
448
|
+
const [isNarrow, setIsNarrow] = useState(false);
|
|
449
|
+
const [isMedium, setIsMedium] = useState(false);
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
const el = shellRef.current;
|
|
452
|
+
if (!el)
|
|
453
|
+
return;
|
|
454
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
455
|
+
const w = entry.contentRect.width;
|
|
456
|
+
setIsNarrow(w < 640);
|
|
457
|
+
setIsMedium(w >= 640 && w < 960);
|
|
458
|
+
// Notify ReactFlow (and other layout-sensitive children) that our container resized
|
|
459
|
+
window.dispatchEvent(new Event("resize"));
|
|
460
|
+
});
|
|
461
|
+
ro.observe(el);
|
|
462
|
+
return () => ro.disconnect();
|
|
463
|
+
}, []);
|
|
464
|
+
// Auto-detect recorder views from runtimeSnapshot.recorders
|
|
465
|
+
const autoRecorderViews = useMemo(() => {
|
|
466
|
+
const recorders = runtimeSnapshot?.recorders;
|
|
467
|
+
if (!recorders?.length)
|
|
468
|
+
return [];
|
|
469
|
+
// Don't auto-generate for IDs that have explicit recorderViews
|
|
470
|
+
const explicitIds = new Set((recorderViews ?? []).map((v) => v.id));
|
|
471
|
+
return recorders
|
|
472
|
+
.filter((r) => !explicitIds.has(r.id))
|
|
473
|
+
.map((r) => ({ id: r.id, name: r.name, description: r.description, preferredOperation: r.preferredOperation, data: r.data }));
|
|
474
|
+
}, [runtimeSnapshot, recorderViews]);
|
|
475
|
+
// Build tab list: Result + Memory (always), Narrative (when data exists),
|
|
476
|
+
// explicit recorder views, auto-detected recorder views
|
|
477
|
+
const hasNarrative = !!narrativeEntries?.length;
|
|
478
|
+
const allTabs = useMemo(() => {
|
|
479
|
+
const tabs = [
|
|
480
|
+
{ id: "result", name: "Result", description: "Final output and console logs" },
|
|
481
|
+
{ id: "memory", name: "Memory", description: "Accumulator — progressive shared state at each stage" },
|
|
482
|
+
];
|
|
483
|
+
if (hasNarrative) {
|
|
484
|
+
tabs.push({ id: "narrative", name: "Narrative", description: "Translator (SequenceRecorder) — interleaved flow + data narrative per execution step" });
|
|
485
|
+
}
|
|
486
|
+
for (const v of recorderViews ?? []) {
|
|
487
|
+
tabs.push({ id: v.id, name: v.name, description: v.description });
|
|
488
|
+
}
|
|
489
|
+
for (const v of autoRecorderViews) {
|
|
490
|
+
tabs.push({ id: v.id, name: v.name, description: v.description });
|
|
491
|
+
}
|
|
492
|
+
// Filter hidden tabs
|
|
493
|
+
const hideSet = new Set(hideTabsProp ?? []);
|
|
494
|
+
return hideSet.size > 0 ? tabs.filter((t) => !hideSet.has(t.id)) : tabs;
|
|
495
|
+
}, [hasNarrative, recorderViews, autoRecorderViews, hideTabsProp]);
|
|
496
|
+
const validTabIds = new Set(allTabs.map((t) => t.id));
|
|
497
|
+
const resolvedDefault = defaultTab && validTabIds.has(defaultTab) ? defaultTab : allTabs[0]?.id ?? "result";
|
|
498
|
+
const [activeTab, setActiveTab] = useState(resolvedDefault);
|
|
15
499
|
const [snapshotIdx, setSnapshotIdx] = useState(0);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
500
|
+
const [drillDownStack, setDrillDownStack] = useState([]);
|
|
501
|
+
const [rightExpanded, setRightExpanded] = useState(defaultExpanded?.details ?? true);
|
|
502
|
+
const [rightPanelMode, setRightPanelMode] = useState("insights");
|
|
503
|
+
const [leftExpanded, setLeftExpanded] = useState(defaultExpanded?.topology ?? false);
|
|
504
|
+
const [timelineExpanded, setTimelineExpanded] = useState(defaultExpanded?.timeline ?? false);
|
|
505
|
+
// Auto-collapse all panels when switching to narrow (mobile)
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
if (isNarrow) {
|
|
508
|
+
setLeftExpanded(false);
|
|
509
|
+
setRightExpanded(false);
|
|
510
|
+
setTimelineExpanded(false);
|
|
511
|
+
}
|
|
512
|
+
}, [isNarrow]);
|
|
513
|
+
// Notify ReactFlow (and any ResizeObserver-based children) when panels toggle
|
|
514
|
+
const triggerReflow = useCallback(() => {
|
|
515
|
+
// Fire twice: once immediately for fast response, once after CSS transition ends
|
|
516
|
+
requestAnimationFrame(() => window.dispatchEvent(new Event("resize")));
|
|
517
|
+
setTimeout(() => window.dispatchEvent(new Event("resize")), 320);
|
|
518
|
+
}, []);
|
|
519
|
+
const toggleLeft = useCallback((v) => { setLeftExpanded(v); triggerReflow(); }, [triggerReflow]);
|
|
520
|
+
const toggleRight = useCallback((v) => { setRightExpanded(v); triggerReflow(); }, [triggerReflow]);
|
|
521
|
+
const toggleTimeline = useCallback(() => { setTimelineExpanded((p) => !p); triggerReflow(); }, [triggerReflow]);
|
|
522
|
+
const isInSubflow = drillDownStack.length > 0;
|
|
523
|
+
const currentLevel = useMemo(() => {
|
|
524
|
+
if (drillDownStack.length > 0) {
|
|
525
|
+
const top = drillDownStack[drillDownStack.length - 1];
|
|
526
|
+
return { spec: top.spec, snapshots: top.snapshots };
|
|
527
|
+
}
|
|
528
|
+
return { spec: spec ?? null, snapshots };
|
|
529
|
+
}, [drillDownStack, spec, snapshots]);
|
|
530
|
+
const activeSnapshots = currentLevel.snapshots;
|
|
531
|
+
const activeSpec = currentLevel.spec;
|
|
532
|
+
const safeIdx = activeSnapshots.length > 0
|
|
533
|
+
? Math.max(0, Math.min(snapshotIdx, activeSnapshots.length - 1))
|
|
534
|
+
: 0;
|
|
535
|
+
const activeNarrativeEntries = isInSubflow ? undefined : narrativeEntries;
|
|
536
|
+
const breadcrumbs = useMemo(() => {
|
|
537
|
+
const root = { label: title || "Flowchart", spec: spec, description: spec?.description };
|
|
538
|
+
return [root, ...drillDownStack.map((e) => ({ label: e.label, spec: e.spec, description: undefined }))];
|
|
539
|
+
}, [spec, title, drillDownStack]);
|
|
540
|
+
// Recorder-driven: derive subflow presence from the build-time graph.
|
|
541
|
+
// Falls back to the legacy spec walk only when traceGraph is absent
|
|
542
|
+
// (e.g., a consumer still threading raw spec). When both are absent,
|
|
543
|
+
// the tree sidebar is hidden.
|
|
544
|
+
const showTreeSidebar = useMemo(() => {
|
|
545
|
+
if (traceGraph?.nodes?.length) {
|
|
546
|
+
return traceGraph.nodes.some((n) => n.data?.isSubflow === true);
|
|
547
|
+
}
|
|
548
|
+
return !!spec && hasSubflowNodes(spec);
|
|
549
|
+
}, [traceGraph, spec]);
|
|
550
|
+
const rootOverlay = useMemo(() => {
|
|
551
|
+
if (isInSubflow || !snapshots.length)
|
|
552
|
+
return { activeStage: undefined, doneStages: undefined };
|
|
553
|
+
const doneStages = new Set(snapshots.slice(0, safeIdx).map((s) => s.stageLabel));
|
|
554
|
+
const activeStage = snapshots[safeIdx]?.stageLabel ?? null;
|
|
555
|
+
return { activeStage, doneStages };
|
|
556
|
+
}, [isInSubflow, snapshots, safeIdx]);
|
|
557
|
+
// ── Handlers ──
|
|
558
|
+
const handleTabChange = useCallback((tab) => {
|
|
559
|
+
setActiveTab(tab);
|
|
560
|
+
setDrillDownStack([]);
|
|
561
|
+
}, []);
|
|
18
562
|
const handleSnapshotChange = useCallback((idx) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
return
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
563
|
+
if (typeof idx === "number")
|
|
564
|
+
setSnapshotIdx(idx);
|
|
565
|
+
}, []);
|
|
566
|
+
const handleDrillDown = useCallback((nodeName) => {
|
|
567
|
+
if (!activeSpec)
|
|
568
|
+
return;
|
|
569
|
+
const entry = resolveSubflowLevel(activeSpec, activeSnapshots, nodeName, narrativeEntries);
|
|
570
|
+
if (entry) {
|
|
571
|
+
setDrillDownStack((prev) => [...prev, { ...entry, parentSnapshotIdx: snapshotIdx }]);
|
|
572
|
+
setSnapshotIdx(0);
|
|
573
|
+
}
|
|
574
|
+
}, [activeSpec, activeSnapshots, narrativeEntries, snapshotIdx]);
|
|
575
|
+
const handleBreadcrumbNavigate = useCallback((level) => {
|
|
576
|
+
setDrillDownStack((prev) => {
|
|
577
|
+
const popped = level === 0 ? prev[0] : prev[level];
|
|
578
|
+
if (popped)
|
|
579
|
+
setSnapshotIdx(popped.parentSnapshotIdx);
|
|
580
|
+
return level === 0 ? [] : prev.slice(0, level);
|
|
581
|
+
});
|
|
582
|
+
}, []);
|
|
583
|
+
const handleNodeClick = useCallback((indexOrId) => {
|
|
584
|
+
if (typeof indexOrId === "number") {
|
|
585
|
+
setSnapshotIdx(indexOrId);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (activeSpec) {
|
|
589
|
+
const sfNode = findSubflowSpecNode(activeSpec, indexOrId);
|
|
590
|
+
if (sfNode?.subflowStructure) {
|
|
591
|
+
handleDrillDown(indexOrId);
|
|
592
|
+
return;
|
|
31
593
|
}
|
|
32
594
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
595
|
+
const idx = activeSnapshots.findIndex((s) => s.stageLabel === indexOrId);
|
|
596
|
+
if (idx >= 0)
|
|
597
|
+
setSnapshotIdx(idx);
|
|
598
|
+
}, [activeSpec, activeSnapshots, handleDrillDown]);
|
|
599
|
+
const handleTreeNodeSelect = useCallback((name, isSubflow) => {
|
|
600
|
+
if (isSubflow && spec) {
|
|
601
|
+
setDrillDownStack([]);
|
|
602
|
+
const entry = resolveSubflowLevel(spec, snapshots, name, narrativeEntries);
|
|
603
|
+
if (entry) {
|
|
604
|
+
setDrillDownStack([{ ...entry, parentSnapshotIdx: snapshotIdx }]);
|
|
605
|
+
setSnapshotIdx(0);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
setDrillDownStack([]);
|
|
610
|
+
const idx = snapshots.findIndex((s) => s.stageLabel === name);
|
|
611
|
+
if (idx >= 0)
|
|
612
|
+
setSnapshotIdx(idx);
|
|
613
|
+
}
|
|
614
|
+
}, [spec, snapshots, narrativeEntries, snapshotIdx]);
|
|
615
|
+
// Map tab id → label for rendering
|
|
616
|
+
const tabLabels = new Map(allTabs.map((t) => [t.id, t.name]));
|
|
617
|
+
// ── Unstyled mode ──
|
|
49
618
|
if (unstyled) {
|
|
50
|
-
return (_jsxs("div", { className: className, style: style, "data-fp": "explainable-shell", children: [_jsx("div", { "data-fp": "shell-tabs", children:
|
|
619
|
+
return (_jsxs("div", { className: className, style: style, "data-fp": "explainable-shell", children: [_jsx("div", { "data-fp": "shell-tabs", children: allTabs.map((tab) => (_jsx("button", { "data-fp": "shell-tab", "data-active": tab.id === activeTab, onClick: () => handleTabChange(tab.id), children: tab.name }, tab.id))) }), _jsxs("div", { "data-fp": "shell-content", "data-tab": activeTab, children: [activeTab === "result" && _jsx(ResultPanel, { data: resultData ?? null, logs: logs, hideConsole: hideConsole, unstyled: true }), (activeTab === "explainable" || activeTab === "ai-compatible") && (_jsxs(_Fragment, { children: [_jsx(TimeTravelControls, { snapshots: activeSnapshots, selectedIndex: safeIdx, onIndexChange: handleSnapshotChange, unstyled: true }), isInSubflow && _jsx(SubflowBreadcrumb, { breadcrumbs: breadcrumbs, onNavigate: handleBreadcrumbNavigate }), activeSpec && effectiveRenderFlowchart?.({ spec: activeSpec, snapshots: activeSnapshots, selectedIndex: safeIdx, onNodeClick: handleNodeClick, showStageId }), _jsx(MemoryPanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, unstyled: true }), _jsx(NarrativePanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, narrativeEntries: activeNarrativeEntries, unstyled: true }), _jsx(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, unstyled: true })] }))] })] }));
|
|
51
620
|
}
|
|
52
621
|
// ── Styled mode ──
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
622
|
+
// Show topology when spec has subflows
|
|
623
|
+
const showTopology = !!effectiveRenderFlowchart && !!activeSpec;
|
|
624
|
+
// Render the active details tab content
|
|
625
|
+
const detailsContent = useMemo(() => {
|
|
626
|
+
if (activeTab === "result") {
|
|
627
|
+
return _jsx(ResultPanel, { data: resultData ?? null, logs: logs, hideConsole: hideConsole, size: size });
|
|
628
|
+
}
|
|
629
|
+
if (activeTab === "memory") {
|
|
630
|
+
return _jsx(MemoryPanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, size: size, style: { height: "100%" } });
|
|
631
|
+
}
|
|
632
|
+
if (activeTab === "narrative") {
|
|
633
|
+
return _jsx(NarrativePanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, narrativeEntries: activeNarrativeEntries, size: size, style: { height: "100%" } });
|
|
634
|
+
}
|
|
635
|
+
const customView = recorderViews?.find((v) => v.id === activeTab);
|
|
636
|
+
if (customView?.render) {
|
|
637
|
+
return customView.render({ snapshots: activeSnapshots, selectedIndex: safeIdx });
|
|
638
|
+
}
|
|
639
|
+
// Auto-detected recorder view — time-travel aware for keyed recorders, JSON fallback
|
|
640
|
+
const autoView = autoRecorderViews.find((v) => v.id === activeTab);
|
|
641
|
+
if (autoView) {
|
|
642
|
+
return (_jsx(KeyedRecorderView, { data: autoView.data, description: autoView.description, preferredOperation: autoView.preferredOperation, snapshots: activeSnapshots, selectedIndex: safeIdx }));
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}, [activeTab, resultData, logs, hideConsole, size, activeSnapshots, safeIdx, activeNarrativeEntries, recorderViews, autoRecorderViews]);
|
|
646
|
+
// Details panel with internal tabs
|
|
647
|
+
const detailsPanel = (_jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }, children: [_jsx("div", { style: {
|
|
63
648
|
display: "flex",
|
|
64
|
-
gap: 0,
|
|
65
649
|
borderBottom: `1px solid ${theme.border}`,
|
|
66
650
|
background: theme.bgSecondary,
|
|
67
651
|
flexShrink: 0,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
652
|
+
overflowX: "auto",
|
|
653
|
+
}, children: allTabs.map((tab) => {
|
|
654
|
+
const active = tab.id === activeTab;
|
|
655
|
+
return (_jsx("button", { onClick: () => handleTabChange(tab.id), title: tab.description, style: {
|
|
656
|
+
padding: "6px 14px",
|
|
657
|
+
fontSize: 11,
|
|
73
658
|
fontWeight: active ? 700 : 500,
|
|
74
659
|
textTransform: "uppercase",
|
|
75
660
|
letterSpacing: "0.08em",
|
|
@@ -78,17 +663,40 @@ export function ExplainableShell({ snapshots, resultData, logs = [], narrative =
|
|
|
78
663
|
border: "none",
|
|
79
664
|
borderBottom: active ? `2px solid ${theme.primary}` : "2px solid transparent",
|
|
80
665
|
cursor: "pointer",
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
666
|
+
fontFamily: "inherit",
|
|
667
|
+
whiteSpace: "nowrap",
|
|
668
|
+
}, children: tab.name }, tab.id));
|
|
669
|
+
}) }), _jsx("div", { style: { flex: 1, overflow: "auto" }, children: detailsContent })] }));
|
|
670
|
+
return (_jsxs("div", { ref: shellRef, className: className, style: {
|
|
671
|
+
height: "100%",
|
|
672
|
+
display: "flex",
|
|
673
|
+
flexDirection: "column",
|
|
674
|
+
overflow: "hidden",
|
|
675
|
+
background: theme.bgPrimary,
|
|
676
|
+
color: theme.textPrimary,
|
|
677
|
+
fontFamily: theme.fontSans,
|
|
678
|
+
fontSize: 12,
|
|
679
|
+
...style,
|
|
680
|
+
}, "data-fp": "explainable-shell", children: [_jsx(TimeTravelControls, { snapshots: activeSnapshots, selectedIndex: safeIdx, onIndexChange: handleSnapshotChange, size: size }), isInSubflow && (_jsx(SubflowBreadcrumb, { breadcrumbs: breadcrumbs, onNavigate: handleBreadcrumbNavigate })), _jsx("div", { style: { flex: 1, overflow: isNarrow ? "auto" : "hidden", display: "flex", flexDirection: "column" }, children: isNarrow ? (
|
|
681
|
+
/* ── Mobile: stacked vertical ── */
|
|
682
|
+
_jsxs(_Fragment, { children: [showTopology && (_jsx("div", { style: { height: 350, flexShrink: 0, overflow: "hidden" }, children: effectiveRenderFlowchart({
|
|
683
|
+
spec: activeSpec,
|
|
684
|
+
snapshots: activeSnapshots,
|
|
685
|
+
selectedIndex: safeIdx,
|
|
686
|
+
onNodeClick: handleNodeClick,
|
|
687
|
+
showStageId,
|
|
688
|
+
}) })), showTreeSidebar && (_jsxs(_Fragment, { children: [_jsx(HLinePill, { label: leftLabel, expanded: leftExpanded, onClick: () => toggleLeft(!leftExpanded) }), leftExpanded && (_jsx("div", { style: { maxHeight: 180, overflow: "auto", flexShrink: 0 }, children: _jsx(SubflowTree, { graph: traceGraph ?? { nodes: [], edges: [] }, activeStage: rootOverlay.activeStage, doneStages: rootOverlay.doneStages, onNodeSelect: handleTreeNodeSelect }) }))] })), _jsx(HLinePill, { label: rightLabel, expanded: rightExpanded, onClick: () => toggleRight(!rightExpanded) }), rightExpanded && (_jsx("div", { style: { maxHeight: 350, flexShrink: 0, overflow: "hidden" }, children: detailsPanel })), _jsx(HLinePill, { label: bottomLabel, detail: `${activeSnapshots.length} stages`, expanded: timelineExpanded, onClick: toggleTimeline }), timelineExpanded && (_jsx("div", { style: { flexShrink: 0, overflow: "hidden" }, children: _jsx(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, size: size }) }))] })) : (
|
|
689
|
+
/* ── Desktop: two-column — Flowchart | Right Panel ── */
|
|
690
|
+
_jsxs(_Fragment, { children: [_jsxs("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [showTreeSidebar && (leftExpanded ? (_jsxs("div", { style: { width: 180, flexShrink: 0, display: "flex", flexDirection: "row", overflow: "hidden" }, children: [_jsx("div", { style: { flex: 1, overflow: "auto" }, children: _jsx(SubflowTree, { graph: traceGraph ?? { nodes: [], edges: [] }, activeStage: rootOverlay.activeStage, doneStages: rootOverlay.doneStages, onNodeSelect: handleTreeNodeSelect }) }), _jsx(VLinePill, { label: "Topology", expanded: true, side: "left", onClick: () => toggleLeft(false) })] })) : (_jsx(VLinePill, { label: "Topology", expanded: false, side: "left", onClick: () => toggleLeft(true) }))), showTopology ? (_jsx("div", { style: { flex: 1, overflow: "hidden", minWidth: 0 }, children: effectiveRenderFlowchart({
|
|
691
|
+
spec: activeSpec,
|
|
692
|
+
snapshots: activeSnapshots,
|
|
693
|
+
selectedIndex: safeIdx,
|
|
694
|
+
onNodeClick: handleNodeClick,
|
|
695
|
+
showStageId,
|
|
696
|
+
}) })) : (_jsx("div", { style: { flex: 1 } })), _jsx(VLinePill, { label: "Details", expanded: rightExpanded, onClick: () => toggleRight(!rightExpanded) }), rightExpanded && (_jsx("div", { style: { width: "42%", minWidth: 320, maxWidth: 550, display: "flex", flexDirection: "column", overflow: "hidden" }, children: _jsx(RightPanel, { mode: rightPanelMode, onModeChange: setRightPanelMode, snapshots: activeSnapshots, selectedIndex: safeIdx, runtimeSnapshot: runtimeSnapshot, spec: spec, activeTab: activeTab, allTabs: allTabs, activeNarrativeEntries: activeNarrativeEntries, recorderViews: recorderViews, autoRecorderViews: autoRecorderViews, size: size, onNavigateToStage: (id) => {
|
|
697
|
+
const idx = activeSnapshots.findIndex((s) => s.runtimeStageId === id);
|
|
698
|
+
if (idx >= 0)
|
|
699
|
+
setSnapshotIdx(idx);
|
|
700
|
+
} }) }))] }), _jsx(CompactTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, defaultExpanded: timelineExpanded })] })) })] }));
|
|
93
701
|
}
|
|
94
702
|
//# sourceMappingURL=ExplainableShell.js.map
|