@calliopelabs/ui 0.7.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -3
- package/dist/appearance/AppearancePicker.js +4 -2
- package/dist/appearance/AppearanceProvider.d.ts +10 -2
- package/dist/appearance/AppearanceProvider.js +6 -2
- package/dist/appearance/AppearanceScript.d.ts +15 -4
- package/dist/appearance/AppearanceScript.js +11 -5
- package/dist/appearance/appearance.js +1 -1
- package/dist/appearance/boot.d.ts +13 -2
- package/dist/appearance/boot.js +35 -5
- package/dist/appearance/product.d.ts +13 -0
- package/dist/appearance/product.js +2 -2
- package/dist/appearance/runtime-brand.d.ts +106 -0
- package/dist/appearance/runtime-brand.js +420 -0
- package/dist/css/appearance.js +147 -0
- package/dist/css/calliope.css +1221 -0
- package/dist/css/components.css +729 -0
- package/dist/css/tokens.css +491 -0
- package/dist/data/DefinitionList.d.ts +16 -0
- package/dist/data/DefinitionList.js +9 -0
- package/dist/data/Stat.d.ts +25 -0
- package/dist/data/Stat.js +19 -0
- package/dist/data/Tag.d.ts +21 -0
- package/dist/data/Tag.js +18 -0
- package/dist/data/types.d.ts +3 -2
- package/dist/data/use-local-table.d.ts +36 -0
- package/dist/data/use-local-table.js +127 -0
- package/dist/index.d.ts +42 -3
- package/dist/index.js +39 -1
- package/dist/nav/Nav.d.ts +2 -1
- package/dist/nav/Nav.js +34 -15
- package/dist/nav/model.d.ts +21 -0
- package/dist/page/PageHeader.d.ts +5 -3
- package/dist/page/PageHeader.js +5 -4
- package/dist/page/Panel.d.ts +19 -0
- package/dist/page/Panel.js +12 -0
- package/dist/primitives/Avatar.d.ts +37 -0
- package/dist/primitives/Avatar.js +43 -0
- package/dist/primitives/Field.d.ts +33 -0
- package/dist/primitives/Field.js +30 -0
- package/dist/primitives/Segmented.d.ts +30 -0
- package/dist/primitives/Segmented.js +20 -0
- package/dist/primitives/Select.d.ts +40 -0
- package/dist/primitives/Select.js +47 -0
- package/dist/primitives/Switch.d.ts +7 -0
- package/dist/primitives/Switch.js +8 -0
- package/dist/primitives/Textarea.d.ts +3 -0
- package/dist/primitives/Textarea.js +6 -0
- package/dist/provider/messages.d.ts +412 -0
- package/dist/provider/messages.js +633 -0
- package/dist/shell/BrandMark.d.ts +25 -3
- package/dist/shell/BrandMark.js +34 -4
- package/dist/shell/RailPanel.js +2 -2
- package/dist/shell/RailTree.js +2 -2
- package/dist/shell/Sidebar.d.ts +9 -2
- package/dist/shell/Sidebar.js +10 -4
- package/dist/shell/UserMenu.d.ts +3 -1
- package/dist/shell/UserMenu.js +3 -11
- package/dist/shell/rail-boot.d.ts +28 -0
- package/dist/shell/rail-boot.js +45 -0
- package/dist/shell/rail-state.d.ts +6 -1
- package/dist/shell/rail-state.js +18 -2
- package/dist/styles/preset.css +4 -0
- package/dist/styles/theme.css +36 -13
- package/dist/theme/css-layer.d.ts +19 -0
- package/dist/theme/css-layer.js +171 -0
- package/dist/theme/css.d.ts +1 -0
- package/dist/theme/css.js +45 -9
- package/dist/workflow/ArtifactCard.d.ts +47 -0
- package/dist/workflow/ArtifactCard.js +45 -0
- package/dist/workflow/DocumentReview.d.ts +74 -0
- package/dist/workflow/DocumentReview.js +54 -0
- package/dist/workflow/FormReview.d.ts +53 -0
- package/dist/workflow/FormReview.js +83 -0
- package/dist/workflow/GateBadge.d.ts +11 -0
- package/dist/workflow/GateBadge.js +14 -0
- package/dist/workflow/MediaReview.d.ts +63 -0
- package/dist/workflow/MediaReview.js +166 -0
- package/dist/workflow/ReviewPanel.d.ts +41 -0
- package/dist/workflow/ReviewPanel.js +59 -0
- package/dist/workflow/RunTimeline.d.ts +25 -0
- package/dist/workflow/RunTimeline.js +56 -0
- package/dist/workflow/WorkflowCard.d.ts +39 -0
- package/dist/workflow/WorkflowCard.js +56 -0
- package/dist/workflow/WorkflowGallery.d.ts +43 -0
- package/dist/workflow/WorkflowGallery.js +69 -0
- package/dist/workflow/canvas/WorkflowCanvas.d.ts +39 -0
- package/dist/workflow/canvas/WorkflowCanvas.js +648 -0
- package/dist/workflow/definition.d.ts +408 -0
- package/dist/workflow/definition.js +411 -0
- package/dist/workflow/editor/WorkflowDiff.d.ts +17 -0
- package/dist/workflow/editor/WorkflowDiff.js +25 -0
- package/dist/workflow/editor/WorkflowImportPreview.d.ts +19 -0
- package/dist/workflow/editor/WorkflowImportPreview.js +50 -0
- package/dist/workflow/editor/WorkflowInspector.d.ts +40 -0
- package/dist/workflow/editor/WorkflowInspector.js +349 -0
- package/dist/workflow/editor/WorkflowJsonView.d.ts +16 -0
- package/dist/workflow/editor/WorkflowJsonView.js +40 -0
- package/dist/workflow/editor/WorkflowPalette.d.ts +16 -0
- package/dist/workflow/editor/WorkflowPalette.js +36 -0
- package/dist/workflow/editor/WorkflowProblems.d.ts +20 -0
- package/dist/workflow/editor/WorkflowProblems.js +51 -0
- package/dist/workflow/editor/WorkflowWorkspace.d.ts +49 -0
- package/dist/workflow/editor/WorkflowWorkspace.js +154 -0
- package/dist/workflow/gallery.d.ts +21 -0
- package/dist/workflow/gallery.js +29 -0
- package/dist/workflow/layout.d.ts +49 -0
- package/dist/workflow/layout.js +162 -0
- package/dist/workflow/model.d.ts +50 -0
- package/dist/workflow/model.js +7 -0
- package/dist/workflow/palette.d.ts +34 -0
- package/dist/workflow/palette.js +70 -0
- package/dist/workflow/portable.d.ts +162 -0
- package/dist/workflow/portable.js +547 -0
- package/dist/workflow/problems.d.ts +38 -0
- package/dist/workflow/problems.js +409 -0
- package/dist/workflow/run.d.ts +94 -0
- package/dist/workflow/run.js +31 -0
- package/dist/workflow/workspace.d.ts +33 -0
- package/dist/workflow/workspace.js +17 -0
- package/package.json +6 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { PlusIcon, Trash2Icon, XIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../../lib/cn.js";
|
|
6
|
+
import { Button } from "../../primitives/Button.js";
|
|
7
|
+
import { Checkbox } from "../../primitives/Checkbox.js";
|
|
8
|
+
import { Field } from "../../primitives/Field.js";
|
|
9
|
+
import { Input } from "../../primitives/Input.js";
|
|
10
|
+
import { Select } from "../../primitives/Select.js";
|
|
11
|
+
import { Switch } from "../../primitives/Switch.js";
|
|
12
|
+
import { Textarea } from "../../primitives/Textarea.js";
|
|
13
|
+
import { useUI } from "../../provider/UIProvider.js";
|
|
14
|
+
import { WORKFLOW_SELECTORS, dataTypes, edgeId, exitLabel, findFact, findGateKind, findPreset, freshId, inputsOf, operatorsFor, outputsOf, portOf, removeEdge, removeNode, updateEdge, updateNode, withFlow, } from "../definition.js";
|
|
15
|
+
const DEFAULT = "__default";
|
|
16
|
+
const NONE = "__none";
|
|
17
|
+
/** A number field that commits a number, or nothing when emptied. */
|
|
18
|
+
function NumberInput({ value, onValue, min, max, step, ...props }) {
|
|
19
|
+
return (_jsx(Input, { ...props, type: "number", inputMode: "decimal", min: min, max: max, step: step, value: value ?? "", onChange: (e) => {
|
|
20
|
+
const n = e.target.valueAsNumber;
|
|
21
|
+
onValue(e.target.value === "" || Number.isNaN(n) ? undefined : n);
|
|
22
|
+
} }));
|
|
23
|
+
}
|
|
24
|
+
/** A 0 to 1 value as a percentage. */
|
|
25
|
+
function PercentInput({ value, onValue, ...props }) {
|
|
26
|
+
return (_jsx(NumberInput, { ...props, min: 0, max: 100, step: 1, value: value === undefined ? undefined : Math.round(value * 100), onValue: (v) => onValue(v === undefined ? undefined : Math.min(1, Math.max(0, v / 100))) }));
|
|
27
|
+
}
|
|
28
|
+
/** A host picker: the host's options, with "the default" first. */
|
|
29
|
+
function HostPicker({ label, options, value, onValue, disabled, }) {
|
|
30
|
+
const { messages } = useUI();
|
|
31
|
+
if (!options?.length)
|
|
32
|
+
return null;
|
|
33
|
+
const known = !value || options.some((o) => o.id === value);
|
|
34
|
+
return (_jsx(Field, { label: label, children: _jsx(Select, { value: value ?? DEFAULT, disabled: disabled, onValueChange: (v) => onValue(v === DEFAULT ? undefined : v), options: [
|
|
35
|
+
{ value: DEFAULT, label: messages.editor.hostDefault },
|
|
36
|
+
...options.map((o) => ({ value: o.id, label: o.label })),
|
|
37
|
+
...(known ? [] : [{ value: value, label: value }]),
|
|
38
|
+
] }) }));
|
|
39
|
+
}
|
|
40
|
+
/** Ticks for a set of the host's options: roles, members. */
|
|
41
|
+
function OptionChecks({ legend, options, value, onValue, disabled, }) {
|
|
42
|
+
const id = React.useId();
|
|
43
|
+
return (_jsxs("fieldset", { className: "flex flex-col gap-1.5", children: [_jsx("legend", { className: "mb-1 text-sm font-medium", children: legend }), options.map((o) => (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: `${id}-${o.id}`, checked: value.includes(o.id), disabled: disabled, onCheckedChange: (checked) => onValue(checked === true ? [...value, o.id] : value.filter((v) => v !== o.id)) }), _jsx("label", { htmlFor: `${id}-${o.id}`, children: o.label })] }, o.id)))] }));
|
|
44
|
+
}
|
|
45
|
+
/** One comparison on a run fact: the fact, its node when it is a node's, the operator and the value. */
|
|
46
|
+
export function ConditionEditor({ condition, onChange, flow, registry, label, disabled = false, }) {
|
|
47
|
+
const { messages } = useUI();
|
|
48
|
+
const m = messages.editor;
|
|
49
|
+
const fact = findFact(registry, condition.fact);
|
|
50
|
+
const ops = operatorsFor(registry, fact);
|
|
51
|
+
const op = registry.operators.find((o) => o.id === condition.op);
|
|
52
|
+
const nodes = fact?.node
|
|
53
|
+
? flow.nodes.filter((n) => fact.node === true || (Array.isArray(fact.node) && fact.node.includes(n.kind)))
|
|
54
|
+
: [];
|
|
55
|
+
const setFact = (id) => {
|
|
56
|
+
const next = findFact(registry, id);
|
|
57
|
+
const nextOp = operatorsFor(registry, next)[0];
|
|
58
|
+
onChange({
|
|
59
|
+
fact: id,
|
|
60
|
+
op: nextOp?.id ?? "",
|
|
61
|
+
...(next?.node && condition.node ? { node: condition.node } : {}),
|
|
62
|
+
...(nextOp?.unary
|
|
63
|
+
? {}
|
|
64
|
+
: {
|
|
65
|
+
value: next?.type === "boolean"
|
|
66
|
+
? true
|
|
67
|
+
: next?.type === "number"
|
|
68
|
+
? 0
|
|
69
|
+
: next?.type === "enum"
|
|
70
|
+
? (next.values?.[0]?.id ?? "")
|
|
71
|
+
: "",
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
return (_jsxs("fieldset", { className: "flex flex-col gap-3 rounded-lg border border-border p-3", children: [_jsx("legend", { className: "px-1 text-sm font-medium", children: label }), _jsx(Field, { label: m.fact, children: _jsx(Select, { value: condition.fact || NONE, disabled: disabled, onValueChange: setFact, options: [
|
|
76
|
+
...(fact
|
|
77
|
+
? []
|
|
78
|
+
: [
|
|
79
|
+
{
|
|
80
|
+
value: condition.fact || NONE,
|
|
81
|
+
label: condition.fact || m.choose,
|
|
82
|
+
disabled: true,
|
|
83
|
+
},
|
|
84
|
+
]),
|
|
85
|
+
...registry.facts.map((f) => ({ value: f.id, label: f.label })),
|
|
86
|
+
] }) }), fact?.node ? (_jsx(Field, { label: m.factNode, children: _jsx(Select, { value: condition.node ?? NONE, disabled: disabled, onValueChange: (v) => onChange({ ...condition, node: v }), options: [
|
|
87
|
+
...(condition.node ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
|
|
88
|
+
...nodes.map((n) => ({ value: n.id, label: n.label })),
|
|
89
|
+
] }) })) : null, _jsx(Field, { label: m.operator, children: _jsx(Select, { value: op && ops.includes(op) ? op.id : NONE, disabled: disabled, onValueChange: (v) => {
|
|
90
|
+
const next = registry.operators.find((o) => o.id === v);
|
|
91
|
+
const { value: _value, ...rest } = condition;
|
|
92
|
+
onChange(next?.unary ? { ...rest, op: v } : { ...condition, op: v });
|
|
93
|
+
}, options: [
|
|
94
|
+
...(op && ops.includes(op)
|
|
95
|
+
? []
|
|
96
|
+
: [{ value: NONE, label: condition.op || m.choose, disabled: true }]),
|
|
97
|
+
...ops.map((o) => ({ value: o.id, label: o.label })),
|
|
98
|
+
] }) }), op?.unary ? null : fact?.type === "number" ? (_jsx(Field, { label: m.value, children: _jsx(NumberInput, { value: typeof condition.value === "number" ? condition.value : undefined, disabled: disabled, onValue: (v) => onChange({ ...condition, value: v }) }) })) : fact?.type === "boolean" ? (_jsx(Field, { label: m.value, children: _jsx(Select, { value: String(condition.value === true), disabled: disabled, onValueChange: (v) => onChange({ ...condition, value: v === "true" }), options: [
|
|
99
|
+
{ value: "true", label: m.boolean(true) },
|
|
100
|
+
{ value: "false", label: m.boolean(false) },
|
|
101
|
+
] }) })) : fact?.type === "enum" && fact.values ? (_jsx(Field, { label: m.value, children: _jsx(Select, { value: typeof condition.value === "string" && condition.value ? condition.value : NONE, disabled: disabled, onValueChange: (v) => onChange({ ...condition, value: v }), options: [
|
|
102
|
+
...(fact.values.some((v) => v.id === condition.value)
|
|
103
|
+
? []
|
|
104
|
+
: [{ value: NONE, label: m.choose, disabled: true }]),
|
|
105
|
+
...fact.values.map((v) => ({ value: v.id, label: v.label })),
|
|
106
|
+
] }) })) : (_jsx(Field, { label: m.value, children: _jsx(Input, { value: typeof condition.value === "string" ? condition.value : String(condition.value ?? ""), disabled: disabled, onChange: (e) => onChange({ ...condition, value: e.target.value }) }) }))] }));
|
|
107
|
+
}
|
|
108
|
+
/** Who approves a gate: roles, named members, a quorum, and a timeout with what happens then. */
|
|
109
|
+
export function GatePolicyEditor({ policy, onChange, registry, approvers = true, timeout = true, disabled = false, }) {
|
|
110
|
+
const { messages } = useUI();
|
|
111
|
+
const m = messages.editor;
|
|
112
|
+
const set = (patch) => {
|
|
113
|
+
const next = { ...policy, ...patch };
|
|
114
|
+
for (const k of Object.keys(next))
|
|
115
|
+
if (next[k] === undefined)
|
|
116
|
+
delete next[k];
|
|
117
|
+
onChange(next);
|
|
118
|
+
};
|
|
119
|
+
return (_jsxs("fieldset", { className: "flex flex-col gap-3 rounded-lg border border-border p-3", children: [_jsx("legend", { className: "px-1 text-sm font-medium", children: m.approvers }), approvers ? (_jsxs(_Fragment, { children: [registry.roles?.length ? (_jsx(OptionChecks, { legend: m.roles, options: registry.roles, value: policy.roles ?? [], onValue: (roles) => set({ roles }), disabled: disabled })) : null, registry.members?.length ? (_jsx(OptionChecks, { legend: m.members, options: [
|
|
120
|
+
...registry.members,
|
|
121
|
+
...(policy.members ?? [])
|
|
122
|
+
.filter((id) => !registry.members?.some((o) => o.id === id))
|
|
123
|
+
.map((id) => ({ id, label: id })),
|
|
124
|
+
], value: policy.members ?? [], onValue: (members) => set({ members }), disabled: disabled })) : null, _jsx(Field, { label: m.quorum, hint: m.quorumHint, children: _jsx(NumberInput, { value: policy.quorum, min: 1, step: 1, disabled: disabled, onValue: (quorum) => set({ quorum }) }) })] })) : null, timeout ? (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.timeoutHours, children: _jsx(NumberInput, { value: policy.timeoutHours, min: 0, step: 1, disabled: disabled, onValue: (timeoutHours) => set({ timeoutHours }) }) }), policy.timeoutHours ? (_jsx(Field, { label: m.onTimeout, children: _jsx(Select, { value: policy.onTimeout ?? NONE, disabled: disabled, onValueChange: (v) => set({ onTimeout: v }), options: [
|
|
125
|
+
...(policy.onTimeout ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
|
|
126
|
+
{ value: "fail", label: m.onTimeoutOption("fail") },
|
|
127
|
+
{ value: "escalate", label: m.onTimeoutOption("escalate") },
|
|
128
|
+
] }) })) : null] })) : null] }));
|
|
129
|
+
}
|
|
130
|
+
/** A list of rows with add and remove, for cases, labels, variants and ports. */
|
|
131
|
+
function RowList({ legend, rows, render, name, onAdd, onRemove, addLabel, disabled, }) {
|
|
132
|
+
const { messages } = useUI();
|
|
133
|
+
return (_jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "mb-1 text-sm font-medium", children: legend }), rows.map((row, i) => (_jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-border p-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: render(row, i) }), disabled ? null : (_jsx(Button, { type: "button", size: "icon-xs", variant: "ghost", "aria-label": messages.editor.remove(name(row)), title: messages.editor.remove(name(row)), onClick: () => onRemove(i), children: _jsx(XIcon, { "aria-hidden": true }) }))] }, i))), disabled ? null : (_jsxs(Button, { type: "button", size: "sm", variant: "outline", className: "self-start", onClick: onAdd, children: [_jsx(PlusIcon, { "aria-hidden": true }), addLabel] }))] }));
|
|
134
|
+
}
|
|
135
|
+
const replace = (rows, i, row) => rows.map((r, j) => (j === i ? row : r));
|
|
136
|
+
function PortsEditor({ legend, ports, onChange, registry, disabled, }) {
|
|
137
|
+
const { messages } = useUI();
|
|
138
|
+
const m = messages.editor;
|
|
139
|
+
const types = [{ id: "any", label: m.anyType }, ...dataTypes(registry)];
|
|
140
|
+
return (_jsx(RowList, { legend: legend, rows: ports, name: (p) => p.label, addLabel: m.addPort, disabled: disabled, onAdd: () => {
|
|
141
|
+
const id = freshId(m.newPort, ports.map((p) => p.id));
|
|
142
|
+
onChange([...ports, { id, label: m.newPort, type: "any" }]);
|
|
143
|
+
}, onRemove: (i) => onChange(ports.filter((_, j) => j !== i)), render: (p, i) => (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.portLabel, children: _jsx(Input, { value: p.label, disabled: disabled, onChange: (e) => onChange(replace(ports, i, { ...p, label: e.target.value })) }) }), _jsx(Field, { label: m.portType, children: _jsx(Select, { value: p.type, disabled: disabled, onValueChange: (type) => onChange(replace(ports, i, { ...p, type })), options: [
|
|
144
|
+
...types.map((t) => ({ value: t.id, label: t.label })),
|
|
145
|
+
...(types.some((t) => t.id === p.type) ? [] : [{ value: p.type, label: p.type }]),
|
|
146
|
+
] }) })] })) }));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The selected node's or connection's fields, edited in place: per-kind
|
|
150
|
+
* fields, a condition editor over the host's facts and operators, a gate
|
|
151
|
+
* policy editor (roles, members, quorum, timeout), and the host's model,
|
|
152
|
+
* harness and effort pickers. With nothing selected, the workflow's name and
|
|
153
|
+
* description. Every edit goes out as the whole next definition.
|
|
154
|
+
*/
|
|
155
|
+
export function WorkflowInspector({ definition, registry, selection = null, onChange, onSelectionChange, readOnly = false, className, }) {
|
|
156
|
+
const { messages } = useUI();
|
|
157
|
+
const m = messages.editor;
|
|
158
|
+
const disabled = readOnly || !onChange;
|
|
159
|
+
const flowName = selection?.flow && definition.flows[selection.flow] ? selection.flow : definition.main;
|
|
160
|
+
const flow = definition.flows[flowName] ?? { nodes: [], edges: [] };
|
|
161
|
+
const node = selection?.type === "node" ? flow.nodes.find((n) => n.id === selection.id) : undefined;
|
|
162
|
+
const edge = selection?.type === "edge" ? flow.edges.find((e) => edgeId(e) === selection.id) : undefined;
|
|
163
|
+
const setFlow = (next) => onChange?.(withFlow(definition, flowName, next));
|
|
164
|
+
let body;
|
|
165
|
+
let title;
|
|
166
|
+
if (node) {
|
|
167
|
+
title = m.inspectorFor(m.kind(node.kind), node.label);
|
|
168
|
+
body = (_jsx(NodeFields, { node: node, flow: flow, definition: definition, registry: registry, disabled: disabled, onPatch: (patch) => setFlow(updateNode(flow, node.id, patch)), onDelete: () => {
|
|
169
|
+
setFlow(removeNode(flow, node.id));
|
|
170
|
+
onSelectionChange?.(null);
|
|
171
|
+
} }));
|
|
172
|
+
}
|
|
173
|
+
else if (edge) {
|
|
174
|
+
const from = flow.nodes.find((n) => n.id === edge.from);
|
|
175
|
+
const to = flow.nodes.find((n) => n.id === edge.to);
|
|
176
|
+
title = m.edgeName(edge.kind, from?.label ?? edge.from, to?.label ?? edge.to);
|
|
177
|
+
body = (_jsx(EdgeFields, { edge: edge, flow: flow, definition: definition, registry: registry, disabled: disabled,
|
|
178
|
+
// A connection's id is its ends and ports, none of which the inspector changes.
|
|
179
|
+
onPatch: (patch) => setFlow(updateEdge(flow, edgeId(edge), patch)), onDelete: () => {
|
|
180
|
+
setFlow(removeEdge(flow, edgeId(edge)));
|
|
181
|
+
onSelectionChange?.(null);
|
|
182
|
+
} }));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
title = m.workflowDetails;
|
|
186
|
+
body = (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(Field, { label: m.name, children: _jsx(Input, { value: definition.name, disabled: disabled, onChange: (e) => onChange?.({ ...definition, name: e.target.value }) }) }), _jsx(Field, { label: m.description, children: _jsx(Textarea, { value: definition.description ?? "", disabled: disabled, onChange: (e) => onChange?.({ ...definition, description: e.target.value || undefined }) }) }), _jsx(PortsEditor, { legend: m.runInputs, ports: flow.inputs ?? [], registry: registry, disabled: disabled, onChange: (inputs) => setFlow({ ...flow, inputs }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: m.inspectorEmpty })] }));
|
|
187
|
+
}
|
|
188
|
+
return (_jsxs("section", { "aria-label": m.inspector, "data-slot": "workflow-inspector", className: cn("flex min-h-0 flex-col gap-3", className), children: [_jsx("h2", { className: "truncate text-sm font-medium", children: title }), _jsx("div", { className: "flex min-h-0 flex-col gap-4 overflow-y-auto pb-2", children: body })] }));
|
|
189
|
+
}
|
|
190
|
+
function NodeFields({ node, flow, definition, registry, disabled, onPatch, onDelete, }) {
|
|
191
|
+
const { messages } = useUI();
|
|
192
|
+
const m = messages.editor;
|
|
193
|
+
const presetOptions = registry.modules.flatMap((mod) => mod.presets.map((p) => ({ value: `${mod.id}/${p.id}`, label: `${mod.label} · ${p.label}` })));
|
|
194
|
+
const presetValue = (ref) => ref ? `${ref.module}/${ref.id}` : NONE;
|
|
195
|
+
const toRef = (v) => {
|
|
196
|
+
if (v === NONE)
|
|
197
|
+
return undefined;
|
|
198
|
+
const [module, id] = v.split("/");
|
|
199
|
+
return { module, id };
|
|
200
|
+
};
|
|
201
|
+
const others = flow.nodes.filter((n) => n.id !== node.id);
|
|
202
|
+
const common = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.label, children: _jsx(Input, { value: node.label, disabled: disabled, onChange: (e) => onPatch({ label: e.target.value }) }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: m.nodeId(node.id) })] }));
|
|
203
|
+
const agent = (target, patch) => (_jsxs(_Fragment, { children: [_jsx(HostPicker, { label: m.model, options: registry.models, value: target.model, onValue: (model) => patch({ model }), disabled: disabled }), _jsx(HostPicker, { label: m.harness, options: registry.harnesses, value: target.harness, onValue: (harness) => patch({ harness }), disabled: disabled }), _jsx(HostPicker, { label: m.effort, options: registry.efforts, value: target.effort, onValue: (effort) => patch({ effort }), disabled: disabled })] }));
|
|
204
|
+
let fields = null;
|
|
205
|
+
switch (node.kind) {
|
|
206
|
+
case "step": {
|
|
207
|
+
const found = findPreset(registry, node.preset);
|
|
208
|
+
const unknown = node.preset && !found;
|
|
209
|
+
fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.preset, children: _jsx(Select, { value: presetValue(node.preset), disabled: disabled, onValueChange: (v) => {
|
|
210
|
+
const preset = toRef(v);
|
|
211
|
+
onPatch(preset
|
|
212
|
+
? { preset, inputs: undefined, outputs: undefined }
|
|
213
|
+
: { preset: undefined, inputs: [], outputs: [] });
|
|
214
|
+
}, options: [
|
|
215
|
+
{ value: NONE, label: m.customStep },
|
|
216
|
+
...presetOptions,
|
|
217
|
+
...(unknown && node.preset
|
|
218
|
+
? [{ value: presetValue(node.preset), label: presetValue(node.preset) }]
|
|
219
|
+
: []),
|
|
220
|
+
] }) }), found ? (_jsx(PortSummary, { node: node, definition: definition, registry: registry })) : (_jsxs(_Fragment, { children: [_jsx(PortsEditor, { legend: m.inputs, ports: node.inputs ?? [], registry: registry, disabled: disabled, onChange: (inputs) => onPatch({ inputs }) }), _jsx(PortsEditor, { legend: m.outputs, ports: node.outputs ?? [], registry: registry, disabled: disabled, onChange: (outputs) => onPatch({ outputs }) })] })), !found || found.preset.agent ? agent(node, (p) => onPatch(p)) : null] }));
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case "subflow":
|
|
224
|
+
fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.subflow, children: _jsx(Select, { value: node.flow || NONE, disabled: disabled, onValueChange: (flow) => onPatch({ flow }), options: [
|
|
225
|
+
...(node.flow ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
|
|
226
|
+
...Object.entries(definition.flows)
|
|
227
|
+
.filter(([name]) => name !== definition.main)
|
|
228
|
+
.map(([name, f]) => ({ value: name, label: f.label ?? name })),
|
|
229
|
+
] }) }), _jsx(Field, { label: m.each, hint: m.eachHint, children: _jsx(Input, { value: node.each ?? "", disabled: disabled, onChange: (e) => onPatch({ each: e.target.value || undefined }) }) })] }));
|
|
230
|
+
break;
|
|
231
|
+
case "gate": {
|
|
232
|
+
const kind = findGateKind(registry, node.gate);
|
|
233
|
+
const fieldsOf = kind?.fields ?? [];
|
|
234
|
+
fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.gateKind, hint: kind?.hint, children: _jsx(Select, { value: kind ? node.gate : NONE, disabled: disabled, onValueChange: (gate) => onPatch({ gate }), options: [
|
|
235
|
+
...(kind ? [] : [{ value: NONE, label: node.gate || m.choose, disabled: true }]),
|
|
236
|
+
...registry.gateKinds.map((g) => ({ value: g.id, label: g.label })),
|
|
237
|
+
] }) }), fieldsOf.includes("approvers") || fieldsOf.includes("timeout") ? (_jsx(GatePolicyEditor, { policy: node.policy ?? {}, registry: registry, approvers: fieldsOf.includes("approvers"), timeout: fieldsOf.includes("timeout"), disabled: disabled, onChange: (policy) => onPatch({ policy }) })) : null, fieldsOf.includes("threshold") ? (_jsx(Field, { label: m.threshold, children: _jsx(PercentInput, { value: node.threshold, disabled: disabled, onValue: (threshold) => onPatch({ threshold }) }) })) : null, fieldsOf.includes("rule") ? (_jsx(Field, { label: m.rule, children: _jsx(Textarea, { value: node.rule ?? "", disabled: disabled, onChange: (e) => onPatch({ rule: e.target.value }) }) })) : null] }));
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "condition": {
|
|
241
|
+
const n = node;
|
|
242
|
+
fields = (_jsx(RowList, { legend: m.cases, rows: n.cases, name: (c) => c.label, addLabel: m.addCase, disabled: disabled, onAdd: () => {
|
|
243
|
+
const id = freshId("case", n.cases.map((c) => c.id));
|
|
244
|
+
const fact = registry.facts[0];
|
|
245
|
+
const op = operatorsFor(registry, fact)[0];
|
|
246
|
+
onPatch({
|
|
247
|
+
cases: [
|
|
248
|
+
...n.cases,
|
|
249
|
+
{
|
|
250
|
+
id,
|
|
251
|
+
label: m.newCase(n.cases.length + 1),
|
|
252
|
+
when: {
|
|
253
|
+
fact: fact?.id ?? "",
|
|
254
|
+
op: op?.id ?? "",
|
|
255
|
+
...(op?.unary ? {} : { value: "" }),
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
}, onRemove: (i) => onPatch({ cases: n.cases.filter((_, j) => j !== i) }), render: (c, i) => (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.caseLabel, children: _jsx(Input, { value: c.label, disabled: disabled, onChange: (e) => onPatch({ cases: replace(n.cases, i, { ...c, label: e.target.value }) }) }) }), _jsx(ConditionEditor, { label: m.when(c.label), condition: c.when, flow: flow, registry: registry, disabled: disabled, onChange: (when) => onPatch({ cases: replace(n.cases, i, { ...c, when }) }) })] })) }));
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case "goto":
|
|
264
|
+
fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.target, children: _jsx(Select, { value: node.target || NONE, disabled: disabled, onValueChange: (target) => onPatch({ target }), options: [
|
|
265
|
+
...(others.some((o) => o.id === node.target)
|
|
266
|
+
? []
|
|
267
|
+
: [
|
|
268
|
+
{
|
|
269
|
+
value: node.target || NONE,
|
|
270
|
+
label: node.target || m.choose,
|
|
271
|
+
disabled: true,
|
|
272
|
+
},
|
|
273
|
+
]),
|
|
274
|
+
...others.map((o) => ({ value: o.id, label: o.label })),
|
|
275
|
+
] }) }), _jsx(Field, { label: m.limit, children: _jsx(NumberInput, { value: node.max, min: 1, step: 1, disabled: disabled, onValue: (max) => onPatch({ max }) }) })] }));
|
|
276
|
+
break;
|
|
277
|
+
case "fanout":
|
|
278
|
+
fields = _jsx("p", { className: "text-sm text-muted-foreground", children: m.kindHint("fanout") });
|
|
279
|
+
break;
|
|
280
|
+
case "join":
|
|
281
|
+
fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.wait, children: _jsx(Select, { value: node.wait, disabled: disabled, onValueChange: (wait) => onPatch({ wait: wait }), options: ["all", "any", "n"].map((w) => ({
|
|
282
|
+
value: w,
|
|
283
|
+
label: m.waitOption(w),
|
|
284
|
+
})) }) }), node.wait === "n" ? (_jsx(Field, { label: m.waitN, children: _jsx(NumberInput, { value: node.n, min: 1, step: 1, disabled: disabled, onValue: (n) => onPatch({ n }) }) })) : null] }));
|
|
285
|
+
break;
|
|
286
|
+
case "variants": {
|
|
287
|
+
const n = node;
|
|
288
|
+
const evaluators = others.filter((o) => o.kind === "evaluator");
|
|
289
|
+
fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.preset, children: _jsx(Select, { value: presetValue(n.preset), disabled: disabled, onValueChange: (v) => onPatch({ preset: toRef(v) }), options: [{ value: NONE, label: m.noPreset }, ...presetOptions] }) }), _jsx(Field, { label: m.winner, children: _jsx(Select, { value: n.select ?? NONE, disabled: disabled, onValueChange: (v) => onPatch({ select: v }), options: [
|
|
290
|
+
...(n.select ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
|
|
291
|
+
...WORKFLOW_SELECTORS.map((s) => ({ value: s, label: m.selector(s) })),
|
|
292
|
+
] }) }), n.select === "evaluator" ? (_jsx(Field, { label: m.evaluator, children: _jsx(Select, { value: n.evaluator ?? NONE, disabled: disabled, onValueChange: (evaluator) => onPatch({ evaluator }), options: [
|
|
293
|
+
...(n.evaluator ? [] : [{ value: NONE, label: m.choose, disabled: true }]),
|
|
294
|
+
...evaluators.map((o) => ({ value: o.id, label: o.label })),
|
|
295
|
+
] }) })) : null, _jsx(RowList, { legend: m.variants, rows: n.variants, name: (v) => v.label, addLabel: m.addVariant, disabled: disabled, onAdd: () => {
|
|
296
|
+
const id = freshId("variant", n.variants.map((v) => v.id));
|
|
297
|
+
onPatch({
|
|
298
|
+
variants: [...n.variants, { id, label: m.newVariant(n.variants.length + 1) }],
|
|
299
|
+
});
|
|
300
|
+
}, onRemove: (i) => onPatch({ variants: n.variants.filter((_, j) => j !== i) }), render: (v, i) => (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.variantLabel, children: _jsx(Input, { value: v.label, disabled: disabled, onChange: (e) => onPatch({ variants: replace(n.variants, i, { ...v, label: e.target.value }) }) }) }), agent(v, (p) => onPatch({ variants: replace(n.variants, i, { ...v, ...p }) }))] })) })] }));
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case "evaluator":
|
|
304
|
+
fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.question, children: _jsx(Textarea, { value: node.question ?? "", disabled: disabled, onChange: (e) => onPatch({ question: e.target.value || undefined }) }) }), _jsx(Field, { label: m.levels, hint: m.levelsHint, children: _jsx(Textarea, { value: node.levels.join("\n"), disabled: disabled, onChange: (e) => onPatch({
|
|
305
|
+
levels: e.target.value
|
|
306
|
+
.split("\n")
|
|
307
|
+
.map((l) => l.trim())
|
|
308
|
+
.filter(Boolean),
|
|
309
|
+
}) }) }), _jsx(Field, { label: m.routes, orientation: "horizontal", children: _jsx(Switch, { checked: node.routes, disabled: disabled, onCheckedChange: (routes) => onPatch({ routes }) }) }), _jsx(Field, { label: m.threshold, children: _jsx(PercentInput, { value: node.threshold, disabled: disabled, onValue: (threshold) => onPatch({ threshold }) }) }), _jsx(Field, { label: m.minConfidence, hint: m.minConfidenceHint, children: _jsx(PercentInput, { value: node.minConfidence, disabled: disabled, onValue: (minConfidence) => onPatch({ minConfidence }) }) })] }));
|
|
310
|
+
break;
|
|
311
|
+
case "classifier": {
|
|
312
|
+
const n = node;
|
|
313
|
+
fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.question, children: _jsx(Textarea, { value: n.question ?? "", disabled: disabled, onChange: (e) => onPatch({ question: e.target.value || undefined }) }) }), _jsx(RowList, { legend: m.options, rows: n.options, name: (o) => o.label, addLabel: m.addOption, disabled: disabled, onAdd: () => {
|
|
314
|
+
const id = freshId("label", n.options.map((o) => o.id));
|
|
315
|
+
onPatch({
|
|
316
|
+
options: [...n.options, { id, label: m.newOption(n.options.length + 1) }],
|
|
317
|
+
});
|
|
318
|
+
}, onRemove: (i) => onPatch({ options: n.options.filter((_, j) => j !== i) }), render: (o, i) => (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.optionLabel, children: _jsx(Input, { value: o.label, disabled: disabled, onChange: (e) => onPatch({ options: replace(n.options, i, { ...o, label: e.target.value }) }) }) }), _jsx(Field, { label: m.optionDescription, children: _jsx(Input, { value: o.description ?? "", disabled: disabled, onChange: (e) => onPatch({
|
|
319
|
+
options: replace(n.options, i, {
|
|
320
|
+
...o,
|
|
321
|
+
description: e.target.value || undefined,
|
|
322
|
+
}),
|
|
323
|
+
}) }) })] })) }), _jsx(Field, { label: m.otherExit, orientation: "horizontal", children: _jsx(Switch, { checked: n.other, disabled: disabled, onCheckedChange: (other) => onPatch({ other }) }) }), _jsx(Field, { label: m.minConfidence, hint: m.minConfidenceHint, children: _jsx(PercentInput, { value: n.minConfidence, disabled: disabled, onValue: (minConfidence) => onPatch({ minConfidence }) }) })] }));
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case "check":
|
|
327
|
+
fields = (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.question, children: _jsx(Textarea, { value: node.question, disabled: disabled, onChange: (e) => onPatch({ question: e.target.value }) }) }), _jsx(Field, { label: m.yesAt, children: _jsx(PercentInput, { value: node.threshold, disabled: disabled, onValue: (threshold) => onPatch({ threshold: threshold ?? 0.5 }) }) })] }));
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [common, fields, _jsx(Field, { label: m.note, children: _jsx(Textarea, { value: node.note ?? "", disabled: disabled, onChange: (e) => onPatch({ note: e.target.value || undefined }) }) }), disabled ? null : (_jsxs(Button, { type: "button", variant: "destructive", size: "sm", className: "self-start", onClick: onDelete, children: [_jsx(Trash2Icon, { "aria-hidden": true }), m.deleteNode(node.label)] }))] }));
|
|
331
|
+
}
|
|
332
|
+
/** A preset's contract, read from the registry. */
|
|
333
|
+
function PortSummary({ node, definition, registry, }) {
|
|
334
|
+
const { messages } = useUI();
|
|
335
|
+
const m = messages.editor;
|
|
336
|
+
const list = (title, ports) => (_jsxs("div", { children: [_jsx("h3", { className: "text-xs font-medium text-muted-foreground", children: title }), ports.length ? (_jsx("ul", { className: "text-sm", children: ports.map((p) => (_jsxs("li", { children: [m.port(p.label, p.type), p.optional ? _jsxs("span", { className: "text-muted-foreground", children: [", ", m.optional] }) : null] }, p.id))) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.run.none }))] }));
|
|
337
|
+
return (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [list(m.inputs, inputsOf(node, registry, definition)), list(m.outputs, outputsOf(node, registry, definition))] }));
|
|
338
|
+
}
|
|
339
|
+
function EdgeFields({ edge, flow, definition, registry, disabled, onPatch, onDelete, }) {
|
|
340
|
+
const { messages } = useUI();
|
|
341
|
+
const m = messages.editor;
|
|
342
|
+
const from = flow.nodes.find((n) => n.id === edge.from);
|
|
343
|
+
const to = flow.nodes.find((n) => n.id === edge.to);
|
|
344
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [edge.kind === "next" && from ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm", children: m.exitIs(m.exit(exitLabel(from, portOf(edge)))) }), _jsx(Field, { label: m.edgeLabel, children: _jsx(Input, { value: edge.label ?? "", disabled: disabled, onChange: (e) => onPatch({ label: e.target.value || undefined }) }) })] })) : null, edge.kind === "data" && from && to ? (_jsx("p", { className: "text-sm", children: m.dataFlow(outputsOf(from, registry, definition).find((p) => p.id === edge.port)?.label ??
|
|
345
|
+
edge.port ??
|
|
346
|
+
"", inputsOf(to, registry, definition).find((p) => p.id === edge.input)?.label ??
|
|
347
|
+
edge.input ??
|
|
348
|
+
"") })) : null, edge.kind === "loop" ? (_jsxs(_Fragment, { children: [_jsx(Field, { label: m.limit, children: _jsx(NumberInput, { value: edge.max, min: 1, step: 1, disabled: disabled, onValue: (max) => onPatch({ max }) }) }), _jsx(ConditionEditor, { label: m.until, condition: edge.until ?? { fact: "", op: "" }, flow: flow, registry: registry, disabled: disabled, onChange: (until) => onPatch({ until }) })] })) : null, disabled ? null : (_jsxs(Button, { type: "button", variant: "destructive", size: "sm", className: "self-start", onClick: onDelete, children: [_jsx(Trash2Icon, { "aria-hidden": true }), m.deleteEdge] }))] }));
|
|
349
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { WorkflowDefinition } from "../definition.js";
|
|
3
|
+
export interface WorkflowJsonViewProps {
|
|
4
|
+
definition: WorkflowDefinition;
|
|
5
|
+
/** A valid edit, read through the format. Omitted: read-only. */
|
|
6
|
+
onChange?: (definition: WorkflowDefinition) => void;
|
|
7
|
+
readOnly?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The definition as JSON, kept in step with the canvas and the inspector:
|
|
12
|
+
* an edit elsewhere rewrites the text, and text that reads as a definition
|
|
13
|
+
* goes back as an edit. Text that does not read stays as typed, with the
|
|
14
|
+
* reason, until it does; nothing outside the format is ever kept.
|
|
15
|
+
*/
|
|
16
|
+
export declare function WorkflowJsonView({ definition, onChange, readOnly, className, }: WorkflowJsonViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../lib/cn.js";
|
|
5
|
+
import { Field } from "../../primitives/Field.js";
|
|
6
|
+
import { Textarea } from "../../primitives/Textarea.js";
|
|
7
|
+
import { useUI } from "../../provider/UIProvider.js";
|
|
8
|
+
import { readWorkflowDefinition } from "../portable.js";
|
|
9
|
+
const show = (d) => JSON.stringify(d, null, 2);
|
|
10
|
+
/**
|
|
11
|
+
* The definition as JSON, kept in step with the canvas and the inspector:
|
|
12
|
+
* an edit elsewhere rewrites the text, and text that reads as a definition
|
|
13
|
+
* goes back as an edit. Text that does not read stays as typed, with the
|
|
14
|
+
* reason, until it does; nothing outside the format is ever kept.
|
|
15
|
+
*/
|
|
16
|
+
export function WorkflowJsonView({ definition, onChange, readOnly = false, className, }) {
|
|
17
|
+
const { messages } = useUI();
|
|
18
|
+
const m = messages.editor;
|
|
19
|
+
const editable = !readOnly && !!onChange;
|
|
20
|
+
// The text, and the definition it was last in step with.
|
|
21
|
+
const [state, setState] = React.useState(() => ({ text: show(definition), from: definition }));
|
|
22
|
+
const [error, setError] = React.useState(null);
|
|
23
|
+
if (state.from !== definition) {
|
|
24
|
+
setState({ text: show(definition), from: definition });
|
|
25
|
+
setError(null);
|
|
26
|
+
}
|
|
27
|
+
return (_jsx("div", { "data-slot": "workflow-json", className: cn("flex min-h-0 flex-col gap-2", className), children: _jsx(Field, { label: m.json, hint: editable ? m.jsonHint : undefined, error: error ? m.readError(error) : undefined, children: _jsx(Textarea, { value: state.text, readOnly: !editable, spellCheck: false, rows: 18, className: "min-h-64 font-mono text-xs", onChange: (e) => {
|
|
28
|
+
const text = e.target.value;
|
|
29
|
+
const read = readWorkflowDefinition(text);
|
|
30
|
+
if (!read.ok) {
|
|
31
|
+
setState((s) => ({ ...s, text }));
|
|
32
|
+
setError(read.error);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setError(null);
|
|
36
|
+
// In step with what goes out, so the host handing it back keeps the person's text.
|
|
37
|
+
setState({ text, from: read.value });
|
|
38
|
+
onChange?.(read.value);
|
|
39
|
+
} }) }) }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { WorkflowRegistry } from "../definition.js";
|
|
3
|
+
import { type WorkflowPaletteItem } from "../palette.js";
|
|
4
|
+
export interface WorkflowPaletteProps {
|
|
5
|
+
registry: WorkflowRegistry;
|
|
6
|
+
/** Adds the item; the editor puts it after the selection. Omitted: read-only, nothing to add. */
|
|
7
|
+
onAdd?: (item: WorkflowPaletteItem) => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* What a flow is built from, grouped by module: each installed module's step
|
|
12
|
+
* presets, then gates by kind, logic, judges, a custom step and a sub-flow.
|
|
13
|
+
* A click adds after the selection. An item this destination does not run is
|
|
14
|
+
* marked, never hidden: a definition can be written for another factory.
|
|
15
|
+
*/
|
|
16
|
+
export declare function WorkflowPalette({ registry, onAdd, className }: WorkflowPaletteProps): React.JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { PlusIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../../lib/cn.js";
|
|
6
|
+
import { Input } from "../../primitives/Input.js";
|
|
7
|
+
import { useUI } from "../../provider/UIProvider.js";
|
|
8
|
+
import { paletteOf } from "../palette.js";
|
|
9
|
+
/**
|
|
10
|
+
* What a flow is built from, grouped by module: each installed module's step
|
|
11
|
+
* presets, then gates by kind, logic, judges, a custom step and a sub-flow.
|
|
12
|
+
* A click adds after the selection. An item this destination does not run is
|
|
13
|
+
* marked, never hidden: a definition can be written for another factory.
|
|
14
|
+
*/
|
|
15
|
+
export function WorkflowPalette({ registry, onAdd, className }) {
|
|
16
|
+
const { messages } = useUI();
|
|
17
|
+
const m = messages.editor;
|
|
18
|
+
const [query, setQuery] = React.useState("");
|
|
19
|
+
const terms = query.toLocaleLowerCase().split(/\s+/).filter(Boolean);
|
|
20
|
+
const name = (item) => item.label || (item.template.kind === "step" ? m.customStep : m.kind(item.template.kind));
|
|
21
|
+
const hint = (item) => item.hint ?? (item.label ? undefined : m.kindHint(item.template.kind));
|
|
22
|
+
const groups = paletteOf(registry)
|
|
23
|
+
.map((g) => ({
|
|
24
|
+
...g,
|
|
25
|
+
title: g.module ? g.label : m.paletteGroup(g.key),
|
|
26
|
+
}))
|
|
27
|
+
.map((g) => ({
|
|
28
|
+
...g,
|
|
29
|
+
items: g.items.filter((item) => {
|
|
30
|
+
const text = `${g.title} ${name(item)} ${hint(item) ?? ""}`.toLocaleLowerCase();
|
|
31
|
+
return terms.every((t) => text.includes(t));
|
|
32
|
+
}),
|
|
33
|
+
}))
|
|
34
|
+
.filter((g) => g.items.length);
|
|
35
|
+
return (_jsxs("nav", { "aria-label": m.palette, "data-slot": "workflow-palette", className: cn("flex min-h-0 flex-col gap-3", className), children: [_jsx(Input, { type: "search", "aria-label": m.paletteSearch, placeholder: m.paletteSearch, value: query, onChange: (e) => setQuery(e.target.value) }), _jsx("div", { className: "flex min-h-0 flex-col gap-4 overflow-y-auto", children: groups.length ? (groups.map((g) => (_jsxs("section", { "aria-label": g.title, className: "flex flex-col gap-1", children: [_jsx("h3", { className: "text-2xs font-medium tracking-wide text-muted-foreground uppercase", children: g.title }), _jsx("ul", { className: "flex flex-col gap-1", children: g.items.map((item) => (_jsx("li", { children: _jsxs("button", { type: "button", disabled: !onAdd, onClick: () => onAdd?.(item), className: "group flex w-full items-start gap-2 rounded-lg border border-border bg-card px-2.5 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-default disabled:hover:bg-card", children: [_jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate", children: name(item) }), hint(item) ? (_jsx("span", { className: "line-clamp-2 text-xs text-muted-foreground", children: hint(item) })) : null, item.available ? null : (_jsx("span", { className: "text-xs text-warning-fg", children: m.notRun }))] }), onAdd ? (_jsx(PlusIcon, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0 text-muted-foreground group-hover:text-foreground" })) : null] }) }, item.key))) })] }, g.key)))) : (_jsx("p", { className: "text-sm text-muted-foreground", children: m.paletteEmpty })) })] }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { WorkflowDefinition, WorkflowSelection } from "../definition.js";
|
|
3
|
+
import type { WorkflowProblem } from "../problems.js";
|
|
4
|
+
export interface WorkflowProblemsProps {
|
|
5
|
+
problems: readonly WorkflowProblem[];
|
|
6
|
+
/** Names each problem's flow when the definition has more than one. */
|
|
7
|
+
definition?: WorkflowDefinition;
|
|
8
|
+
/** Selects what a problem points at. Omitted: the rows do not select. */
|
|
9
|
+
onSelect?: (selection: WorkflowSelection) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Where a problem points, as a selection; null for one about a whole flow or the definition. */
|
|
13
|
+
export declare function problemTarget(problem: WorkflowProblem, main: string): WorkflowSelection | null;
|
|
14
|
+
/**
|
|
15
|
+
* Every problem in the definition at its three levels: blocking (stops a
|
|
16
|
+
* publish), warning, and not run here. Each row names its problem and selects
|
|
17
|
+
* what it points at. The not-run notes fold away, since a definition written
|
|
18
|
+
* for another factory has many.
|
|
19
|
+
*/
|
|
20
|
+
export declare function WorkflowProblems({ problems, definition, onSelect, className, }: WorkflowProblemsProps): React.JSX.Element;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../lib/cn.js";
|
|
5
|
+
import { useUI } from "../../provider/UIProvider.js";
|
|
6
|
+
import { StatusBadge } from "../../status/StatusBadge.js";
|
|
7
|
+
const LEVELS = ["blocking", "warning", "not-run"];
|
|
8
|
+
const TONE = {
|
|
9
|
+
blocking: "error",
|
|
10
|
+
warning: "warn",
|
|
11
|
+
"not-run": "muted",
|
|
12
|
+
};
|
|
13
|
+
/** Where a problem points, as a selection; null for one about a whole flow or the definition. */
|
|
14
|
+
export function problemTarget(problem, main) {
|
|
15
|
+
const flow = problem.flow ?? main;
|
|
16
|
+
if (problem.node)
|
|
17
|
+
return { type: "node", flow, id: problem.node };
|
|
18
|
+
if (problem.edge)
|
|
19
|
+
return { type: "edge", flow, id: problem.edge };
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Every problem in the definition at its three levels: blocking (stops a
|
|
24
|
+
* publish), warning, and not run here. Each row names its problem and selects
|
|
25
|
+
* what it points at. The not-run notes fold away, since a definition written
|
|
26
|
+
* for another factory has many.
|
|
27
|
+
*/
|
|
28
|
+
export function WorkflowProblems({ problems, definition, onSelect, className, }) {
|
|
29
|
+
const { messages } = useUI();
|
|
30
|
+
const m = messages.editor;
|
|
31
|
+
const [showNotRun, setShowNotRun] = React.useState(false);
|
|
32
|
+
const multiFlow = definition ? Object.keys(definition.flows).length > 1 : false;
|
|
33
|
+
const flowName = (name) => (name && definition?.flows[name]?.label) || name;
|
|
34
|
+
const counts = Object.fromEntries(LEVELS.map((l) => [l, problems.filter((p) => p.level === l).length]));
|
|
35
|
+
const listId = React.useId();
|
|
36
|
+
return (_jsxs("section", { "aria-label": m.problems, "data-slot": "workflow-problems", className: cn("flex min-h-0 flex-col gap-2", className), children: [_jsx("p", { role: "status", className: "flex flex-wrap items-center gap-2 text-sm", children: counts.blocking || counts.warning ? (_jsxs(_Fragment, { children: [_jsx(StatusBadge, { tone: counts.blocking ? "error" : "ok", children: m.problemCount("blocking", counts.blocking) }), " ", _jsx(StatusBadge, { tone: counts.warning ? "warn" : "muted", children: m.problemCount("warning", counts.warning) })] })) : (_jsx(StatusBadge, { tone: "ok", children: m.noProblems })) }), counts["not-run"] ? (_jsx("button", { type: "button", className: "self-start text-sm text-muted-foreground underline-offset-2 hover:underline", "aria-expanded": showNotRun, "aria-controls": listId, onClick: () => setShowNotRun((v) => !v), children: m.problemCount("not-run", counts["not-run"]) })) : null, _jsx("ul", { id: listId, className: "flex min-h-0 flex-col gap-1 overflow-y-auto", children: LEVELS.flatMap((level) => level === "not-run" && !showNotRun
|
|
37
|
+
? []
|
|
38
|
+
: problems
|
|
39
|
+
.filter((p) => p.level === level)
|
|
40
|
+
.map((p, i) => {
|
|
41
|
+
const target = definition ? problemTarget(p, definition.main) : null;
|
|
42
|
+
const text = m.problem(p);
|
|
43
|
+
const where = multiFlow && p.flow ? flowName(p.flow) : undefined;
|
|
44
|
+
const reason = p.availability?.status === "missing-capabilities"
|
|
45
|
+
? messages.workflow.missingCapabilities(p.availability.missing ?? [])
|
|
46
|
+
: (p.availability?.reason ??
|
|
47
|
+
(p.availability ? messages.workflow.unsupported : undefined));
|
|
48
|
+
const body = (_jsxs(_Fragment, { children: [_jsx(StatusBadge, { tone: TONE[level], className: "shrink-0", children: m.problemLevel(level) }), " ", _jsxs("span", { className: "min-w-0 flex-1", children: [where ? _jsxs("span", { className: "text-muted-foreground", children: [where, ": "] }) : null, text, reason ? (_jsx("span", { className: "block text-xs text-muted-foreground", children: reason })) : null] })] }));
|
|
49
|
+
return (_jsx("li", { children: target && onSelect ? (_jsx("button", { type: "button", onClick: () => onSelect(target), className: "flex w-full items-start gap-2 rounded-lg border border-border px-2.5 py-1.5 text-left text-sm hover:bg-muted", children: body })) : (_jsx("div", { className: "flex items-start gap-2 rounded-lg border border-border px-2.5 py-1.5 text-sm", children: body })) }, `${level}-${i}`));
|
|
50
|
+
})) })] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { WorkflowDefinition, WorkflowRegistry } from "../definition.js";
|
|
3
|
+
import { type WorkflowImportSummary as ImportPreview } from "../portable.js";
|
|
4
|
+
import { type WorkflowRevision } from "../workspace.js";
|
|
5
|
+
/** A host action: may return a promise; a rejection's message is shown. */
|
|
6
|
+
type HostAction<A extends unknown[]> = (...args: A) => Promise<unknown> | unknown;
|
|
7
|
+
export interface WorkflowWorkspaceProps {
|
|
8
|
+
/** The working copy, held by the host. Null while it loads. */
|
|
9
|
+
definition: WorkflowDefinition | null;
|
|
10
|
+
/** Every edit. Omitted, or `readOnly`: a viewer. */
|
|
11
|
+
onChange?: (definition: WorkflowDefinition) => void;
|
|
12
|
+
registry: WorkflowRegistry;
|
|
13
|
+
/** The latest saved draft revision on the host. */
|
|
14
|
+
saved?: WorkflowRevision | null;
|
|
15
|
+
/** The published revision: what runs. */
|
|
16
|
+
published?: WorkflowRevision | null;
|
|
17
|
+
/** Saves the working copy as a draft over `base`, the revision it started from. */
|
|
18
|
+
onSave?: HostAction<[definition: WorkflowDefinition, base: WorkflowRevision | null]>;
|
|
19
|
+
/** Publishes the working copy: it becomes what runs. */
|
|
20
|
+
onPublish?: HostAction<[definition: WorkflowDefinition]>;
|
|
21
|
+
/** Saves the working copy as a new, named reusable definition. */
|
|
22
|
+
onSaveAs?: HostAction<[definition: WorkflowDefinition, name: string]>;
|
|
23
|
+
onExport?: (definition: WorkflowDefinition) => void;
|
|
24
|
+
/** Starts an import: the host picks a file and hands back `importPreview`. */
|
|
25
|
+
onImport?: () => void;
|
|
26
|
+
importPreview?: ImportPreview | null;
|
|
27
|
+
onImportConfirm?: HostAction<[]>;
|
|
28
|
+
onImportCancel?: () => void;
|
|
29
|
+
readOnly?: boolean;
|
|
30
|
+
loading?: boolean;
|
|
31
|
+
error?: string | {
|
|
32
|
+
message: string;
|
|
33
|
+
} | null;
|
|
34
|
+
onRetry?: () => void;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The whole editor on one page: the palette, the canvas and a side panel
|
|
39
|
+
* (properties, problems, JSON), around a controlled working copy. It knows
|
|
40
|
+
* the saved and published revisions the host holds: it marks unsaved edits
|
|
41
|
+
* and a draft that differs from what runs, takes a newer saved revision when
|
|
42
|
+
* there are no edits, and when the saved revision moves under unsaved edits
|
|
43
|
+
* it stops and asks (use theirs, keep mine, compare). Publishing shows the
|
|
44
|
+
* draft against live first and is held while anything blocks. Save, publish,
|
|
45
|
+
* save as, import and export are the host's; it never persists anything. On
|
|
46
|
+
* narrow containers the palette and the side panel open as drawers.
|
|
47
|
+
*/
|
|
48
|
+
export declare function WorkflowWorkspace(props: WorkflowWorkspaceProps): React.JSX.Element;
|
|
49
|
+
export {};
|