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