@tangle-network/sandbox-ui 0.73.0 → 0.75.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.
@@ -0,0 +1,636 @@
1
+ import {
2
+ ProviderIcon
3
+ } from "./chunk-GTJ5PSK5.js";
4
+ import {
5
+ COMPACT_NODE_SIZE,
6
+ buildWorkflowGraph,
7
+ providerLabel
8
+ } from "./chunk-FNE5QLU3.js";
9
+ import "./chunk-U67V476Y.js";
10
+
11
+ // src/workflows/WorkflowGraph.tsx
12
+ import {
13
+ Background,
14
+ Controls,
15
+ Handle,
16
+ MarkerType,
17
+ Panel,
18
+ Position as Position2,
19
+ ReactFlow,
20
+ useEdgesState,
21
+ useNodesState
22
+ } from "@xyflow/react";
23
+ import "@xyflow/react/dist/style.css";
24
+ import { Maximize2, Minimize2 } from "lucide-react";
25
+ import {
26
+ createContext,
27
+ useCallback,
28
+ useContext,
29
+ useEffect,
30
+ useMemo,
31
+ useRef,
32
+ useState
33
+ } from "react";
34
+
35
+ // src/workflows/flow-graph.ts
36
+ import { Position } from "@xyflow/react";
37
+ function normalizeFlowGraphOptions(arg) {
38
+ if (!arg) return {};
39
+ if ("nodeState" in arg || "direction" in arg || "compact" in arg || "measure" in arg) {
40
+ return arg;
41
+ }
42
+ return { nodeState: arg };
43
+ }
44
+ function buildFlowGraph(yaml, optionsOrNodeState) {
45
+ const options = normalizeFlowGraphOptions(optionsOrNodeState);
46
+ const nodeState = options.nodeState;
47
+ const direction = options.direction ?? "LR";
48
+ const compact = options.compact ?? false;
49
+ const graph = buildWorkflowGraph(yaml, {
50
+ reserveRunState: nodeState !== void 0,
51
+ direction,
52
+ // Compact nodes are a fixed size; otherwise honor the caller's `measure` (or
53
+ // fall back to the default content sizing).
54
+ measure: compact ? () => COMPACT_NODE_SIZE : options?.measure
55
+ });
56
+ const isLR = direction === "LR";
57
+ const sourcePosition = isLR ? Position.Right : Position.Bottom;
58
+ const targetPosition = isLR ? Position.Left : Position.Top;
59
+ return {
60
+ error: graph.error,
61
+ nodes: graph.nodes.map(
62
+ (n) => ({
63
+ id: n.id,
64
+ type: "wfNode",
65
+ position: n.position,
66
+ width: n.width,
67
+ height: n.height,
68
+ // Fix the DOM box to the laid-out size so the card can't grow past its
69
+ // reserved space and overlap a neighbour (see model.ts nodeHeight).
70
+ style: { width: n.width, height: n.height },
71
+ sourcePosition,
72
+ targetPosition,
73
+ data: nodeState?.[n.id] ? { ...n.data, state: nodeState[n.id] } : n.data
74
+ })
75
+ ),
76
+ edges: graph.edges.map((e) => ({
77
+ id: e.id,
78
+ source: e.source,
79
+ target: e.target,
80
+ type: "smoothstep"
81
+ }))
82
+ };
83
+ }
84
+ function sameRunState(a, b) {
85
+ if (a === b) return true;
86
+ if (!a || !b) return false;
87
+ const aKeys = Object.keys(a);
88
+ const bKeys = Object.keys(b);
89
+ if (aKeys.length !== bKeys.length) return false;
90
+ return aKeys.every((k) => a[k] === b[k]);
91
+ }
92
+ function mergeRunState(prev, baseDataById, nodeState) {
93
+ return prev.map((n) => {
94
+ const base = baseDataById.get(n.id) ?? n.data;
95
+ const s = nodeState?.[n.id];
96
+ const nextData = s ? { ...base, state: s } : base;
97
+ return sameRunState(n.data.state, nextData.state) ? n : { ...n, data: nextData };
98
+ });
99
+ }
100
+
101
+ // src/workflows/format.ts
102
+ function fmtDuration(ms) {
103
+ if (ms === void 0 || !Number.isFinite(ms) || ms < 0) return void 0;
104
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
105
+ const s = ms / 1e3;
106
+ if (s < 60) return `${s.toFixed(1)}s`;
107
+ const totalSeconds = Math.round(s);
108
+ return `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
109
+ }
110
+ function fmtCost(usd) {
111
+ if (usd === void 0 || !Number.isFinite(usd) || usd < 0) return void 0;
112
+ if (usd === 0) return "$0";
113
+ return usd < 0.01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
114
+ }
115
+ function clampPreview(text, max = 200) {
116
+ if (text.length <= max) return text;
117
+ const lastCode = text.charCodeAt(max - 1);
118
+ const end = lastCode >= 55296 && lastCode <= 56319 ? max - 1 : max;
119
+ return `${text.slice(0, end)}\u2026`;
120
+ }
121
+ function fmtTokens(input, output) {
122
+ const valid = (n) => n !== void 0 && Number.isFinite(n) && n >= 0 ? n : void 0;
123
+ const i = valid(input);
124
+ const o = valid(output);
125
+ if (i === void 0 && o === void 0) return void 0;
126
+ return `${i ?? 0}/${o ?? 0} tok`;
127
+ }
128
+
129
+ // src/workflows/node-output.tsx
130
+ import { jsx, jsxs } from "react/jsx-runtime";
131
+ var MAX_JSON_ENTRIES = 6;
132
+ function scalar(value) {
133
+ if (value === null) return "null";
134
+ if (typeof value === "string") return value;
135
+ if (typeof value === "number" || typeof value === "boolean") {
136
+ return String(value);
137
+ }
138
+ if (Array.isArray(value)) {
139
+ return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
140
+ }
141
+ if (typeof value === "object") return "{\u2026}";
142
+ return "";
143
+ }
144
+ function classifyOutput(preview) {
145
+ let trimmed = preview?.trim() ?? "";
146
+ trimmed = trimmed.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, "").replace(/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "").trimEnd();
147
+ if (trimmed === "") return { kind: "empty" };
148
+ const structured = trimmed.startsWith("{") || trimmed.startsWith("[");
149
+ if (structured) {
150
+ try {
151
+ const parsed = JSON.parse(trimmed);
152
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
153
+ const all = Object.entries(parsed);
154
+ const entries = all.slice(0, MAX_JSON_ENTRIES).map(([k, v]) => [k, scalar(v)]);
155
+ if (entries.length > 0) {
156
+ return { kind: "json", entries, truncated: all.length > entries.length };
157
+ }
158
+ }
159
+ return { kind: "code", text: trimmed };
160
+ } catch {
161
+ return { kind: "code", text: trimmed };
162
+ }
163
+ }
164
+ return { kind: "text", text: trimmed };
165
+ }
166
+ var CLAMP_BY_ROWS = {
167
+ 1: "truncate",
168
+ 2: "line-clamp-2",
169
+ 3: "line-clamp-3"
170
+ };
171
+ function NodeOutputBody({
172
+ shape,
173
+ rows = 2,
174
+ tone = "default"
175
+ }) {
176
+ const clampClass = CLAMP_BY_ROWS[rows] ?? "line-clamp-2";
177
+ const codeWrap = rows === 1 ? "" : "whitespace-pre-wrap break-all";
178
+ if (shape.kind === "empty") return null;
179
+ if (shape.kind === "json") {
180
+ const overflow = shape.truncated || shape.entries.length > rows;
181
+ const visible = shape.entries.slice(0, overflow ? Math.max(0, rows - 1) : rows);
182
+ const isError = tone === "error";
183
+ const keyClass = isError ? "text-red-400/80" : "text-muted-foreground";
184
+ const valueClass = isError ? "text-red-400" : "text-foreground";
185
+ return /* @__PURE__ */ jsxs("dl", { className: "space-y-0.5 font-mono text-[10px] leading-tight", children: [
186
+ visible.map(([key, value]) => /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
187
+ /* @__PURE__ */ jsx("dt", { className: `max-w-[45%] shrink-0 truncate ${keyClass}`, title: key, children: key }),
188
+ /* @__PURE__ */ jsx("dd", { className: `min-w-0 flex-1 truncate ${valueClass}`, title: value, children: value })
189
+ ] }, key)),
190
+ overflow && /* @__PURE__ */ jsx("div", { className: `text-[9px] ${keyClass}`, children: "\u2026" })
191
+ ] });
192
+ }
193
+ if (shape.kind === "code") {
194
+ return /* @__PURE__ */ jsx(
195
+ "pre",
196
+ {
197
+ className: `${clampClass} ${codeWrap} font-mono text-[10px] leading-tight ${tone === "error" ? "text-red-400" : "text-foreground/80"}`,
198
+ title: shape.text,
199
+ children: shape.text
200
+ }
201
+ );
202
+ }
203
+ return /* @__PURE__ */ jsx(
204
+ "p",
205
+ {
206
+ className: `${clampClass} text-[10px] leading-snug ${tone === "error" ? "text-red-400" : "text-muted-foreground"}`,
207
+ title: shape.text,
208
+ children: shape.text
209
+ }
210
+ );
211
+ }
212
+
213
+ // src/workflows/node-ui.tsx
214
+ import {
215
+ Bell,
216
+ Box,
217
+ Cable,
218
+ Circle,
219
+ Clock,
220
+ Repeat,
221
+ Sparkles,
222
+ Split,
223
+ Webhook
224
+ } from "lucide-react";
225
+ import { jsx as jsx2 } from "react/jsx-runtime";
226
+ var TONE_ACCENT = {
227
+ trigger: "hsl(var(--primary))",
228
+ structural: "var(--surface-warning-text)",
229
+ action: "hsl(var(--muted-foreground))"
230
+ };
231
+ var EDGE_MUTED = "hsl(var(--muted-foreground))";
232
+ var EDGE_DONE = "#22c55e";
233
+ var EDGE_FAIL = "#ef4444";
234
+ var EDGE_RUNNING = "hsl(var(--primary))";
235
+ var STATUS_COLOR = {
236
+ queued: EDGE_MUTED,
237
+ running: EDGE_RUNNING,
238
+ succeeded: EDGE_DONE,
239
+ failed: EDGE_FAIL
240
+ };
241
+ function edgeColor(status) {
242
+ switch (status) {
243
+ case "running":
244
+ return EDGE_RUNNING;
245
+ case "succeeded":
246
+ return EDGE_DONE;
247
+ case "failed":
248
+ return EDGE_FAIL;
249
+ default:
250
+ return EDGE_MUTED;
251
+ }
252
+ }
253
+ var STATUS_LABEL = {
254
+ queued: "Queued",
255
+ running: "Running",
256
+ succeeded: "Done",
257
+ failed: "Failed"
258
+ };
259
+ var STATUS_BADGE = {
260
+ queued: "bg-surface-container-high text-muted-foreground",
261
+ running: "bg-primary/15 text-primary",
262
+ succeeded: "bg-green-600/10 text-green-600",
263
+ failed: "bg-red-500/10 text-red-400"
264
+ };
265
+ function statusBorder(status) {
266
+ switch (status) {
267
+ case "running":
268
+ return "border-primary ring-1 ring-primary/40";
269
+ case "succeeded":
270
+ return "border-green-500";
271
+ case "failed":
272
+ return "border-red-500";
273
+ default:
274
+ return "opacity-70";
275
+ }
276
+ }
277
+ function progressFill(status) {
278
+ return status === "succeeded" || status === "failed" ? "100%" : status === "running" ? "58%" : "6%";
279
+ }
280
+ var KIND_ICON = {
281
+ schedule: Clock,
282
+ provider_event: Webhook,
283
+ trigger: Webhook,
284
+ "agent.run": Sparkles,
285
+ "integration.invoke": Cable,
286
+ notify: Bell,
287
+ parallel: Split,
288
+ foreach: Repeat,
289
+ "sandbox.spawn": Box
290
+ };
291
+ function NodeIcon({
292
+ kind,
293
+ accent,
294
+ box,
295
+ glyph
296
+ }) {
297
+ const Icon = kind && KIND_ICON[kind] || Circle;
298
+ return /* @__PURE__ */ jsx2(
299
+ "span",
300
+ {
301
+ "aria-hidden": "true",
302
+ className: "flex shrink-0 items-center justify-center rounded-md",
303
+ style: {
304
+ width: box,
305
+ height: box,
306
+ background: `color-mix(in srgb, ${accent} 16%, transparent)`,
307
+ color: accent
308
+ },
309
+ children: /* @__PURE__ */ jsx2(Icon, { size: glyph, strokeWidth: 2 })
310
+ }
311
+ );
312
+ }
313
+ function MetaChip({
314
+ children,
315
+ title
316
+ }) {
317
+ return /* @__PURE__ */ jsx2(
318
+ "span",
319
+ {
320
+ title,
321
+ className: "inline-block max-w-[150px] truncate rounded bg-surface-container-high px-1.5 py-0.5 align-middle text-[10px] text-muted-foreground",
322
+ children
323
+ }
324
+ );
325
+ }
326
+
327
+ // src/workflows/WorkflowGraph.tsx
328
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
329
+ var MUTED_TRACK = "color-mix(in srgb, hsl(var(--muted-foreground)) 22%, transparent)";
330
+ function useColorMode() {
331
+ const [mode, setMode] = useState(
332
+ () => typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light"
333
+ );
334
+ useEffect(() => {
335
+ const el = document.documentElement;
336
+ const update = () => setMode(el.classList.contains("dark") ? "dark" : "light");
337
+ update();
338
+ const observer = new MutationObserver(update);
339
+ observer.observe(el, { attributes: true, attributeFilter: ["class"] });
340
+ return () => observer.disconnect();
341
+ }, []);
342
+ return mode;
343
+ }
344
+ var DirectionContext = createContext("LR");
345
+ var DensityContext = createContext(false);
346
+ function StatusFooter({
347
+ status,
348
+ rounds,
349
+ elapsed
350
+ }) {
351
+ const roundsLabel = rounds !== void 0 ? `${rounds} round${rounds === 1 ? "" : "s"}` : void 0;
352
+ return /* @__PURE__ */ jsxs2("div", { className: "mt-auto border-border/60 border-t", children: [
353
+ /* @__PURE__ */ jsx3("div", { className: "h-1 w-full overflow-hidden", style: { background: MUTED_TRACK }, children: /* @__PURE__ */ jsx3(
354
+ "div",
355
+ {
356
+ className: `h-full ${status === "running" ? "animate-pulse" : ""}`,
357
+ style: { width: progressFill(status), background: STATUS_COLOR[status] }
358
+ }
359
+ ) }),
360
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2 px-3 py-1 text-[9px] text-muted-foreground", children: [
361
+ /* @__PURE__ */ jsx3("span", { className: "truncate", children: roundsLabel ?? "" }),
362
+ elapsed && /* @__PURE__ */ jsx3("span", { className: "shrink-0", children: elapsed })
363
+ ] })
364
+ ] });
365
+ }
366
+ var HANDLE_CLASS = "!h-2 !w-2 !min-w-0 !border-0 !bg-transparent opacity-0";
367
+ function WorkflowNode({ data }) {
368
+ const d = data;
369
+ const state = d.state;
370
+ const direction = useContext(DirectionContext);
371
+ const compact = useContext(DensityContext);
372
+ const isLR = direction === "LR";
373
+ const targetPos = isLR ? Position2.Left : Position2.Top;
374
+ const sourcePos = isLR ? Position2.Right : Position2.Bottom;
375
+ const accent = TONE_ACCENT[d.tone];
376
+ const isAgent = d.kind === "agent.run";
377
+ const model = state?.model ?? d.model;
378
+ const duration = fmtDuration(state?.durationMs);
379
+ const cost = fmtCost(state?.costUsd);
380
+ const tokens = isAgent ? fmtTokens(state?.inputTokens, state?.outputTokens) : void 0;
381
+ const runOutput = (() => {
382
+ const source = state?.status === "failed" && state.error ? { text: state.error, tone: "error", label: "Error" } : state && state.status !== "failed" && state.outputPreview ? { text: state.outputPreview, tone: "default", label: "Output" } : null;
383
+ if (!source) return null;
384
+ const trimmed = source.text.trim();
385
+ if (!trimmed) return null;
386
+ const shape = classifyOutput(clampPreview(trimmed));
387
+ return shape.kind === "empty" ? null : { shape, tone: source.tone, label: source.label };
388
+ })();
389
+ const wrapperClass = `relative flex h-full w-full flex-col overflow-hidden rounded-lg border bg-card shadow-sm transition-colors ${state ? statusBorder(state.status) : ""}`;
390
+ const wrapperStyle = state ? state.status === "running" ? { boxShadow: "0 0 22px -6px hsl(var(--primary))" } : void 0 : {
391
+ borderColor: `color-mix(in srgb, ${accent} 42%, transparent)`,
392
+ backgroundColor: `color-mix(in srgb, ${accent} 6%, hsl(var(--card)))`
393
+ };
394
+ const handles = /* @__PURE__ */ jsxs2(Fragment, { children: [
395
+ !d.isRoot && /* @__PURE__ */ jsx3(Handle, { type: "target", position: targetPos, className: HANDLE_CLASS }),
396
+ /* @__PURE__ */ jsx3(Handle, { type: "source", position: sourcePos, className: HANDLE_CLASS })
397
+ ] });
398
+ if (compact) {
399
+ return /* @__PURE__ */ jsxs2("div", { className: `${wrapperClass} justify-center px-2.5`, style: wrapperStyle, children: [
400
+ handles,
401
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2.5", children: [
402
+ /* @__PURE__ */ jsx3(NodeIcon, { kind: d.kind, accent, box: 32, glyph: 16 }),
403
+ /* @__PURE__ */ jsxs2("div", { className: "min-w-0 flex-1", children: [
404
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5", children: [
405
+ /* @__PURE__ */ jsx3("span", { className: "truncate font-medium text-[12px] text-foreground", children: d.title }),
406
+ state ? /* @__PURE__ */ jsx3(
407
+ "span",
408
+ {
409
+ className: "ml-auto h-1.5 w-1.5 shrink-0 rounded-full",
410
+ style: { backgroundColor: STATUS_COLOR[state.status] }
411
+ }
412
+ ) : d.badge && /* @__PURE__ */ jsx3("span", { className: "ml-auto shrink-0 rounded bg-surface-container-high px-1 py-0.5 text-[9px] text-muted-foreground", children: d.badge })
413
+ ] }),
414
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-[10px] text-muted-foreground", children: [
415
+ /* @__PURE__ */ jsx3("span", { className: "truncate", children: model ?? d.subtitle ?? d.kind }),
416
+ cost && /* @__PURE__ */ jsx3("span", { className: "ml-auto shrink-0", children: cost }),
417
+ duration && /* @__PURE__ */ jsxs2("span", { className: "shrink-0", children: [
418
+ "\xB7 ",
419
+ duration
420
+ ] })
421
+ ] })
422
+ ] })
423
+ ] }),
424
+ state?.status === "running" && /* @__PURE__ */ jsx3(
425
+ "div",
426
+ {
427
+ className: "mt-1.5 h-0.5 w-full overflow-hidden rounded-full",
428
+ style: { background: MUTED_TRACK },
429
+ children: /* @__PURE__ */ jsx3(
430
+ "div",
431
+ {
432
+ className: "h-full w-1/2 animate-pulse rounded-full",
433
+ style: { background: EDGE_RUNNING }
434
+ }
435
+ )
436
+ }
437
+ )
438
+ ] });
439
+ }
440
+ return /* @__PURE__ */ jsxs2("div", { className: wrapperClass, style: wrapperStyle, children: [
441
+ handles,
442
+ /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1 flex-col px-3 pt-2 pb-2", children: [
443
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
444
+ /* @__PURE__ */ jsx3(NodeIcon, { kind: d.kind, accent, box: 24, glyph: 14 }),
445
+ /* @__PURE__ */ jsx3("span", { className: "truncate font-medium text-[12px] text-foreground", children: d.title }),
446
+ state ? /* @__PURE__ */ jsx3(
447
+ "span",
448
+ {
449
+ className: `ml-auto shrink-0 rounded px-1.5 py-0.5 text-[10px] ${STATUS_BADGE[state.status]}`,
450
+ children: STATUS_LABEL[state.status]
451
+ }
452
+ ) : d.badge && /* @__PURE__ */ jsx3("span", { className: "ml-auto shrink-0 rounded bg-surface-container-high px-1.5 py-0.5 text-[10px] text-muted-foreground", children: d.badge })
453
+ ] }),
454
+ d.subtitle && /* @__PURE__ */ jsx3(
455
+ "p",
456
+ {
457
+ className: "mt-1 truncate text-[11px] text-muted-foreground",
458
+ title: d.subtitle,
459
+ children: d.subtitle
460
+ }
461
+ ),
462
+ d.provider && /* @__PURE__ */ jsxs2("span", { className: "mt-1.5 inline-flex w-fit items-center gap-1 rounded bg-surface-container-high px-1.5 py-0.5 text-[10px] text-muted-foreground", children: [
463
+ /* @__PURE__ */ jsx3(
464
+ ProviderIcon,
465
+ {
466
+ id: d.provider,
467
+ displayName: providerLabel(d.provider),
468
+ size: 12,
469
+ className: "rounded-[2px]"
470
+ }
471
+ ),
472
+ providerLabel(d.provider)
473
+ ] }),
474
+ (model || cost || tokens) && /* @__PURE__ */ jsxs2("div", { className: "mt-1.5 flex flex-wrap items-center gap-1", children: [
475
+ model && /* @__PURE__ */ jsx3(MetaChip, { title: model, children: model }),
476
+ cost && /* @__PURE__ */ jsx3(MetaChip, { children: cost }),
477
+ tokens && /* @__PURE__ */ jsx3(MetaChip, { children: tokens })
478
+ ] }),
479
+ runOutput && /* @__PURE__ */ jsxs2("div", { className: "mt-1.5 min-h-0", children: [
480
+ /* @__PURE__ */ jsx3("div", { className: "mb-0.5 font-medium text-[8px] text-muted-foreground/70 uppercase tracking-[0.08em]", children: runOutput.label }),
481
+ /* @__PURE__ */ jsx3(NodeOutputBody, { shape: runOutput.shape, tone: runOutput.tone, rows: 2 })
482
+ ] })
483
+ ] }),
484
+ state && d.tone !== "trigger" && /* @__PURE__ */ jsx3(
485
+ StatusFooter,
486
+ {
487
+ status: state.status,
488
+ rounds: isAgent ? state.rounds : void 0,
489
+ elapsed: duration
490
+ }
491
+ )
492
+ ] });
493
+ }
494
+ var NODE_TYPES = { wfNode: WorkflowNode };
495
+ function buildStyledEdges(base, nodeState) {
496
+ return base.map((e) => {
497
+ const status = nodeState?.[e.target]?.status;
498
+ const color = edgeColor(nodeState ? status : void 0);
499
+ return {
500
+ ...e,
501
+ // The active hop flows; everything else is static.
502
+ animated: status === "running",
503
+ style: { strokeWidth: 1.75, stroke: color },
504
+ markerEnd: {
505
+ type: MarkerType.ArrowClosed,
506
+ width: 15,
507
+ height: 15,
508
+ color
509
+ }
510
+ };
511
+ });
512
+ }
513
+ function WorkflowGraph({
514
+ yaml,
515
+ variant = "full",
516
+ direction = "LR",
517
+ defaultCompact = false,
518
+ className,
519
+ nodeState,
520
+ onNodeClick
521
+ }) {
522
+ const colorMode = useColorMode();
523
+ const isPreview = variant === "preview";
524
+ const [userCompact, setUserCompact] = useState(defaultCompact);
525
+ const compact = isPreview || userCompact;
526
+ const rfRef = useRef(null);
527
+ const hasRunOverlay = nodeState !== void 0;
528
+ const structural = useMemo(
529
+ () => buildFlowGraph(yaml, {
530
+ nodeState: hasRunOverlay ? {} : void 0,
531
+ direction,
532
+ compact
533
+ }),
534
+ [yaml, hasRunOverlay, direction, compact]
535
+ );
536
+ const styledEdges = useMemo(
537
+ () => buildStyledEdges(structural.edges, nodeState),
538
+ [structural.edges, nodeState]
539
+ );
540
+ const [nodes, setNodes, onNodesChange] = useNodesState(structural.nodes);
541
+ const [edges, setEdges, onEdgesChange] = useEdgesState(styledEdges);
542
+ useEffect(() => {
543
+ setNodes(structural.nodes);
544
+ }, [structural, setNodes]);
545
+ useEffect(() => {
546
+ setEdges(styledEdges);
547
+ }, [styledEdges, setEdges]);
548
+ useEffect(() => {
549
+ const baseById = new Map(structural.nodes.map((n) => [n.id, n.data]));
550
+ setNodes((prev) => mergeRunState(prev, baseById, nodeState));
551
+ }, [nodeState, structural, setNodes]);
552
+ const didInitialFitRef = useRef(false);
553
+ useEffect(() => {
554
+ if (!didInitialFitRef.current) {
555
+ didInitialFitRef.current = true;
556
+ return;
557
+ }
558
+ const inst = rfRef.current;
559
+ if (!inst || typeof requestAnimationFrame === "undefined") return;
560
+ const raf = requestAnimationFrame(() => inst.fitView({ padding: 0.18 }));
561
+ return () => cancelAnimationFrame(raf);
562
+ }, [structural]);
563
+ const handleNodeClick = useCallback(
564
+ (_event, node) => {
565
+ onNodeClick?.(node.id, node.data);
566
+ },
567
+ [onNodeClick]
568
+ );
569
+ if (structural.error || structural.nodes.length === 0) {
570
+ return /* @__PURE__ */ jsx3(
571
+ "div",
572
+ {
573
+ className: `flex items-center justify-center rounded-lg border border-border border-dashed p-4 text-center text-muted-foreground text-xs ${className ?? ""}`,
574
+ children: structural.error ?? "Nothing to show"
575
+ }
576
+ );
577
+ }
578
+ return /* @__PURE__ */ jsx3(DirectionContext.Provider, { value: direction, children: /* @__PURE__ */ jsx3(DensityContext.Provider, { value: compact, children: /* @__PURE__ */ jsx3("div", { className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs2(
579
+ ReactFlow,
580
+ {
581
+ nodes,
582
+ edges,
583
+ onNodesChange,
584
+ onEdgesChange,
585
+ nodeTypes: NODE_TYPES,
586
+ colorMode,
587
+ onInit: (inst) => {
588
+ rfRef.current = inst;
589
+ },
590
+ onNodeClick: onNodeClick ? handleNodeClick : void 0,
591
+ fitView: true,
592
+ fitViewOptions: { padding: 0.18 },
593
+ proOptions: { hideAttribution: true },
594
+ nodesDraggable: !isPreview,
595
+ nodesConnectable: false,
596
+ deleteKeyCode: null,
597
+ edgesReconnectable: false,
598
+ elementsSelectable: !isPreview,
599
+ edgesFocusable: false,
600
+ zoomOnScroll: false,
601
+ zoomOnPinch: true,
602
+ zoomOnDoubleClick: !isPreview,
603
+ panOnDrag: true,
604
+ preventScrolling: false,
605
+ minZoom: 0.2,
606
+ maxZoom: 1.5,
607
+ children: [
608
+ /* @__PURE__ */ jsx3(Background, { gap: 18 }),
609
+ !isPreview && /* @__PURE__ */ jsxs2(Fragment, { children: [
610
+ /* @__PURE__ */ jsx3(Controls, { showInteractive: false, position: "bottom-right" }),
611
+ /* @__PURE__ */ jsx3(Panel, { position: "top-left", children: /* @__PURE__ */ jsxs2(
612
+ "button",
613
+ {
614
+ type: "button",
615
+ onClick: () => setUserCompact((c) => !c),
616
+ "aria-pressed": userCompact,
617
+ title: userCompact ? "Expand nodes" : "Collapse nodes",
618
+ className: "flex items-center gap-1 rounded-md border border-border bg-card/80 px-2 py-1 text-[11px] text-muted-foreground shadow-sm backdrop-blur transition hover:text-foreground",
619
+ children: [
620
+ userCompact ? /* @__PURE__ */ jsx3(Maximize2, { size: 12 }) : /* @__PURE__ */ jsx3(Minimize2, { size: 12 }),
621
+ userCompact ? "Expand" : "Compact"
622
+ ]
623
+ }
624
+ ) })
625
+ ] })
626
+ ]
627
+ }
628
+ ) }) }) });
629
+ }
630
+ export {
631
+ DensityContext,
632
+ DirectionContext,
633
+ WorkflowGraph,
634
+ WorkflowNode,
635
+ buildStyledEdges
636
+ };