@particle-academy/fancy-flow 0.45.0 → 0.47.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 +29 -0
- package/dist/{ConfigFieldRenderer-Dzy4f6rG.d.cts → ConfigFieldRenderer-Cyir8-B8.d.cts} +2 -2
- package/dist/{ConfigFieldRenderer-WqJr-td_.d.ts → ConfigFieldRenderer-iMkZj0VK.d.ts} +2 -2
- package/dist/{FlowViewer-BbbUJe2b.d.cts → FlowViewer-CY3_vHSg.d.cts} +1 -1
- package/dist/{FlowViewer-csuaTL5t.d.ts → FlowViewer-DGW6t-80.d.ts} +1 -1
- package/dist/{capabilities-Brn74URS.d.ts → capabilities-DMUhnyZD.d.ts} +1 -1
- package/dist/{capabilities-CYCeIx6I.d.cts → capabilities-Dmd26aAs.d.cts} +1 -1
- package/dist/chunk-BHM5HGXH.js +79 -0
- package/dist/chunk-BHM5HGXH.js.map +1 -0
- package/dist/chunk-DR3GWVDG.js +196 -0
- package/dist/chunk-DR3GWVDG.js.map +1 -0
- package/dist/{chunk-3BKU4SQZ.js → chunk-ISXRQELT.js} +3 -4
- package/dist/chunk-ISXRQELT.js.map +1 -0
- package/dist/{chunk-OWENS2H5.js → chunk-NDEX655E.js} +1603 -36
- package/dist/chunk-NDEX655E.js.map +1 -0
- package/dist/{chunk-QZTGV3ZL.js → chunk-QVFVL5LQ.js} +3 -3
- package/dist/{chunk-QZTGV3ZL.js.map → chunk-QVFVL5LQ.js.map} +1 -1
- package/dist/{chunk-DHUFUDEX.js → chunk-Z2GRNYR2.js} +7 -6
- package/dist/chunk-Z2GRNYR2.js.map +1 -0
- package/dist/{chunk-A6RFLGWV.js → chunk-ZKJJWBDJ.js} +3 -5
- package/dist/chunk-ZKJJWBDJ.js.map +1 -0
- package/dist/durable/index.d.cts +490 -0
- package/dist/durable/index.d.ts +490 -0
- package/dist/durable.cjs +10991 -0
- package/dist/durable.cjs.map +1 -0
- package/dist/durable.js +552 -0
- package/dist/durable.js.map +1 -0
- package/dist/engine.cjs +10223 -47
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +9 -7
- package/dist/engine.d.ts +9 -7
- package/dist/engine.js +4 -6
- package/dist/engine.js.map +1 -1
- package/dist/fields/react-fancy.d.cts +4 -3
- package/dist/fields/react-fancy.d.ts +4 -3
- package/dist/index.cjs +336 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -10
- package/dist/index.d.ts +12 -10
- package/dist/index.js +15 -93
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.cts +1 -1
- package/dist/layout/index.d.ts +1 -1
- package/dist/llm/prism.d.cts +2 -2
- package/dist/llm/prism.d.ts +2 -2
- package/dist/llm/vercel-ai.d.cts +2 -2
- package/dist/llm/vercel-ai.d.ts +2 -2
- package/dist/pause-9iT4tCEV.d.cts +96 -0
- package/dist/pause-9iT4tCEV.d.ts +96 -0
- package/dist/registry/index.d.cts +7 -6
- package/dist/registry/index.d.ts +7 -6
- package/dist/{registry-Budds73w.d.cts → registry-Bt55f63_.d.ts} +21 -2
- package/dist/{registry-D0I20HJ3.d.ts → registry-CkcSwmfR.d.cts} +21 -2
- package/dist/registry.cjs +3961 -3821
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +3 -6
- package/dist/{run-cohort-CV7EcwGG.d.ts → run-cohort-Cyeny2Uu.d.ts} +3 -34
- package/dist/{run-cohort-DitsaYgS.d.cts → run-cohort-DdPPowJ3.d.cts} +3 -34
- package/dist/run-flow-1KiXhy59.d.cts +55 -0
- package/dist/run-flow-CQvTDBYk.d.ts +55 -0
- package/dist/runtime/index.d.cts +5 -3
- package/dist/runtime/index.d.ts +5 -3
- package/dist/runtime.cjs +1712 -430
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +5 -6
- package/dist/schema/index.d.cts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema.cjs +10369 -24
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +4 -3
- package/dist/screens.cjs +1366 -38
- package/dist/screens.cjs.map +1 -1
- package/dist/screens.d.cts +2 -2
- package/dist/screens.d.ts +2 -2
- package/dist/screens.js +5 -8
- package/dist/screens.js.map +1 -1
- package/dist/{types-D6ZSKBly.d.ts → types--S2Kzx7w.d.cts} +3 -97
- package/dist/{types-nQOA8jpk.d.cts → types-ChkCRonF.d.ts} +3 -97
- package/dist/types-Jx1TwehV.d.cts +247 -0
- package/dist/types-Jx1TwehV.d.ts +247 -0
- package/dist/ux.cjs +10363 -2
- package/dist/ux.cjs.map +1 -1
- package/dist/ux.d.cts +3 -2
- package/dist/ux.d.ts +3 -2
- package/dist/ux.js +3 -1
- package/dist/ux.js.map +1 -1
- package/package.json +17 -3
- package/dist/chunk-3BKU4SQZ.js.map +0 -1
- package/dist/chunk-A6RFLGWV.js.map +0 -1
- package/dist/chunk-DHUFUDEX.js.map +0 -1
- package/dist/chunk-OWENS2H5.js.map +0 -1
- package/dist/chunk-PVROYW7C.js +0 -187
- package/dist/chunk-PVROYW7C.js.map +0 -1
- package/dist/chunk-TITD5W4Y.js +0 -26
- package/dist/chunk-TITD5W4Y.js.map +0 -1
- package/dist/chunk-VLATLVGH.js +0 -158
- package/dist/chunk-VLATLVGH.js.map +0 -1
- package/dist/chunk-WBXGZVLB.js +0 -1268
- package/dist/chunk-WBXGZVLB.js.map +0 -1
- package/dist/types-CMSrWVYM.d.cts +0 -109
- package/dist/types-CMSrWVYM.d.ts +0 -109
package/dist/chunk-WBXGZVLB.js
DELETED
|
@@ -1,1268 +0,0 @@
|
|
|
1
|
-
import { NodeResizer, NodeToolbar, Handle, Position } from './chunk-OWENS2H5.js';
|
|
2
|
-
import { getWorkflowResolver, isResolutionFailure, getLlmClient } from './chunk-USL4FMFU.js';
|
|
3
|
-
import { runFlow } from './chunk-VLATLVGH.js';
|
|
4
|
-
import { resolveNodePorts, nodeConfig } from './chunk-TITD5W4Y.js';
|
|
5
|
-
import { getNodeKind, categoryAccent, registerNodeKind, listNodeKinds, kindIds } from './chunk-PVROYW7C.js';
|
|
6
|
-
import { RichInputPreview } from './chunk-F5RPRB7A.js';
|
|
7
|
-
import { memo, createContext, useMemo, useState, useRef, useEffect, useContext, createElement } from 'react';
|
|
8
|
-
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
9
|
-
|
|
10
|
-
// src/registry/connection.ts
|
|
11
|
-
var ANY_PORT_TYPE = "any";
|
|
12
|
-
var defaultPortCompatibility = (source, target) => {
|
|
13
|
-
const s = source?.type;
|
|
14
|
-
const t = target?.type;
|
|
15
|
-
if (!s || !t) return true;
|
|
16
|
-
if (s === ANY_PORT_TYPE || t === ANY_PORT_TYPE) return true;
|
|
17
|
-
return s === t;
|
|
18
|
-
};
|
|
19
|
-
function portsFor(node, side) {
|
|
20
|
-
const kind = getNodeKind(node.data?.kind ?? node.type);
|
|
21
|
-
const resolved = resolveNodePorts(node, kind ?? void 0);
|
|
22
|
-
return resolved[side] ?? [];
|
|
23
|
-
}
|
|
24
|
-
function findPort(ports, handleId) {
|
|
25
|
-
if (handleId == null) return ports.length === 1 ? ports[0] : void 0;
|
|
26
|
-
return ports.find((p) => p.id === handleId);
|
|
27
|
-
}
|
|
28
|
-
function createConnectionValidator(getNodes, options = {}) {
|
|
29
|
-
const compatible = options.compatible ?? defaultPortCompatibility;
|
|
30
|
-
return (connection) => {
|
|
31
|
-
const { source, target, sourceHandle, targetHandle } = connection;
|
|
32
|
-
if (!source || !target) return false;
|
|
33
|
-
if (!options.allowSelfConnection && source === target) return false;
|
|
34
|
-
const nodes = getNodes();
|
|
35
|
-
const src = nodes.find((n) => n.id === source);
|
|
36
|
-
const tgt = nodes.find((n) => n.id === target);
|
|
37
|
-
if (!src || !tgt) return false;
|
|
38
|
-
const outPort = findPort(portsFor(src, "outputs"), sourceHandle);
|
|
39
|
-
const inPort = findPort(portsFor(tgt, "inputs"), targetHandle);
|
|
40
|
-
return compatible(outPort, inPort);
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// src/registry/subflow.ts
|
|
45
|
-
var DEFAULT_MAX_DEPTH = 8;
|
|
46
|
-
function subflowMode(config) {
|
|
47
|
-
const mode = config.mode;
|
|
48
|
-
return mode === "stream" || mode === "both" ? mode : "output";
|
|
49
|
-
}
|
|
50
|
-
function subflowPorts(config) {
|
|
51
|
-
const mode = subflowMode(config);
|
|
52
|
-
const ports = [{ id: "out", label: "result" }];
|
|
53
|
-
if (mode === "stream" || mode === "both") ports.unshift({ id: "stream", label: "stream" });
|
|
54
|
-
return ports;
|
|
55
|
-
}
|
|
56
|
-
var subflowExecutor = async (ctx) => {
|
|
57
|
-
const config = ctx.node.data?.config ?? {};
|
|
58
|
-
const ref = String(config.workflow ?? "").trim();
|
|
59
|
-
if (!ref) ctx.abort("subflow has no workflow reference configured");
|
|
60
|
-
const resolver = getWorkflowResolver();
|
|
61
|
-
if (!resolver) {
|
|
62
|
-
ctx.abort(
|
|
63
|
-
"No workflow resolver registered. Call registerWorkflowResolver() so subflow can find the workflow it references."
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
const maxDepth = Number.isFinite(config.maxDepth) ? Number(config.maxDepth) : DEFAULT_MAX_DEPTH;
|
|
67
|
-
const depth = ctx.depth ?? 0;
|
|
68
|
-
if (depth + 1 > maxDepth) {
|
|
69
|
-
ctx.abort(
|
|
70
|
-
`subflow depth limit reached (${maxDepth}) at "${ref}" \u2014 a workflow is referencing itself, directly or through a chain.`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
const pinned = config.version === void 0 || config.version === "" ? void 0 : Number(config.version);
|
|
74
|
-
if (pinned !== void 0 && !Number.isInteger(pinned)) {
|
|
75
|
-
ctx.abort(`subflow "${ref}" has a non-integer version pin (${String(config.version)}).`);
|
|
76
|
-
}
|
|
77
|
-
const resolved = await resolver(ref, pinned);
|
|
78
|
-
const child = isResolutionFailure(resolved) ? ctx.abort(
|
|
79
|
-
resolved.reason === "version-mismatch" ? resolved.message ?? `subflow "${ref}" is pinned to version ${pinned}, but the host has ${resolved.available ?? "a different version"}.` : resolved.message ?? `subflow could not resolve workflow "${ref}"`
|
|
80
|
-
) : resolved;
|
|
81
|
-
if (!child) ctx.abort(`subflow could not resolve workflow "${ref}"`);
|
|
82
|
-
const mode = subflowMode(config);
|
|
83
|
-
const streaming = mode === "stream" || mode === "both";
|
|
84
|
-
const forward = (event) => {
|
|
85
|
-
if (!streaming) return;
|
|
86
|
-
const detail = event.type === "node-status" ? `${event.nodeId} ${event.status}` : event.type === "run-end" ? `finished (${event.ok ? "ok" : "failed"})` : event.type;
|
|
87
|
-
ctx.emit({
|
|
88
|
-
type: "log",
|
|
89
|
-
nodeId: ctx.node.id,
|
|
90
|
-
level: "info",
|
|
91
|
-
message: `[${ref}] ${detail}`
|
|
92
|
-
});
|
|
93
|
-
};
|
|
94
|
-
const result = await runFlow(
|
|
95
|
-
child,
|
|
96
|
-
config.executors ?? {},
|
|
97
|
-
forward,
|
|
98
|
-
{
|
|
99
|
-
initialInputs: config.inputs ?? {
|
|
100
|
-
// With no explicit mapping, hand the parent's inputs to the child's
|
|
101
|
-
// entry points — the obvious default, and it makes the simple case
|
|
102
|
-
// require no configuration at all.
|
|
103
|
-
__parent: ctx.inputs
|
|
104
|
-
},
|
|
105
|
-
depth: depth + 1
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
if (!result.ok) {
|
|
109
|
-
ctx.abort(`subflow "${ref}" failed: ${result.error ?? "unknown error"}`);
|
|
110
|
-
}
|
|
111
|
-
if (mode === "stream") {
|
|
112
|
-
return { __port: "stream", value: result.outputs };
|
|
113
|
-
}
|
|
114
|
-
if (mode === "both") {
|
|
115
|
-
return result.outputs;
|
|
116
|
-
}
|
|
117
|
-
return { __port: "out", value: result.outputs };
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// src/registry/llm-router.ts
|
|
121
|
-
function declaredRoutes(config) {
|
|
122
|
-
const raw = config.routes;
|
|
123
|
-
if (!Array.isArray(raw)) return [];
|
|
124
|
-
return raw.map((r) => ({ port: String(r?.port ?? "").trim(), description: r?.description })).filter((r) => r.port !== "");
|
|
125
|
-
}
|
|
126
|
-
function resolveFallbackPort(routes, fallbackEnabled) {
|
|
127
|
-
if (fallbackEnabled) return "fallback";
|
|
128
|
-
return routes[0]?.port ?? "out";
|
|
129
|
-
}
|
|
130
|
-
var llmRouterExecutor = async (ctx) => {
|
|
131
|
-
const config = ctx.node.data?.config ?? {};
|
|
132
|
-
const routes = declaredRoutes(config);
|
|
133
|
-
if (routes.length === 0) {
|
|
134
|
-
ctx.abort("llm_router has no routes configured");
|
|
135
|
-
}
|
|
136
|
-
const client = getLlmClient();
|
|
137
|
-
if (!client) {
|
|
138
|
-
ctx.abort(
|
|
139
|
-
"No LLM client registered. Call registerLlmClient() with your provider adapter \u2014 fancy-flow ships the routing, not the model call."
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
const fallbackEnabled = config.fallback !== false;
|
|
143
|
-
const choice = await client.chooseRoute({
|
|
144
|
-
system: typeof config.system === "string" ? config.system : void 0,
|
|
145
|
-
prompt: String(config.prompt ?? ctx.inputs ?? ""),
|
|
146
|
-
routes,
|
|
147
|
-
provider: typeof config.provider === "string" ? config.provider : void 0,
|
|
148
|
-
model: typeof config.model === "string" ? config.model : void 0,
|
|
149
|
-
credential: typeof config.credential === "string" ? config.credential : void 0
|
|
150
|
-
});
|
|
151
|
-
const offered = new Set(routes.map((r) => r.port));
|
|
152
|
-
let port = choice?.port ?? "";
|
|
153
|
-
let reason = choice?.reason;
|
|
154
|
-
if (!offered.has(port)) {
|
|
155
|
-
const safe = resolveFallbackPort(routes, fallbackEnabled);
|
|
156
|
-
ctx.emit({
|
|
157
|
-
type: "log",
|
|
158
|
-
nodeId: ctx.node.id,
|
|
159
|
-
level: "warn",
|
|
160
|
-
message: `llm_router: model returned "${port || "(nothing)"}", which is not a declared route. Routing to "${safe}".`
|
|
161
|
-
});
|
|
162
|
-
reason = reason ?? `unrecognised route "${port}"`;
|
|
163
|
-
port = safe;
|
|
164
|
-
}
|
|
165
|
-
return { __port: port, value: { route: port, reason, input: ctx.inputs } };
|
|
166
|
-
};
|
|
167
|
-
function RegistryNodeInner(props) {
|
|
168
|
-
const kindName = props.data.kind ?? props.type;
|
|
169
|
-
const kind = useMemo(() => getNodeKind(kindName), [kindName]);
|
|
170
|
-
if (!kind) {
|
|
171
|
-
return /* @__PURE__ */ jsxs("div", { className: "ff-node ff-node--unknown", children: [
|
|
172
|
-
/* @__PURE__ */ jsxs("div", { className: "ff-node__header", style: { background: "#71717a" }, children: [
|
|
173
|
-
/* @__PURE__ */ jsx("span", { className: "ff-node__tag", children: "UNKNOWN" }),
|
|
174
|
-
/* @__PURE__ */ jsx("span", { className: "ff-node__label", children: kindName })
|
|
175
|
-
] }),
|
|
176
|
-
/* @__PURE__ */ jsxs("p", { className: "ff-node__desc", children: [
|
|
177
|
-
'No registered kind for "',
|
|
178
|
-
kindName,
|
|
179
|
-
'".'
|
|
180
|
-
] })
|
|
181
|
-
] });
|
|
182
|
-
}
|
|
183
|
-
const data = props.data;
|
|
184
|
-
const status = data.status ?? "idle";
|
|
185
|
-
const accent = kind.accent ?? categoryAccent(kind.category);
|
|
186
|
-
const resolved = resolveNodePorts(props, kind);
|
|
187
|
-
const inputs = resolved.inputs ?? defaultInputs(kind.category);
|
|
188
|
-
const outputs = resolved.outputs ?? defaultOutputs(kind.category);
|
|
189
|
-
const config = nodeConfig(props);
|
|
190
|
-
const label = data.label ?? kind.label;
|
|
191
|
-
return /* @__PURE__ */ jsxs(
|
|
192
|
-
"div",
|
|
193
|
-
{
|
|
194
|
-
className: [
|
|
195
|
-
"ff-node",
|
|
196
|
-
`ff-node--status-${status}`,
|
|
197
|
-
`ff-node--cat-${kind.category}`,
|
|
198
|
-
props.selected ? "ff-node--selected" : "",
|
|
199
|
-
kind.resizable ? "ff-node--resizable" : ""
|
|
200
|
-
].filter(Boolean).join(" "),
|
|
201
|
-
style: { borderColor: props.selected ? accent : void 0 },
|
|
202
|
-
children: [
|
|
203
|
-
kind.resizable && /* @__PURE__ */ jsx(
|
|
204
|
-
NodeResizer,
|
|
205
|
-
{
|
|
206
|
-
isVisible: props.selected ?? false,
|
|
207
|
-
color: accent,
|
|
208
|
-
...typeof kind.resizable === "object" ? kind.resizable : {}
|
|
209
|
-
}
|
|
210
|
-
),
|
|
211
|
-
kind.toolbar && /* @__PURE__ */ jsx(NodeToolbar, { isVisible: props.selected ?? false, children: kind.toolbar({ nodeId: props.id, config, selected: props.selected ?? false }) }),
|
|
212
|
-
/* @__PURE__ */ jsxs("header", { className: "ff-node__header", style: { background: accent }, children: [
|
|
213
|
-
kind.icon && /* @__PURE__ */ jsx("span", { className: "ff-node__icon", "aria-hidden": true, children: kind.icon }),
|
|
214
|
-
/* @__PURE__ */ jsx("span", { className: "ff-node__tag", children: kind.label.toUpperCase() }),
|
|
215
|
-
/* @__PURE__ */ jsx("span", { className: "ff-node__label", children: label }),
|
|
216
|
-
status !== "idle" && /* @__PURE__ */ jsx("span", { className: `ff-node__dot ff-node__dot--${status}`, "aria-label": `status ${status}` })
|
|
217
|
-
] }),
|
|
218
|
-
data.description && /* @__PURE__ */ jsx("p", { className: "ff-node__desc", children: data.description }),
|
|
219
|
-
/* @__PURE__ */ jsx("div", { className: "ff-node__body", children: kind.renderBody ? kind.renderBody({ nodeId: props.id, config, selected: props.selected ?? false }) : /* @__PURE__ */ jsx(DefaultBody, { config, kind: kind.configSchema }) }),
|
|
220
|
-
data.statusText && /* @__PURE__ */ jsx("p", { className: "ff-node__status-text", children: data.statusText }),
|
|
221
|
-
data.output !== void 0 && /* @__PURE__ */ jsxs("p", { className: "ff-node__output", title: "Latest output", children: [
|
|
222
|
-
"\u2192 ",
|
|
223
|
-
previewValue(data.output)
|
|
224
|
-
] }),
|
|
225
|
-
inputs.map((p, i) => /* @__PURE__ */ jsx(
|
|
226
|
-
Handle,
|
|
227
|
-
{
|
|
228
|
-
type: "target",
|
|
229
|
-
position: Position.Left,
|
|
230
|
-
id: p.id,
|
|
231
|
-
style: portStyle(i, inputs.length),
|
|
232
|
-
title: p.label ?? p.id
|
|
233
|
-
},
|
|
234
|
-
p.id
|
|
235
|
-
)),
|
|
236
|
-
outputs.map((p, i) => /* @__PURE__ */ jsx(
|
|
237
|
-
Handle,
|
|
238
|
-
{
|
|
239
|
-
type: "source",
|
|
240
|
-
position: Position.Right,
|
|
241
|
-
id: p.id,
|
|
242
|
-
style: portStyle(i, outputs.length),
|
|
243
|
-
title: p.label ?? p.id
|
|
244
|
-
},
|
|
245
|
-
p.id
|
|
246
|
-
))
|
|
247
|
-
]
|
|
248
|
-
}
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
var RegistryNode = memo(RegistryNodeInner);
|
|
252
|
-
function defaultInputs(category) {
|
|
253
|
-
return category === "trigger" ? [] : [{ id: "in" }];
|
|
254
|
-
}
|
|
255
|
-
function defaultOutputs(category) {
|
|
256
|
-
return category === "output" ? [] : [{ id: "out" }];
|
|
257
|
-
}
|
|
258
|
-
function portStyle(i, total) {
|
|
259
|
-
if (total <= 1) return {};
|
|
260
|
-
const slot = 100 / (total + 1) * (i + 1);
|
|
261
|
-
return { top: `${slot}%` };
|
|
262
|
-
}
|
|
263
|
-
function DefaultBody({ config, kind }) {
|
|
264
|
-
const fields = kind ?? [];
|
|
265
|
-
if (fields.length === 0) return null;
|
|
266
|
-
const visible = fields.map((f) => ({ field: f, value: config[f.key] })).filter(({ value }) => value !== void 0 && value !== "" && value !== null);
|
|
267
|
-
if (visible.length === 0) {
|
|
268
|
-
return /* @__PURE__ */ jsx("div", { className: "ff-node__body-empty", children: "\u2014 configure in the panel" });
|
|
269
|
-
}
|
|
270
|
-
return /* @__PURE__ */ jsxs("ul", { className: "ff-node__summary", children: [
|
|
271
|
-
visible.slice(0, 4).map(({ field, value }) => /* @__PURE__ */ jsxs("li", { children: [
|
|
272
|
-
/* @__PURE__ */ jsxs("span", { className: "ff-node__summary-key", children: [
|
|
273
|
-
field.label,
|
|
274
|
-
":"
|
|
275
|
-
] }),
|
|
276
|
-
/* @__PURE__ */ jsx("span", { className: "ff-node__summary-value", children: previewValue(value) })
|
|
277
|
-
] }, field.key)),
|
|
278
|
-
visible.length > 4 && /* @__PURE__ */ jsxs("li", { className: "ff-node__summary-more", children: [
|
|
279
|
-
"+ ",
|
|
280
|
-
visible.length - 4,
|
|
281
|
-
" more"
|
|
282
|
-
] })
|
|
283
|
-
] });
|
|
284
|
-
}
|
|
285
|
-
var truncate = (s, n = 30) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
|
|
286
|
-
function itemLabel(item) {
|
|
287
|
-
if (item && typeof item === "object") {
|
|
288
|
-
const o = item;
|
|
289
|
-
const name = o.label ?? o.name ?? o.key ?? o.title ?? o.id;
|
|
290
|
-
if (typeof name === "string" && name) return name;
|
|
291
|
-
if (typeof name === "number") return String(name);
|
|
292
|
-
return "item";
|
|
293
|
-
}
|
|
294
|
-
return String(item ?? "");
|
|
295
|
-
}
|
|
296
|
-
function previewValue(v) {
|
|
297
|
-
if (v === null || v === void 0) return "";
|
|
298
|
-
if (typeof v === "string") return truncate(v);
|
|
299
|
-
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
300
|
-
if (Array.isArray(v)) {
|
|
301
|
-
if (v.length === 0) return "none";
|
|
302
|
-
const names = v.slice(0, 3).map(itemLabel).filter(Boolean).join(", ");
|
|
303
|
-
const rest = v.length > 3 ? `, +${v.length - 3}` : "";
|
|
304
|
-
return names ? truncate(names + rest) : `${v.length} item${v.length === 1 ? "" : "s"}`;
|
|
305
|
-
}
|
|
306
|
-
if (typeof v === "object") {
|
|
307
|
-
const keys = Object.keys(v);
|
|
308
|
-
return keys.length === 0 ? "empty" : `${keys.length} field${keys.length === 1 ? "" : "s"}`;
|
|
309
|
-
}
|
|
310
|
-
return "\u2026";
|
|
311
|
-
}
|
|
312
|
-
function LaneNodeInner(props) {
|
|
313
|
-
const data = props.data;
|
|
314
|
-
const config = data.config ?? {};
|
|
315
|
-
const title = config.title ?? data.label ?? "Lane";
|
|
316
|
-
const orientation = config.orientation ?? "horizontal";
|
|
317
|
-
const accent = config.color ?? data.color ?? categoryAccent("layout");
|
|
318
|
-
return /* @__PURE__ */ jsxs(
|
|
319
|
-
"div",
|
|
320
|
-
{
|
|
321
|
-
className: ["ff-lane", `ff-lane--${orientation}`, props.selected ? "ff-lane--selected" : ""].filter(Boolean).join(" "),
|
|
322
|
-
style: { borderColor: props.selected ? accent : void 0 },
|
|
323
|
-
children: [
|
|
324
|
-
/* @__PURE__ */ jsx(NodeResizer, { isVisible: props.selected ?? false, color: accent, minWidth: 160, minHeight: 72 }),
|
|
325
|
-
/* @__PURE__ */ jsx("div", { className: "ff-lane__header", style: { background: accent }, children: /* @__PURE__ */ jsx("span", { className: "ff-lane__title", children: title }) })
|
|
326
|
-
]
|
|
327
|
-
}
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
var LaneNode = memo(LaneNodeInner);
|
|
331
|
-
var FlowEditorContext = createContext(null);
|
|
332
|
-
var FlowEditorProvider = FlowEditorContext.Provider;
|
|
333
|
-
function useFlowEditor() {
|
|
334
|
-
const api = useContext(FlowEditorContext);
|
|
335
|
-
if (api === null) {
|
|
336
|
-
throw new Error("useFlowEditor() must be called inside <FlowEditor>.");
|
|
337
|
-
}
|
|
338
|
-
return api;
|
|
339
|
-
}
|
|
340
|
-
function useFlowEditorOptional() {
|
|
341
|
-
return useContext(FlowEditorContext);
|
|
342
|
-
}
|
|
343
|
-
var NOTE_COLORS = {
|
|
344
|
-
amber: { bg: "rgba(234, 179, 8, 0.14)", border: "rgba(234, 179, 8, 0.55)", title: "#a16207" },
|
|
345
|
-
sky: { bg: "rgba(14, 165, 233, 0.12)", border: "rgba(14, 165, 233, 0.5)", title: "#0369a1" },
|
|
346
|
-
violet: { bg: "rgba(139, 92, 246, 0.13)", border: "rgba(139, 92, 246, 0.5)", title: "#6d28d9" },
|
|
347
|
-
emerald: { bg: "rgba(16, 185, 129, 0.13)", border: "rgba(16, 185, 129, 0.5)", title: "#047857" },
|
|
348
|
-
rose: { bg: "rgba(244, 63, 94, 0.12)", border: "rgba(244, 63, 94, 0.5)", title: "#be123c" },
|
|
349
|
-
slate: { bg: "rgba(100, 116, 139, 0.12)", border: "rgba(100, 116, 139, 0.5)", title: "#475569" }
|
|
350
|
-
};
|
|
351
|
-
function coerceColor(v) {
|
|
352
|
-
return typeof v === "string" && v in NOTE_COLORS ? v : "amber";
|
|
353
|
-
}
|
|
354
|
-
function NoteNodeInner(props) {
|
|
355
|
-
const data = props.data;
|
|
356
|
-
const config = data.config ?? {};
|
|
357
|
-
const api = useFlowEditorOptional();
|
|
358
|
-
const hasConfigText = typeof config.text === "string" && config.text.length > 0;
|
|
359
|
-
const text = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
|
|
360
|
-
const title = typeof config.title === "string" && config.title || (!hasConfigText && typeof data.body === "string" && typeof data.label === "string" ? data.label : "");
|
|
361
|
-
const skin = NOTE_COLORS[coerceColor(config.color)];
|
|
362
|
-
const [editing, setEditing] = useState(false);
|
|
363
|
-
const [draft, setDraft] = useState(text);
|
|
364
|
-
const areaRef = useRef(null);
|
|
365
|
-
useEffect(() => {
|
|
366
|
-
if (editing && areaRef.current) {
|
|
367
|
-
const el = areaRef.current;
|
|
368
|
-
el.focus();
|
|
369
|
-
el.setSelectionRange(el.value.length, el.value.length);
|
|
370
|
-
}
|
|
371
|
-
}, [editing]);
|
|
372
|
-
const commit = () => {
|
|
373
|
-
setEditing(false);
|
|
374
|
-
if (!api || draft === text) return;
|
|
375
|
-
const current = api.nodes.find((n) => n.id === props.id);
|
|
376
|
-
if (!current) return;
|
|
377
|
-
api.updateNode({
|
|
378
|
-
...current,
|
|
379
|
-
data: { ...current.data, config: { ...config, text: draft } }
|
|
380
|
-
});
|
|
381
|
-
};
|
|
382
|
-
return /* @__PURE__ */ jsxs(
|
|
383
|
-
"div",
|
|
384
|
-
{
|
|
385
|
-
className: ["ff-note", props.selected ? "ff-note--selected" : ""].filter(Boolean).join(" "),
|
|
386
|
-
style: { background: skin.bg, borderColor: skin.border },
|
|
387
|
-
onDoubleClick: api ? () => {
|
|
388
|
-
setDraft(text);
|
|
389
|
-
setEditing(true);
|
|
390
|
-
} : void 0,
|
|
391
|
-
children: [
|
|
392
|
-
/* @__PURE__ */ jsx(NodeResizer, { isVisible: props.selected ?? false, color: skin.border, minWidth: 140, minHeight: 80 }),
|
|
393
|
-
title && /* @__PURE__ */ jsx("div", { className: "ff-note__title", style: { color: skin.title }, children: title }),
|
|
394
|
-
editing ? /* @__PURE__ */ jsx(
|
|
395
|
-
"textarea",
|
|
396
|
-
{
|
|
397
|
-
ref: areaRef,
|
|
398
|
-
className: "ff-note__input nodrag nowheel",
|
|
399
|
-
value: draft,
|
|
400
|
-
onChange: (e) => setDraft(e.target.value),
|
|
401
|
-
onBlur: commit,
|
|
402
|
-
onKeyDown: (e) => {
|
|
403
|
-
if (e.key === "Escape") setEditing(false);
|
|
404
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commit();
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
) : /* @__PURE__ */ jsx("div", { className: "ff-note__body", children: text || /* @__PURE__ */ jsx("span", { className: "ff-note__placeholder", children: api ? "Double-click to write a note\u2026" : "Note" }) })
|
|
408
|
-
]
|
|
409
|
-
}
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
var NoteNode = memo(NoteNodeInner);
|
|
413
|
-
|
|
414
|
-
// src/registry/builtin.ts
|
|
415
|
-
function casePorts(cases) {
|
|
416
|
-
const byPort = /* @__PURE__ */ new Map();
|
|
417
|
-
if (cases && typeof cases === "object" && !Array.isArray(cases)) {
|
|
418
|
-
for (const [match, port] of Object.entries(cases)) {
|
|
419
|
-
if (typeof port !== "string" || port === "" || port === "default") continue;
|
|
420
|
-
const matches = byPort.get(port) ?? [];
|
|
421
|
-
matches.push(match);
|
|
422
|
-
byPort.set(port, matches);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
const ports = [...byPort].map(([id, matches]) => ({
|
|
426
|
-
id,
|
|
427
|
-
label: matches.join("|")
|
|
428
|
-
}));
|
|
429
|
-
return [...ports, { id: "default", label: "default" }];
|
|
430
|
-
}
|
|
431
|
-
function routePorts(routes, fallback) {
|
|
432
|
-
const ports = [];
|
|
433
|
-
const seen = /* @__PURE__ */ new Set();
|
|
434
|
-
if (Array.isArray(routes)) {
|
|
435
|
-
for (const route of routes) {
|
|
436
|
-
const id = route?.port;
|
|
437
|
-
if (typeof id !== "string" || id.trim() === "" || seen.has(id)) continue;
|
|
438
|
-
seen.add(id);
|
|
439
|
-
ports.push({ id, label: id });
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (fallback !== false && !seen.has("fallback")) {
|
|
443
|
-
ports.push({ id: "fallback", label: "fallback" });
|
|
444
|
-
}
|
|
445
|
-
if (ports.length === 0) ports.push({ id: "out" });
|
|
446
|
-
return ports;
|
|
447
|
-
}
|
|
448
|
-
var HTTP_METHODS = [
|
|
449
|
-
{ type: "select", key: "method", label: "Method", options: [
|
|
450
|
-
{ value: "GET", label: "GET" },
|
|
451
|
-
{ value: "POST", label: "POST" },
|
|
452
|
-
{ value: "PUT", label: "PUT" },
|
|
453
|
-
{ value: "PATCH", label: "PATCH" },
|
|
454
|
-
{ value: "DELETE", label: "DELETE" }
|
|
455
|
-
], default: "GET", required: true }
|
|
456
|
-
];
|
|
457
|
-
var KINDS = [
|
|
458
|
-
// ───────────── Triggers ─────────────
|
|
459
|
-
{
|
|
460
|
-
name: "@particle-academy/manual_trigger",
|
|
461
|
-
aliases: ["manual_trigger", "@fancy/manual_trigger"],
|
|
462
|
-
category: "trigger",
|
|
463
|
-
label: "Manual",
|
|
464
|
-
description: "Entry point fired when the user clicks Run.",
|
|
465
|
-
icon: "\u26A1",
|
|
466
|
-
inputs: [],
|
|
467
|
-
outputs: [{ id: "out" }]
|
|
468
|
-
},
|
|
469
|
-
{
|
|
470
|
-
name: "@particle-academy/webhook_trigger",
|
|
471
|
-
aliases: ["webhook_trigger", "@fancy/webhook_trigger"],
|
|
472
|
-
category: "trigger",
|
|
473
|
-
label: "Webhook",
|
|
474
|
-
description: "Triggered by an inbound HTTP request to a host-provided URL.",
|
|
475
|
-
icon: "\u{1F4E1}",
|
|
476
|
-
inputs: [],
|
|
477
|
-
outputs: [{ id: "out", label: "payload" }],
|
|
478
|
-
configSchema: [
|
|
479
|
-
{ type: "text", key: "path", label: "Path", placeholder: "/hooks/my-flow", required: true },
|
|
480
|
-
{ type: "select", key: "method", label: "Method", options: [
|
|
481
|
-
{ value: "POST", label: "POST" },
|
|
482
|
-
{ value: "GET", label: "GET" }
|
|
483
|
-
], default: "POST" },
|
|
484
|
-
{ type: "credential", key: "secret", label: "Verifying secret", credentialType: "webhook_secret" }
|
|
485
|
-
]
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
name: "@particle-academy/schedule_trigger",
|
|
489
|
-
aliases: ["schedule_trigger", "@fancy/schedule_trigger"],
|
|
490
|
-
category: "trigger",
|
|
491
|
-
label: "Schedule",
|
|
492
|
-
description: "Fires on a cron schedule (host-implemented).",
|
|
493
|
-
icon: "\u23F1",
|
|
494
|
-
inputs: [],
|
|
495
|
-
outputs: [{ id: "out" }],
|
|
496
|
-
configSchema: [
|
|
497
|
-
{
|
|
498
|
-
type: "text",
|
|
499
|
-
key: "cron",
|
|
500
|
-
label: "Cron",
|
|
501
|
-
placeholder: "*/5 * * * *",
|
|
502
|
-
required: true,
|
|
503
|
-
description: "Standard 5-field cron expression."
|
|
504
|
-
},
|
|
505
|
-
{ type: "text", key: "timezone", label: "Timezone", placeholder: "UTC", default: "UTC" }
|
|
506
|
-
]
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
name: "@particle-academy/user_input",
|
|
510
|
-
// The keys an author declared on THIS node — the case a static list cannot
|
|
511
|
-
// express, and the one issue #5 named.
|
|
512
|
-
outputShape: (config) => (config.fields ?? []).filter((f) => typeof f.key === "string" && f.key !== "").map((f) => ({ path: f.key, type: "string", description: f.label })),
|
|
513
|
-
aliases: ["user_input", "@fancy/user_input"],
|
|
514
|
-
pausesForHuman: "input",
|
|
515
|
-
category: "human",
|
|
516
|
-
label: "User Input",
|
|
517
|
-
description: "Pause the flow until the user submits the configured form.",
|
|
518
|
-
icon: "\u270E",
|
|
519
|
-
inputs: [{ id: "in" }],
|
|
520
|
-
outputs: [{ id: "out", label: "values" }],
|
|
521
|
-
configSchema: [
|
|
522
|
-
{ type: "text", key: "title", label: "Form title", default: "Need your input" },
|
|
523
|
-
{
|
|
524
|
-
type: "repeater",
|
|
525
|
-
key: "fields",
|
|
526
|
-
label: "Fields",
|
|
527
|
-
description: "The form the run pauses on.",
|
|
528
|
-
titleKey: "label",
|
|
529
|
-
addLabel: "Add field",
|
|
530
|
-
minItems: 1,
|
|
531
|
-
fields: [
|
|
532
|
-
{ type: "text", key: "key", label: "Key", required: true, placeholder: "answer" },
|
|
533
|
-
{ type: "text", key: "label", label: "Label", required: true, placeholder: "Your answer" },
|
|
534
|
-
{
|
|
535
|
-
type: "select",
|
|
536
|
-
key: "type",
|
|
537
|
-
label: "Type",
|
|
538
|
-
default: "text",
|
|
539
|
-
options: [
|
|
540
|
-
{ value: "text", label: "Text" },
|
|
541
|
-
{ value: "textarea", label: "Long text" },
|
|
542
|
-
{ value: "number", label: "Number" },
|
|
543
|
-
{ value: "select", label: "Select" },
|
|
544
|
-
{ value: "switch", label: "Switch" }
|
|
545
|
-
]
|
|
546
|
-
},
|
|
547
|
-
{ type: "switch", key: "required", label: "Required", default: false }
|
|
548
|
-
],
|
|
549
|
-
default: [{ key: "answer", label: "Your answer", type: "textarea", required: true }]
|
|
550
|
-
}
|
|
551
|
-
]
|
|
552
|
-
},
|
|
553
|
-
{
|
|
554
|
-
name: "@particle-academy/rich_user_input",
|
|
555
|
-
aliases: ["rich_user_input", "@fancy/rich_user_input"],
|
|
556
|
-
pausesForHuman: "input",
|
|
557
|
-
category: "human",
|
|
558
|
-
label: "Rich User Input",
|
|
559
|
-
description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
|
|
560
|
-
icon: "\u25A4",
|
|
561
|
-
inputs: [{ id: "in" }],
|
|
562
|
-
outputs: [{ id: "out", label: "values" }],
|
|
563
|
-
configSchema: [
|
|
564
|
-
{ type: "text", key: "title", label: "Step title", default: "Please review" },
|
|
565
|
-
{
|
|
566
|
-
type: "document",
|
|
567
|
-
key: "document",
|
|
568
|
-
label: "Page content",
|
|
569
|
-
documentType: "stages",
|
|
570
|
-
description: "Authored with the host's document editor (fancy-cms Stages)."
|
|
571
|
-
},
|
|
572
|
-
{ type: "switch", key: "requireConfirm", label: "Require explicit confirmation", default: true },
|
|
573
|
-
{ type: "text", key: "submitLabel", label: "Submit button", default: "Continue" }
|
|
574
|
-
],
|
|
575
|
-
// Preview the authored page inside a FauxClient frame, so the canvas shows
|
|
576
|
-
// what the person hitting this step will actually see.
|
|
577
|
-
renderBody: (ctx) => createElement(RichInputPreview, { config: ctx.config ?? {} })
|
|
578
|
-
},
|
|
579
|
-
{
|
|
580
|
-
name: "@particle-academy/subflow",
|
|
581
|
-
aliases: ["subflow", "@fancy/subflow"],
|
|
582
|
-
category: "logic",
|
|
583
|
-
label: "SubFlow",
|
|
584
|
-
description: "Run another workflow and bring its result \u2014 or its live progress \u2014 back into this one.",
|
|
585
|
-
icon: "\u29C9",
|
|
586
|
-
inputs: [{ id: "in" }],
|
|
587
|
-
// The stream port only exists when something actually streams.
|
|
588
|
-
outputs: (config) => subflowPorts(config ?? {}),
|
|
589
|
-
// Core, not marketplace: it runs a child graph through this same engine and
|
|
590
|
-
// needs nothing from outside except where workflows live.
|
|
591
|
-
executor: subflowExecutor,
|
|
592
|
-
configSchema: [
|
|
593
|
-
{
|
|
594
|
-
type: "text",
|
|
595
|
-
key: "workflow",
|
|
596
|
-
label: "Workflow",
|
|
597
|
-
required: true,
|
|
598
|
-
placeholder: "onboarding-v2",
|
|
599
|
-
description: "Reference resolved by the host's registerWorkflowResolver()."
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
type: "number",
|
|
603
|
-
key: "version",
|
|
604
|
-
label: "Pin to version",
|
|
605
|
-
description: "Optional. Leave blank to always run the child's current version. Pinning fails the run loudly if the child has moved on \u2014 without it, someone edits the child and this flow silently runs different logic."
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
type: "select",
|
|
609
|
-
key: "mode",
|
|
610
|
-
label: "Return",
|
|
611
|
-
default: "output",
|
|
612
|
-
options: [
|
|
613
|
-
{ value: "output", label: "Output when it finishes" },
|
|
614
|
-
{ value: "stream", label: "Stream progress as it runs" },
|
|
615
|
-
{ value: "both", label: "Both \u2014 stream, then output" }
|
|
616
|
-
],
|
|
617
|
-
description: "Streaming adds a second port so a parent can show progress instead of a spinner."
|
|
618
|
-
},
|
|
619
|
-
{
|
|
620
|
-
type: "keyvalue",
|
|
621
|
-
key: "inputs",
|
|
622
|
-
label: "Input mapping",
|
|
623
|
-
description: "Values handed to the child's entry points. Omit to pass this node's inputs straight through.",
|
|
624
|
-
keyLabel: "Name",
|
|
625
|
-
valueLabel: "Value",
|
|
626
|
-
addLabel: "Add input"
|
|
627
|
-
},
|
|
628
|
-
{
|
|
629
|
-
type: "number",
|
|
630
|
-
key: "maxDepth",
|
|
631
|
-
label: "Max nesting depth",
|
|
632
|
-
default: DEFAULT_MAX_DEPTH,
|
|
633
|
-
min: 1,
|
|
634
|
-
max: 32,
|
|
635
|
-
description: "Guards against a workflow referencing itself."
|
|
636
|
-
}
|
|
637
|
-
]
|
|
638
|
-
},
|
|
639
|
-
// ───────────── Logic ─────────────
|
|
640
|
-
{
|
|
641
|
-
name: "@particle-academy/branch",
|
|
642
|
-
aliases: ["branch", "@fancy/branch"],
|
|
643
|
-
category: "logic",
|
|
644
|
-
label: "Branch",
|
|
645
|
-
description: "Multi-way branch on a condition or value.",
|
|
646
|
-
icon: "\u25C7",
|
|
647
|
-
inputs: [{ id: "in" }],
|
|
648
|
-
outputs: [{ id: "true", label: "true" }, { id: "false", label: "false" }],
|
|
649
|
-
configSchema: [
|
|
650
|
-
{
|
|
651
|
-
type: "select",
|
|
652
|
-
key: "match",
|
|
653
|
-
label: "Match",
|
|
654
|
-
default: "all",
|
|
655
|
-
options: [
|
|
656
|
-
{ value: "all", label: "All conditions (AND)" },
|
|
657
|
-
{ value: "any", label: "Any condition (OR)" }
|
|
658
|
-
]
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
type: "repeater",
|
|
662
|
-
key: "conditions",
|
|
663
|
-
label: "Conditions",
|
|
664
|
-
description: "Routes to `true` when these match, otherwise `false`.",
|
|
665
|
-
titleKey: "left",
|
|
666
|
-
addLabel: "Add condition",
|
|
667
|
-
minItems: 1,
|
|
668
|
-
fields: [
|
|
669
|
-
{ type: "expression", key: "left", label: "Value", example: "{{ $json.status }}", required: true },
|
|
670
|
-
{
|
|
671
|
-
type: "select",
|
|
672
|
-
key: "operator",
|
|
673
|
-
label: "Is",
|
|
674
|
-
default: "eq",
|
|
675
|
-
options: [
|
|
676
|
-
{ value: "eq", label: "equal to" },
|
|
677
|
-
{ value: "neq", label: "not equal to" },
|
|
678
|
-
{ value: "contains", label: "contains" },
|
|
679
|
-
{ value: "not_contains", label: "does not contain" },
|
|
680
|
-
{ value: "gt", label: "greater than" },
|
|
681
|
-
{ value: "gte", label: "greater than or equal to" },
|
|
682
|
-
{ value: "lt", label: "less than" },
|
|
683
|
-
{ value: "lte", label: "less than or equal to" },
|
|
684
|
-
{ value: "truthy", label: "true" },
|
|
685
|
-
{ value: "falsy", label: "false" },
|
|
686
|
-
{ value: "empty", label: "empty" },
|
|
687
|
-
{ value: "not_empty", label: "not empty" }
|
|
688
|
-
]
|
|
689
|
-
},
|
|
690
|
-
{ type: "text", key: "right", label: "Compared to", placeholder: "active" }
|
|
691
|
-
],
|
|
692
|
-
default: [{ left: "", operator: "eq", right: "" }]
|
|
693
|
-
},
|
|
694
|
-
{
|
|
695
|
-
type: "expression",
|
|
696
|
-
key: "condition",
|
|
697
|
-
label: "Raw expression (advanced)",
|
|
698
|
-
example: "{{ $json.active && $json.score > 10 }}",
|
|
699
|
-
description: "Escape hatch for logic the builder can't express. Overrides the conditions above when set."
|
|
700
|
-
}
|
|
701
|
-
]
|
|
702
|
-
},
|
|
703
|
-
{
|
|
704
|
-
name: "@particle-academy/switch_case",
|
|
705
|
-
aliases: ["switch_case", "@fancy/switch_case"],
|
|
706
|
-
category: "logic",
|
|
707
|
-
label: "Switch",
|
|
708
|
-
description: "Route to one of N labelled outputs based on a key.",
|
|
709
|
-
icon: "\u2933",
|
|
710
|
-
inputs: [{ id: "in" }],
|
|
711
|
-
// Ports ARE the config: every distinct port a case routes to becomes an
|
|
712
|
-
// output handle, plus the always-present `default`. Editing the cases map
|
|
713
|
-
// moves the ports on the canvas and the ports the runtime activates.
|
|
714
|
-
outputs: (config) => casePorts(config?.cases),
|
|
715
|
-
configSchema: [
|
|
716
|
-
{ type: "expression", key: "value", label: "Switch on", example: "{{ $json.kind }}", required: true },
|
|
717
|
-
{
|
|
718
|
-
type: "keyvalue",
|
|
719
|
-
key: "cases",
|
|
720
|
-
label: "Cases",
|
|
721
|
-
description: "Match value \u2192 output port. Unmatched input takes `default`.",
|
|
722
|
-
keyLabel: "When value is",
|
|
723
|
-
valueLabel: "Route to port",
|
|
724
|
-
keyPlaceholder: "a",
|
|
725
|
-
valuePlaceholder: "case_a",
|
|
726
|
-
addLabel: "Add case",
|
|
727
|
-
default: { a: "case_a", b: "case_b" }
|
|
728
|
-
}
|
|
729
|
-
]
|
|
730
|
-
},
|
|
731
|
-
{
|
|
732
|
-
name: "@particle-academy/for_each",
|
|
733
|
-
outputShape: [
|
|
734
|
-
{ path: "items", type: "array" },
|
|
735
|
-
{ path: "count", type: "number" }
|
|
736
|
-
],
|
|
737
|
-
aliases: ["for_each", "@fancy/for_each"],
|
|
738
|
-
category: "logic",
|
|
739
|
-
label: "For Each",
|
|
740
|
-
description: "Iterate over a list, emitting each item on `item`.",
|
|
741
|
-
icon: "\u21BB",
|
|
742
|
-
inputs: [{ id: "in" }],
|
|
743
|
-
outputs: [{ id: "item", label: "item" }, { id: "done", label: "done" }],
|
|
744
|
-
configSchema: [
|
|
745
|
-
{ type: "expression", key: "source", label: "List", example: "{{ $json.users }}", required: true },
|
|
746
|
-
{ type: "number", key: "concurrency", label: "Concurrency", default: 1, min: 1, max: 50 }
|
|
747
|
-
]
|
|
748
|
-
},
|
|
749
|
-
{
|
|
750
|
-
name: "@particle-academy/merge",
|
|
751
|
-
aliases: ["merge", "@fancy/merge"],
|
|
752
|
-
category: "logic",
|
|
753
|
-
label: "Merge",
|
|
754
|
-
description: "Combine multiple inputs into one object or array.",
|
|
755
|
-
icon: "\u2295",
|
|
756
|
-
inputs: [{ id: "a" }, { id: "b" }],
|
|
757
|
-
outputs: [{ id: "out" }],
|
|
758
|
-
configSchema: [
|
|
759
|
-
{
|
|
760
|
-
type: "select",
|
|
761
|
-
key: "mode",
|
|
762
|
-
label: "Mode",
|
|
763
|
-
default: "merge",
|
|
764
|
-
options: [{ value: "merge", label: "Object merge" }, { value: "concat", label: "Array concat" }]
|
|
765
|
-
}
|
|
766
|
-
]
|
|
767
|
-
},
|
|
768
|
-
{
|
|
769
|
-
name: "@particle-academy/wait",
|
|
770
|
-
aliases: ["wait", "@fancy/wait"],
|
|
771
|
-
category: "logic",
|
|
772
|
-
label: "Wait",
|
|
773
|
-
description: "Sleep or wait for an external event.",
|
|
774
|
-
icon: "\u23F8",
|
|
775
|
-
configSchema: [
|
|
776
|
-
{
|
|
777
|
-
type: "select",
|
|
778
|
-
key: "mode",
|
|
779
|
-
label: "Mode",
|
|
780
|
-
default: "duration",
|
|
781
|
-
options: [{ value: "duration", label: "Duration" }, { value: "until", label: "Until timestamp" }, { value: "event", label: "External event" }]
|
|
782
|
-
},
|
|
783
|
-
{ type: "text", key: "duration", label: "Duration", placeholder: "5s, 10m, 1h", description: "Used when mode = duration." }
|
|
784
|
-
]
|
|
785
|
-
},
|
|
786
|
-
{
|
|
787
|
-
name: "@particle-academy/transform",
|
|
788
|
-
aliases: ["transform", "@fancy/transform"],
|
|
789
|
-
category: "logic",
|
|
790
|
-
label: "Transform",
|
|
791
|
-
description: "Reshape data with an expression.",
|
|
792
|
-
icon: "\u0192",
|
|
793
|
-
configSchema: [
|
|
794
|
-
{
|
|
795
|
-
type: "select",
|
|
796
|
-
key: "mode",
|
|
797
|
-
label: "Build the output",
|
|
798
|
-
default: "fields",
|
|
799
|
-
options: [
|
|
800
|
-
{ value: "fields", label: "Field by field" },
|
|
801
|
-
{ value: "expression", label: "One expression" }
|
|
802
|
-
]
|
|
803
|
-
},
|
|
804
|
-
{
|
|
805
|
-
type: "repeater",
|
|
806
|
-
key: "fields",
|
|
807
|
-
label: "Output fields",
|
|
808
|
-
description: "Each row becomes a key on the result.",
|
|
809
|
-
titleKey: "key",
|
|
810
|
-
addLabel: "Add field",
|
|
811
|
-
fields: [
|
|
812
|
-
{ type: "text", key: "key", label: "Key", required: true, placeholder: "name" },
|
|
813
|
-
{ type: "expression", key: "value", label: "Value", example: "{{ $json.first }}", required: true }
|
|
814
|
-
],
|
|
815
|
-
default: [{ key: "", value: "" }]
|
|
816
|
-
},
|
|
817
|
-
{
|
|
818
|
-
type: "expression",
|
|
819
|
-
key: "expression",
|
|
820
|
-
label: "Expression (advanced)",
|
|
821
|
-
example: "{{ { id: $json.id, name: $json.first + ' ' + $json.last } }}",
|
|
822
|
-
description: "Used when the mode above is set to one expression."
|
|
823
|
-
}
|
|
824
|
-
]
|
|
825
|
-
},
|
|
826
|
-
// ───────────── Data ─────────────
|
|
827
|
-
{
|
|
828
|
-
name: "@particle-academy/memory_store",
|
|
829
|
-
aliases: ["memory_store", "@fancy/memory_store"],
|
|
830
|
-
category: "data",
|
|
831
|
-
label: "Memory Store",
|
|
832
|
-
description: "Read or write per-conversation memory.",
|
|
833
|
-
icon: "\u{1F9E0}",
|
|
834
|
-
configSchema: [
|
|
835
|
-
{
|
|
836
|
-
type: "select",
|
|
837
|
-
key: "operation",
|
|
838
|
-
label: "Operation",
|
|
839
|
-
required: true,
|
|
840
|
-
default: "read",
|
|
841
|
-
options: [{ value: "read", label: "Read" }, { value: "write", label: "Write" }, { value: "append", label: "Append" }]
|
|
842
|
-
},
|
|
843
|
-
{ type: "text", key: "key", label: "Key", placeholder: "user.preferences", required: true },
|
|
844
|
-
{ type: "expression", key: "value", label: "Value (write/append only)", example: "{{ $json }}" },
|
|
845
|
-
{ type: "credential", key: "store", label: "Memory store", credentialType: "memory_store" }
|
|
846
|
-
]
|
|
847
|
-
},
|
|
848
|
-
{
|
|
849
|
-
name: "@particle-academy/data_store",
|
|
850
|
-
aliases: ["data_store", "@fancy/data_store"],
|
|
851
|
-
category: "data",
|
|
852
|
-
label: "Data Store",
|
|
853
|
-
description: "Key-value or table read/write against a host store.",
|
|
854
|
-
icon: "\u{1F5C3}",
|
|
855
|
-
configSchema: [
|
|
856
|
-
{
|
|
857
|
-
type: "select",
|
|
858
|
-
key: "operation",
|
|
859
|
-
label: "Operation",
|
|
860
|
-
required: true,
|
|
861
|
-
default: "get",
|
|
862
|
-
options: [
|
|
863
|
-
{ value: "get", label: "Get" },
|
|
864
|
-
{ value: "set", label: "Set" },
|
|
865
|
-
{ value: "delete", label: "Delete" },
|
|
866
|
-
{ value: "query", label: "Query" },
|
|
867
|
-
{ value: "list", label: "List" }
|
|
868
|
-
]
|
|
869
|
-
},
|
|
870
|
-
{ type: "text", key: "table", label: "Table / collection", required: true },
|
|
871
|
-
{ type: "text", key: "key", label: "Key" },
|
|
872
|
-
{
|
|
873
|
-
type: "keyvalue",
|
|
874
|
-
key: "where",
|
|
875
|
-
label: "Where",
|
|
876
|
-
description: "Field/value pairs to match. For query and list operations.",
|
|
877
|
-
keyLabel: "Field",
|
|
878
|
-
valueLabel: "Equals",
|
|
879
|
-
addLabel: "Add filter"
|
|
880
|
-
},
|
|
881
|
-
{ type: "expression", key: "value", label: "Value (set only)", example: "{{ $json }}" },
|
|
882
|
-
{ type: "credential", key: "store", label: "Data store", credentialType: "data_store" }
|
|
883
|
-
]
|
|
884
|
-
},
|
|
885
|
-
{
|
|
886
|
-
name: "@particle-academy/variable",
|
|
887
|
-
aliases: ["variable", "@fancy/variable"],
|
|
888
|
-
category: "data",
|
|
889
|
-
label: "Variable",
|
|
890
|
-
description: "Workflow-scoped value used by other nodes.",
|
|
891
|
-
icon: "\u{1D4CD}",
|
|
892
|
-
configSchema: [
|
|
893
|
-
{ type: "text", key: "name", label: "Name", required: true },
|
|
894
|
-
{ type: "expression", key: "value", label: "Value", required: true }
|
|
895
|
-
]
|
|
896
|
-
},
|
|
897
|
-
// ───────────── AI ─────────────
|
|
898
|
-
{
|
|
899
|
-
name: "@particle-academy/llm_call",
|
|
900
|
-
// LlmClient::complete() -> array{text:string,data?:mixed,usage?:array,raw?:mixed}
|
|
901
|
-
//
|
|
902
|
-
// A FUNCTION of config, because `data` exists only when this node was asked
|
|
903
|
-
// for a schema. Declaring it statically would offer `{{ $json.data }}` on
|
|
904
|
-
// every llm_call in the kit, including the ones that emit nothing of the
|
|
905
|
-
// sort — and a suggestion that resolves to null at runtime is worse than no
|
|
906
|
-
// suggestion, since the picker is exactly what gave the author confidence.
|
|
907
|
-
outputShape: (config) => {
|
|
908
|
-
const asked = config?.response_schema != null && config.response_schema !== "" && !(Array.isArray(config.response_schema) && config.response_schema.length === 0);
|
|
909
|
-
return [
|
|
910
|
-
{ path: "text", type: "string", description: "The model's completion." },
|
|
911
|
-
...asked ? [{ path: "data", type: "unknown", description: "The parsed, schema-checked result." }] : [],
|
|
912
|
-
{ path: "usage", type: "object", description: "Token counts, when the provider reports them." },
|
|
913
|
-
{ path: "raw", type: "unknown", description: "The provider's untouched response." }
|
|
914
|
-
];
|
|
915
|
-
},
|
|
916
|
-
aliases: ["llm_call", "@fancy/llm_call"],
|
|
917
|
-
category: "ai",
|
|
918
|
-
label: "LLM Call",
|
|
919
|
-
description: "Send a prompt + context to a model and receive a response.",
|
|
920
|
-
icon: "\u2726",
|
|
921
|
-
configSchema: [
|
|
922
|
-
{
|
|
923
|
-
type: "select",
|
|
924
|
-
key: "provider",
|
|
925
|
-
label: "Provider",
|
|
926
|
-
default: "anthropic",
|
|
927
|
-
options: [
|
|
928
|
-
{ value: "anthropic", label: "Anthropic" },
|
|
929
|
-
{ value: "openai", label: "OpenAI" },
|
|
930
|
-
{ value: "custom", label: "Custom" }
|
|
931
|
-
]
|
|
932
|
-
},
|
|
933
|
-
{ type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5", required: true },
|
|
934
|
-
{ type: "textarea", key: "system", label: "System prompt", rows: 4 },
|
|
935
|
-
{ type: "expression", key: "prompt", label: "User prompt", example: "{{ $json.question }}", required: true },
|
|
936
|
-
{ type: "number", key: "temperature", label: "Temperature", min: 0, max: 2, step: 0.1, default: 0.7 },
|
|
937
|
-
{ type: "number", key: "max_tokens", label: "Max tokens", min: 1, max: 8192, default: 1024 },
|
|
938
|
-
{
|
|
939
|
-
type: "repeater",
|
|
940
|
-
key: "tools",
|
|
941
|
-
label: "Tools",
|
|
942
|
-
description: "Tools the model may call.",
|
|
943
|
-
titleKey: "name",
|
|
944
|
-
addLabel: "Add tool",
|
|
945
|
-
fields: [
|
|
946
|
-
{ type: "text", key: "name", label: "Name", required: true, placeholder: "search_index" },
|
|
947
|
-
{ type: "text", key: "description", label: "When to use it" },
|
|
948
|
-
{
|
|
949
|
-
type: "json",
|
|
950
|
-
key: "input_schema",
|
|
951
|
-
label: "Input schema",
|
|
952
|
-
description: "JSON Schema for the tool's arguments."
|
|
953
|
-
}
|
|
954
|
-
]
|
|
955
|
-
},
|
|
956
|
-
{
|
|
957
|
-
type: "json",
|
|
958
|
-
key: "response_schema",
|
|
959
|
-
label: "Response schema",
|
|
960
|
-
description: "Optional JSON Schema. When set, the runtime asks the provider for schema-valid JSON and the node emits the parsed value as `data` \u2014 reference it as {{ $json.data }}. A response that cannot be parsed or does not match FAILS the node rather than passing an empty value downstream."
|
|
961
|
-
},
|
|
962
|
-
{ type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
|
|
963
|
-
]
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
name: "@particle-academy/llm_router",
|
|
967
|
-
// Every id this node has ever shipped under keeps resolving — MOIC's saved
|
|
968
|
-
// flows carry the bare `llm_branch`.
|
|
969
|
-
aliases: ["llm_router", "llm_branch", "@fancy/llm_branch", "@fancy/llm_router"],
|
|
970
|
-
category: "ai",
|
|
971
|
-
label: "LLM Router",
|
|
972
|
-
description: "Let a model choose which route the flow takes.",
|
|
973
|
-
icon: "\u2727",
|
|
974
|
-
inputs: [{ id: "in" }],
|
|
975
|
-
// Each declared route is a port. The executor returns `{ __port: id }`
|
|
976
|
-
// (or `Port.only(id)` on the PHP runtime) to pick one.
|
|
977
|
-
outputs: (config) => routePorts(config?.routes, config?.fallback),
|
|
978
|
-
// A shuttle, not an engine: it carries the routes out to whatever LLM
|
|
979
|
-
// client the host registered and carries the choice back. No provider SDK
|
|
980
|
-
// reaches core, so this stays a builtin without adding a dependency.
|
|
981
|
-
executor: llmRouterExecutor,
|
|
982
|
-
configSchema: [
|
|
983
|
-
{
|
|
984
|
-
type: "textarea",
|
|
985
|
-
key: "system",
|
|
986
|
-
label: "System prompt",
|
|
987
|
-
rows: 3,
|
|
988
|
-
description: "Optional framing for the routing decision."
|
|
989
|
-
},
|
|
990
|
-
{
|
|
991
|
-
type: "expression",
|
|
992
|
-
key: "prompt",
|
|
993
|
-
label: "What to route on",
|
|
994
|
-
example: "{{ $json.message }}",
|
|
995
|
-
required: true
|
|
996
|
-
},
|
|
997
|
-
{
|
|
998
|
-
type: "repeater",
|
|
999
|
-
key: "routes",
|
|
1000
|
-
label: "Routes",
|
|
1001
|
-
description: "The model picks exactly one. Descriptions are what it chooses between \u2014 make them distinct.",
|
|
1002
|
-
titleKey: "port",
|
|
1003
|
-
addLabel: "Add route",
|
|
1004
|
-
minItems: 2,
|
|
1005
|
-
fields: [
|
|
1006
|
-
{ type: "text", key: "port", label: "Port", required: true, placeholder: "billing" },
|
|
1007
|
-
{
|
|
1008
|
-
type: "text",
|
|
1009
|
-
key: "description",
|
|
1010
|
-
label: "When to choose it",
|
|
1011
|
-
required: true,
|
|
1012
|
-
placeholder: "The user is asking about an invoice, refund, or payment."
|
|
1013
|
-
}
|
|
1014
|
-
],
|
|
1015
|
-
default: [
|
|
1016
|
-
{ port: "a", description: "Describe when the model should pick this route." },
|
|
1017
|
-
{ port: "b", description: "Describe when the model should pick this route." }
|
|
1018
|
-
]
|
|
1019
|
-
},
|
|
1020
|
-
{
|
|
1021
|
-
type: "select",
|
|
1022
|
-
key: "provider",
|
|
1023
|
-
label: "Provider",
|
|
1024
|
-
default: "anthropic",
|
|
1025
|
-
options: [
|
|
1026
|
-
{ value: "anthropic", label: "Anthropic" },
|
|
1027
|
-
{ value: "openai", label: "OpenAI" },
|
|
1028
|
-
{ value: "custom", label: "Custom" }
|
|
1029
|
-
]
|
|
1030
|
-
},
|
|
1031
|
-
{ type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5" },
|
|
1032
|
-
{
|
|
1033
|
-
type: "switch",
|
|
1034
|
-
key: "fallback",
|
|
1035
|
-
label: "Add a `fallback` port",
|
|
1036
|
-
default: true,
|
|
1037
|
-
description: "Where the flow goes if the model returns no usable route."
|
|
1038
|
-
},
|
|
1039
|
-
{ type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
|
|
1040
|
-
]
|
|
1041
|
-
},
|
|
1042
|
-
{
|
|
1043
|
-
name: "@particle-academy/tool_use",
|
|
1044
|
-
aliases: ["tool_use", "@fancy/tool_use"],
|
|
1045
|
-
category: "ai",
|
|
1046
|
-
label: "Tool Use",
|
|
1047
|
-
description: "Hand control to a host-registered tool by name.",
|
|
1048
|
-
icon: "\u{1F6E0}",
|
|
1049
|
-
configSchema: [
|
|
1050
|
-
{ type: "text", key: "tool", label: "Tool name", placeholder: "search_index", required: true },
|
|
1051
|
-
{ type: "expression", key: "args", label: "Arguments", example: "{{ { query: $json.q } }}" }
|
|
1052
|
-
]
|
|
1053
|
-
},
|
|
1054
|
-
{
|
|
1055
|
-
name: "@particle-academy/embed_search",
|
|
1056
|
-
outputShape: [
|
|
1057
|
-
{ path: "query", type: "string" },
|
|
1058
|
-
{ path: "matches", type: "array", description: "Ranked results from the vector store." }
|
|
1059
|
-
],
|
|
1060
|
-
aliases: ["embed_search", "@fancy/embed_search"],
|
|
1061
|
-
category: "ai",
|
|
1062
|
-
label: "Embed & Search",
|
|
1063
|
-
description: "Embed a query and search a vector store.",
|
|
1064
|
-
icon: "\u273A",
|
|
1065
|
-
configSchema: [
|
|
1066
|
-
{ type: "expression", key: "query", label: "Query", required: true, example: "{{ $json.question }}" },
|
|
1067
|
-
{ type: "number", key: "topK", label: "Top K", default: 5, min: 1, max: 50 },
|
|
1068
|
-
{ type: "credential", key: "vectorStore", label: "Vector store", credentialType: "vector_store" }
|
|
1069
|
-
]
|
|
1070
|
-
},
|
|
1071
|
-
// ───────────── IO ─────────────
|
|
1072
|
-
{
|
|
1073
|
-
name: "@particle-academy/api_request",
|
|
1074
|
-
// HttpClient::send() -> array{status:int,headers:array,body:mixed}
|
|
1075
|
-
outputShape: [
|
|
1076
|
-
{ path: "status", type: "number", description: "HTTP status code." },
|
|
1077
|
-
{ path: "headers", type: "object" },
|
|
1078
|
-
{ path: "body", type: "unknown", description: "Decoded JSON when the response is JSON, otherwise the raw body." }
|
|
1079
|
-
],
|
|
1080
|
-
aliases: ["api_request", "@fancy/api_request"],
|
|
1081
|
-
category: "io",
|
|
1082
|
-
label: "API Request",
|
|
1083
|
-
description: "HTTP request to any URL.",
|
|
1084
|
-
icon: "\u2194",
|
|
1085
|
-
configSchema: [
|
|
1086
|
-
...HTTP_METHODS,
|
|
1087
|
-
{ type: "text", key: "url", label: "URL", placeholder: "https://api.example.com/...", required: true },
|
|
1088
|
-
{
|
|
1089
|
-
type: "keyvalue",
|
|
1090
|
-
key: "headers",
|
|
1091
|
-
label: "Headers",
|
|
1092
|
-
keyLabel: "Header",
|
|
1093
|
-
valueLabel: "Value",
|
|
1094
|
-
keyPlaceholder: "content-type",
|
|
1095
|
-
valuePlaceholder: "application/json",
|
|
1096
|
-
addLabel: "Add header",
|
|
1097
|
-
default: { "content-type": "application/json" }
|
|
1098
|
-
},
|
|
1099
|
-
{ type: "json", key: "body", label: "Body" },
|
|
1100
|
-
{ type: "credential", key: "auth", label: "Auth", credentialType: "api_credential" }
|
|
1101
|
-
]
|
|
1102
|
-
},
|
|
1103
|
-
{
|
|
1104
|
-
name: "@particle-academy/webhook_out",
|
|
1105
|
-
aliases: ["webhook_out", "@fancy/webhook_out"],
|
|
1106
|
-
category: "io",
|
|
1107
|
-
label: "Send Webhook",
|
|
1108
|
-
description: "POST a payload to a configured URL.",
|
|
1109
|
-
icon: "\u2197",
|
|
1110
|
-
configSchema: [
|
|
1111
|
-
{ type: "text", key: "url", label: "URL", required: true },
|
|
1112
|
-
{
|
|
1113
|
-
type: "keyvalue",
|
|
1114
|
-
key: "headers",
|
|
1115
|
-
label: "Headers",
|
|
1116
|
-
keyLabel: "Header",
|
|
1117
|
-
valueLabel: "Value",
|
|
1118
|
-
addLabel: "Add header"
|
|
1119
|
-
},
|
|
1120
|
-
{ type: "expression", key: "payload", label: "Payload", required: true, example: "{{ $json }}" }
|
|
1121
|
-
]
|
|
1122
|
-
},
|
|
1123
|
-
// ───────────── Human ─────────────
|
|
1124
|
-
{
|
|
1125
|
-
name: "@particle-academy/human_approval",
|
|
1126
|
-
aliases: ["human_approval", "@fancy/human_approval"],
|
|
1127
|
-
pausesForHuman: "approval",
|
|
1128
|
-
category: "human",
|
|
1129
|
-
label: "Human Approval",
|
|
1130
|
-
description: "Pause until a human approves or denies.",
|
|
1131
|
-
icon: "\u2713",
|
|
1132
|
-
inputs: [{ id: "in" }],
|
|
1133
|
-
outputs: [{ id: "approved", label: "approved" }, { id: "denied", label: "denied" }],
|
|
1134
|
-
configSchema: [
|
|
1135
|
-
{ type: "text", key: "title", label: "Approval title", default: "Approve action" },
|
|
1136
|
-
{ type: "textarea", key: "description", label: "Description for approver", rows: 3 },
|
|
1137
|
-
{ type: "credential", key: "channel", label: "Notify channel", credentialType: "notify_channel" }
|
|
1138
|
-
]
|
|
1139
|
-
},
|
|
1140
|
-
{
|
|
1141
|
-
name: "@particle-academy/notify",
|
|
1142
|
-
aliases: ["notify", "@fancy/notify"],
|
|
1143
|
-
category: "human",
|
|
1144
|
-
label: "Notify",
|
|
1145
|
-
description: "Send a message via Slack / email / SMS / etc.",
|
|
1146
|
-
icon: "\u{1F514}",
|
|
1147
|
-
configSchema: [
|
|
1148
|
-
{
|
|
1149
|
-
type: "select",
|
|
1150
|
-
key: "channel",
|
|
1151
|
-
label: "Channel",
|
|
1152
|
-
default: "slack",
|
|
1153
|
-
options: [
|
|
1154
|
-
{ value: "slack", label: "Slack" },
|
|
1155
|
-
{ value: "email", label: "Email" },
|
|
1156
|
-
{ value: "sms", label: "SMS" },
|
|
1157
|
-
{ value: "discord", label: "Discord" }
|
|
1158
|
-
]
|
|
1159
|
-
},
|
|
1160
|
-
{ type: "text", key: "to", label: "To", required: true },
|
|
1161
|
-
{ type: "expression", key: "message", label: "Message", required: true, example: "{{ $json.summary }}" }
|
|
1162
|
-
]
|
|
1163
|
-
},
|
|
1164
|
-
// ───────────── Output ─────────────
|
|
1165
|
-
{
|
|
1166
|
-
name: "@particle-academy/output",
|
|
1167
|
-
aliases: ["output", "@fancy/output"],
|
|
1168
|
-
category: "output",
|
|
1169
|
-
label: "Output",
|
|
1170
|
-
description: "Terminal node \u2014 captures the workflow's result.",
|
|
1171
|
-
icon: "\u25CF",
|
|
1172
|
-
inputs: [{ id: "in" }],
|
|
1173
|
-
outputs: []
|
|
1174
|
-
},
|
|
1175
|
-
{
|
|
1176
|
-
name: "@particle-academy/log",
|
|
1177
|
-
aliases: ["log", "@fancy/log"],
|
|
1178
|
-
category: "output",
|
|
1179
|
-
label: "Log",
|
|
1180
|
-
description: "Send to the run feed.",
|
|
1181
|
-
icon: "\u2261",
|
|
1182
|
-
inputs: [{ id: "in" }],
|
|
1183
|
-
outputs: [],
|
|
1184
|
-
configSchema: [
|
|
1185
|
-
{
|
|
1186
|
-
type: "select",
|
|
1187
|
-
key: "level",
|
|
1188
|
-
label: "Level",
|
|
1189
|
-
default: "info",
|
|
1190
|
-
options: [{ value: "info", label: "info" }, { value: "warn", label: "warn" }, { value: "error", label: "error" }]
|
|
1191
|
-
},
|
|
1192
|
-
{ type: "expression", key: "message", label: "Message", required: true, example: "{{ $json }}" }
|
|
1193
|
-
]
|
|
1194
|
-
},
|
|
1195
|
-
{
|
|
1196
|
-
// Swimlane — a portless, resizable container. Child nodes are parented into
|
|
1197
|
-
// it; it never runs (the runtime skips the `layout` category). Uses its own
|
|
1198
|
-
// renderer (LaneNode) instead of the default card.
|
|
1199
|
-
name: "@particle-academy/lane",
|
|
1200
|
-
aliases: ["lane", "@fancy/lane"],
|
|
1201
|
-
category: "layout",
|
|
1202
|
-
label: "Lane",
|
|
1203
|
-
description: "A resizable swimlane \u2014 drop nodes into it to group them.",
|
|
1204
|
-
icon: "\u25A4",
|
|
1205
|
-
inputs: [],
|
|
1206
|
-
outputs: [],
|
|
1207
|
-
configSchema: [
|
|
1208
|
-
{ type: "text", key: "title", label: "Title", default: "Lane" },
|
|
1209
|
-
{
|
|
1210
|
-
type: "select",
|
|
1211
|
-
key: "orientation",
|
|
1212
|
-
label: "Orientation",
|
|
1213
|
-
default: "horizontal",
|
|
1214
|
-
options: [{ value: "horizontal", label: "Row" }, { value: "vertical", label: "Column" }]
|
|
1215
|
-
}
|
|
1216
|
-
],
|
|
1217
|
-
defaultConfig: { title: "Lane", orientation: "horizontal" },
|
|
1218
|
-
resizable: { minWidth: 160, minHeight: 72 },
|
|
1219
|
-
component: LaneNode
|
|
1220
|
-
},
|
|
1221
|
-
{
|
|
1222
|
-
// Note — a sticky-note annotation. Portless + visual-only: the runtime skips
|
|
1223
|
-
// the `annotation` category, so a note's text NEVER reaches a runner — it
|
|
1224
|
-
// rides in the document purely for people, editors, and MCP tools. Uses its
|
|
1225
|
-
// own renderer (NoteNode); double-click on the canvas to edit in place.
|
|
1226
|
-
name: "@particle-academy/note",
|
|
1227
|
-
aliases: ["note", "@fancy/note"],
|
|
1228
|
-
category: "annotation",
|
|
1229
|
-
label: "Note",
|
|
1230
|
-
description: "A sticky note that documents the canvas. Never runs \u2014 editor + agent only.",
|
|
1231
|
-
icon: "\u{1F5D2}",
|
|
1232
|
-
inputs: [],
|
|
1233
|
-
outputs: [],
|
|
1234
|
-
configSchema: [
|
|
1235
|
-
{ type: "text", key: "title", label: "Title", placeholder: "Optional heading" },
|
|
1236
|
-
{ type: "textarea", key: "text", label: "Note", rows: 5, placeholder: "What does this part of the flow do?" },
|
|
1237
|
-
{ type: "select", key: "color", label: "Color", default: "amber", options: [
|
|
1238
|
-
{ value: "amber", label: "Amber" },
|
|
1239
|
-
{ value: "sky", label: "Sky" },
|
|
1240
|
-
{ value: "violet", label: "Violet" },
|
|
1241
|
-
{ value: "emerald", label: "Emerald" },
|
|
1242
|
-
{ value: "rose", label: "Rose" },
|
|
1243
|
-
{ value: "slate", label: "Slate" }
|
|
1244
|
-
] }
|
|
1245
|
-
],
|
|
1246
|
-
defaultConfig: { text: "", color: "amber" },
|
|
1247
|
-
resizable: { minWidth: 140, minHeight: 80 },
|
|
1248
|
-
component: NoteNode
|
|
1249
|
-
}
|
|
1250
|
-
];
|
|
1251
|
-
function registerBuiltinKinds() {
|
|
1252
|
-
for (const k of KINDS) registerNodeKind(k);
|
|
1253
|
-
}
|
|
1254
|
-
var BUILTIN_KINDS = KINDS;
|
|
1255
|
-
|
|
1256
|
-
// src/registry/index.ts
|
|
1257
|
-
function buildNodeTypes() {
|
|
1258
|
-
const map = {};
|
|
1259
|
-
for (const k of listNodeKinds()) {
|
|
1260
|
-
const renderer = k.component ?? RegistryNode;
|
|
1261
|
-
for (const id of kindIds(k)) map[id] = renderer;
|
|
1262
|
-
}
|
|
1263
|
-
return map;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
export { ANY_PORT_TYPE, BUILTIN_KINDS, DEFAULT_MAX_DEPTH, FlowEditorProvider, LaneNode, NoteNode, RegistryNode, buildNodeTypes, createConnectionValidator, declaredRoutes, defaultPortCompatibility, llmRouterExecutor, registerBuiltinKinds, resolveFallbackPort, subflowExecutor, subflowMode, subflowPorts, useFlowEditor, useFlowEditorOptional };
|
|
1267
|
-
//# sourceMappingURL=chunk-WBXGZVLB.js.map
|
|
1268
|
-
//# sourceMappingURL=chunk-WBXGZVLB.js.map
|