jev-devtools 0.2.0 → 0.3.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 +47 -3
- package/lib/flow/index.d.ts +82 -0
- package/lib/flow/index.js +588 -0
- package/lib/server/flows.d.ts +25 -0
- package/lib/server/flows.js +115 -0
- package/lib/server/index.d.ts +8 -0
- package/lib/server/index.js +85 -1
- package/lib/server/recorder.d.ts +4 -1
- package/lib/server/recorder.js +12 -1
- package/lib/src/ChoiceProbabilities.js +1 -1
- package/lib/src/ConnectedInspector.js +13 -4
- package/lib/src/DiagramContent.d.ts +6 -0
- package/lib/src/DiagramContent.js +32 -0
- package/lib/src/FlowOverview.d.ts +7 -0
- package/lib/src/FlowOverview.js +228 -0
- package/lib/src/JevDevtools.d.ts +6 -1
- package/lib/src/JevDevtools.js +18 -4
- package/lib/src/TraceDiagram.d.ts +4 -1
- package/lib/src/TraceDiagram.js +30 -12
- package/lib/src/condition-inputs.d.ts +9 -0
- package/lib/src/condition-inputs.js +100 -0
- package/lib/src/flow-condition.d.ts +4 -0
- package/lib/src/flow-condition.js +47 -0
- package/lib/src/flow-model.d.ts +7 -2
- package/lib/src/flow-model.js +2 -11
- package/lib/src/flow-protocol.d.ts +68 -0
- package/lib/src/flow-protocol.js +1 -0
- package/lib/src/flow-stream.d.ts +8 -0
- package/lib/src/flow-stream.js +80 -0
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +501 -8
- package/lib/src/protocol.d.ts +5 -0
- package/lib/src/useDiagramViewport.d.ts +4 -0
- package/lib/src/useDiagramViewport.js +20 -3
- package/package.json +6 -2
- package/skills/setup-jev-devtools/SKILL.md +6 -2
- package/skills/setup-jev-devtools/references/flows.md +150 -0
- package/skills/setup-jev-devtools/references/nextjs.md +1 -1
- package/skills/setup-jev-devtools/references/verify.md +1 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useDiagramViewport } from "./useDiagramViewport.js";
|
|
4
|
+
import { RequestDetail } from "./JevDevtools.js";
|
|
5
|
+
import { JsonBlock } from "./TraceContent.js";
|
|
6
|
+
import { conditionInputs, conditionRequirement, } from "./condition-inputs.js";
|
|
7
|
+
import { formatFlowCondition } from "./flow-condition.js";
|
|
8
|
+
import { TraceDiagram } from "./TraceDiagram.js";
|
|
9
|
+
const RequestNode = memo(function RequestNode({ node, trace, status, selected, x, y, onSelect, onResize, }) {
|
|
10
|
+
const headingHeight = node.description ? 104 : 64;
|
|
11
|
+
const measure = useCallback((height) => onResize(node.id, height + headingHeight), [node.id, onResize, headingHeight]);
|
|
12
|
+
return (_jsxs("article", { className: `flow-step flow-request ${selected ? "selected" : ""}`, "data-status": status, "aria-label": `Request ${node.id}`, style: { left: x, top: y, width: 1120 }, children: [_jsxs("button", { className: "flow-request-heading", style: { height: headingHeight }, "aria-label": `Step ${node.id}, jev, ${status}`, "aria-pressed": selected, onClick: () => onSelect(node.id), children: [_jsxs("span", { children: [_jsx("span", { className: "eyebrow", children: "Jev request" }), _jsx("strong", { children: node.title ?? node.id }), node.description && (_jsx("span", { className: "flow-request-description", children: node.description }))] }), _jsxs("span", { children: [status, trace.durationMs !== undefined
|
|
13
|
+
? ` · ${Math.round(trace.durationMs)} ms`
|
|
14
|
+
: ""] })] }), _jsx(TraceDiagram, { trace: trace, embedded: true, onHeightChange: measure })] }));
|
|
15
|
+
});
|
|
16
|
+
function joinTitle(count) {
|
|
17
|
+
return count === 2 ? "Wait for both checks" : `Wait for all ${count} paths`;
|
|
18
|
+
}
|
|
19
|
+
const Card = memo(function Card({ node, status, decision, selected, bounds, onSelect, lanes, inputs, }) {
|
|
20
|
+
const join = node.kind === "parallel";
|
|
21
|
+
const finished = lanes.filter((lane) => lane.status === "completed").length;
|
|
22
|
+
const route = [...node.path]
|
|
23
|
+
.reverse()
|
|
24
|
+
.find((part) => part.endsWith(":then") || part.endsWith(":else"));
|
|
25
|
+
const stateLabel = status === "untaken"
|
|
26
|
+
? "Not taken"
|
|
27
|
+
: node.kind === "output" && status === "completed"
|
|
28
|
+
? "Returned outcome"
|
|
29
|
+
: status;
|
|
30
|
+
return (_jsxs("button", { className: `flow-step ${selected ? "selected" : ""}`, "data-status": status, style: {
|
|
31
|
+
left: bounds.x,
|
|
32
|
+
top: bounds.y,
|
|
33
|
+
width: bounds.width,
|
|
34
|
+
height: bounds.height,
|
|
35
|
+
}, "aria-label": `Step ${node.id}, ${node.kind}, ${status}`, "aria-pressed": selected, onClick: () => onSelect(node.id), children: [_jsxs("span", { className: "flow-card-heading", children: [_jsx("span", { className: "flow-kind-icon", "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: join ? (_jsx("path", { d: "M4 5h4l5 7h7M4 19h4l5-7m4-3 3 3-3 3" })) : node.kind === "output" ? (_jsxs(_Fragment, { children: [_jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "4" }), _jsx("path", { d: "m8 12 3 3 5-6" })] })) : (_jsx("path", { d: "m12 3 9 9-9 9-9-9zM8 12h8" })) }) }), _jsx("span", { className: "eyebrow", children: join
|
|
36
|
+
? "Parallel checks"
|
|
37
|
+
: node.kind === "branch"
|
|
38
|
+
? "Condition"
|
|
39
|
+
: node.kind }), node.kind === "output" && (_jsx("span", { className: "flow-status", children: stateLabel }))] }), _jsx("strong", { children: node.title ?? (join ? joinTitle(lanes.length) : node.id) }), node.description && (_jsx("span", { className: "flow-step-description", children: node.description })), node.kind === "branch" &&
|
|
40
|
+
(node.conditionRule !== undefined || node.condition) && (_jsxs("span", { className: "flow-condition-rule", children: [inputs && inputs.length > 0 && (_jsxs("span", { className: "flow-condition-incoming", children: [_jsx("span", { className: "flow-condition-label", children: inputs.length === 1 ? "Incoming value" : "Incoming values" }), inputs.slice(0, 3).map((input) => (_jsxs("span", { className: "flow-condition-input", children: [_jsx("span", { children: input.name }), _jsx("b", { children: input.value })] }, input.reference))), inputs.length > 3 && (_jsxs("small", { children: [inputs.length - 3, " more inputs \u00B7 select to inspect"] }))] })), _jsx("span", { className: "flow-condition-label", children: "Requirement" }), _jsx("code", { children: node.conditionRule !== undefined
|
|
41
|
+
? conditionRequirement(node.conditionRule, inputs ?? [])
|
|
42
|
+
: node.condition })] })), join && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flow-join-summary", children: [joinTitle(lanes.length), " ", _jsxs("span", { children: [finished, "/", lanes.length] })] }), _jsxs("span", { className: "flow-lanes", children: [lanes.slice(0, 4).map((lane) => (_jsxs("span", { className: "flow-lane", "data-status": lane.status, children: [_jsxs("span", { children: [_jsx("span", { "aria-hidden": "true", children: lane.status === "completed"
|
|
43
|
+
? "✓"
|
|
44
|
+
: lane.status === "error"
|
|
45
|
+
? "!"
|
|
46
|
+
: "○" }), " ", lane.name] }), _jsx("small", { children: lane.status })] }, lane.name))), lanes.length > 4 && (_jsxs("small", { children: [lanes.length - 4, " more \u00B7 select to see all checks"] }))] })] })), _jsxs("span", { className: "flow-card-footer", children: [_jsx("code", { children: node.id }), _jsx("small", { children: join
|
|
47
|
+
? status === "completed"
|
|
48
|
+
? "All finished · continue"
|
|
49
|
+
: stateLabel
|
|
50
|
+
: node.kind === "output"
|
|
51
|
+
? route?.endsWith(":then")
|
|
52
|
+
? "Then route"
|
|
53
|
+
: route?.endsWith(":else")
|
|
54
|
+
? "Else route"
|
|
55
|
+
: "Final outcome"
|
|
56
|
+
: decision !== undefined
|
|
57
|
+
? decision
|
|
58
|
+
? "Condition met · then"
|
|
59
|
+
: "Condition not met · else"
|
|
60
|
+
: stateLabel })] }), node.kind === "jev" && (_jsx("small", { children: status === "untaken"
|
|
61
|
+
? "This request was not run."
|
|
62
|
+
: "Request details are not captured yet or no longer retained." }))] }));
|
|
63
|
+
});
|
|
64
|
+
export const FlowOverview = memo(function FlowOverview({ definition, run, traces, }) {
|
|
65
|
+
const [selected, setSelected] = useState(() => definition.nodes.find((n) => n.kind === "jev")?.id ?? "");
|
|
66
|
+
const [details, setDetails] = useState(false);
|
|
67
|
+
const [heights, setHeights] = useState({});
|
|
68
|
+
const tracesById = useMemo(() => new Map(traces.map((trace) => [trace.id, trace])), [traces]);
|
|
69
|
+
const inputsByStep = useMemo(() => new Map(definition.nodes
|
|
70
|
+
.filter((node) => node.conditionRule !== undefined)
|
|
71
|
+
.map((node) => [
|
|
72
|
+
node.id,
|
|
73
|
+
conditionInputs(node.conditionRule, run, tracesById, new Map(definition.nodes.map((n) => [n.id, n.title ?? n.id]))),
|
|
74
|
+
])), [definition, run, tracesById]);
|
|
75
|
+
// Omit redundant transitive edges so a reference does not draw through an
|
|
76
|
+
// intervening condition. Execution ordering still includes every dependency.
|
|
77
|
+
const links = useMemo(() => {
|
|
78
|
+
const byId = new Map(definition.nodes.map((node) => [node.id, node]));
|
|
79
|
+
const ancestors = (id) => {
|
|
80
|
+
const seen = new Set(), pending = [...(byId.get(id)?.dependencies ?? [])];
|
|
81
|
+
while (pending.length) {
|
|
82
|
+
const next = pending.pop();
|
|
83
|
+
if (seen.has(next))
|
|
84
|
+
continue;
|
|
85
|
+
seen.add(next);
|
|
86
|
+
pending.push(...(byId.get(next)?.dependencies ?? []));
|
|
87
|
+
}
|
|
88
|
+
return seen;
|
|
89
|
+
};
|
|
90
|
+
const parents = new Map(definition.nodes.map((node) => [node.id, ancestors(node.id)]));
|
|
91
|
+
return new Map(definition.nodes.map((node) => [
|
|
92
|
+
node.id,
|
|
93
|
+
node.dependencies.filter((id) => !node.dependencies.some((other) => other !== id && parents.get(other)?.has(id))),
|
|
94
|
+
]));
|
|
95
|
+
}, [definition]);
|
|
96
|
+
const parallelLanes = useMemo(() => new Map(definition.nodes
|
|
97
|
+
.filter((n) => n.kind === "parallel")
|
|
98
|
+
.map((node) => [
|
|
99
|
+
node.id,
|
|
100
|
+
(node.lanes ?? node.dependencies).map((name) => {
|
|
101
|
+
const children = node.lanes
|
|
102
|
+
? definition.nodes.filter((child) => child.path.includes(`${node.id}:${name}`))
|
|
103
|
+
: definition.nodes.filter((child) => child.id === name);
|
|
104
|
+
const states = children.map((child) => run.steps[child.id]?.status ?? "waiting");
|
|
105
|
+
const status = run.steps[node.id]?.status === "untaken"
|
|
106
|
+
? "untaken"
|
|
107
|
+
: states.includes("error")
|
|
108
|
+
? "error"
|
|
109
|
+
: states.includes("cancelled")
|
|
110
|
+
? "cancelled"
|
|
111
|
+
: states.includes("blocked")
|
|
112
|
+
? "blocked"
|
|
113
|
+
: run.steps[node.id]?.status === "completed" ||
|
|
114
|
+
(states.length > 0 &&
|
|
115
|
+
states.every((state) => state === "completed" || state === "untaken"))
|
|
116
|
+
? "completed"
|
|
117
|
+
: states.some((state) => state === "running" || state === "completed")
|
|
118
|
+
? "running"
|
|
119
|
+
: "waiting";
|
|
120
|
+
return { name, status };
|
|
121
|
+
}),
|
|
122
|
+
])), [definition, run.steps]);
|
|
123
|
+
const layout = useMemo(() => {
|
|
124
|
+
const columns = new Map();
|
|
125
|
+
let remaining = [...definition.nodes];
|
|
126
|
+
while (remaining.length) {
|
|
127
|
+
const ready = remaining.filter((node) => node.dependencies.every((id) => columns.has(id)));
|
|
128
|
+
if (!ready.length)
|
|
129
|
+
break;
|
|
130
|
+
for (const node of ready)
|
|
131
|
+
columns.set(node.id, node.dependencies.length
|
|
132
|
+
? Math.max(...node.dependencies.map((id) => columns.get(id))) + 1
|
|
133
|
+
: 0);
|
|
134
|
+
const ids = new Set(ready.map((node) => node.id));
|
|
135
|
+
remaining = remaining.filter((node) => !ids.has(node.id));
|
|
136
|
+
}
|
|
137
|
+
const points = new Map();
|
|
138
|
+
let x = 40;
|
|
139
|
+
for (let column = 0; column <= Math.max(0, ...columns.values()); column++) {
|
|
140
|
+
const nodes = definition.nodes.filter((node) => columns.get(node.id) === column);
|
|
141
|
+
let bottom = 40, width = 280;
|
|
142
|
+
for (const node of nodes) {
|
|
143
|
+
const captured = node.kind === "jev" &&
|
|
144
|
+
tracesById.has(run.steps[node.id]?.traceId ?? "");
|
|
145
|
+
const w = captured ? 1120 : 360, h = captured
|
|
146
|
+
? (heights[node.id] ?? 640)
|
|
147
|
+
: node.kind === "parallel"
|
|
148
|
+
? 190 +
|
|
149
|
+
(node.description ? 64 : 0) +
|
|
150
|
+
Math.min(4, (node.lanes ?? node.dependencies).length) * 32
|
|
151
|
+
: (node.description ? 244 : 172) +
|
|
152
|
+
(node.kind === "branch"
|
|
153
|
+
? 110 +
|
|
154
|
+
Math.min(3, inputsByStep.get(node.id)?.length ?? 0) * 24
|
|
155
|
+
: 0);
|
|
156
|
+
const parents = (links.get(node.id) ?? [])
|
|
157
|
+
.map((id) => points.get(id))
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
const center = parents.length
|
|
160
|
+
? parents.reduce((sum, p) => sum + p.y + p.height / 2, 0) /
|
|
161
|
+
parents.length
|
|
162
|
+
: h / 2 + 40;
|
|
163
|
+
const y = Math.max(bottom, center - h / 2);
|
|
164
|
+
points.set(node.id, { x, y, width: w, height: h });
|
|
165
|
+
bottom = y + h + 100;
|
|
166
|
+
width = Math.max(width, w);
|
|
167
|
+
}
|
|
168
|
+
x += width + 160;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
points,
|
|
172
|
+
width: Math.max(700, x - 120),
|
|
173
|
+
height: Math.max(400, ...[...points.values()].map((p) => p.y + p.height + 40)),
|
|
174
|
+
};
|
|
175
|
+
}, [definition, run.steps, tracesById, heights, links, inputsByStep]);
|
|
176
|
+
const { viewport, canvas, zoom, zoomBy, fit, preserveCamera, focusArea } = useDiagramViewport(layout.width, layout.height, run.id);
|
|
177
|
+
const onResize = useCallback((id, height) => {
|
|
178
|
+
preserveCamera();
|
|
179
|
+
setHeights((previous) => previous[id] === height ? previous : { ...previous, [id]: height });
|
|
180
|
+
}, [preserveCamera]);
|
|
181
|
+
const started = useRef(false);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (started.current)
|
|
184
|
+
return;
|
|
185
|
+
const first = layout.points.get(selected);
|
|
186
|
+
if (first) {
|
|
187
|
+
started.current = true;
|
|
188
|
+
focusArea(first);
|
|
189
|
+
}
|
|
190
|
+
}, [layout, selected, focusArea]);
|
|
191
|
+
const selectStep = (id) => {
|
|
192
|
+
setSelected(id);
|
|
193
|
+
const bounds = layout.points.get(id);
|
|
194
|
+
if (bounds)
|
|
195
|
+
focusArea(bounds);
|
|
196
|
+
};
|
|
197
|
+
const node = definition.nodes.find((n) => n.id === selected);
|
|
198
|
+
const step = node ? run.steps[node.id] : undefined;
|
|
199
|
+
const trace = step?.traceId
|
|
200
|
+
? traces.find((t) => t.id === step.traceId)
|
|
201
|
+
: undefined;
|
|
202
|
+
return (_jsxs("section", { className: "request-detail whole-flow", "aria-label": "Whole flow", children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: ["Flow run \u00B7 ", run.status] }), _jsx("h2", { children: run.name })] }), _jsxs("div", { children: [run.outcome ? (_jsxs("strong", { children: ["Outcome: ", run.outcome] })) : ((run.error ?? "Execution paths")), _jsx("small", { className: "muted", children: run.durationMs !== undefined
|
|
203
|
+
? ` · ${Math.round(run.durationMs)} ms`
|
|
204
|
+
: "" })] })] }), _jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("div", { children: [_jsx("button", { "aria-label": "Zoom flow out", onClick: () => zoomBy(1 / 1.2), children: "\u2212" }), _jsxs("output", { "aria-label": "Flow zoom", children: [zoom, "%"] }), _jsx("button", { "aria-label": "Zoom flow in", onClick: () => zoomBy(1.2), children: "+" }), _jsx("button", { onClick: fit, children: "Fit flow" }), _jsx("button", { disabled: !selected, onClick: () => selectStep(selected), children: "Focus step" })] }), _jsxs("label", { children: ["Step", _jsxs("select", { "aria-label": "Flow step", value: selected, onChange: (e) => selectStep(e.target.value), children: [_jsx("option", { value: "", children: "Select a step" }), definition.nodes.map((n) => (_jsxs("option", { value: n.id, children: [n.title ? `${n.title} (${n.id})` : n.id, " \u00B7", " ", run.steps[n.id]?.status ?? "waiting"] }, n.id)))] })] })] }), _jsx("div", { className: "flow-overview-viewport diagram-viewport", ref: viewport, tabIndex: 0, "aria-label": "Flow canvas", role: "region", children: _jsxs("div", { ref: canvas, className: "flow-overview-canvas", style: { width: layout.width, height: layout.height }, children: [_jsxs("svg", { width: layout.width, height: layout.height, "aria-hidden": "true", className: "flow-connections", children: [_jsx("defs", { children: _jsx("marker", { id: `arrow-${run.id}`, viewBox: "0 0 10 10", refX: "8", refY: "5", markerWidth: "5", markerHeight: "5", orient: "auto", children: _jsx("path", { d: "M0 0L10 5L0 10z", fill: "currentColor" }) }) }), definition.nodes.flatMap((n) => (links.get(n.id) ?? []).map((id) => {
|
|
205
|
+
const a = layout.points.get(id), b = layout.points.get(n.id);
|
|
206
|
+
if (!a || !b)
|
|
207
|
+
return null;
|
|
208
|
+
const active = ["running", "completed"].includes(run.steps[n.id]?.status);
|
|
209
|
+
return (_jsx("path", { d: `M${a.x + a.width},${a.y + a.height / 2} C${a.x + a.width + 80},${a.y + a.height / 2} ${b.x - 80},${b.y + b.height / 2} ${b.x},${b.y + b.height / 2}`, stroke: active ? "#E551BA" : "currentColor", opacity: active ? 1 : 0.35, fill: "none", strokeWidth: "2", markerEnd: `url(#arrow-${run.id})` }, `${id}-${n.id}`));
|
|
210
|
+
}))] }), definition.nodes.map((n) => {
|
|
211
|
+
const p = layout.points.get(n.id);
|
|
212
|
+
if (!p)
|
|
213
|
+
return null;
|
|
214
|
+
const captured = tracesById.get(run.steps[n.id]?.traceId ?? "");
|
|
215
|
+
return n.kind === "jev" && captured ? (_jsx(RequestNode, { node: n, trace: captured, status: run.steps[n.id]?.status ?? "waiting", selected: selected === n.id, x: p.x, y: p.y, onSelect: setSelected, onResize: onResize }, n.id)) : (_jsx(Card, { node: n, status: run.steps[n.id]?.status ?? "waiting", decision: run.steps[n.id]?.decision, selected: selected === n.id, bounds: p, onSelect: setSelected, inputs: inputsByStep.get(n.id), lanes: parallelLanes.get(n.id) ?? [] }, n.id));
|
|
216
|
+
})] }) }), _jsxs("div", { className: "flow-selection", children: [node && step ? (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [node.title ?? node.id, " \u00B7 ", step.status] }), _jsx("span", { className: "muted", children: step.durationMs !== undefined
|
|
217
|
+
? ` · ${Math.round(step.durationMs)} ms`
|
|
218
|
+
: "" }), node.description && _jsx("p", { children: node.description }), node.kind === "parallel" && (_jsxs("p", { children: ["These paths run at the same time:", " ", (node.lanes ?? node.dependencies).join(", "), ". The next step starts only after all finish successfully. This marker waits for their results; it does not make another Jev request."] })), node.kind === "parallel" && (_jsx("ul", { className: "flow-lane-details", children: parallelLanes.get(node.id)?.map((lane) => (_jsxs("li", { children: [lane.name, " \u00B7 ", lane.status] }, lane.name))) })), (inputsByStep.get(node.id)?.length ?? 0) > 0 && (_jsx("dl", { className: "flow-input-details", children: inputsByStep.get(node.id)?.map((input) => (_jsxs("div", { children: [_jsxs("dt", { children: [input.name, " ", _jsx("code", { children: input.reference })] }), _jsx("dd", { children: input.value })] }, input.reference))) })), (node.conditionRule !== undefined || node.condition) && (_jsxs("p", { children: [_jsx("code", { children: node.conditionRule !== undefined
|
|
219
|
+
? formatFlowCondition(node.conditionRule)
|
|
220
|
+
: node.condition }), " ", "\u00B7", " ", step.decision === undefined
|
|
221
|
+
? "Not evaluated"
|
|
222
|
+
: step.decision
|
|
223
|
+
? "Passed · then route"
|
|
224
|
+
: "Not met · else route"] })), _jsx("p", { className: "muted", children: node.path.length ? node.path.join(" / ") : "Main path" }), step.error && _jsx("p", { role: "alert", children: step.error }), node.kind === "jev" &&
|
|
225
|
+
(trace ? (_jsx("button", { onClick: () => setDetails(!details), children: details ? "Hide request details" : "Inspect request" })) : (_jsx("p", { children: step.traceId
|
|
226
|
+
? "Request capture was cleared or evicted."
|
|
227
|
+
: "No captured request linked yet. Use a client created by this inspector." })))] })) : (_jsx("p", { className: "muted", children: "Each request diagram shows its context, question and answer. Use its question picker for other answers. Lines between requests show execution dependencies." })), run.value !== undefined && (_jsxs("details", { children: [_jsx("summary", { children: "Captured outcome value" }), _jsx(JsonBlock, { value: run.value })] }))] }), details && trace && (_jsx("div", { className: "flow-request-detail", children: _jsx(RequestDetail, { trace: trace }) }))] }));
|
|
228
|
+
});
|
package/lib/src/JevDevtools.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { type FlowHistory } from "./flow-stream.js";
|
|
1
2
|
import type { Trace } from "./protocol.js";
|
|
2
|
-
export declare
|
|
3
|
+
export declare const RequestDetail: import("react").MemoExoticComponent<({ trace, }: {
|
|
4
|
+
trace?: Trace;
|
|
5
|
+
}) => import("react").JSX.Element>;
|
|
6
|
+
export declare function JevPanel({ traces, flows, connection, onClear, }: {
|
|
3
7
|
traces: Trace[];
|
|
8
|
+
flows?: FlowHistory;
|
|
4
9
|
connection: string;
|
|
5
10
|
onClear: () => Promise<unknown>;
|
|
6
11
|
}): import("react").JSX.Element;
|
package/lib/src/JevDevtools.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { emptyFlows } from "./flow-stream.js";
|
|
2
3
|
import { lazy, memo, startTransition, Suspense, useMemo, useState, } from "react";
|
|
3
4
|
import { TraceDiagram } from "./TraceDiagram.js";
|
|
4
5
|
import { JsonBlock, findImage } from "./TraceContent.js";
|
|
6
|
+
const FlowOverview = lazy(() => import("./FlowOverview.js").then((module) => ({
|
|
7
|
+
default: module.FlowOverview,
|
|
8
|
+
})));
|
|
5
9
|
let questionsLoaded = false;
|
|
6
10
|
const Questions = lazy(() => import("./Questions.js").then((module) => {
|
|
7
11
|
questionsLoaded = true;
|
|
@@ -13,7 +17,7 @@ const ms = (value) => value === undefined ? "—" : `${Math.round(value)} ms`;
|
|
|
13
17
|
const RequestRow = memo(function RequestRow({ trace, selected, onSelect, }) {
|
|
14
18
|
return (_jsxs("button", { className: `request-row ${selected ? "selected" : ""}`, onClick: () => onSelect(trace.id), children: [_jsx("span", { className: `status-dot ${trace.status}` }), _jsxs("span", { children: [_jsx("strong", { children: trace.label }), _jsxs("small", { children: [new Date(trace.startedAt).toLocaleTimeString(), " \u00B7 ", trace.source, " \u00B7", " ", ms(trace.durationMs)] })] }), _jsx("span", { className: "row-status", children: trace.status })] }));
|
|
15
19
|
});
|
|
16
|
-
const RequestDetail = memo(function RequestDetail({ trace, }) {
|
|
20
|
+
export const RequestDetail = memo(function RequestDetail({ trace, }) {
|
|
17
21
|
// Keep tab selection across request switches and clear, as before. Local tab
|
|
18
22
|
// changes no longer rerender the history list.
|
|
19
23
|
const [tab, setTab] = useState("Flow");
|
|
@@ -31,7 +35,11 @@ const RequestDetail = memo(function RequestDetail({ trace, }) {
|
|
|
31
35
|
setTab(name);
|
|
32
36
|
}, children: name }, name))) }), _jsx("div", { className: `detail-body ${tab === "Flow" ? "detail-body-flow" : tab === "Questions" ? "detail-body-questions" : ""}`, children: _jsxs(Suspense, { fallback: _jsx("p", { className: "muted", role: "status", children: "Loading questions\u2026" }), children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" && (_jsx(Questions, { trace: trace }, trace.id)), tab === "Context" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "muted", children: "Captured before dispatch. Known credential fields and email are redacted; captures are size limited." }), findImage(trace.request.state) && (_jsx("img", { className: "context-image", src: findImage(trace.request.state), alt: "Captured context" })), _jsx(JsonBlock, { value: trace.request.state }), trace.request.assets && (_jsx(JsonBlock, { value: trace.request.assets }))] })), tab === "Transport" && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "HTTP attempts" }), _jsx("p", { className: "muted", children: "Attempt duration ends at response headers. Total duration includes SDK processing and retry waits." }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Attempt" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: trace.attempts.map((a) => (_jsxs("tr", { children: [_jsx("td", { children: a.number }), _jsx("td", { children: a.error || a.status || "Pending" }), _jsx("td", { children: ms(a.durationMs) })] }, a.number))) })] }), !trace.attempts.length && _jsx("p", { children: "No HTTP attempt recorded." })] })), tab === "Raw" && _jsx(JsonBlock, { value: trace })] }) })] })) : (_jsxs("div", { className: "empty-detail", children: [_jsx("span", { className: "empty-symbol", children: "\u21B3" }), _jsx("h2", { children: "Follow a Jev request" }), _jsxs("p", { children: ["Context, parallel questions, typed answers.", _jsx("br", {}), "Run a sample above to see the complete flow."] })] })) }));
|
|
33
37
|
});
|
|
34
|
-
export function JevPanel({ traces, connection, onClear, }) {
|
|
38
|
+
export function JevPanel({ traces, flows = emptyFlows, connection, onClear, }) {
|
|
39
|
+
const [view, setView] = useState("requests");
|
|
40
|
+
const [selectedRun, setSelectedRun] = useState();
|
|
41
|
+
const run = flows.runs.find((r) => r.id === selectedRun) ?? flows.runs[0];
|
|
42
|
+
const definition = flows.definitions.find((d) => d.id === run?.definitionId);
|
|
35
43
|
const [selected, setSelected] = useState();
|
|
36
44
|
const [search, setSearch] = useState("");
|
|
37
45
|
const [status, setStatus] = useState("all");
|
|
@@ -42,7 +50,13 @@ export function JevPanel({ traces, connection, onClear, }) {
|
|
|
42
50
|
`${t.label} ${t.request.model} ${t.id}`.toLowerCase().includes(query));
|
|
43
51
|
}, [traces, search, status]);
|
|
44
52
|
const trace = filtered.find((t) => t.id === selected) || filtered[0];
|
|
45
|
-
return (_jsxs("div", { className: "jev-inspector", children: [_jsxs("aside", { className: "request-list", children: [_jsxs("header", { children: [_jsxs("h2", { children: ["Jev requests ", _jsx("span", { children: traces.length })] }), _jsx("button", { onClick: () => void onClear().catch((e) => setError(e.message)), disabled: !traces.length, children: "Clear" })] }), _jsxs("
|
|
53
|
+
return (_jsxs("div", { className: "jev-inspector", children: [_jsxs("aside", { className: "request-list", children: [_jsxs("header", { children: [_jsxs("h2", { children: [view === "requests" ? "Jev requests" : "Jev flows", " ", _jsx("span", { children: view === "requests" ? traces.length : flows.runs.length })] }), _jsx("button", { onClick: () => void onClear().catch((e) => setError(e.message)), disabled: !traces.length && !flows.runs.length, children: "Clear" })] }), (flows.runs.length > 0 || view === "flows") && (_jsxs("nav", { className: "detail-tabs", "aria-label": "History type", children: [_jsx("button", { "aria-pressed": view === "requests", onClick: () => setView("requests"), children: "Requests" }), _jsx("button", { "aria-pressed": view === "flows", onClick: () => {
|
|
54
|
+
setView("flows");
|
|
55
|
+
if (run)
|
|
56
|
+
setSelectedRun(run.id);
|
|
57
|
+
}, children: "Flows" })] })), view === "requests" && (_jsxs("div", { className: "list-filters", children: [_jsx("input", { "aria-label": "Search requests", placeholder: "Search requests\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), _jsx("select", { "aria-label": "Filter request status", value: status, onChange: (e) => setStatus(e.target.value), children: ["all", "pending", "success", "error", "cancelled"].map((s) => (_jsx("option", { children: s }, s))) })] })), _jsxs("div", { className: "request-items", children: [view === "flows"
|
|
58
|
+
? flows.runs.map((r) => (_jsxs("button", { className: `request-row ${run?.id === r.id ? "selected" : ""}`, onClick: () => setSelectedRun(r.id), children: [_jsx("span", { className: `status-dot ${r.status}` }), _jsxs("span", { children: [_jsx("strong", { children: r.name }), _jsxs("small", { children: [new Date(r.startedAt).toLocaleTimeString(), " \u00B7", " ", r.outcome ?? r.status] })] }), _jsx("span", { className: "row-status", children: r.status })] }, r.id)))
|
|
59
|
+
: filtered.map((t) => (_jsx(RequestRow, { trace: t, selected: trace?.id === t.id, onSelect: setSelected }, t.id))), view === "requests" && !filtered.length && (_jsx("p", { className: "empty", children: traces.length
|
|
46
60
|
? "No matching requests."
|
|
47
|
-
: "Run a scenario to capture your first request." }))] }), (connection !== "connected" || error) && (_jsxs("footer", { children: [connection !== "connected" && (_jsx("span", { role: "status", children: connection })), error && _jsx("p", { role: "alert", children: error })] }))] }), _jsx(RequestDetail, { trace: trace })] }));
|
|
61
|
+
: "Run a scenario to capture your first request." }))] }), (connection !== "connected" || error) && (_jsxs("footer", { children: [connection !== "connected" && (_jsx("span", { role: "status", children: connection })), error && _jsx("p", { role: "alert", children: error })] }))] }), view === "flows" && run && definition ? (_jsx(Suspense, { fallback: _jsx("p", { role: "status", children: "Loading flow\u2026" }), children: _jsx(FlowOverview, { run: run, definition: definition, traces: traces }, run.id) })) : (_jsx(RequestDetail, { trace: trace }))] }));
|
|
48
62
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Trace } from "./protocol.js";
|
|
2
|
-
export declare function TraceDiagram({ trace }: {
|
|
2
|
+
export declare function TraceDiagram({ trace, embedded, onHeightChange, }: {
|
|
3
3
|
trace: Trace;
|
|
4
|
+
/** Render in a parent diagram's coordinates, without a nested camera. */
|
|
5
|
+
embedded?: boolean;
|
|
6
|
+
onHeightChange?: (height: number) => void;
|
|
4
7
|
}): import("react").JSX.Element;
|
package/lib/src/TraceDiagram.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useDiagramViewport } from "./useDiagramViewport.js";
|
|
4
|
-
import { findImage, flowAnswer
|
|
4
|
+
import { findImage, flowAnswer } from "./flow-model.js";
|
|
5
|
+
import { DiagramContent } from "./DiagramContent.js";
|
|
5
6
|
import { ChoiceProbabilities } from "./ChoiceProbabilities.js";
|
|
6
7
|
const pct = (v) => `${Math.round(v * 100)}%`;
|
|
7
8
|
function Result({ answer, image, probabilities, }) {
|
|
@@ -24,7 +25,20 @@ function Result({ answer, image, probabilities, }) {
|
|
|
24
25
|
height: pct(answer.box.bottom - answer.box.top),
|
|
25
26
|
} }))] }), _jsxs("p", { className: "lab-small lab-coordinates", children: ["left ", answer.box.left.toFixed(2), " \u00B7 top ", answer.box.top.toFixed(2), _jsx("br", {}), "right ", answer.box.right.toFixed(2), " \u00B7 bottom", " ", answer.box.bottom.toFixed(2)] })] })), answer.confidence !== undefined && (_jsxs("p", { className: "lab-small", children: ["Confidence ", pct(answer.confidence)] }))] }));
|
|
26
27
|
}
|
|
27
|
-
export function TraceDiagram({ trace }) {
|
|
28
|
+
export function TraceDiagram({ trace, embedded = false, onHeightChange, }) {
|
|
29
|
+
const root = useRef(null);
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
if (!embedded || !root.current || !onHeightChange)
|
|
32
|
+
return;
|
|
33
|
+
const element = root.current;
|
|
34
|
+
const observer = new ResizeObserver(() => {
|
|
35
|
+
// Closed drawers and skipped offscreen content cannot replace retained bounds.
|
|
36
|
+
if (element.offsetHeight > 0)
|
|
37
|
+
onHeightChange(element.offsetHeight);
|
|
38
|
+
});
|
|
39
|
+
observer.observe(element);
|
|
40
|
+
return () => observer.disconnect();
|
|
41
|
+
}, [embedded, onHeightChange]);
|
|
28
42
|
const [selected, setSelected] = useState(""), [flow, setFlow] = useState(true), [extraCategories, setExtraCategories] = useState({}), [entering, setEntering] = useState(null), [probabilityView, setProbabilityView] = useState(null);
|
|
29
43
|
const entries = Object.entries(trace.request.questions ?? {}).filter(([, q]) => q && typeof q === "object" && typeof q.type === "string"), [key, question] = entries.find(([key]) => key === selected) ??
|
|
30
44
|
entries[0] ?? ["", undefined];
|
|
@@ -79,7 +93,7 @@ export function TraceDiagram({ trace }) {
|
|
|
79
93
|
active: output.active,
|
|
80
94
|
})),
|
|
81
95
|
];
|
|
82
|
-
return (_jsxs("div", { className: `lab-flow ${compact ? "lab-flow-compact" : ""}`, children: [_jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("label", { children: ["Question", _jsx("select", { "aria-label": "Diagram question", value: key, onChange: (event) => {
|
|
96
|
+
return (_jsxs("div", { ref: root, className: `lab-flow ${compact ? "lab-flow-compact" : ""} ${embedded ? "lab-flow-embedded" : ""}`, children: [_jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("label", { children: ["Question", _jsx("select", { "aria-label": "Diagram question", value: key, onChange: (event) => {
|
|
83
97
|
setEntering(null);
|
|
84
98
|
setSelected(event.target.value);
|
|
85
99
|
}, children: entries.map(([name, q]) => (_jsxs("option", { value: name, children: [name, " \u00B7 ", q.type] }, name))) })] }), _jsxs("div", { children: [answer?.type === "choice" && (_jsx("button", { "aria-pressed": showProbabilities, onClick: () => {
|
|
@@ -91,21 +105,19 @@ export function TraceDiagram({ trace }) {
|
|
|
91
105
|
preserveCamera();
|
|
92
106
|
setEntering(null);
|
|
93
107
|
setExtraCategories((counts) => ({ ...counts, [key]: 0 }));
|
|
94
|
-
}, children: "Show fewer" })), _jsx("button", { "aria-label": "Zoom diagram out", disabled: zoom <= 10, onClick: () => zoomBy(1 / 1.2), children: "\u2212" }), _jsxs("output", { "aria-label": "Diagram zoom", children: [zoom, "%"] }), _jsx("button", { "aria-label": "Zoom diagram in", disabled: zoom >= 400, onClick: () => zoomBy(1.2), children: "+" }), _jsx("button", { onClick: fit, children: "Fit" }), _jsx("button", { "aria-pressed": flow, onClick: () => setFlow(!flow), children: flow ? "Pause flow" : "Play flow" })] }))] })] }), showProbabilities && answer && (_jsx(ChoiceProbabilities, { answer: answer }, key)), _jsx("div", { hidden: showProbabilities, className: "lab-flow-
|
|
108
|
+
}, children: "Show fewer" })), !embedded && (_jsxs(_Fragment, { children: [_jsx("button", { "aria-label": "Zoom diagram out", disabled: zoom <= 10, onClick: () => zoomBy(1 / 1.2), children: "\u2212" }), _jsxs("output", { "aria-label": "Diagram zoom", children: [zoom, "%"] }), _jsx("button", { "aria-label": "Zoom diagram in", disabled: zoom >= 400, onClick: () => zoomBy(1.2), children: "+" }), _jsx("button", { onClick: fit, children: "Fit" })] })), _jsx("button", { "aria-pressed": flow, onClick: () => setFlow(!flow), children: flow ? "Pause flow" : "Play flow" })] }))] })] }), showProbabilities && answer && (_jsx(ChoiceProbabilities, { answer: answer }, key)), _jsx("div", { hidden: showProbabilities, className: embedded ? "lab-flow-surface" : "lab-flow-viewport", ref: embedded ? undefined : viewport, style: embedded ? { height } : undefined, tabIndex: embedded ? undefined : 0, role: "group", "aria-label": embedded
|
|
109
|
+
? "Jev request diagram"
|
|
110
|
+
: "Jev diagram. Drag to pan, scroll or pinch to zoom. Arrow keys pan, plus and minus zoom, zero fits.", children: _jsxs("div", { ref: embedded ? undefined : canvas, className: "lab-flow-canvas", style: { width: 1120, height }, children: [_jsxs("svg", { className: "lab-connections", width: "1120", height: height, "aria-hidden": "true", children: [paths.map((path) => (_jsxs("g", { children: [_jsx("path", { d: path.d, pathLength: 1, className: `${path.active ? "active" : ""} ${"delay" in path && path.delay !== undefined ? "lab-branch-enter" : ""}`, style: {
|
|
95
111
|
animationDelay: "delay" in path ? path.delay : undefined,
|
|
96
112
|
} }), flow &&
|
|
97
113
|
!showProbabilities &&
|
|
98
114
|
path.active &&
|
|
99
|
-
!["error", "cancelled"].includes(trace.status) && (_jsx("circle", { r: "3", className: "lab-flow-dot", children: _jsx("animateMotion", { dur: "3.2s", repeatCount: "indefinite", path: path.d }) }))] }, path.id))), _jsx("text", { x: "305", y: mid - 65, children: "state" }), _jsx("text", { x: "300", y: mid + 65, children: "question" })] }), _jsxs("article", { className: "lab-card", style: { left: 20, top: 32, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: image ? "IMAGE STATE" : "STATE" }), _jsx("h3", { children: "Shared context" }), image ? (_jsx("img", { className: "lab-source-image", src: image, alt: "Captured image state" })) : (_jsx(
|
|
100
|
-
? question.instructions
|
|
101
|
-
: JSON.stringify(question.instructions), children: typeof question.instructions === "string"
|
|
102
|
-
? question.instructions
|
|
103
|
-
: JSON.stringify(question.instructions) })] }), _jsxs("article", { className: "lab-card lab-answer-card", style: {
|
|
115
|
+
!["error", "cancelled"].includes(trace.status) && (_jsx("circle", { r: "3", className: "lab-flow-dot", children: _jsx("animateMotion", { dur: "3.2s", repeatCount: "indefinite", path: path.d }) }))] }, path.id))), _jsx("text", { x: "305", y: mid - 65, children: "state" }), _jsx("text", { x: "300", y: mid + 65, children: "question" })] }), _jsxs("article", { className: "lab-card lab-content-card", style: { left: 20, top: 32, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: image ? "IMAGE STATE" : "STATE" }), _jsx("h3", { children: "Shared context" }), image ? (_jsx("img", { className: "lab-source-image", src: image, alt: "Captured image state" })) : (_jsx(DiagramContent, { value: trace.request.state, label: "State properties" }, trace.id))] }), _jsxs("article", { className: "lab-card lab-content-card", style: { left: 20, top: 288, width: 240, height: 180 }, children: [_jsx("span", { className: "lab-eyebrow", children: "INSTRUCTIONS" }), _jsxs("h3", { children: ["Question \u00B7 ", question.type] }), _jsx(DiagramContent, { value: question.instructions, label: "Question instructions" }, `${trace.id}:${key}`)] }), _jsxs("article", { className: "lab-card lab-answer-card", style: {
|
|
104
116
|
left: 390,
|
|
105
117
|
top: ry,
|
|
106
118
|
width: 320,
|
|
107
119
|
minHeight: resultHeight,
|
|
108
|
-
}, children: [_jsxs("span", { className: "lab-eyebrow", children: ["JEV \u00B7 ", question.type.toUpperCase()] }), _jsx("h3", { title: key, children: key }), compact && (_jsxs("p", { className: "lab-small", children: ["Showing ", shownProbabilities.length, " of ", allProbabilities.length, " ", "probabilities"] })), _jsx(Result, { answer: answer, image: image, probabilities: compact ? shownProbabilities : undefined })] }), outputs.map((output, i) => (_jsxs("article", { className: `lab-card lab-output ${output.active ? "" : "lab-idle"} ${enteringDelay(i) !== undefined ? "lab-card-enter" : ""}`, onAnimationEnd: () => {
|
|
120
|
+
}, children: [_jsxs("span", { className: "lab-eyebrow", children: ["JEV \u00B7 ", question.type.toUpperCase()] }), _jsx("h3", { title: key, children: key }), compact && (_jsxs("p", { className: "lab-small", children: ["Showing ", shownProbabilities.length, " of ", allProbabilities.length, " ", "probabilities"] })), _jsx(Result, { answer: answer, image: image, probabilities: compact ? shownProbabilities : undefined })] }), outputs.map((output, i) => (_jsxs("article", { className: `lab-card lab-output ${"probability" in output ? "lab-noul-output" : ""} ${output.active ? "" : "lab-idle"} ${enteringDelay(i) !== undefined ? "lab-card-enter" : ""}`, onAnimationEnd: () => {
|
|
109
121
|
if (entering?.question === key && i === entering.to - 1)
|
|
110
122
|
setEntering(null);
|
|
111
123
|
}, style: {
|
|
@@ -114,6 +126,12 @@ export function TraceDiagram({ trace }) {
|
|
|
114
126
|
width: 230,
|
|
115
127
|
minHeight: compact ? 96 : 104,
|
|
116
128
|
animationDelay: enteringDelay(i),
|
|
129
|
+
...("probability" in output &&
|
|
130
|
+
Number.isFinite(output.probability)
|
|
131
|
+
? {
|
|
132
|
+
"--lab-noul-probability": Math.max(0, Math.min(1, output.probability)),
|
|
133
|
+
}
|
|
134
|
+
: {}),
|
|
117
135
|
}, children: [_jsx("span", { className: "lab-eyebrow", children: output.eyebrow }), _jsx("h3", { title: output.label, children: output.label }), _jsx("p", { children: output.detail })] }, `${key}-${output.label}`))), remaining > 0 && (_jsxs("article", { className: "lab-card lab-choice-remainder", style: {
|
|
118
136
|
left: 870,
|
|
119
137
|
top: 40 + outputs.length * spacing,
|
|
@@ -130,5 +148,5 @@ export function TraceDiagram({ trace }) {
|
|
|
130
148
|
...counts,
|
|
131
149
|
[key]: (counts[key] ?? 0) + 3,
|
|
132
150
|
}));
|
|
133
|
-
}, children: "Show More" }), _jsxs("p", { className: "lab-small", children: [remaining, " remaining \u00B7 Add ", Math.min(3, remaining)] })] }))] }) }), !showProbabilities && (_jsxs("p", { className: "muted lab-flow-caption", children: [_jsx("span", { className: "lab-gesture-hint", children: "Drag to pan \u00B7 Scroll or pinch to zoom \u00B7 0 to fit" }), entries.length, " parallel question", entries.length === 1 ? "" : "s", " in one request. Connections show data flow, not model timing. Application routes and thresholds are not captured. Full inputs and criteria are in Questions and Context."] }))] }));
|
|
151
|
+
}, children: "Show More" }), _jsxs("p", { className: "lab-small", children: [remaining, " remaining \u00B7 Add ", Math.min(3, remaining)] })] }))] }) }), !showProbabilities && !embedded && (_jsxs("p", { className: "muted lab-flow-caption", children: [_jsx("span", { className: "lab-gesture-hint", children: "Drag to pan \u00B7 Scroll or pinch to zoom \u00B7 0 to fit" }), entries.length, " parallel question", entries.length === 1 ? "" : "s", " in one request. Connections show data flow, not model timing. Application routes and thresholds are not captured. Full inputs and criteria are in Questions and Context."] }))] }));
|
|
134
152
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Json, Trace } from "./protocol.js";
|
|
2
|
+
import type { FlowRun } from "./flow-protocol.js";
|
|
3
|
+
export type ConditionInput = {
|
|
4
|
+
reference: string;
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function conditionInputs(rule: Json, run: FlowRun, traces: Map<string, Trace>, stepNames?: Map<string, string>): ConditionInput[];
|
|
9
|
+
export declare function conditionRequirement(rule: Json, inputs: ConditionInput[]): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { formatFlowCondition, formatConditionLiteral, } from "./flow-condition.js";
|
|
2
|
+
export function conditionInputs(rule, run, traces, stepNames = new Map()) {
|
|
3
|
+
const inputs = new Map();
|
|
4
|
+
const sources = new Map();
|
|
5
|
+
const visit = (node, depth = 0) => {
|
|
6
|
+
if (depth > 16 || !node || typeof node !== "object" || Array.isArray(node))
|
|
7
|
+
return;
|
|
8
|
+
if (node.op === "reference" &&
|
|
9
|
+
typeof node.label === "string" &&
|
|
10
|
+
!inputs.has(node.label)) {
|
|
11
|
+
const name = typeof node.name === "string" ? node.name : "Input";
|
|
12
|
+
const readable = name
|
|
13
|
+
.replace(/[_-]+/g, " ")
|
|
14
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
15
|
+
const trace = typeof node.stepId === "string"
|
|
16
|
+
? traces.get(run.steps[node.stepId]?.traceId ?? "")
|
|
17
|
+
: undefined;
|
|
18
|
+
let value = trace?.result;
|
|
19
|
+
let available = !!trace?.result;
|
|
20
|
+
if (!Array.isArray(node.path) ||
|
|
21
|
+
!node.path.every((key) => typeof key === "string"))
|
|
22
|
+
available = false;
|
|
23
|
+
else
|
|
24
|
+
for (const key of node.path) {
|
|
25
|
+
if (!available ||
|
|
26
|
+
!value ||
|
|
27
|
+
typeof value !== "object" ||
|
|
28
|
+
!Object.hasOwn(value, String(key))) {
|
|
29
|
+
available = false;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
value = value[String(key)];
|
|
33
|
+
}
|
|
34
|
+
const text = !available
|
|
35
|
+
? "Not captured"
|
|
36
|
+
: node.probability === true &&
|
|
37
|
+
typeof value === "number" &&
|
|
38
|
+
Number.isFinite(value)
|
|
39
|
+
? formatConditionLiteral(value, true)
|
|
40
|
+
: typeof value === "object" && value !== null
|
|
41
|
+
? "Structured value"
|
|
42
|
+
: String(value);
|
|
43
|
+
if (typeof node.stepId === "string")
|
|
44
|
+
sources.set(node.label, stepNames.get(node.stepId) ?? node.stepId);
|
|
45
|
+
inputs.set(node.label, {
|
|
46
|
+
reference: node.label,
|
|
47
|
+
name: readable.charAt(0).toUpperCase() + readable.slice(1),
|
|
48
|
+
value: text,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(node.args))
|
|
52
|
+
for (const arg of node.args)
|
|
53
|
+
visit(arg, depth + 1);
|
|
54
|
+
};
|
|
55
|
+
visit(rule);
|
|
56
|
+
const list = [...inputs.values()];
|
|
57
|
+
return list.map((input) => list.filter((other) => other.name === input.name).length > 1
|
|
58
|
+
? {
|
|
59
|
+
...input,
|
|
60
|
+
name: `${input.name} · ${sources.get(input.reference) ?? input.reference}`,
|
|
61
|
+
}
|
|
62
|
+
: input);
|
|
63
|
+
}
|
|
64
|
+
export function conditionRequirement(rule, inputs) {
|
|
65
|
+
const names = new Map(inputs.map((input) => [input.reference, input.name]));
|
|
66
|
+
// A single reference-to-literal comparison already has its subject in Incoming.
|
|
67
|
+
if (rule &&
|
|
68
|
+
typeof rule === "object" &&
|
|
69
|
+
!Array.isArray(rule) &&
|
|
70
|
+
Array.isArray(rule.args) &&
|
|
71
|
+
rule.args.length === 2) {
|
|
72
|
+
const [left, right] = rule.args;
|
|
73
|
+
if (left &&
|
|
74
|
+
right &&
|
|
75
|
+
typeof left === "object" &&
|
|
76
|
+
typeof right === "object" &&
|
|
77
|
+
!Array.isArray(left) &&
|
|
78
|
+
!Array.isArray(right) &&
|
|
79
|
+
left.op === "reference" &&
|
|
80
|
+
right.op === "literal" &&
|
|
81
|
+
["gte", "lt", "eq"].includes(String(rule.op))) {
|
|
82
|
+
return `${rule.op === "gte" ? "At least" : rule.op === "lt" ? "Less than" : "Equal to"} ${formatConditionLiteral(right.value, left.probability === true)}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const rename = (node, depth = 0) => {
|
|
86
|
+
if (depth > 16 || !node || typeof node !== "object" || Array.isArray(node))
|
|
87
|
+
return node;
|
|
88
|
+
if (node.op === "reference")
|
|
89
|
+
return {
|
|
90
|
+
...node,
|
|
91
|
+
label: typeof node.label === "string"
|
|
92
|
+
? (names.get(node.label) ?? "Input")
|
|
93
|
+
: "Input",
|
|
94
|
+
};
|
|
95
|
+
if (Array.isArray(node.args))
|
|
96
|
+
return { ...node, args: node.args.map((arg) => rename(arg, depth + 1)) };
|
|
97
|
+
return node;
|
|
98
|
+
};
|
|
99
|
+
return formatFlowCondition(rename(rule));
|
|
100
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Json } from "./protocol.js";
|
|
2
|
+
export declare function formatConditionLiteral(value: Json, probability?: boolean): string;
|
|
3
|
+
/** Format captured rules only; never evaluate or infer the branch result. */
|
|
4
|
+
export declare function formatFlowCondition(value: Json, depth?: number): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function formatConditionLiteral(value, probability = false) {
|
|
2
|
+
if (probability && typeof value === "number" && value >= 0 && value <= 1) {
|
|
3
|
+
const [coefficient, exponent = "0"] = String(value).split("e");
|
|
4
|
+
return `${Number(`${coefficient}e${Number(exponent) + 2}`)}%`;
|
|
5
|
+
}
|
|
6
|
+
return JSON.stringify(value) ?? "[value unavailable]";
|
|
7
|
+
}
|
|
8
|
+
/** Format captured rules only; never evaluate or infer the branch result. */
|
|
9
|
+
export function formatFlowCondition(value, depth = 0) {
|
|
10
|
+
if (depth > 16)
|
|
11
|
+
return "[rule truncated]";
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
13
|
+
return typeof value === "string" ? value : "[rule unavailable]";
|
|
14
|
+
if (value.op === "reference")
|
|
15
|
+
return typeof value.label === "string"
|
|
16
|
+
? value.label
|
|
17
|
+
: "[reference unavailable]";
|
|
18
|
+
if (value.op === "literal")
|
|
19
|
+
return formatConditionLiteral(value.value);
|
|
20
|
+
if (!Array.isArray(value.args))
|
|
21
|
+
return "[rule unavailable]";
|
|
22
|
+
const args = value.args;
|
|
23
|
+
const format = (v) => formatFlowCondition(v, depth + 1);
|
|
24
|
+
if (value.op === "not")
|
|
25
|
+
return `NOT (${format(args[0])})`;
|
|
26
|
+
if (value.op === "all" || value.op === "any") {
|
|
27
|
+
if (!args.length)
|
|
28
|
+
return value.op === "all"
|
|
29
|
+
? "TRUE (no conditions)"
|
|
30
|
+
: "FALSE (no conditions)";
|
|
31
|
+
return `(${args.map(format).join(value.op === "all" ? " AND " : " OR ")})`;
|
|
32
|
+
}
|
|
33
|
+
if (!["eq", "lt", "gte"].includes(String(value.op)) || args.length !== 2)
|
|
34
|
+
return "[rule unavailable]";
|
|
35
|
+
const probability = args.some((arg) => arg &&
|
|
36
|
+
typeof arg === "object" &&
|
|
37
|
+
!Array.isArray(arg) &&
|
|
38
|
+
arg.op === "reference" &&
|
|
39
|
+
arg.probability === true);
|
|
40
|
+
const operand = (arg) => arg &&
|
|
41
|
+
typeof arg === "object" &&
|
|
42
|
+
!Array.isArray(arg) &&
|
|
43
|
+
arg.op === "literal"
|
|
44
|
+
? formatConditionLiteral(arg.value, probability)
|
|
45
|
+
: format(arg);
|
|
46
|
+
return `${operand(args[0])} ${value.op === "eq" ? "=" : value.op === "lt" ? "<" : "≥"} ${operand(args[1])}`;
|
|
47
|
+
}
|
package/lib/src/flow-model.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type { AnswerView, Json
|
|
1
|
+
import type { AnswerView, Json } from "./protocol.js";
|
|
2
2
|
export declare function findImage(value: Json): string | undefined;
|
|
3
3
|
export declare function flowAnswer(answer?: AnswerView): {
|
|
4
4
|
label: string;
|
|
5
5
|
detail: string;
|
|
6
6
|
active: boolean;
|
|
7
7
|
eyebrow: string;
|
|
8
|
+
}[] | {
|
|
9
|
+
label: string;
|
|
10
|
+
detail: string;
|
|
11
|
+
active: boolean;
|
|
12
|
+
eyebrow: string;
|
|
13
|
+
probability: number;
|
|
8
14
|
}[];
|
|
9
|
-
export declare function stateText(trace: Trace): string;
|