jev-devtools 0.1.3 → 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.
Files changed (52) hide show
  1. package/README.md +50 -4
  2. package/THIRD_PARTY_NOTICES.txt +27 -0
  3. package/lib/flow/index.d.ts +82 -0
  4. package/lib/flow/index.js +588 -0
  5. package/lib/server/flows.d.ts +25 -0
  6. package/lib/server/flows.js +115 -0
  7. package/lib/server/index.d.ts +8 -0
  8. package/lib/server/index.js +163 -9
  9. package/lib/server/recorder.d.ts +12 -2
  10. package/lib/server/recorder.js +47 -9
  11. package/lib/src/ChoiceProbabilities.d.ts +4 -0
  12. package/lib/src/ChoiceProbabilities.js +19 -0
  13. package/lib/src/ConnectedInspector.js +46 -29
  14. package/lib/src/DiagramContent.d.ts +6 -0
  15. package/lib/src/DiagramContent.js +32 -0
  16. package/lib/src/FlowOverview.d.ts +7 -0
  17. package/lib/src/FlowOverview.js +228 -0
  18. package/lib/src/JevDevtools.d.ts +6 -1
  19. package/lib/src/JevDevtools.js +52 -48
  20. package/lib/src/JevDrawer.d.ts +3 -2
  21. package/lib/src/JevDrawer.js +12 -3
  22. package/lib/src/Questions.d.ts +5 -0
  23. package/lib/src/Questions.js +174 -0
  24. package/lib/src/TraceContent.d.ts +11 -0
  25. package/lib/src/TraceContent.js +32 -0
  26. package/lib/src/TraceDiagram.d.ts +4 -1
  27. package/lib/src/TraceDiagram.js +111 -25
  28. package/lib/src/condition-inputs.d.ts +9 -0
  29. package/lib/src/condition-inputs.js +100 -0
  30. package/lib/src/flow-condition.d.ts +4 -0
  31. package/lib/src/flow-condition.js +47 -0
  32. package/lib/src/flow-model.d.ts +7 -2
  33. package/lib/src/flow-model.js +3 -12
  34. package/lib/src/flow-protocol.d.ts +68 -0
  35. package/lib/src/flow-protocol.js +1 -0
  36. package/lib/src/flow-stream.d.ts +8 -0
  37. package/lib/src/flow-stream.js +80 -0
  38. package/lib/src/index.d.ts +1 -1
  39. package/lib/src/inspector-styles.js +1 -1
  40. package/lib/src/inspector.css +725 -8
  41. package/lib/src/protocol.d.ts +18 -0
  42. package/lib/src/trace-store.d.ts +6 -0
  43. package/lib/src/trace-store.js +66 -0
  44. package/lib/src/trace-stream.d.ts +2 -0
  45. package/lib/src/trace-stream.js +65 -0
  46. package/lib/src/useDiagramViewport.d.ts +5 -0
  47. package/lib/src/useDiagramViewport.js +34 -6
  48. package/package.json +7 -2
  49. package/skills/setup-jev-devtools/SKILL.md +6 -2
  50. package/skills/setup-jev-devtools/references/flows.md +150 -0
  51. package/skills/setup-jev-devtools/references/nextjs.md +1 -1
  52. package/skills/setup-jev-devtools/references/verify.md +3 -1
@@ -0,0 +1,174 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { memo, useCallback, useEffect, useLayoutEffect, useId, useMemo, useRef, useState, } from "react";
3
+ import { defaultRangeExtractor, measureElement, observeElementRect, useVirtualizer, } from "@tanstack/react-virtual";
4
+ import { Answer, findImage, JsonBlock } from "./TraceContent.js";
5
+ const QuestionContent = memo(function QuestionContent({ name, question, answer, image, legendOpen, onLegendChange, }) {
6
+ return (_jsxs(_Fragment, { children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx(JsonBlock, { value: question }), _jsx(Answer, { answer: answer, image: image, legendOpen: legendOpen, onLegendToggle: (open) => onLegendChange(name, open) })] }));
7
+ });
8
+ /** Variable-height rows retain full answers; only offscreen DOM is omitted. */
9
+ export function Questions({ trace }) {
10
+ const helpId = useId();
11
+ const entries = useMemo(() => Object.entries(trace.request.questions ?? {}).filter(([, q]) => q && typeof q === "object" && typeof q.type === "string"), [trace.request.questions]);
12
+ const image = useMemo(() => findImage(trace.request.state), [trace.request.state]);
13
+ const viewport = useRef(null);
14
+ const sizes = useRef(new Map());
15
+ const [seeking, setSeekingState] = useState(false);
16
+ const seekingValue = useRef(false);
17
+ const setSeeking = useCallback((next) => {
18
+ // A range update may already be queued by the virtualizer. Avoid enqueuing
19
+ // the same preview state on every scroll event alongside that React work.
20
+ if (seekingValue.current === next)
21
+ return;
22
+ seekingValue.current = next;
23
+ setSeekingState(next);
24
+ }, []);
25
+ const scrollSample = useRef(null);
26
+ const scrollIdle = useRef(undefined);
27
+ const explicitJump = useRef(false);
28
+ useEffect(() => () => clearTimeout(scrollIdle.current), []);
29
+ const pendingFocus = useRef(null);
30
+ const [focused, setFocused] = useState(null);
31
+ const [destination, setDestination] = useState(0);
32
+ const [legends, setLegends] = useState(() => new Map());
33
+ const onLegendChange = useCallback((name, open) => {
34
+ setLegends((previous) => previous.get(name) === open
35
+ ? previous
36
+ : new Map(previous).set(name, open));
37
+ }, []);
38
+ const getItemKey = useCallback((index) => entries[index][0], [entries]);
39
+ const rangeExtractor = useCallback((range) => {
40
+ const indices = defaultRangeExtractor(range);
41
+ // Never remove a focused control when it scrolls outside the visible range.
42
+ if (focused !== null &&
43
+ focused < entries.length &&
44
+ !indices.includes(focused))
45
+ indices.push(focused);
46
+ return indices.sort((a, b) => a - b);
47
+ }, [focused, entries.length]);
48
+ const virtualizer = useVirtualizer({
49
+ count: entries.length,
50
+ getScrollElement: () => viewport.current,
51
+ getItemKey,
52
+ estimateSize: () => 600,
53
+ overscan: 1,
54
+ rangeExtractor,
55
+ directDomUpdates: true,
56
+ // A hidden drawer reports zero sizes. Retain its measurements and scroll
57
+ // range instead of collapsing rows or resetting the reader's position.
58
+ observeElementRect: (instance, callback) => observeElementRect(instance, (rect) => {
59
+ if (rect.width > 0 && rect.height > 0)
60
+ callback(rect);
61
+ }),
62
+ measureElement: (element, entry, instance) => {
63
+ const key = element.dataset.questionKey;
64
+ // A fast-scroll preview reserves the last measured height. Never replace
65
+ // the real measurement cache with its temporary placeholder geometry.
66
+ if (element.dataset.preview === "true")
67
+ return sizes.current.get(key) || 600;
68
+ const height = measureElement(element, entry, instance);
69
+ if (height > 0)
70
+ sizes.current.set(key, height);
71
+ return height || sizes.current.get(key) || 600;
72
+ },
73
+ });
74
+ const items = virtualizer.getVirtualItems();
75
+ const goTo = useCallback((index, focus = false) => {
76
+ const next = Math.max(0, Math.min(entries.length - 1, index));
77
+ explicitJump.current = true;
78
+ setSeeking(false);
79
+ scrollSample.current = null;
80
+ setDestination(next);
81
+ if (focus) {
82
+ pendingFocus.current = next;
83
+ setFocused(next);
84
+ }
85
+ virtualizer.scrollToIndex(next, { align: "start" });
86
+ }, [entries.length, virtualizer, setSeeking]);
87
+ useLayoutEffect(() => {
88
+ if (pendingFocus.current === null)
89
+ return;
90
+ const card = viewport.current?.querySelector(`[data-index="${pendingFocus.current}"] article`);
91
+ if (card) {
92
+ card.focus({ preventScroll: true });
93
+ pendingFocus.current = null;
94
+ }
95
+ });
96
+ // Scroll-range updates must not rebuild the full question picker.
97
+ const navigation = useMemo(() => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "questions-navigation", children: [_jsxs("label", { children: ["Jump to question", _jsx("select", { "aria-label": "Jump to question", value: Math.min(destination, entries.length - 1), onChange: (event) => goTo(Number(event.target.value)), children: entries.map(([name, q], index) => (_jsxs("option", { value: index, children: [index + 1, ". ", name, " \u00B7 ", q.type] }, name))) })] }), _jsx("button", { "aria-label": "Previous question", disabled: destination === 0, onClick: () => goTo(destination - 1), children: "Previous" }), _jsx("button", { "aria-label": "Next question", disabled: destination >= entries.length - 1, onClick: () => goTo(destination + 1), children: "Next" }), _jsxs("span", { children: [entries.length, " question", entries.length === 1 ? "" : "s"] })] }), _jsx("p", { className: "questions-help muted", id: helpId, children: "\u2191 / \u2193 previous / next \u00B7 Home / End first / last when the list is focused." })] })), [entries, destination, goTo, helpId]);
98
+ if (!entries.length)
99
+ return _jsx("p", { className: "muted", children: "No questions available in this capture." });
100
+ return (_jsxs("div", { className: "questions-list", children: [navigation, _jsx("div", { className: "questions-viewport", ref: viewport, tabIndex: 0, role: "region", "aria-label": "Questions and answers", "aria-describedby": helpId, onScroll: (event) => {
101
+ const offset = event.currentTarget.scrollTop;
102
+ const time = performance.now();
103
+ const previous = scrollSample.current;
104
+ clearTimeout(scrollIdle.current);
105
+ scrollIdle.current = setTimeout(() => {
106
+ scrollSample.current = null;
107
+ explicitJump.current = false;
108
+ setSeeking(false);
109
+ }, 120);
110
+ if (explicitJump.current)
111
+ return;
112
+ if (!previous) {
113
+ scrollSample.current = { offset, time, fastDirection: 0 };
114
+ return;
115
+ }
116
+ const elapsed = time - previous.time;
117
+ if (elapsed < 8)
118
+ return;
119
+ const velocity = (Math.abs(offset - previous.offset) / elapsed) * 1000;
120
+ // A jump across most of the list is a destination change, not a
121
+ // scan. Measurement corrections around its endpoints can otherwise
122
+ // look like additional high-speed samples.
123
+ // Read cached geometry; querying scrollHeight here can force layout
124
+ // immediately after the virtualizer has mounted a new row.
125
+ const viewportHeight = virtualizer.scrollRect?.height || 0;
126
+ const jump = Math.abs(offset - previous.offset) >
127
+ Math.max(viewportHeight, (virtualizer.getTotalSize() - viewportHeight) / 2);
128
+ const fastDirection = !jump && velocity > 4000 ? Math.sign(offset - previous.offset) : 0;
129
+ scrollSample.current = { offset, time, fastDirection };
130
+ // Two fast samples in the same direction distinguish continuous
131
+ // scanning from an isolated jump. A lower exit threshold prevents
132
+ // flickering as the reader slows down.
133
+ if (jump)
134
+ setSeeking(false);
135
+ else if (fastDirection && fastDirection === previous.fastDirection)
136
+ setSeeking(true);
137
+ else if (velocity < 1800)
138
+ setSeeking(false);
139
+ }, onFocusCapture: (event) => {
140
+ const row = event.target.closest("[data-index]");
141
+ if (row) {
142
+ const index = Number(row.dataset.index);
143
+ setFocused(index);
144
+ setDestination(index);
145
+ }
146
+ }, onBlurCapture: (event) => {
147
+ if (!event.currentTarget.contains(event.relatedTarget))
148
+ setFocused(null);
149
+ }, onKeyDown: (event) => {
150
+ // Inputs, disclosures and scrollable JSON keep their native keys.
151
+ if (event.target !== event.currentTarget &&
152
+ !event.target.matches("article"))
153
+ return;
154
+ const next = event.key === "Home"
155
+ ? 0
156
+ : event.key === "End"
157
+ ? entries.length - 1
158
+ : event.key === "ArrowDown"
159
+ ? destination + 1
160
+ : event.key === "ArrowUp"
161
+ ? destination - 1
162
+ : undefined;
163
+ if (next !== undefined) {
164
+ event.preventDefault();
165
+ goTo(next, true);
166
+ }
167
+ }, children: _jsx("div", { role: "list", "aria-label": "Captured questions", className: "questions-spacer", ref: virtualizer.containerRef, children: items.map((item) => {
168
+ const [name, question] = entries[item.index];
169
+ const preview = seeking && focused !== item.index;
170
+ return (_jsx("div", { "data-index": item.index, "data-question-key": name, "data-preview": preview || undefined, style: preview
171
+ ? { height: sizes.current.get(name) || 600 }
172
+ : undefined, ref: virtualizer.measureElement, className: "questions-row", role: "listitem", "aria-posinset": item.index + 1, "aria-setsize": entries.length, children: _jsx("article", { className: "question-node standalone", tabIndex: -1, "aria-busy": preview || undefined, "aria-label": `Question ${item.index + 1}: ${name}`, children: preview ? (_jsxs("div", { className: "question-scroll-preview", children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx("p", { className: "muted", children: "Pause scrolling to inspect this answer." })] })) : (_jsx(QuestionContent, { name: name, question: question, answer: trace.result?.answers[name], image: image, legendOpen: !!legends.get(name), onLegendChange: onLegendChange })) }) }, item.key));
173
+ }) }) })] }));
174
+ }
@@ -0,0 +1,11 @@
1
+ import type { AnswerView, Json } from "./protocol.js";
2
+ export declare function findImage(value: Json): string | undefined;
3
+ export declare function JsonBlock({ value }: {
4
+ value: unknown;
5
+ }): import("react").JSX.Element;
6
+ export declare function Answer({ answer, image, legendOpen, onLegendToggle, }: {
7
+ answer?: AnswerView;
8
+ image?: string;
9
+ legendOpen: boolean;
10
+ onLegendToggle: (open: boolean) => void;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const pretty = (value) => JSON.stringify(value, null, 2);
3
+ const percent = (value) => `${Math.round(value * 100)}%`;
4
+ export function findImage(value) {
5
+ if (!value || typeof value !== "object")
6
+ return;
7
+ if (!Array.isArray(value) &&
8
+ typeof value.preview === "string" &&
9
+ /^data:image\/(png|jpeg);base64,/.test(value.preview))
10
+ return value.preview;
11
+ for (const child of Object.values(value)) {
12
+ const found = findImage(child);
13
+ if (found)
14
+ return found;
15
+ }
16
+ }
17
+ export function JsonBlock({ value }) {
18
+ return _jsx("pre", { children: pretty(value) });
19
+ }
20
+ export function Answer({ answer, image, legendOpen, onLegendToggle, }) {
21
+ if (!answer)
22
+ return _jsx("p", { className: "muted", children: "No answer received." });
23
+ return (_jsxs("div", { className: "answer", children: [answer.choice !== undefined && (_jsx("strong", { className: "answer-value", children: answer.choice })), answer.score !== undefined && (_jsxs("strong", { className: "answer-value", children: [answer.score.toFixed(2), " ", _jsx("small", { children: "score" })] })), answer.noul !== undefined && (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "answer-value", children: [percent(answer.noul), " ", _jsx("small", { children: "probability of true" })] }), _jsx("div", { className: "bar", children: _jsx("i", { style: { width: percent(answer.noul) } }) })] })), answer.probabilities &&
24
+ Object.entries(answer.probabilities).map(([label, value]) => (_jsxs("div", { className: "probability", children: [_jsxs("div", { children: [_jsx("span", { children: label }), _jsx("b", { children: percent(value) })] }), _jsx("div", { className: "bar", children: _jsx("i", { style: { width: percent(value) } }) })] }, label))), answer.legend && (_jsxs("details", { open: legendOpen, onToggle: (event) => onLegendToggle(event.currentTarget.open), children: [_jsx("summary", { children: "Score legend" }), _jsx(JsonBlock, { value: answer.legend })] })), answer.box && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "box-preview", children: [image ? (_jsx("img", { src: image, alt: "Captured request image" })) : (_jsx("span", { children: "Image preview unavailable" })), _jsx("div", { className: "bounding-box", style: {
25
+ left: percent(answer.box.left),
26
+ top: percent(answer.box.top),
27
+ width: percent(answer.box.right - answer.box.left),
28
+ height: percent(answer.box.bottom - answer.box.top),
29
+ } })] }), _jsx("code", { className: "coordinates", children: Object.entries(answer.box)
30
+ .map(([k, v]) => `${k} ${v.toFixed(2)}`)
31
+ .join(" · ") })] })), answer.confidence !== undefined && (_jsxs("p", { className: "confidence", children: ["Confidence ", _jsx("b", { children: percent(answer.confidence) })] }))] }));
32
+ }
@@ -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;
@@ -1,19 +1,21 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { useDiagramViewport } from "./useDiagramViewport.js";
4
- import { findImage, flowAnswer, stateText } from "./flow-model.js";
4
+ import { findImage, flowAnswer } from "./flow-model.js";
5
+ import { DiagramContent } from "./DiagramContent.js";
6
+ import { ChoiceProbabilities } from "./ChoiceProbabilities.js";
5
7
  const pct = (v) => `${Math.round(v * 100)}%`;
6
- function Result({ answer, image }) {
8
+ function Result({ answer, image, probabilities, }) {
7
9
  if (!answer)
8
10
  return _jsx("p", { className: "lab-copy", children: "No answer received." });
9
11
  return (_jsxs(_Fragment, { children: [answer.probabilities &&
10
- Object.entries(answer.probabilities).map(([key, value]) => (_jsxs("div", { className: "lab-probability", children: [_jsxs("div", { children: [_jsx("span", { title: typeof answer.legend?.[key] === "string"
12
+ (probabilities ?? Object.entries(answer.probabilities)).map(([key, value]) => (_jsxs("div", { className: "lab-probability", children: [_jsxs("div", { children: [_jsx("span", { title: typeof answer.legend?.[key] === "string"
11
13
  ? answer.legend[key]
12
14
  : key, children: answer.type === "score" && answer.legend?.[key]
13
15
  ? `${key} · ${answer.legend[key]}`
14
- : key }), _jsx("code", { children: pct(value) })] }), _jsx("div", { className: "lab-track", children: _jsx("i", { className: answer.type === "choice" && answer.choice !== key
16
+ : key }), _jsxs("code", { title: `Exact probability: ${value}`, children: [Number((value * 100).toPrecision(6)), "%"] })] }), _jsx("div", { className: "lab-track", children: _jsx("i", { className: answer.type === "choice" && answer.choice !== key
15
17
  ? "lab-alternative"
16
- : "", style: { width: pct(Math.max(0, Math.min(1, value))) } }) })] }, key))), answer.choice !== undefined && (_jsxs("p", { className: "lab-result", children: ["Selected: ", answer.choice] })), answer.score !== undefined && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "lab-result", children: ["Score ", answer.score.toFixed(2)] }), _jsxs("div", { className: "lab-scale", children: [_jsx("span", { style: {
18
+ : "", style: { width: pct(Math.max(0, Math.min(1, value))) } }) })] }, key))), answer.choice !== undefined && (_jsxs("p", { className: "lab-result", title: `Selected: ${answer.choice}`, children: ["Selected: ", answer.choice] })), answer.score !== undefined && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "lab-result", children: ["Score ", answer.score.toFixed(2)] }), _jsxs("div", { className: "lab-scale", children: [_jsx("span", { style: {
17
19
  left: pct(Math.max(0, Math.min(1, answer.score /
18
20
  Math.max(1, Object.keys(answer.probabilities ?? {}).length - 1)))),
19
21
  } }), Object.keys(answer.probabilities ?? {}).map((k) => (_jsx("small", { children: k }, k)))] })] })), answer.noul !== undefined && (_jsxs(_Fragment, { children: [_jsx("strong", { className: "lab-big", children: pct(answer.noul) }), _jsx("p", { className: "lab-small", children: "probability of true" }), _jsx("div", { className: "lab-track", children: _jsx("i", { style: { width: pct(answer.noul) } }) }), _jsxs("div", { className: "lab-endpoints", children: [_jsx("span", { children: "False" }), _jsx("span", { children: "True" })] })] })), answer.box && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "lab-image", children: [image ? (_jsx("img", { src: image, alt: "Captured image with returned Box overlay" })) : (_jsx("p", { children: "Image preview unavailable" })), image && (_jsx("div", { className: "lab-box", style: {
@@ -23,44 +25,128 @@ function Result({ answer, image }) {
23
25
  height: pct(answer.box.bottom - answer.box.top),
24
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)] }))] }));
25
27
  }
26
- export function TraceDiagram({ trace }) {
27
- const [selected, setSelected] = useState(""), [flow, setFlow] = useState(true);
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]);
42
+ const [selected, setSelected] = useState(""), [flow, setFlow] = useState(true), [extraCategories, setExtraCategories] = useState({}), [entering, setEntering] = useState(null), [probabilityView, setProbabilityView] = useState(null);
28
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) ??
29
44
  entries[0] ?? ["", undefined];
30
45
  const answer = trace.result?.answers[key], image = findImage(trace.request.state) ||
31
- (trace.request.assets ? findImage(trace.request.assets) : undefined), outputs = flowAnswer(answer);
32
- const resultHeight = Math.max(280, 166 +
33
- Object.keys(answer?.probabilities ?? {}).length * 44 +
46
+ (trace.request.assets ? findImage(trace.request.assets) : undefined), allProbabilities = useMemo(() => Object.entries(answer?.probabilities ?? {}).sort((a, b) => b[1] - a[1]), [answer?.probabilities]);
47
+ const compact = answer?.type === "choice" && allProbabilities.length > 6;
48
+ const shownProbabilities = compact
49
+ ? allProbabilities.slice(0, 3)
50
+ : allProbabilities;
51
+ if (compact &&
52
+ answer.choice !== undefined &&
53
+ !shownProbabilities.some(([label]) => label === answer.choice)) {
54
+ const returned = allProbabilities.find(([label]) => label === answer.choice);
55
+ if (returned)
56
+ shownProbabilities.push(returned);
57
+ }
58
+ if (compact && (extraCategories[key] ?? 0) > 0) {
59
+ const initialLabels = new Set(shownProbabilities.map(([label]) => label));
60
+ shownProbabilities.push(...allProbabilities
61
+ .filter(([label]) => !initialLabels.has(label))
62
+ .slice(0, extraCategories[key] ?? 0));
63
+ }
64
+ const outputs = flowAnswer(compact
65
+ ? { ...answer, probabilities: Object.fromEntries(shownProbabilities) }
66
+ : answer);
67
+ const remaining = compact
68
+ ? allProbabilities.length - shownProbabilities.length
69
+ : 0;
70
+ const spacing = compact ? 112 : 136;
71
+ const showProbabilities = probabilityView === key && answer?.type === "choice";
72
+ const resultHeight = Math.max(280, (compact ? 230 : 166) +
73
+ (compact
74
+ ? shownProbabilities.length
75
+ : Object.keys(answer?.probabilities ?? {}).length) *
76
+ 44 +
34
77
  (answer?.score !== undefined ? 50 : 0), answer?.box ? 390 : 0);
35
- const height = Math.max(520, resultHeight + 90, outputs.length * 136 + 60), ry = Math.max(40, (height - resultHeight) / 2), mid = ry + resultHeight / 2;
36
- const { viewport, canvas, zoom, zoomBy, fit } = useDiagramViewport(1120, height, key);
78
+ const height = Math.max(520, resultHeight + 90, (outputs.length + (remaining ? 1 : 0)) * spacing + 60), ry = compact ? 40 : Math.max(40, (height - resultHeight) / 2), mid = ry + resultHeight / 2;
79
+ const { viewport, canvas, zoom, zoomBy, fit, preserveCamera } = useDiagramViewport(1120, height, key);
37
80
  if (!question)
38
81
  return _jsx("p", { className: "muted", children: "No questions available in this capture." });
39
82
  const sourcePath = (y) => `M260 ${y} C325 ${y},325 ${mid},390 ${mid}`;
83
+ const enteringDelay = (index) => entering?.question === key && index >= entering.from && index < entering.to
84
+ ? `${(index - entering.from) * 55}ms`
85
+ : undefined;
40
86
  const paths = [
41
87
  { id: "state", d: sourcePath(122), active: true },
42
88
  { id: "question", d: sourcePath(378), active: true },
43
89
  ...outputs.map((output, i) => ({
44
- id: `output-${i}`,
45
- d: `M710 ${mid} C790 ${mid},790 ${92 + i * 136},870 ${92 + i * 136}`,
90
+ id: `output-${key}-${output.label}`,
91
+ delay: enteringDelay(i),
92
+ d: `M710 ${mid} C790 ${mid},790 ${92 + i * spacing},870 ${92 + i * spacing}`,
46
93
  active: output.active,
47
94
  })),
48
95
  ];
49
- return (_jsxs("div", { className: "lab-flow", children: [_jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("label", { children: ["Question", _jsx("select", { "aria-label": "Diagram question", value: key, onChange: (event) => setSelected(event.target.value), children: entries.map(([name, q]) => (_jsxs("option", { value: name, children: [name, " \u00B7 ", q.type] }, name))) })] }), _jsxs("div", { 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" })] })] }), _jsx("div", { className: "lab-flow-viewport", ref: viewport, tabIndex: 0, role: "group", "aria-label": "Jev diagram. Drag to pan, scroll or pinch to zoom. Arrow keys pan, plus and minus zoom, zero fits.", children: _jsxs("div", { ref: 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, className: path.active ? "active" : "" }), flow &&
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) => {
97
+ setEntering(null);
98
+ setSelected(event.target.value);
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: () => {
100
+ setEntering(null);
101
+ setProbabilityView(showProbabilities ? null : key);
102
+ }, children: showProbabilities
103
+ ? "Back to overview"
104
+ : `All probabilities (${allProbabilities.length})` })), !showProbabilities && (_jsxs(_Fragment, { children: [compact && (_jsx("button", { disabled: (extraCategories[key] ?? 0) === 0, onClick: () => {
105
+ preserveCamera();
106
+ setEntering(null);
107
+ setExtraCategories((counts) => ({ ...counts, [key]: 0 }));
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: {
111
+ animationDelay: "delay" in path ? path.delay : undefined,
112
+ } }), flow &&
113
+ !showProbabilities &&
50
114
  path.active &&
51
- !["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("p", { className: "lab-copy", title: stateText(trace), children: stateText(trace) }))] }), _jsxs("article", { className: "lab-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("p", { className: "lab-copy", title: typeof question.instructions === "string"
52
- ? question.instructions
53
- : JSON.stringify(question.instructions), children: typeof question.instructions === "string"
54
- ? question.instructions
55
- : 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: {
56
116
  left: 390,
57
117
  top: ry,
58
118
  width: 320,
59
119
  minHeight: resultHeight,
60
- }, children: [_jsxs("span", { className: "lab-eyebrow", children: ["JEV \u00B7 ", question.type.toUpperCase()] }), _jsx("h3", { title: key, children: key }), _jsx(Result, { answer: answer, image: image })] }), outputs.map((output, i) => (_jsxs("article", { className: `lab-card lab-output ${output.active ? "" : "lab-idle"}`, style: {
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: () => {
121
+ if (entering?.question === key && i === entering.to - 1)
122
+ setEntering(null);
123
+ }, style: {
61
124
  left: 870,
62
- top: 40 + i * 136,
125
+ top: 40 + i * spacing,
63
126
  width: 230,
64
- minHeight: 104,
65
- }, children: [_jsx("span", { className: "lab-eyebrow", children: output.eyebrow }), _jsx("h3", { title: output.label, children: output.label }), _jsx("p", { children: output.detail })] }, i)))] }) }), _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."] })] }));
127
+ minHeight: compact ? 96 : 104,
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
+ : {}),
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: {
136
+ left: 870,
137
+ top: 40 + outputs.length * spacing,
138
+ width: 230,
139
+ minHeight: 96,
140
+ }, children: [_jsx("span", { className: "lab-eyebrow", children: "MORE CATEGORIES" }), _jsx("button", { onClick: () => {
141
+ preserveCamera();
142
+ setEntering({
143
+ question: key,
144
+ from: outputs.length,
145
+ to: outputs.length + Math.min(3, remaining),
146
+ });
147
+ setExtraCategories((counts) => ({
148
+ ...counts,
149
+ [key]: (counts[key] ?? 0) + 3,
150
+ }));
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."] }))] }));
66
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
+ }
@@ -1,9 +1,14 @@
1
- import type { AnswerView, Json, Trace } from "./protocol.js";
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;