@tangle-network/sandbox-ui 0.109.0 → 0.111.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.
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
isStructuralKind,
|
|
17
17
|
providerLabel,
|
|
18
18
|
shortModel,
|
|
19
|
-
triggerNodeIndex
|
|
20
|
-
|
|
19
|
+
triggerNodeIndex,
|
|
20
|
+
wfEdgeId,
|
|
21
|
+
worstSeverity
|
|
22
|
+
} from "./chunk-AI25G5MX.js";
|
|
21
23
|
import {
|
|
22
24
|
ModelBrandStack,
|
|
23
25
|
modelBrandFor
|
|
@@ -42,7 +44,7 @@ import {
|
|
|
42
44
|
useNodesState
|
|
43
45
|
} from "@xyflow/react";
|
|
44
46
|
import "@xyflow/react/dist/style.css";
|
|
45
|
-
import { Maximize2, Minimize2 } from "lucide-react";
|
|
47
|
+
import { Maximize2, Minimize2, Plus, X } from "lucide-react";
|
|
46
48
|
import {
|
|
47
49
|
createContext,
|
|
48
50
|
useCallback,
|
|
@@ -68,9 +70,17 @@ function isNodeStateMap(arg) {
|
|
|
68
70
|
(v) => v !== null && typeof v === "object" && "status" in v
|
|
69
71
|
);
|
|
70
72
|
}
|
|
73
|
+
var FLOW_GRAPH_OPTION_KEYS = [
|
|
74
|
+
"nodeState",
|
|
75
|
+
"direction",
|
|
76
|
+
"compact",
|
|
77
|
+
"wrap",
|
|
78
|
+
"edges",
|
|
79
|
+
"reserveEdgeInsert"
|
|
80
|
+
];
|
|
71
81
|
function normalizeFlowGraphOptions(arg) {
|
|
72
82
|
if (!arg) return {};
|
|
73
|
-
if (
|
|
83
|
+
if (FLOW_GRAPH_OPTION_KEYS.some((key) => key in arg)) {
|
|
74
84
|
return arg;
|
|
75
85
|
}
|
|
76
86
|
return isNodeStateMap(arg) ? { nodeState: arg } : {};
|
|
@@ -85,7 +95,8 @@ function buildFlowGraph(yaml, optionsOrNodeState) {
|
|
|
85
95
|
direction,
|
|
86
96
|
compact,
|
|
87
97
|
...options.wrap ? { wrap: true } : {},
|
|
88
|
-
...options.edges ? { edges: options.edges } : {}
|
|
98
|
+
...options.edges ? { edges: options.edges } : {},
|
|
99
|
+
...options.reserveEdgeInsert ? { reserveEdgeInsert: true } : {}
|
|
89
100
|
});
|
|
90
101
|
const isLR = direction === "LR";
|
|
91
102
|
const sourcePosition = isLR ? Position.Right : Position.Bottom;
|
|
@@ -149,6 +160,69 @@ function mergeRunState(prev, baseDataById, nodeState) {
|
|
|
149
160
|
return sameRunState(n.data.state, nextData.state) ? n : { ...n, data: nextData };
|
|
150
161
|
});
|
|
151
162
|
}
|
|
163
|
+
function indexProblems(problems) {
|
|
164
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
165
|
+
const byEdge = /* @__PURE__ */ new Map();
|
|
166
|
+
const named = (value) => typeof value === "string" && value !== "";
|
|
167
|
+
for (const problem of problems ?? []) {
|
|
168
|
+
const into = problem.anchor === "node" ? named(problem.node) ? byNode : null : problem.anchor === "edge" && named(problem.from) && named(problem.to) ? byEdge : null;
|
|
169
|
+
if (!into) continue;
|
|
170
|
+
const key = problem.anchor === "node" ? problem.node : wfEdgeId(problem.from, problem.to);
|
|
171
|
+
const at = into.get(key);
|
|
172
|
+
if (at) at.push(problem);
|
|
173
|
+
else into.set(key, [problem]);
|
|
174
|
+
}
|
|
175
|
+
return { byNode, byEdge };
|
|
176
|
+
}
|
|
177
|
+
var INSERT_CLEARANCE = 8;
|
|
178
|
+
var CLUSTER_HALF_SIZE = {
|
|
179
|
+
/** The insert control alone: a 20-unit square. */
|
|
180
|
+
control: { width: 10, height: 10 },
|
|
181
|
+
/**
|
|
182
|
+
* A problem chip — and, since the two stack in one column, the chip WITH the
|
|
183
|
+
* control under it, which is the taller of the two and so the one the bound
|
|
184
|
+
* has to cover. Measured on the rendered page at 160 x 38.5 flow units (the
|
|
185
|
+
* chip's `max-w-40` cap, over chip + gap + control), and rounded outward: a
|
|
186
|
+
* bound that understates is a bound that lets the cluster lap over a card.
|
|
187
|
+
*/
|
|
188
|
+
chip: { width: 80, height: 20 }
|
|
189
|
+
};
|
|
190
|
+
var INSERT_MAX_CARDS_CROSSED = 2;
|
|
191
|
+
function clearOfNodeBoxes(point, boxes, direction, half) {
|
|
192
|
+
const isLR = direction === "LR";
|
|
193
|
+
const base = isLR ? point.y : point.x;
|
|
194
|
+
const main = isLR ? point.x : point.y;
|
|
195
|
+
const padMain = isLR ? half.width : half.height;
|
|
196
|
+
const padCross = isLR ? half.height : half.width;
|
|
197
|
+
const column = boxes.filter((b) => {
|
|
198
|
+
const lo = (isLR ? b.x : b.y) - padMain;
|
|
199
|
+
const size = (isLR ? b.width : b.height) + padMain * 2;
|
|
200
|
+
return main >= lo && main <= lo + size;
|
|
201
|
+
});
|
|
202
|
+
if (column.length === 0) return 0;
|
|
203
|
+
const walk = (sign) => {
|
|
204
|
+
let at = base;
|
|
205
|
+
for (let crossed = 0; crossed <= INSERT_MAX_CARDS_CROSSED; crossed += 1) {
|
|
206
|
+
const hit = column.find((b) => {
|
|
207
|
+
const lo2 = (isLR ? b.y : b.x) - padCross;
|
|
208
|
+
const size2 = (isLR ? b.height : b.width) + padCross * 2;
|
|
209
|
+
return at >= lo2 && at <= lo2 + size2;
|
|
210
|
+
});
|
|
211
|
+
if (!hit) return at - base;
|
|
212
|
+
if (crossed === INSERT_MAX_CARDS_CROSSED) return null;
|
|
213
|
+
const lo = isLR ? hit.y : hit.x;
|
|
214
|
+
const size = isLR ? hit.height : hit.width;
|
|
215
|
+
at = sign > 0 ? lo + size + padCross + INSERT_CLEARANCE : lo - padCross - INSERT_CLEARANCE;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
};
|
|
219
|
+
const up = walk(-1);
|
|
220
|
+
const down = walk(1);
|
|
221
|
+
if (up === null && down === null) return null;
|
|
222
|
+
if (up === null) return down;
|
|
223
|
+
if (down === null) return up;
|
|
224
|
+
return Math.abs(up) <= Math.abs(down) ? up : down;
|
|
225
|
+
}
|
|
152
226
|
|
|
153
227
|
// src/workflows/node-output.tsx
|
|
154
228
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -284,6 +358,7 @@ function NodeOutputBody({
|
|
|
284
358
|
|
|
285
359
|
// src/workflows/node-ui.tsx
|
|
286
360
|
import {
|
|
361
|
+
AlertTriangle,
|
|
287
362
|
Bell,
|
|
288
363
|
Box,
|
|
289
364
|
Cable,
|
|
@@ -395,6 +470,73 @@ var KIND_ICON = {
|
|
|
395
470
|
"script.run": Code,
|
|
396
471
|
"trace.analyze": ScanSearch
|
|
397
472
|
};
|
|
473
|
+
var PROBLEM_SURFACE_BY_SEVERITY = {
|
|
474
|
+
error: {
|
|
475
|
+
background: "var(--surface-danger-bg)",
|
|
476
|
+
color: "var(--surface-danger-text)",
|
|
477
|
+
borderColor: "var(--surface-danger-border)"
|
|
478
|
+
},
|
|
479
|
+
warning: {
|
|
480
|
+
background: "var(--surface-warning-bg)",
|
|
481
|
+
color: "var(--surface-warning-text)",
|
|
482
|
+
borderColor: "var(--surface-warning-border)"
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
function problemSurface(severity) {
|
|
486
|
+
return PROBLEM_SURFACE_BY_SEVERITY[severity] ?? PROBLEM_SURFACE_BY_SEVERITY.error;
|
|
487
|
+
}
|
|
488
|
+
function problemBorder(severity) {
|
|
489
|
+
const color = problemSurface(severity).color;
|
|
490
|
+
return {
|
|
491
|
+
borderColor: color,
|
|
492
|
+
boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 35%, transparent)`
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
var SEVERITY_WORD = {
|
|
496
|
+
error: "Error",
|
|
497
|
+
warning: "Warning"
|
|
498
|
+
};
|
|
499
|
+
function problemSentence(problem) {
|
|
500
|
+
const raw = typeof problem.message === "string" ? problem.message : "";
|
|
501
|
+
const message = raw.split("\n").map((line) => line.trim()).filter(Boolean).join(" ");
|
|
502
|
+
const word = SEVERITY_WORD[problem.severity] ?? SEVERITY_WORD.error;
|
|
503
|
+
return message ? `${word}: ${message}` : word;
|
|
504
|
+
}
|
|
505
|
+
function joinSentences(parts) {
|
|
506
|
+
return parts.reduce(
|
|
507
|
+
(joined, part) => joined ? `${joined}${/[.!?;,]$/.test(joined) ? "" : "."} ${part}` : part,
|
|
508
|
+
""
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
function problemTitle(problems) {
|
|
512
|
+
return problems.map(problemSentence).join("\n");
|
|
513
|
+
}
|
|
514
|
+
function ProblemMessages({ problems }) {
|
|
515
|
+
if (problems.length === 0) return null;
|
|
516
|
+
return /* @__PURE__ */ jsx2("span", { className: "sr-only", children: problems.length === 1 ? problemSentence(problems[0]) : `${problems.length} problems. ${joinSentences(problems.map(problemSentence))}` });
|
|
517
|
+
}
|
|
518
|
+
function ProblemMarker({
|
|
519
|
+
problems,
|
|
520
|
+
className = ""
|
|
521
|
+
}) {
|
|
522
|
+
const severity = worstSeverity(problems) ?? "error";
|
|
523
|
+
return /* @__PURE__ */ jsxs2(
|
|
524
|
+
"span",
|
|
525
|
+
{
|
|
526
|
+
"data-testid": "wf-node-problem",
|
|
527
|
+
"data-severity": severity,
|
|
528
|
+
className: `flex shrink-0 items-center gap-0.5 rounded-full border px-1 py-[1px] font-medium text-[10px] leading-none ${className}`,
|
|
529
|
+
style: problemSurface(severity),
|
|
530
|
+
children: [
|
|
531
|
+
/* @__PURE__ */ jsxs2("span", { "aria-hidden": true, className: "flex items-center gap-0.5", title: problemTitle(problems), children: [
|
|
532
|
+
/* @__PURE__ */ jsx2(AlertTriangle, { size: 9 }),
|
|
533
|
+
problems.length > 1 && /* @__PURE__ */ jsx2("span", { children: problems.length })
|
|
534
|
+
] }),
|
|
535
|
+
/* @__PURE__ */ jsx2(ProblemMessages, { problems })
|
|
536
|
+
]
|
|
537
|
+
}
|
|
538
|
+
);
|
|
539
|
+
}
|
|
398
540
|
function StatusPill({ status }) {
|
|
399
541
|
return /* @__PURE__ */ jsx2(
|
|
400
542
|
"span",
|
|
@@ -500,7 +642,7 @@ function NodeMark({
|
|
|
500
642
|
|
|
501
643
|
// src/workflows/WorkflowGraph.tsx
|
|
502
644
|
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
503
|
-
var FIT_VIEW = { padding:
|
|
645
|
+
var FIT_VIEW = { padding: "12px" };
|
|
504
646
|
function fitZoomCeiling(compact) {
|
|
505
647
|
return compact ? 1.5 : 1;
|
|
506
648
|
}
|
|
@@ -567,6 +709,9 @@ var SELECTION_OUTLINE = {
|
|
|
567
709
|
outlineOffset: 2
|
|
568
710
|
};
|
|
569
711
|
var ConnectableContext = createContext(false);
|
|
712
|
+
var NodeProblemsContext = createContext(/* @__PURE__ */ new Map());
|
|
713
|
+
var TriggerDeleteContext = createContext(null);
|
|
714
|
+
var EdgeInsertContext = createContext(null);
|
|
570
715
|
function isEditableEdge(edge) {
|
|
571
716
|
return edge.data?.kind !== "fork" && triggerNodeIndex(edge.source) === null;
|
|
572
717
|
}
|
|
@@ -609,6 +754,10 @@ function WorkflowNode({
|
|
|
609
754
|
const runMode = useContext(RunModeContext);
|
|
610
755
|
const connectable = useContext(ConnectableContext);
|
|
611
756
|
const hostSelection = useContext(SelectedNodeContext);
|
|
757
|
+
const problems = useContext(NodeProblemsContext).get(id);
|
|
758
|
+
const problemSeverity = worstSeverity(problems);
|
|
759
|
+
const removeTrigger = useContext(TriggerDeleteContext);
|
|
760
|
+
const isTrigger = triggerNodeIndex(id) !== null;
|
|
612
761
|
const selected = hostSelection !== void 0 && hostSelection === id;
|
|
613
762
|
const isLR = direction === "LR";
|
|
614
763
|
const targetPos = targetPosition ?? (isLR ? Position2.Left : Position2.Top);
|
|
@@ -637,6 +786,21 @@ function WorkflowNode({
|
|
|
637
786
|
);
|
|
638
787
|
return shape.kind === "empty" ? null : { shape, tone: source.tone, label: source.label };
|
|
639
788
|
})();
|
|
789
|
+
const triggerDeleteButton = isTrigger && removeTrigger ? /* @__PURE__ */ jsx3(
|
|
790
|
+
"button",
|
|
791
|
+
{
|
|
792
|
+
type: "button",
|
|
793
|
+
"data-testid": "wf-trigger-delete",
|
|
794
|
+
title: "Remove this trigger",
|
|
795
|
+
"aria-label": "Remove this trigger",
|
|
796
|
+
className: "nodrag nopan -m-1 flex h-6 w-6 shrink-0 items-center justify-center p-1 text-muted-foreground transition hover:text-[var(--surface-danger-text)]",
|
|
797
|
+
onClick: (event) => {
|
|
798
|
+
event.stopPropagation();
|
|
799
|
+
removeTrigger(id);
|
|
800
|
+
},
|
|
801
|
+
children: /* @__PURE__ */ jsx3("span", { className: "flex h-4 w-4 items-center justify-center rounded-full border border-border bg-card shadow-sm", children: /* @__PURE__ */ jsx3(X, { size: 10, "aria-hidden": true }) })
|
|
802
|
+
}
|
|
803
|
+
) : null;
|
|
640
804
|
const handleClass = connectable ? HANDLE_CONNECTABLE_CLASS : HANDLE_CLASS;
|
|
641
805
|
const handles = /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
642
806
|
!d.isRoot && /* @__PURE__ */ jsx3(
|
|
@@ -668,11 +832,12 @@ function WorkflowNode({
|
|
|
668
832
|
/* @__PURE__ */ jsxs3(
|
|
669
833
|
"span",
|
|
670
834
|
{
|
|
835
|
+
"data-testid": "wf-node-card",
|
|
671
836
|
className: "relative flex items-center justify-center rounded-xl border bg-card shadow-sm transition-colors",
|
|
672
837
|
style: {
|
|
673
838
|
width: COMPACT_TILE,
|
|
674
839
|
height: COMPACT_TILE,
|
|
675
|
-
...state ? statusBorder(state.status) : {
|
|
840
|
+
...state ? statusBorder(state.status) : problemSeverity ? problemBorder(problemSeverity) : {
|
|
676
841
|
borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
|
|
677
842
|
},
|
|
678
843
|
// The TILE is the compact node (the name beside it is unboxed), so
|
|
@@ -690,6 +855,14 @@ function WorkflowNode({
|
|
|
690
855
|
tile: Math.round(COMPACT_TILE * 0.62)
|
|
691
856
|
}
|
|
692
857
|
) }),
|
|
858
|
+
problems && problemSeverity && /* @__PURE__ */ jsx3(
|
|
859
|
+
ProblemMarker,
|
|
860
|
+
{
|
|
861
|
+
problems,
|
|
862
|
+
className: "-left-1.5 -top-1.5 absolute"
|
|
863
|
+
}
|
|
864
|
+
),
|
|
865
|
+
triggerDeleteButton && /* @__PURE__ */ jsx3("span", { className: "-right-1.5 -top-1.5 absolute", children: triggerDeleteButton }),
|
|
693
866
|
d.badge && !state && /* @__PURE__ */ jsx3("span", { className: "-right-1.5 -top-1.5 absolute rounded-full border border-border bg-card px-1.5 py-[1px] font-medium text-[10px] text-muted-foreground", children: d.badge }),
|
|
694
867
|
state && /* @__PURE__ */ jsx3(
|
|
695
868
|
"span",
|
|
@@ -738,9 +911,10 @@ function WorkflowNode({
|
|
|
738
911
|
const cardShell = (children) => /* @__PURE__ */ jsxs3(
|
|
739
912
|
"div",
|
|
740
913
|
{
|
|
914
|
+
"data-testid": "wf-node-card",
|
|
741
915
|
className: "relative flex h-full w-full flex-col overflow-hidden rounded-xl border bg-card shadow-sm transition-colors",
|
|
742
916
|
style: {
|
|
743
|
-
...runStatus ? statusBorder(runStatus) : {
|
|
917
|
+
...runStatus ? statusBorder(runStatus) : problemSeverity ? problemBorder(problemSeverity) : {
|
|
744
918
|
borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
|
|
745
919
|
},
|
|
746
920
|
...selected ? SELECTION_OUTLINE : {}
|
|
@@ -780,7 +954,9 @@ function WorkflowNode({
|
|
|
780
954
|
}
|
|
781
955
|
)
|
|
782
956
|
] }),
|
|
783
|
-
|
|
957
|
+
problems && problemSeverity && /* @__PURE__ */ jsx3(ProblemMarker, { problems }),
|
|
958
|
+
runStatus ? /* @__PURE__ */ jsx3(StatusPill, { status: runStatus }) : d.badge && /* @__PURE__ */ jsx3("span", { className: "shrink-0 rounded-full border border-border bg-surface-container-high px-2 py-[1px] font-medium text-[10px] text-muted-foreground", children: d.badge }),
|
|
959
|
+
triggerDeleteButton
|
|
784
960
|
] });
|
|
785
961
|
const footer = runStatus ? /* @__PURE__ */ jsx3(
|
|
786
962
|
StatusFooter,
|
|
@@ -981,9 +1157,25 @@ function WorkflowNode({
|
|
|
981
1157
|
] })
|
|
982
1158
|
);
|
|
983
1159
|
}
|
|
1160
|
+
function releasePoint(event) {
|
|
1161
|
+
if (!event) return null;
|
|
1162
|
+
const touch = "changedTouches" in event ? event.changedTouches[0] : null;
|
|
1163
|
+
const source = touch ?? ("clientX" in event ? event : null);
|
|
1164
|
+
return source ? { x: source.clientX, y: source.clientY } : null;
|
|
1165
|
+
}
|
|
1166
|
+
var CANVAS_SURFACE_SELECTOR = ".react-flow__pane, .react-flow__background";
|
|
1167
|
+
function releasedOnCanvas(at, frame) {
|
|
1168
|
+
if (!frame) return false;
|
|
1169
|
+
const under = typeof document === "undefined" ? null : document.elementFromPoint(at.x, at.y);
|
|
1170
|
+
if (!under?.matches(CANVAS_SURFACE_SELECTOR)) return false;
|
|
1171
|
+
return frame.contains(under);
|
|
1172
|
+
}
|
|
1173
|
+
var NodeBoxesContext = createContext([]);
|
|
984
1174
|
var EDGE_CHIP_CLASS = "rounded-full border border-border bg-card/90 px-1.5 py-[1px] text-[10px] leading-tight backdrop-blur";
|
|
985
1175
|
function WfEdgeRenderer({
|
|
986
1176
|
id,
|
|
1177
|
+
source,
|
|
1178
|
+
target,
|
|
987
1179
|
sourceX,
|
|
988
1180
|
sourceY,
|
|
989
1181
|
targetX,
|
|
@@ -994,6 +1186,7 @@ function WfEdgeRenderer({
|
|
|
994
1186
|
markerEnd,
|
|
995
1187
|
style
|
|
996
1188
|
}) {
|
|
1189
|
+
const insertStep = useContext(EdgeInsertContext);
|
|
997
1190
|
const [path, labelX, labelY] = getSmoothStepPath({
|
|
998
1191
|
sourceX,
|
|
999
1192
|
sourceY,
|
|
@@ -1005,6 +1198,25 @@ function WfEdgeRenderer({
|
|
|
1005
1198
|
const backEdge = data?.backEdge === true;
|
|
1006
1199
|
const whenLabel = data?.whenLabel;
|
|
1007
1200
|
const visits = data?.maxNodeVisits;
|
|
1201
|
+
const problems = data?.problems;
|
|
1202
|
+
const problemSeverity = worstSeverity(problems);
|
|
1203
|
+
const offersInsert = data?.insertable === true && insertStep !== null;
|
|
1204
|
+
const nodeBoxes = useContext(NodeBoxesContext);
|
|
1205
|
+
const direction = useContext(DirectionContext);
|
|
1206
|
+
const cleared = offersInsert || problemSeverity ? clearOfNodeBoxes(
|
|
1207
|
+
{ x: labelX, y: labelY },
|
|
1208
|
+
nodeBoxes,
|
|
1209
|
+
direction,
|
|
1210
|
+
// A chip is far wider than the control, and in "TB" that width is what
|
|
1211
|
+
// has to clear the cards — so the cluster is measured by what it
|
|
1212
|
+
// actually carries rather than by its tallest member.
|
|
1213
|
+
problemSeverity ? CLUSTER_HALF_SIZE.chip : CLUSTER_HALF_SIZE.control
|
|
1214
|
+
) : 0;
|
|
1215
|
+
const clearance = cleared ?? 0;
|
|
1216
|
+
const stealsPointer = cleared === null;
|
|
1217
|
+
const isLR = direction === "LR";
|
|
1218
|
+
const clusterX = labelX + (isLR ? 0 : clearance);
|
|
1219
|
+
const clusterY = labelY + (isLR ? clearance : 0);
|
|
1008
1220
|
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
1009
1221
|
/* @__PURE__ */ jsx3(BaseEdge, { id, path, markerEnd, style }),
|
|
1010
1222
|
/* @__PURE__ */ jsx3(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs3(
|
|
@@ -1012,9 +1224,23 @@ function WfEdgeRenderer({
|
|
|
1012
1224
|
{
|
|
1013
1225
|
className: "nodrag nopan absolute flex flex-col items-center gap-0.5",
|
|
1014
1226
|
style: {
|
|
1015
|
-
|
|
1227
|
+
zIndex: 1,
|
|
1228
|
+
transform: `translate(-50%, -50%) translate(${clusterX}px, ${clusterY}px)`
|
|
1016
1229
|
},
|
|
1017
1230
|
children: [
|
|
1231
|
+
problems && problemSeverity && /* @__PURE__ */ jsxs3(
|
|
1232
|
+
"span",
|
|
1233
|
+
{
|
|
1234
|
+
"data-testid": "wf-edge-problem",
|
|
1235
|
+
"data-severity": problemSeverity,
|
|
1236
|
+
className: `${EDGE_CHIP_CLASS} max-w-40 truncate ${stealsPointer ? "" : "pointer-events-auto"}`,
|
|
1237
|
+
style: problemSurface(problemSeverity),
|
|
1238
|
+
children: [
|
|
1239
|
+
/* @__PURE__ */ jsx3("span", { "aria-hidden": true, title: problemTitle(problems), children: problems.length === 1 ? problems[0].message : `${problems.length} problems` }),
|
|
1240
|
+
/* @__PURE__ */ jsx3(ProblemMessages, { problems })
|
|
1241
|
+
]
|
|
1242
|
+
}
|
|
1243
|
+
),
|
|
1018
1244
|
whenLabel && /* @__PURE__ */ jsx3(
|
|
1019
1245
|
"span",
|
|
1020
1246
|
{
|
|
@@ -1030,6 +1256,21 @@ function WfEdgeRenderer({
|
|
|
1030
1256
|
className: `${EDGE_CHIP_CLASS} shrink-0 text-foreground`,
|
|
1031
1257
|
children: visits !== void 0 ? `\u21BA \u2264${visits}` : "\u21BA"
|
|
1032
1258
|
}
|
|
1259
|
+
),
|
|
1260
|
+
offersInsert && /* @__PURE__ */ jsx3(
|
|
1261
|
+
"button",
|
|
1262
|
+
{
|
|
1263
|
+
type: "button",
|
|
1264
|
+
"data-testid": "wf-edge-insert",
|
|
1265
|
+
title: "Insert a step here",
|
|
1266
|
+
"aria-label": "Insert a step here",
|
|
1267
|
+
className: `flex h-5 w-5 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-sm backdrop-blur transition hover:border-primary hover:text-primary ${stealsPointer ? "" : "pointer-events-auto"}`,
|
|
1268
|
+
onClick: (event) => {
|
|
1269
|
+
event.stopPropagation();
|
|
1270
|
+
insertStep(source, target);
|
|
1271
|
+
},
|
|
1272
|
+
children: /* @__PURE__ */ jsx3(Plus, { size: 12, "aria-hidden": true })
|
|
1273
|
+
}
|
|
1033
1274
|
)
|
|
1034
1275
|
]
|
|
1035
1276
|
}
|
|
@@ -1066,11 +1307,35 @@ function buildStyledEdges(base, nodeState, maxNodeVisits) {
|
|
|
1066
1307
|
};
|
|
1067
1308
|
});
|
|
1068
1309
|
}
|
|
1310
|
+
function decorateAuthoringEdges(edges, byEdge, insertable, hasRunOverlay) {
|
|
1311
|
+
if (byEdge.size === 0 && !edges.some(insertable)) return edges;
|
|
1312
|
+
return edges.map((e) => {
|
|
1313
|
+
const problems = byEdge.get(e.id);
|
|
1314
|
+
const severity = worstSeverity(problems);
|
|
1315
|
+
const offersInsert = insertable(e);
|
|
1316
|
+
if (!severity && !offersInsert) return e;
|
|
1317
|
+
const stroke = severity && !hasRunOverlay ? problemSurface(severity).color : void 0;
|
|
1318
|
+
return {
|
|
1319
|
+
...e,
|
|
1320
|
+
type: WF_EDGE_TYPE,
|
|
1321
|
+
data: {
|
|
1322
|
+
...e.data ?? { kind: "spine" },
|
|
1323
|
+
...problems ? { problems } : {},
|
|
1324
|
+
...offersInsert ? { insertable: true } : {}
|
|
1325
|
+
},
|
|
1326
|
+
...stroke ? {
|
|
1327
|
+
style: { ...e.style, stroke },
|
|
1328
|
+
markerEnd: e.markerEnd && typeof e.markerEnd === "object" ? { ...e.markerEnd, color: stroke } : e.markerEnd
|
|
1329
|
+
} : {}
|
|
1330
|
+
};
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1069
1333
|
function WorkflowGraph({
|
|
1070
1334
|
yaml,
|
|
1071
1335
|
variant = "full",
|
|
1072
1336
|
direction = "LR",
|
|
1073
1337
|
defaultCompact,
|
|
1338
|
+
onCompactChange,
|
|
1074
1339
|
className,
|
|
1075
1340
|
wrap = false,
|
|
1076
1341
|
nodeState,
|
|
@@ -1080,14 +1345,25 @@ function WorkflowGraph({
|
|
|
1080
1345
|
onNodeClick,
|
|
1081
1346
|
onEdgeConnect,
|
|
1082
1347
|
onEdgeDelete,
|
|
1083
|
-
onEdgeClick
|
|
1348
|
+
onEdgeClick,
|
|
1349
|
+
onEdgeInsert,
|
|
1350
|
+
onNodeInsert,
|
|
1351
|
+
onTriggerAdd,
|
|
1352
|
+
onTriggerDelete,
|
|
1353
|
+
problems
|
|
1084
1354
|
}) {
|
|
1085
1355
|
const editable = onEdgeConnect !== void 0;
|
|
1356
|
+
const insertOnEdge = editable ? onEdgeInsert : void 0;
|
|
1357
|
+
const insertArmed = insertOnEdge !== void 0;
|
|
1358
|
+
const insertAtNode = editable ? onNodeInsert : void 0;
|
|
1359
|
+
const addTrigger = editable ? onTriggerAdd : void 0;
|
|
1360
|
+
const deleteTrigger = editable ? onTriggerDelete : void 0;
|
|
1086
1361
|
const colorMode = useColorMode();
|
|
1087
1362
|
const isPreview = variant === "preview";
|
|
1088
1363
|
const [userCompact, setUserCompact] = useState(
|
|
1089
1364
|
defaultCompact ?? nodeState === void 0
|
|
1090
1365
|
);
|
|
1366
|
+
const nextDensityFrom = useRef(userCompact);
|
|
1091
1367
|
const compact = isPreview || userCompact;
|
|
1092
1368
|
const [displayCompact, setDisplayCompact] = useState(compact);
|
|
1093
1369
|
const rfRef = useRef(
|
|
@@ -1103,17 +1379,54 @@ function WorkflowGraph({
|
|
|
1103
1379
|
direction,
|
|
1104
1380
|
compact,
|
|
1105
1381
|
wrap,
|
|
1382
|
+
// The insert control lives IN the gap between two layers, so the gap is
|
|
1383
|
+
// reserved here — where the layout is decided — rather than measured
|
|
1384
|
+
// afterwards. It is the one editing prop that reaches the layout, which
|
|
1385
|
+
// is why arming it re-lays the graph out.
|
|
1386
|
+
reserveEdgeInsert: insertArmed,
|
|
1106
1387
|
...declaredEdges ? { edges: declaredEdges } : {}
|
|
1107
1388
|
}),
|
|
1108
|
-
[
|
|
1389
|
+
[
|
|
1390
|
+
yaml,
|
|
1391
|
+
hasRunOverlay,
|
|
1392
|
+
direction,
|
|
1393
|
+
compact,
|
|
1394
|
+
wrap,
|
|
1395
|
+
insertArmed,
|
|
1396
|
+
declaredEdges
|
|
1397
|
+
]
|
|
1109
1398
|
);
|
|
1399
|
+
const nodeBoxes = useMemo(
|
|
1400
|
+
() => structural.nodes.map((node) => ({
|
|
1401
|
+
x: node.position.x,
|
|
1402
|
+
y: node.position.y,
|
|
1403
|
+
width: node.width ?? 0,
|
|
1404
|
+
height: node.height ?? 0
|
|
1405
|
+
})),
|
|
1406
|
+
[structural]
|
|
1407
|
+
);
|
|
1408
|
+
const problemIndex = useMemo(() => indexProblems(problems), [problems]);
|
|
1110
1409
|
const styledEdges = useMemo(() => {
|
|
1111
|
-
const
|
|
1410
|
+
const run = buildStyledEdges(structural.edges, nodeState, maxNodeVisits);
|
|
1411
|
+
const styled = decorateAuthoringEdges(
|
|
1412
|
+
run,
|
|
1413
|
+
problemIndex.byEdge,
|
|
1414
|
+
(edge) => insertArmed && isEditableEdge(edge),
|
|
1415
|
+
hasRunOverlay
|
|
1416
|
+
);
|
|
1112
1417
|
return styled.map((e) => ({
|
|
1113
1418
|
...e,
|
|
1114
1419
|
deletable: editable ? isEditableEdge(e) : false
|
|
1115
1420
|
}));
|
|
1116
|
-
}, [
|
|
1421
|
+
}, [
|
|
1422
|
+
structural.edges,
|
|
1423
|
+
nodeState,
|
|
1424
|
+
maxNodeVisits,
|
|
1425
|
+
editable,
|
|
1426
|
+
problemIndex,
|
|
1427
|
+
insertArmed,
|
|
1428
|
+
hasRunOverlay
|
|
1429
|
+
]);
|
|
1117
1430
|
const [nodes, setNodes, onNodesChange] = useNodesState(structural.nodes);
|
|
1118
1431
|
const [edges, setEdges, onEdgesChange] = useEdgesState(styledEdges);
|
|
1119
1432
|
const nodesRef = useRef(nodes);
|
|
@@ -1242,6 +1555,19 @@ function WorkflowGraph({
|
|
|
1242
1555
|
},
|
|
1243
1556
|
[onNodeClick]
|
|
1244
1557
|
);
|
|
1558
|
+
const handleConnectEnd = useCallback(
|
|
1559
|
+
(event, state) => {
|
|
1560
|
+
if (!insertAtNode) return;
|
|
1561
|
+
if (state.toNode || state.toHandle) return;
|
|
1562
|
+
const from = state.fromNode;
|
|
1563
|
+
const fromHandle = state.fromHandle;
|
|
1564
|
+
if (!from || !fromHandle) return;
|
|
1565
|
+
const at = releasePoint(event);
|
|
1566
|
+
if (!at || !releasedOnCanvas(at, wrapperRef.current)) return;
|
|
1567
|
+
insertAtNode(from.id, fromHandle.type === "target" ? "before" : "after");
|
|
1568
|
+
},
|
|
1569
|
+
[insertAtNode]
|
|
1570
|
+
);
|
|
1245
1571
|
const handleNodeKeyDown = useCallback(
|
|
1246
1572
|
(event) => {
|
|
1247
1573
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
@@ -1265,7 +1591,7 @@ function WorkflowGraph({
|
|
|
1265
1591
|
}
|
|
1266
1592
|
);
|
|
1267
1593
|
}
|
|
1268
|
-
return /* @__PURE__ */ jsx3(DirectionContext.Provider, { value: direction, children: /* @__PURE__ */ jsx3(DensityContext.Provider, { value: displayCompact, children: /* @__PURE__ */ jsx3(RunModeContext.Provider, { value: hasRunOverlay, children: /* @__PURE__ */ jsx3(SelectedNodeContext.Provider, { value: selectedNodeId, children: /* @__PURE__ */ jsx3(ConnectableContext.Provider, { value: editable, children: /* @__PURE__ */ jsx3("div", { ref: wrapperRef, className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs3(
|
|
1594
|
+
return /* @__PURE__ */ jsx3(DirectionContext.Provider, { value: direction, children: /* @__PURE__ */ jsx3(DensityContext.Provider, { value: displayCompact, children: /* @__PURE__ */ jsx3(RunModeContext.Provider, { value: hasRunOverlay, children: /* @__PURE__ */ jsx3(SelectedNodeContext.Provider, { value: selectedNodeId, children: /* @__PURE__ */ jsx3(ConnectableContext.Provider, { value: editable, children: /* @__PURE__ */ jsx3(NodeBoxesContext.Provider, { value: nodeBoxes, children: /* @__PURE__ */ jsx3(NodeProblemsContext.Provider, { value: problemIndex.byNode, children: /* @__PURE__ */ jsx3(TriggerDeleteContext.Provider, { value: deleteTrigger ?? null, children: /* @__PURE__ */ jsx3(EdgeInsertContext.Provider, { value: insertOnEdge ?? null, children: /* @__PURE__ */ jsx3("div", { ref: wrapperRef, className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs3(
|
|
1269
1595
|
ReactFlow,
|
|
1270
1596
|
{
|
|
1271
1597
|
nodes,
|
|
@@ -1306,6 +1632,7 @@ function WorkflowGraph({
|
|
|
1306
1632
|
}
|
|
1307
1633
|
}
|
|
1308
1634
|
} : void 0,
|
|
1635
|
+
onConnectEnd: insertAtNode ? handleConnectEnd : void 0,
|
|
1309
1636
|
onEdgeClick: editable ? (_event, edge) => {
|
|
1310
1637
|
if (isEditableEdge(edge)) {
|
|
1311
1638
|
onEdgeClick?.(edge.source, edge.target);
|
|
@@ -1321,37 +1648,72 @@ function WorkflowGraph({
|
|
|
1321
1648
|
children: [
|
|
1322
1649
|
/* @__PURE__ */ jsx3(Background, { gap: 18 }),
|
|
1323
1650
|
!isPreview && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
1324
|
-
/* @__PURE__ */ jsx3(
|
|
1325
|
-
|
|
1326
|
-
"button",
|
|
1651
|
+
/* @__PURE__ */ jsx3(
|
|
1652
|
+
Controls,
|
|
1327
1653
|
{
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
title: userCompact ? "Expand nodes" : "Collapse nodes",
|
|
1332
|
-
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",
|
|
1333
|
-
children: [
|
|
1334
|
-
userCompact ? /* @__PURE__ */ jsx3(Maximize2, { size: 12 }) : /* @__PURE__ */ jsx3(Minimize2, { size: 12 }),
|
|
1335
|
-
userCompact ? "Expand" : "Compact"
|
|
1336
|
-
]
|
|
1654
|
+
showInteractive: false,
|
|
1655
|
+
position: "bottom-right",
|
|
1656
|
+
fitViewOptions: { padding: FIT_VIEW.padding }
|
|
1337
1657
|
}
|
|
1338
|
-
)
|
|
1658
|
+
),
|
|
1659
|
+
/* @__PURE__ */ jsxs3(Panel, { position: "top-left", className: "flex items-center gap-1", children: [
|
|
1660
|
+
addTrigger && /* @__PURE__ */ jsxs3(
|
|
1661
|
+
"button",
|
|
1662
|
+
{
|
|
1663
|
+
type: "button",
|
|
1664
|
+
"data-testid": "wf-trigger-add",
|
|
1665
|
+
onClick: addTrigger,
|
|
1666
|
+
title: "Add a trigger",
|
|
1667
|
+
"aria-label": "Add a trigger",
|
|
1668
|
+
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",
|
|
1669
|
+
children: [
|
|
1670
|
+
/* @__PURE__ */ jsx3(Plus, { size: 12, "aria-hidden": true }),
|
|
1671
|
+
"Trigger"
|
|
1672
|
+
]
|
|
1673
|
+
}
|
|
1674
|
+
),
|
|
1675
|
+
/* @__PURE__ */ jsxs3(
|
|
1676
|
+
"button",
|
|
1677
|
+
{
|
|
1678
|
+
type: "button",
|
|
1679
|
+
onClick: () => {
|
|
1680
|
+
const next = !nextDensityFrom.current;
|
|
1681
|
+
nextDensityFrom.current = next;
|
|
1682
|
+
setUserCompact(next);
|
|
1683
|
+
onCompactChange?.(next);
|
|
1684
|
+
},
|
|
1685
|
+
"aria-pressed": userCompact,
|
|
1686
|
+
title: userCompact ? "Expand nodes" : "Collapse nodes",
|
|
1687
|
+
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",
|
|
1688
|
+
children: [
|
|
1689
|
+
userCompact ? /* @__PURE__ */ jsx3(Maximize2, { size: 12 }) : /* @__PURE__ */ jsx3(Minimize2, { size: 12 }),
|
|
1690
|
+
userCompact ? "Expand" : "Compact"
|
|
1691
|
+
]
|
|
1692
|
+
}
|
|
1693
|
+
)
|
|
1694
|
+
] })
|
|
1339
1695
|
] })
|
|
1340
1696
|
]
|
|
1341
1697
|
}
|
|
1342
|
-
) }) }) }) }) }) });
|
|
1698
|
+
) }) }) }) }) }) }) }) }) }) });
|
|
1343
1699
|
}
|
|
1344
1700
|
export {
|
|
1345
1701
|
ConnectableContext,
|
|
1346
1702
|
DensityContext,
|
|
1347
1703
|
DirectionContext,
|
|
1704
|
+
EdgeInsertContext,
|
|
1348
1705
|
FIT_VIEW,
|
|
1349
1706
|
LAYOUT_TRANSITION_MS,
|
|
1707
|
+
NodeBoxesContext,
|
|
1708
|
+
NodeProblemsContext,
|
|
1350
1709
|
RunModeContext,
|
|
1351
1710
|
SelectedNodeContext,
|
|
1711
|
+
TriggerDeleteContext,
|
|
1712
|
+
WfEdgeRenderer,
|
|
1352
1713
|
WorkflowGraph,
|
|
1353
1714
|
WorkflowNode,
|
|
1354
1715
|
buildStyledEdges,
|
|
1716
|
+
decorateAuthoringEdges,
|
|
1355
1717
|
fitZoomCeiling,
|
|
1356
1718
|
fitZoomFloor,
|
|
1357
1719
|
framingViewport,
|
|
@@ -230,10 +230,18 @@ function actionNodeId(index) {
|
|
|
230
230
|
function branchNodeId(actionIndex, branchIndex) {
|
|
231
231
|
return `${actionNodeId(actionIndex)}-b${branchIndex}`;
|
|
232
232
|
}
|
|
233
|
+
function wfEdgeId(source, target) {
|
|
234
|
+
return `${source}->${target}`;
|
|
235
|
+
}
|
|
236
|
+
function worstSeverity(problems) {
|
|
237
|
+
if (!problems || problems.length === 0) return null;
|
|
238
|
+
return problems.every((p) => p.severity === "warning") ? "warning" : "error";
|
|
239
|
+
}
|
|
233
240
|
var NODE_W = 292;
|
|
234
241
|
var RANK_SEP = 72;
|
|
235
242
|
var CROSS_SEP = 24;
|
|
236
243
|
var EDGE_LABEL_LANE = 180;
|
|
244
|
+
var EDGE_INSERT_LANE = 44;
|
|
237
245
|
var CARD_CHROME = 20;
|
|
238
246
|
var HEADER_ROW = 34;
|
|
239
247
|
var DESCRIPTION_ROW = 40;
|
|
@@ -814,7 +822,7 @@ function buildWorkflowGraph(yaml, options) {
|
|
|
814
822
|
};
|
|
815
823
|
const edgeIds = /* @__PURE__ */ new Set();
|
|
816
824
|
const addEdge = (source, target, kind, whenLabel) => {
|
|
817
|
-
const id =
|
|
825
|
+
const id = wfEdgeId(source, target);
|
|
818
826
|
if (edgeIds.has(id)) return;
|
|
819
827
|
edgeIds.add(id);
|
|
820
828
|
edges.push({
|
|
@@ -902,7 +910,7 @@ function buildWorkflowGraph(yaml, options) {
|
|
|
902
910
|
const labelled = edges.some(
|
|
903
911
|
(e) => e.whenLabel !== void 0 || e.backEdge === true
|
|
904
912
|
);
|
|
905
|
-
const rankSep = labelled ? Math.max(geo.rankSep, EDGE_LABEL_LANE) : geo.rankSep;
|
|
913
|
+
const rankSep = labelled ? Math.max(geo.rankSep, EDGE_LABEL_LANE) : options?.reserveEdgeInsert ? Math.max(geo.rankSep, EDGE_INSERT_LANE) : geo.rankSep;
|
|
906
914
|
const folded = options?.wrap && direction === "LR" && !labelled ? layoutSerpentine(logical, edges, rankSep, geo.crossSep) : null;
|
|
907
915
|
return {
|
|
908
916
|
nodes: folded ?? layoutLayers(logical, direction, rankSep, geo.crossSep),
|
|
@@ -923,6 +931,8 @@ export {
|
|
|
923
931
|
triggerNodeIndex,
|
|
924
932
|
actionNodeId,
|
|
925
933
|
branchNodeId,
|
|
934
|
+
wfEdgeId,
|
|
935
|
+
worstSeverity,
|
|
926
936
|
AGENT_BODY_ROWS,
|
|
927
937
|
ACTION_OUTPUT_ROWS,
|
|
928
938
|
STRUCTURAL_OUTPUT_ROWS,
|
package/dist/globals.css
CHANGED
|
@@ -407,6 +407,9 @@
|
|
|
407
407
|
.bottom-\[-0\.75rem\] {
|
|
408
408
|
bottom: -0.75rem;
|
|
409
409
|
}
|
|
410
|
+
.-left-1\.5 {
|
|
411
|
+
left: calc(var(--spacing) * -1.5);
|
|
412
|
+
}
|
|
410
413
|
.left-0 {
|
|
411
414
|
left: 0px;
|
|
412
415
|
}
|
|
@@ -485,6 +488,9 @@
|
|
|
485
488
|
max-width: 96rem;
|
|
486
489
|
}
|
|
487
490
|
}
|
|
491
|
+
.-m-1 {
|
|
492
|
+
margin: calc(var(--spacing) * -1);
|
|
493
|
+
}
|
|
488
494
|
.m-4 {
|
|
489
495
|
margin: calc(var(--spacing) * 4);
|
|
490
496
|
}
|
|
@@ -3884,6 +3890,9 @@
|
|
|
3884
3890
|
border-color: color-mix(in oklab, var(--color-foreground) 20%, transparent);
|
|
3885
3891
|
}
|
|
3886
3892
|
}
|
|
3893
|
+
.hover\:border-primary:hover {
|
|
3894
|
+
border-color: var(--color-primary);
|
|
3895
|
+
}
|
|
3887
3896
|
.hover\:border-primary\/20:hover {
|
|
3888
3897
|
border-color: color-mix(in srgb, hsl(var(--primary)) 20%, transparent);
|
|
3889
3898
|
}
|
package/dist/styles.css
CHANGED
|
@@ -407,6 +407,9 @@
|
|
|
407
407
|
.bottom-\[-0\.75rem\] {
|
|
408
408
|
bottom: -0.75rem;
|
|
409
409
|
}
|
|
410
|
+
.-left-1\.5 {
|
|
411
|
+
left: calc(var(--spacing) * -1.5);
|
|
412
|
+
}
|
|
410
413
|
.left-0 {
|
|
411
414
|
left: 0px;
|
|
412
415
|
}
|
|
@@ -485,6 +488,9 @@
|
|
|
485
488
|
max-width: 96rem;
|
|
486
489
|
}
|
|
487
490
|
}
|
|
491
|
+
.-m-1 {
|
|
492
|
+
margin: calc(var(--spacing) * -1);
|
|
493
|
+
}
|
|
488
494
|
.m-4 {
|
|
489
495
|
margin: calc(var(--spacing) * 4);
|
|
490
496
|
}
|
|
@@ -3884,6 +3890,9 @@
|
|
|
3884
3890
|
border-color: color-mix(in oklab, var(--color-foreground) 20%, transparent);
|
|
3885
3891
|
}
|
|
3886
3892
|
}
|
|
3893
|
+
.hover\:border-primary:hover {
|
|
3894
|
+
border-color: var(--color-primary);
|
|
3895
|
+
}
|
|
3887
3896
|
.hover\:border-primary\/20:hover {
|
|
3888
3897
|
border-color: color-mix(in srgb, hsl(var(--primary)) 20%, transparent);
|
|
3889
3898
|
}
|
package/dist/workflows.d.ts
CHANGED
|
@@ -206,6 +206,48 @@ interface WfEdgeSpec {
|
|
|
206
206
|
to: string;
|
|
207
207
|
whenLabel?: string;
|
|
208
208
|
}
|
|
209
|
+
/** How serious an authoring problem is. An `error` stops the definition from
|
|
210
|
+
* compiling; a `warning` compiles but is worth saying out loud. */
|
|
211
|
+
type WfProblemSeverity = "error" | "warning";
|
|
212
|
+
/**
|
|
213
|
+
* One AUTHORING problem, anchored to the node or the edge that carries it.
|
|
214
|
+
*
|
|
215
|
+
* This is the DEFINITION-time counterpart of {@link WfNodeState}, and it is a
|
|
216
|
+
* separate channel on purpose: run state is what a node DID (status, cost,
|
|
217
|
+
* duration), while a problem is what is wrong with what it SAYS. Folding one
|
|
218
|
+
* into the other would make an unsaved compile error render as a failed run.
|
|
219
|
+
*
|
|
220
|
+
* The host supplies the message already written for a reader — this library
|
|
221
|
+
* renders graphs and never interprets a compiler's output. Anchors are node ids
|
|
222
|
+
* from THIS graph, so name them with the exported id helpers (`actionNodeId`,
|
|
223
|
+
* `branchNodeId`, `TRIGGER_NODE_ID`); an edge is named by its endpoints, the
|
|
224
|
+
* same way {@link WfEdgeSpec} names one.
|
|
225
|
+
*
|
|
226
|
+
* An anchor the graph has no slot for is simply not drawn — an authoring surface
|
|
227
|
+
* reports against a draft that is still moving, and a stale anchor must never
|
|
228
|
+
* blank the canvas the way a stale declared edge does. That is the ONLY safety
|
|
229
|
+
* this gives, and it is worth being exact about what it leaves out: these ids are
|
|
230
|
+
* POSITIONAL, so an insert, delete or reorder does not retire `a1` — it makes
|
|
231
|
+
* `a1` a different step. A problem computed against an earlier draft therefore
|
|
232
|
+
* survives the check and lands on whichever step now holds its position. Nothing
|
|
233
|
+
* here can tell the two apart, which is why the pairing contract on
|
|
234
|
+
* `WorkflowGraphProps.problems` is a contract and not a suggestion: a list must
|
|
235
|
+
* be handed over with the definition it was computed from.
|
|
236
|
+
*/
|
|
237
|
+
type WfProblem = {
|
|
238
|
+
anchor: "node";
|
|
239
|
+
/** The node the problem sits on. */
|
|
240
|
+
node: string;
|
|
241
|
+
severity: WfProblemSeverity;
|
|
242
|
+
message: string;
|
|
243
|
+
} | {
|
|
244
|
+
anchor: "edge";
|
|
245
|
+
/** The edge's endpoints, as {@link WfEdgeSpec} names them. */
|
|
246
|
+
from: string;
|
|
247
|
+
to: string;
|
|
248
|
+
severity: WfProblemSeverity;
|
|
249
|
+
message: string;
|
|
250
|
+
};
|
|
209
251
|
interface WfGraph {
|
|
210
252
|
nodes: WfNode[];
|
|
211
253
|
edges: WfEdge[];
|
|
@@ -252,6 +294,16 @@ interface BuildWorkflowGraphOptions {
|
|
|
252
294
|
* worse than one that says it cannot be drawn.
|
|
253
295
|
*/
|
|
254
296
|
edges?: readonly WfEdgeSpec[];
|
|
297
|
+
/**
|
|
298
|
+
* Widen the gap between layers so an edge has room for an in-line control at
|
|
299
|
+
* its midpoint — the insert affordance an EDITING canvas draws there. The
|
|
300
|
+
* compact densities pitch their layers at 20-34px, which is the whole button,
|
|
301
|
+
* so a canvas that renders one without asking for the lane puts it on top of
|
|
302
|
+
* the nodes either side. Reserved by the layout for the same reason run rows
|
|
303
|
+
* are: what a node or an edge renders is spaced for before it is drawn, never
|
|
304
|
+
* measured afterwards. Defaults to `false`.
|
|
305
|
+
*/
|
|
306
|
+
reserveEdgeInsert?: boolean;
|
|
255
307
|
}
|
|
256
308
|
/** Build a positioned graph from a workflow YAML string. Never throws —
|
|
257
309
|
* malformed YAML or an empty definition returns an `error` the UI can fall
|
|
@@ -279,6 +331,17 @@ interface WorkflowGraphProps {
|
|
|
279
331
|
* toggle, and the preview variant is always compact.
|
|
280
332
|
*/
|
|
281
333
|
defaultCompact?: boolean;
|
|
334
|
+
/**
|
|
335
|
+
* Called when the reader moves the density toggle, with the density they
|
|
336
|
+
* chose.
|
|
337
|
+
*
|
|
338
|
+
* `defaultCompact` seeds the graph; this reports back out of it, which is
|
|
339
|
+
* what a host needs to REMEMBER the choice. Without it a host can set an
|
|
340
|
+
* opening density but never learn that the reader wanted the other one, so
|
|
341
|
+
* every navigation puts them back where they started and the toggle has to
|
|
342
|
+
* be found again.
|
|
343
|
+
*/
|
|
344
|
+
onCompactChange?: (compact: boolean) => void;
|
|
282
345
|
/** Sizing for the wrapper; the caller controls height. */
|
|
283
346
|
className?: string;
|
|
284
347
|
/**
|
|
@@ -323,9 +386,15 @@ interface WorkflowGraphProps {
|
|
|
323
386
|
* Editing gestures. Supplying `onEdgeConnect` turns the canvas from a diagram
|
|
324
387
|
* into an EDITOR: node handles become visible and draggable, an edge can be
|
|
325
388
|
* selected and removed with Delete/Backspace, and clicking one asks to edit
|
|
326
|
-
* its guard. Omit
|
|
389
|
+
* its guard. Omit it — the default — and the graph stays the read-only
|
|
327
390
|
* visualisation it has always been.
|
|
328
391
|
*
|
|
392
|
+
* `onEdgeConnect` is the ONE prop that decides it. Every other editing
|
|
393
|
+
* callback below (`onEdgeDelete`, `onEdgeClick`, `onEdgeInsert`,
|
|
394
|
+
* `onNodeInsert`, `onTriggerAdd`, `onTriggerDelete`) refines an editor and is
|
|
395
|
+
* inert without it — a canvas that cannot accept a new connection has no
|
|
396
|
+
* business drawing an add control either.
|
|
397
|
+
*
|
|
329
398
|
* Every callback speaks node ids (`actionNodeId`, `branchNodeId`), never
|
|
330
399
|
* positions: this component reports the gesture, and turning it into a
|
|
331
400
|
* definition edit is the host's job — it owns the YAML, and only it knows
|
|
@@ -338,8 +407,83 @@ interface WorkflowGraphProps {
|
|
|
338
407
|
onEdgeConnect?: (sourceId: string, targetId: string) => void;
|
|
339
408
|
onEdgeDelete?: (sourceId: string, targetId: string) => void;
|
|
340
409
|
onEdgeClick?: (sourceId: string, targetId: string) => void;
|
|
410
|
+
/**
|
|
411
|
+
* Add a step ON an edge: the edge draws a "+" at its midpoint, and pressing it
|
|
412
|
+
* reports the pair it sits between. The layout widens the corridor between
|
|
413
|
+
* layers to hold the control, so arming this RELAYS OUT the graph — a compact
|
|
414
|
+
* canvas pitches its layers at 20px, which is the whole button.
|
|
415
|
+
*
|
|
416
|
+
* Offered on exactly the edges the other three gestures are ({@link
|
|
417
|
+
* isEditableEdge}): inserting on a fan-out or trigger edge would name a pair
|
|
418
|
+
* that exists in no definition. "Add a step at the very start" is the
|
|
419
|
+
* {@link onNodeInsert} drop instead — dragged from the first step's inbound
|
|
420
|
+
* handle onto empty canvas, which names one node rather than a synthesized
|
|
421
|
+
* edge. (A definition with no `on:` at all has no inbound handle on its first
|
|
422
|
+
* step, because nothing points at it; there the step list is the only way in.)
|
|
423
|
+
*/
|
|
424
|
+
onEdgeInsert?: (sourceId: string, targetId: string) => void;
|
|
425
|
+
/**
|
|
426
|
+
* Add a step at the loose end of a connection dragged from a node's handle and
|
|
427
|
+
* released over empty canvas — the canvas's answer to "and then what?".
|
|
428
|
+
* `side` is which handle it left from: `"after"` for the outbound one (the new
|
|
429
|
+
* step follows `nodeId`), `"before"` for the inbound one (it precedes it).
|
|
430
|
+
*
|
|
431
|
+
* Reported for ANY node, including a trigger and a fan-out branch leaf: unlike
|
|
432
|
+
* an edge gesture, this names a node that certainly exists, and whether a step
|
|
433
|
+
* may go beside it is a question about the definition's schema — which this
|
|
434
|
+
* library does not model and the host already answers.
|
|
435
|
+
*/
|
|
436
|
+
onNodeInsert?: (nodeId: string, side: "before" | "after") => void;
|
|
437
|
+
/**
|
|
438
|
+
* The `on:` list gains an entry. Drawn as a canvas control rather than a
|
|
439
|
+
* per-node one, because the workflow that most needs it is the one with NO
|
|
440
|
+
* trigger — which has no trigger node to hang a "+" on.
|
|
441
|
+
*
|
|
442
|
+
* Offered whenever it is supplied, including on a definition that ALREADY has
|
|
443
|
+
* a trigger: `on:` is a list with OR semantics, and a second subscription is a
|
|
444
|
+
* normal thing to want. Whether THIS workflow may take another one is a
|
|
445
|
+
* question about the definition's schema and the provider behind it, which
|
|
446
|
+
* this library does not model — a host that must cap the list withholds the
|
|
447
|
+
* callback.
|
|
448
|
+
*/
|
|
449
|
+
onTriggerAdd?: () => void;
|
|
450
|
+
/** Remove the `on:` entry a trigger node stands for — the one node deletion
|
|
451
|
+
* that IS a canvas gesture (every other node is a `do` entry, reordered and
|
|
452
|
+
* removed in the list the host owns). Read the entry's position back with
|
|
453
|
+
* `triggerNodeIndex`. */
|
|
454
|
+
onTriggerDelete?: (nodeId: string) => void;
|
|
455
|
+
/**
|
|
456
|
+
* AUTHORING problems anchored to the nodes and edges that carry them, so a
|
|
457
|
+
* compile error is visible on the canvas rather than only in a list beside it.
|
|
458
|
+
* See {@link WfProblem} — and note it is a separate channel from `nodeState`,
|
|
459
|
+
* which is what a node DID rather than what is wrong with what it says.
|
|
460
|
+
*
|
|
461
|
+
* A problem tints the node's border in either density, and states itself on a
|
|
462
|
+
* mark in the card's identity row (a corner of the compact tile). A graph
|
|
463
|
+
* showing a RUN is not an authoring surface — its cards and edges state the
|
|
464
|
+
* run instead, which takes precedence over a problem's colour on both, and the
|
|
465
|
+
* problem's mark and chip still render beside it.
|
|
466
|
+
*
|
|
467
|
+
* What the canvas gives is a LOCATOR: which step or dependency is at fault,
|
|
468
|
+
* with the messages on the mark's `title` and in the accessibility tree. It is
|
|
469
|
+
* deliberately not a reader for them — at the zoom a whole pipeline is viewed
|
|
470
|
+
* at, a panel of message text on the canvas is unreadable, and the host
|
|
471
|
+
* already owns the problem list that the messages are read and acted on in.
|
|
472
|
+
*
|
|
473
|
+
* Pairing contract: anchors are positional node ids, so a problem list must be
|
|
474
|
+
* derived from the SAME definition passed as `yaml`. Hand over a list computed
|
|
475
|
+
* against an older draft and an id that still exists may now name a different
|
|
476
|
+
* step, which puts the diagnostic on an innocent one — the graph cannot tell
|
|
477
|
+
* the two apart, any more than it can for a run's `nodeState`. A host that
|
|
478
|
+
* validates asynchronously therefore holds each result against the text it was
|
|
479
|
+
* computed from, and passes neither until they agree.
|
|
480
|
+
*
|
|
481
|
+
* Immutability contract, as for `edges`: the index is memoized on this array's
|
|
482
|
+
* reference, so pass a stable one.
|
|
483
|
+
*/
|
|
484
|
+
problems?: readonly WfProblem[];
|
|
341
485
|
}
|
|
342
486
|
|
|
343
487
|
declare function WorkflowGraph(props: WorkflowGraphProps): React.JSX.Element;
|
|
344
488
|
|
|
345
|
-
export { type BuildWorkflowGraphOptions, TRIGGER_NODE_ID, type WfEdge, type WfEdgeKind, type WfEdgeSpec, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, actionNodeId, branchNodeId, buildWorkflowGraph, triggerNodeId, triggerNodeIndex };
|
|
489
|
+
export { type BuildWorkflowGraphOptions, TRIGGER_NODE_ID, type WfEdge, type WfEdgeKind, type WfEdgeSpec, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, type WfProblem, type WfProblemSeverity, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, actionNodeId, branchNodeId, buildWorkflowGraph, triggerNodeId, triggerNodeIndex };
|
package/dist/workflows.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
buildWorkflowGraph,
|
|
6
6
|
triggerNodeId,
|
|
7
7
|
triggerNodeIndex
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-AI25G5MX.js";
|
|
9
9
|
import {
|
|
10
10
|
ModelBrandStack,
|
|
11
11
|
modelBrandFor
|
|
@@ -36,7 +36,7 @@ function retryImport(factory, retries = 2, delayMs = 350) {
|
|
|
36
36
|
function lazyWorkflowGraph() {
|
|
37
37
|
return lazy(
|
|
38
38
|
() => retryImport(
|
|
39
|
-
() => import("./WorkflowGraph-
|
|
39
|
+
() => import("./WorkflowGraph-2QOORYIR.js").then((m) => ({ default: m.WorkflowGraph }))
|
|
40
40
|
)
|
|
41
41
|
);
|
|
42
42
|
}
|
package/package.json
CHANGED