@particle-academy/fancy-flow 0.65.2 → 0.66.1
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 +100 -3
- package/dist/{FlowViewer-BI_AyyvU.d.ts → FlowViewer-BCc_BvuD.d.ts} +1 -1
- package/dist/{FlowViewer-DABx-6A-.d.cts → FlowViewer-CoAJ3o52.d.cts} +1 -1
- package/dist/{HumanPrompt-Bpp3JiVH.d.cts → HumanPrompt--pXDOyYC.d.cts} +2 -2
- package/dist/{HumanPrompt-BKThiyjB.d.ts → HumanPrompt-DXm_FP9z.d.ts} +2 -2
- package/dist/chunk-27RZJAE2.js +2586 -0
- package/dist/chunk-27RZJAE2.js.map +1 -0
- package/dist/{chunk-MBVX4ZRB.js → chunk-2QTEELJE.js} +4 -3
- package/dist/chunk-2QTEELJE.js.map +1 -0
- package/dist/chunk-2QXKDLGT.js +58 -0
- package/dist/chunk-2QXKDLGT.js.map +1 -0
- package/dist/{chunk-W5DPKJY4.js → chunk-6AYC3BWI.js} +4 -4
- package/dist/chunk-6AYC3BWI.js.map +1 -0
- package/dist/{chunk-5H54OTKT.js → chunk-6TH646CS.js} +127 -5
- package/dist/chunk-6TH646CS.js.map +1 -0
- package/dist/{chunk-EO6444T2.js → chunk-BUNFPBTF.js} +5 -4
- package/dist/chunk-BUNFPBTF.js.map +1 -0
- package/dist/{chunk-77V4QC6Y.js → chunk-FV3D366L.js} +3 -3
- package/dist/{chunk-77V4QC6Y.js.map → chunk-FV3D366L.js.map} +1 -1
- package/dist/{chunk-JF6WCRBU.js → chunk-OWENS2H5.js} +36 -1905
- package/dist/chunk-OWENS2H5.js.map +1 -0
- package/dist/{chunk-RIAFHQT5.js → chunk-RE3XNVSB.js} +3 -3
- package/dist/{chunk-RIAFHQT5.js.map → chunk-RE3XNVSB.js.map} +1 -1
- package/dist/connectors.d.cts +2 -2
- package/dist/connectors.d.ts +2 -2
- package/dist/durable/index.d.cts +2 -2
- package/dist/durable/index.d.ts +2 -2
- package/dist/durable.cjs +865 -9135
- package/dist/durable.cjs.map +1 -1
- package/dist/durable.js +2 -4
- package/dist/durable.js.map +1 -1
- package/dist/engine.cjs +927 -9278
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +6 -7
- package/dist/engine.d.ts +6 -7
- package/dist/engine.js +5 -7
- package/dist/engine.js.map +1 -1
- package/dist/fields/react-fancy.d.cts +3 -3
- package/dist/fields/react-fancy.d.ts +3 -3
- package/dist/index.cjs +11009 -10282
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -36
- package/dist/index.d.ts +65 -36
- package/dist/index.js +22 -19
- 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.cjs.map +1 -1
- package/dist/llm/prism.d.cts +1 -2
- package/dist/llm/prism.d.ts +1 -2
- package/dist/llm/prism.js +1 -1
- package/dist/llm/vercel-ai.cjs.map +1 -1
- package/dist/llm/vercel-ai.d.cts +1 -2
- package/dist/llm/vercel-ai.d.ts +1 -2
- package/dist/llm/vercel-ai.js +1 -1
- package/dist/registry/index.d.cts +20 -9
- package/dist/registry/index.d.ts +20 -9
- package/dist/{registry-CntAdUcY.d.cts → registry-BcgllSk2.d.cts} +1 -1
- package/dist/{registry-DYoV-MQi.d.ts → registry-h-2QswZ5.d.ts} +1 -1
- package/dist/registry.cjs +11252 -10409
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +5 -4
- package/dist/{run-cohort-C1nRh_mi.d.ts → run-cohort-CjUBWg6X.d.ts} +2 -2
- package/dist/{run-cohort-CBRAUj-z.d.cts → run-cohort-DL9JzzPU.d.cts} +2 -2
- package/dist/{run-flow-2XsHBwpd.d.cts → run-flow-B_8hgO5_.d.cts} +1 -1
- package/dist/{run-flow-D7AOCcfE.d.ts → run-flow-CxEGBOxd.d.ts} +1 -1
- package/dist/runtime/index.d.cts +5 -5
- package/dist/runtime/index.d.ts +5 -5
- package/dist/runtime.cjs +10843 -10194
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +5 -5
- package/dist/schema/index.d.cts +19 -6
- package/dist/schema/index.d.ts +19 -6
- package/dist/schema.cjs +881 -9142
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +4 -5
- package/dist/screens.cjs +842 -86
- 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 +7 -6
- package/dist/screens.js.map +1 -1
- package/dist/terminal/fancy-term-host.cjs +99 -0
- package/dist/terminal/fancy-term-host.cjs.map +1 -0
- package/dist/terminal/fancy-term-host.d.cts +76 -0
- package/dist/terminal/fancy-term-host.d.ts +76 -0
- package/dist/terminal/fancy-term-host.js +92 -0
- package/dist/terminal/fancy-term-host.js.map +1 -0
- package/dist/types-B-Syk9-M.d.cts +620 -0
- package/dist/types-B-Syk9-M.d.ts +620 -0
- package/dist/{types-D71SKA5A.d.cts → types-C2ATTmjN.d.cts} +1 -1
- package/dist/{types-Ckhz-YwC.d.ts → types-VunLGqrn.d.ts} +1 -1
- package/dist/ux.cjs +885 -9142
- package/dist/ux.cjs.map +1 -1
- package/dist/ux.d.cts +2 -2
- package/dist/ux.d.ts +2 -2
- package/dist/ux.js +2 -3
- package/dist/ux.js.map +1 -1
- package/package.json +11 -1
- package/dist/capabilities-BYa5p5jw.d.cts +0 -112
- package/dist/capabilities-COOXRiNL.d.ts +0 -112
- package/dist/chunk-5H54OTKT.js.map +0 -1
- package/dist/chunk-EO6444T2.js.map +0 -1
- package/dist/chunk-JF6WCRBU.js.map +0 -1
- package/dist/chunk-MBVX4ZRB.js.map +0 -1
- package/dist/chunk-UM4C46AF.js +0 -103
- package/dist/chunk-UM4C46AF.js.map +0 -1
- package/dist/chunk-USL4FMFU.js +0 -41
- package/dist/chunk-USL4FMFU.js.map +0 -1
- package/dist/chunk-W5DPKJY4.js.map +0 -1
- package/dist/types-JFYjPJAG.d.cts +0 -333
- package/dist/types-JFYjPJAG.d.ts +0 -333
|
@@ -1,27 +1,7 @@
|
|
|
1
|
-
import { RichInputPreview } from './chunk-F5RPRB7A.js';
|
|
2
|
-
import { getLlmClient, getWorkflowResolver, isResolutionFailure } from './chunk-USL4FMFU.js';
|
|
3
|
-
import ReactExports, { createContext, forwardRef, memo, useMemo, useState, useRef, useEffect, useCallback, useContext, createElement, useSyncExternalStore, useDebugValue, useLayoutEffect } from 'react';
|
|
4
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import ReactExports, { createContext, forwardRef, memo, useMemo, useState, useRef, useEffect, useCallback, useContext, useSyncExternalStore, useDebugValue, useLayoutEffect } from 'react';
|
|
5
3
|
import { createPortal } from 'react-dom';
|
|
6
4
|
|
|
7
|
-
// node_modules/classcat/index.js
|
|
8
|
-
function cc(names) {
|
|
9
|
-
if (typeof names === "string" || typeof names === "number") return "" + names;
|
|
10
|
-
let out = "";
|
|
11
|
-
if (Array.isArray(names)) {
|
|
12
|
-
for (let i = 0, tmp; i < names.length; i++) {
|
|
13
|
-
if ((tmp = cc(names[i])) !== "") {
|
|
14
|
-
out += (out && " ") + tmp;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
} else {
|
|
18
|
-
for (let k in names) {
|
|
19
|
-
if (names[k]) out += (out && " ") + k;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return out;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
5
|
// node_modules/d3-dispatch/src/dispatch.js
|
|
26
6
|
var noop = { value: () => {
|
|
27
7
|
} };
|
|
@@ -993,7 +973,7 @@ function defaultTouchable() {
|
|
|
993
973
|
return navigator.maxTouchPoints || "ontouchstart" in this;
|
|
994
974
|
}
|
|
995
975
|
function drag_default() {
|
|
996
|
-
var filter2 = defaultFilter, container = defaultContainer, subject = defaultSubject, touchable = defaultTouchable, gestures = {},
|
|
976
|
+
var filter2 = defaultFilter, container = defaultContainer, subject = defaultSubject, touchable = defaultTouchable, gestures = {}, listeners = dispatch_default("start", "drag", "end"), active = 0, mousedownx, mousedowny, mousemoving, touchending, clickDistance2 = 0;
|
|
997
977
|
function drag(selection2) {
|
|
998
978
|
selection2.on("mousedown.drag", mousedowned).filter(touchable).on("touchstart.drag", touchstarted).on("touchmove.drag", touchmoved, nonpassive).on("touchend.drag touchcancel.drag", touchended).style("touch-action", "none").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
|
|
999
979
|
}
|
|
@@ -1056,7 +1036,7 @@ function drag_default() {
|
|
|
1056
1036
|
}
|
|
1057
1037
|
}
|
|
1058
1038
|
function beforestart(that, container2, event, d, identifier, touch) {
|
|
1059
|
-
var dispatch2 =
|
|
1039
|
+
var dispatch2 = listeners.copy(), p = pointer_default(touch || event, container2), dx, dy, s;
|
|
1060
1040
|
if ((s = subject.call(that, new DragEvent("beforestart", {
|
|
1061
1041
|
sourceEvent: event,
|
|
1062
1042
|
target: drag,
|
|
@@ -1115,8 +1095,8 @@ function drag_default() {
|
|
|
1115
1095
|
return arguments.length ? (touchable = typeof _ === "function" ? _ : constant_default2(!!_), drag) : touchable;
|
|
1116
1096
|
};
|
|
1117
1097
|
drag.on = function() {
|
|
1118
|
-
var value =
|
|
1119
|
-
return value ===
|
|
1098
|
+
var value = listeners.on.apply(listeners, arguments);
|
|
1099
|
+
return value === listeners ? drag : value;
|
|
1120
1100
|
};
|
|
1121
1101
|
drag.clickDistance = function(_) {
|
|
1122
1102
|
return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);
|
|
@@ -2670,7 +2650,7 @@ function defaultConstrain(transform2, extent, translateExtent) {
|
|
|
2670
2650
|
);
|
|
2671
2651
|
}
|
|
2672
2652
|
function zoom_default2() {
|
|
2673
|
-
var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta2 = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default,
|
|
2653
|
+
var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta2 = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners = dispatch_default("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
|
|
2674
2654
|
function zoom(selection2) {
|
|
2675
2655
|
selection2.property("__zoom", defaultTransform).on("wheel.zoom", wheeled, { passive: false }).on("mousedown.zoom", mousedowned).on("dblclick.zoom", dblclicked).filter(touchable).on("touchstart.zoom", touchstarted).on("touchmove.zoom", touchmoved).on("touchend.zoom touchcancel.zoom", touchended).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
|
|
2676
2656
|
}
|
|
@@ -2782,14 +2762,14 @@ function zoom_default2() {
|
|
|
2782
2762
|
},
|
|
2783
2763
|
emit: function(type) {
|
|
2784
2764
|
var d = select_default2(this.that).datum();
|
|
2785
|
-
|
|
2765
|
+
listeners.call(
|
|
2786
2766
|
type,
|
|
2787
2767
|
this.that,
|
|
2788
2768
|
new ZoomEvent(type, {
|
|
2789
2769
|
sourceEvent: this.sourceEvent,
|
|
2790
2770
|
target: zoom,
|
|
2791
2771
|
transform: this.that.__zoom,
|
|
2792
|
-
dispatch:
|
|
2772
|
+
dispatch: listeners
|
|
2793
2773
|
}),
|
|
2794
2774
|
d
|
|
2795
2775
|
);
|
|
@@ -2939,8 +2919,8 @@ function zoom_default2() {
|
|
|
2939
2919
|
return arguments.length ? (interpolate = _, zoom) : interpolate;
|
|
2940
2920
|
};
|
|
2941
2921
|
zoom.on = function() {
|
|
2942
|
-
var value =
|
|
2943
|
-
return value ===
|
|
2922
|
+
var value = listeners.on.apply(listeners, arguments);
|
|
2923
|
+
return value === listeners ? zoom : value;
|
|
2944
2924
|
};
|
|
2945
2925
|
zoom.clickDistance = function(_) {
|
|
2946
2926
|
return arguments.length ? (clickDistance2 = (_ = +_) * _, zoom) : Math.sqrt(clickDistance2);
|
|
@@ -5611,6 +5591,24 @@ function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }) {
|
|
|
5611
5591
|
destroy
|
|
5612
5592
|
};
|
|
5613
5593
|
}
|
|
5594
|
+
|
|
5595
|
+
// node_modules/classcat/index.js
|
|
5596
|
+
function cc(names) {
|
|
5597
|
+
if (typeof names === "string" || typeof names === "number") return "" + names;
|
|
5598
|
+
let out = "";
|
|
5599
|
+
if (Array.isArray(names)) {
|
|
5600
|
+
for (let i = 0, tmp; i < names.length; i++) {
|
|
5601
|
+
if ((tmp = cc(names[i])) !== "") {
|
|
5602
|
+
out += (out && " ") + tmp;
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
} else {
|
|
5606
|
+
for (let k in names) {
|
|
5607
|
+
if (names[k]) out += (out && " ") + k;
|
|
5608
|
+
}
|
|
5609
|
+
}
|
|
5610
|
+
return out;
|
|
5611
|
+
}
|
|
5614
5612
|
function useSyncExternalStoreWithSelector(subscribe, getSnapshot, getServerSnapshot, selector2, isEqual) {
|
|
5615
5613
|
const instRef = useRef(null);
|
|
5616
5614
|
let inst;
|
|
@@ -5671,20 +5669,20 @@ var use_sync_external_store_with_selector_default = { useSyncExternalStoreWithSe
|
|
|
5671
5669
|
// node_modules/zustand/esm/vanilla.mjs
|
|
5672
5670
|
var createStoreImpl = (createState) => {
|
|
5673
5671
|
let state;
|
|
5674
|
-
const
|
|
5672
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
5675
5673
|
const setState = (partial, replace) => {
|
|
5676
5674
|
const nextState = typeof partial === "function" ? partial(state) : partial;
|
|
5677
5675
|
if (!Object.is(nextState, state)) {
|
|
5678
5676
|
const previousState = state;
|
|
5679
5677
|
state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
|
|
5680
|
-
|
|
5678
|
+
listeners.forEach((listener) => listener(state, previousState));
|
|
5681
5679
|
}
|
|
5682
5680
|
};
|
|
5683
5681
|
const getState = () => state;
|
|
5684
5682
|
const getInitialState2 = () => initialState;
|
|
5685
5683
|
const subscribe = (listener) => {
|
|
5686
|
-
|
|
5687
|
-
return () =>
|
|
5684
|
+
listeners.add(listener);
|
|
5685
|
+
return () => listeners.delete(listener);
|
|
5688
5686
|
};
|
|
5689
5687
|
const destroy = () => {
|
|
5690
5688
|
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
|
|
@@ -5692,7 +5690,7 @@ var createStoreImpl = (createState) => {
|
|
|
5692
5690
|
"[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."
|
|
5693
5691
|
);
|
|
5694
5692
|
}
|
|
5695
|
-
|
|
5693
|
+
listeners.clear();
|
|
5696
5694
|
};
|
|
5697
5695
|
const api = { setState, getState, getInitialState: getInitialState2, subscribe, destroy };
|
|
5698
5696
|
const initialState = state = createState(setState, getState, api);
|
|
@@ -9038,1873 +9036,6 @@ function NodeToolbar({ nodeId, children: children2, className, style: style2, is
|
|
|
9038
9036
|
return jsx(NodeToolbarPortal, { children: jsx("div", { style: wrapperStyle2, className: cc(["react-flow__node-toolbar", className]), ...rest, "data-id": nodesArray.reduce((acc, node) => `${acc}${node.id} `, "").trim(), children: children2 }) });
|
|
9039
9037
|
}
|
|
9040
9038
|
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
string: "text",
|
|
9045
|
-
str: "text",
|
|
9046
|
-
input: "text",
|
|
9047
|
-
textarea: "textarea",
|
|
9048
|
-
long_text: "textarea",
|
|
9049
|
-
longtext: "textarea",
|
|
9050
|
-
"long-text": "textarea",
|
|
9051
|
-
multiline: "textarea",
|
|
9052
|
-
paragraph: "textarea",
|
|
9053
|
-
markdown: "textarea",
|
|
9054
|
-
number: "number",
|
|
9055
|
-
numeric: "number",
|
|
9056
|
-
integer: "number",
|
|
9057
|
-
int: "number",
|
|
9058
|
-
float: "number",
|
|
9059
|
-
decimal: "number",
|
|
9060
|
-
select: "select",
|
|
9061
|
-
enum: "select",
|
|
9062
|
-
choice: "select",
|
|
9063
|
-
choices: "select",
|
|
9064
|
-
dropdown: "select",
|
|
9065
|
-
options: "select",
|
|
9066
|
-
radio: "select",
|
|
9067
|
-
switch: "switch",
|
|
9068
|
-
bool: "switch",
|
|
9069
|
-
boolean: "switch",
|
|
9070
|
-
checkbox: "switch",
|
|
9071
|
-
toggle: "switch",
|
|
9072
|
-
date: "date",
|
|
9073
|
-
datetime: "datetime",
|
|
9074
|
-
"datetime-local": "datetime",
|
|
9075
|
-
datetimelocal: "datetime",
|
|
9076
|
-
timestamp: "datetime",
|
|
9077
|
-
time: "time",
|
|
9078
|
-
email: "email",
|
|
9079
|
-
"e-mail": "email",
|
|
9080
|
-
url: "url",
|
|
9081
|
-
uri: "url",
|
|
9082
|
-
link: "url",
|
|
9083
|
-
tel: "tel",
|
|
9084
|
-
phone: "tel",
|
|
9085
|
-
telephone: "tel",
|
|
9086
|
-
password: "password",
|
|
9087
|
-
secret: "password"
|
|
9088
|
-
};
|
|
9089
|
-
function humanFieldType(raw) {
|
|
9090
|
-
if (typeof raw !== "string") return "text";
|
|
9091
|
-
return HUMAN_FIELD_TYPE_ALIASES[raw.trim().toLowerCase()] ?? "text";
|
|
9092
|
-
}
|
|
9093
|
-
function humanFieldOptions(raw) {
|
|
9094
|
-
const entries = [];
|
|
9095
|
-
if (Array.isArray(raw)) {
|
|
9096
|
-
for (const item of raw) {
|
|
9097
|
-
if (typeof item === "string" || typeof item === "number") {
|
|
9098
|
-
const value = String(item);
|
|
9099
|
-
if (value !== "") entries.push({ value, label: value });
|
|
9100
|
-
continue;
|
|
9101
|
-
}
|
|
9102
|
-
if (item && typeof item === "object") {
|
|
9103
|
-
const value = item.value;
|
|
9104
|
-
if (value === void 0 || value === null || value === "") continue;
|
|
9105
|
-
const label = item.label;
|
|
9106
|
-
entries.push({
|
|
9107
|
-
value: String(value),
|
|
9108
|
-
label: typeof label === "string" && label !== "" ? label : String(value)
|
|
9109
|
-
});
|
|
9110
|
-
}
|
|
9111
|
-
}
|
|
9112
|
-
} else if (raw && typeof raw === "object") {
|
|
9113
|
-
for (const [value, label] of Object.entries(raw)) {
|
|
9114
|
-
if (value === "") continue;
|
|
9115
|
-
entries.push({ value, label: typeof label === "string" && label !== "" ? label : value });
|
|
9116
|
-
}
|
|
9117
|
-
}
|
|
9118
|
-
return entries.length ? entries : void 0;
|
|
9119
|
-
}
|
|
9120
|
-
function humanInputFields(config) {
|
|
9121
|
-
const raw = Array.isArray(config?.fields) ? config.fields : [];
|
|
9122
|
-
const fields = raw.filter((f) => f && typeof f === "object" && typeof f.key === "string" && f.key).map((f) => ({
|
|
9123
|
-
key: f.key,
|
|
9124
|
-
label: typeof f.label === "string" && f.label ? f.label : f.key,
|
|
9125
|
-
type: humanFieldType(f.type),
|
|
9126
|
-
required: !!f.required,
|
|
9127
|
-
placeholder: typeof f.placeholder === "string" ? f.placeholder : void 0,
|
|
9128
|
-
options: humanFieldOptions(f.options ?? f.choices),
|
|
9129
|
-
default: f.default
|
|
9130
|
-
}));
|
|
9131
|
-
if (fields.length) return fields;
|
|
9132
|
-
const title = typeof config?.title === "string" && config.title ? config.title : "Your answer";
|
|
9133
|
-
return [{ key: "value", label: title, type: "textarea", required: true }];
|
|
9134
|
-
}
|
|
9135
|
-
var FlowEditorContext = createContext(null);
|
|
9136
|
-
var FlowEditorProvider = FlowEditorContext.Provider;
|
|
9137
|
-
function useFlowEditor() {
|
|
9138
|
-
const api = useContext(FlowEditorContext);
|
|
9139
|
-
if (api === null) {
|
|
9140
|
-
throw new Error("useFlowEditor() must be called inside <FlowEditor>.");
|
|
9141
|
-
}
|
|
9142
|
-
return api;
|
|
9143
|
-
}
|
|
9144
|
-
function useFlowEditorOptional() {
|
|
9145
|
-
return useContext(FlowEditorContext);
|
|
9146
|
-
}
|
|
9147
|
-
var NOTE_COLORS = {
|
|
9148
|
-
amber: { bg: "rgba(234, 179, 8, 0.14)", border: "rgba(234, 179, 8, 0.55)", title: "#a16207" },
|
|
9149
|
-
sky: { bg: "rgba(14, 165, 233, 0.12)", border: "rgba(14, 165, 233, 0.5)", title: "#0369a1" },
|
|
9150
|
-
violet: { bg: "rgba(139, 92, 246, 0.13)", border: "rgba(139, 92, 246, 0.5)", title: "#6d28d9" },
|
|
9151
|
-
emerald: { bg: "rgba(16, 185, 129, 0.13)", border: "rgba(16, 185, 129, 0.5)", title: "#047857" },
|
|
9152
|
-
rose: { bg: "rgba(244, 63, 94, 0.12)", border: "rgba(244, 63, 94, 0.5)", title: "#be123c" },
|
|
9153
|
-
slate: { bg: "rgba(100, 116, 139, 0.12)", border: "rgba(100, 116, 139, 0.5)", title: "#475569" }
|
|
9154
|
-
};
|
|
9155
|
-
function coerceColor(v) {
|
|
9156
|
-
return typeof v === "string" && v in NOTE_COLORS ? v : "amber";
|
|
9157
|
-
}
|
|
9158
|
-
function NoteNodeInner(props) {
|
|
9159
|
-
const data = props.data;
|
|
9160
|
-
const config = data.config ?? {};
|
|
9161
|
-
const api = useFlowEditorOptional();
|
|
9162
|
-
const hasConfigText = typeof config.text === "string" && config.text.length > 0;
|
|
9163
|
-
const text = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
|
|
9164
|
-
const title = typeof config.title === "string" && config.title || (!hasConfigText && typeof data.body === "string" && typeof data.label === "string" ? data.label : "");
|
|
9165
|
-
const skin = NOTE_COLORS[coerceColor(config.color)];
|
|
9166
|
-
const [editing, setEditing] = useState(false);
|
|
9167
|
-
const [draft, setDraft] = useState(text);
|
|
9168
|
-
const areaRef = useRef(null);
|
|
9169
|
-
useEffect(() => {
|
|
9170
|
-
if (editing && areaRef.current) {
|
|
9171
|
-
const el = areaRef.current;
|
|
9172
|
-
el.focus();
|
|
9173
|
-
el.setSelectionRange(el.value.length, el.value.length);
|
|
9174
|
-
}
|
|
9175
|
-
}, [editing]);
|
|
9176
|
-
const commit = () => {
|
|
9177
|
-
setEditing(false);
|
|
9178
|
-
if (!api || draft === text) return;
|
|
9179
|
-
const current = api.nodes.find((n) => n.id === props.id);
|
|
9180
|
-
if (!current) return;
|
|
9181
|
-
api.updateNode({
|
|
9182
|
-
...current,
|
|
9183
|
-
data: { ...current.data, config: { ...config, text: draft } }
|
|
9184
|
-
});
|
|
9185
|
-
};
|
|
9186
|
-
return /* @__PURE__ */ jsxs(
|
|
9187
|
-
"div",
|
|
9188
|
-
{
|
|
9189
|
-
className: ["ff-note", props.selected ? "ff-note--selected" : ""].filter(Boolean).join(" "),
|
|
9190
|
-
style: { background: skin.bg, borderColor: skin.border },
|
|
9191
|
-
onDoubleClick: api ? () => {
|
|
9192
|
-
setDraft(text);
|
|
9193
|
-
setEditing(true);
|
|
9194
|
-
} : void 0,
|
|
9195
|
-
children: [
|
|
9196
|
-
/* @__PURE__ */ jsx(NodeResizer, { isVisible: props.selected ?? false, color: skin.border, minWidth: 140, minHeight: 80 }),
|
|
9197
|
-
title && /* @__PURE__ */ jsx("div", { className: "ff-note__title", style: { color: skin.title }, children: title }),
|
|
9198
|
-
editing ? /* @__PURE__ */ jsx(
|
|
9199
|
-
"textarea",
|
|
9200
|
-
{
|
|
9201
|
-
ref: areaRef,
|
|
9202
|
-
className: "ff-note__input nodrag nowheel",
|
|
9203
|
-
value: draft,
|
|
9204
|
-
onChange: (e) => setDraft(e.target.value),
|
|
9205
|
-
onBlur: commit,
|
|
9206
|
-
onKeyDown: (e) => {
|
|
9207
|
-
if (e.key === "Escape") setEditing(false);
|
|
9208
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commit();
|
|
9209
|
-
}
|
|
9210
|
-
}
|
|
9211
|
-
) : /* @__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" }) })
|
|
9212
|
-
]
|
|
9213
|
-
}
|
|
9214
|
-
);
|
|
9215
|
-
}
|
|
9216
|
-
var NoteNode = memo(NoteNodeInner);
|
|
9217
|
-
|
|
9218
|
-
// src/registry/llm-router.ts
|
|
9219
|
-
function declaredRoutes(config) {
|
|
9220
|
-
const raw = config.routes;
|
|
9221
|
-
if (!Array.isArray(raw)) return [];
|
|
9222
|
-
return raw.map((r) => ({ port: String(r?.port ?? "").trim(), description: r?.description })).filter((r) => r.port !== "");
|
|
9223
|
-
}
|
|
9224
|
-
function resolveFallbackPort(routes, fallbackEnabled) {
|
|
9225
|
-
if (fallbackEnabled) return "fallback";
|
|
9226
|
-
return routes[0]?.port ?? "out";
|
|
9227
|
-
}
|
|
9228
|
-
var llmRouterExecutor = async (ctx) => {
|
|
9229
|
-
const config = ctx.node.data?.config ?? {};
|
|
9230
|
-
const routes = declaredRoutes(config);
|
|
9231
|
-
if (routes.length === 0) {
|
|
9232
|
-
ctx.abort("llm_router has no routes configured");
|
|
9233
|
-
}
|
|
9234
|
-
const client = getLlmClient();
|
|
9235
|
-
if (!client) {
|
|
9236
|
-
ctx.abort(
|
|
9237
|
-
"No LLM client registered. Call registerLlmClient() with your provider adapter \u2014 fancy-flow ships the routing, not the model call."
|
|
9238
|
-
);
|
|
9239
|
-
}
|
|
9240
|
-
const fallbackEnabled = config.fallback !== false;
|
|
9241
|
-
const choice = await client.chooseRoute({
|
|
9242
|
-
system: typeof config.system === "string" ? config.system : void 0,
|
|
9243
|
-
prompt: String(config.prompt ?? ctx.inputs ?? ""),
|
|
9244
|
-
routes,
|
|
9245
|
-
provider: typeof config.provider === "string" ? config.provider : void 0,
|
|
9246
|
-
model: typeof config.model === "string" ? config.model : void 0,
|
|
9247
|
-
credential: typeof config.credential === "string" ? config.credential : void 0
|
|
9248
|
-
});
|
|
9249
|
-
const offered = new Set(routes.map((r) => r.port));
|
|
9250
|
-
let port = choice?.port ?? "";
|
|
9251
|
-
let reason = choice?.reason;
|
|
9252
|
-
if (!offered.has(port)) {
|
|
9253
|
-
const safe = resolveFallbackPort(routes, fallbackEnabled);
|
|
9254
|
-
ctx.emit({
|
|
9255
|
-
type: "log",
|
|
9256
|
-
nodeId: ctx.node.id,
|
|
9257
|
-
level: "warn",
|
|
9258
|
-
message: `llm_router: model returned "${port || "(nothing)"}", which is not a declared route. Routing to "${safe}".`
|
|
9259
|
-
});
|
|
9260
|
-
reason = reason ?? `unrecognised route "${port}"`;
|
|
9261
|
-
port = safe;
|
|
9262
|
-
}
|
|
9263
|
-
return { __port: port, value: { route: port, reason, input: ctx.inputs } };
|
|
9264
|
-
};
|
|
9265
|
-
|
|
9266
|
-
// src/registry/ports.ts
|
|
9267
|
-
function resolvePortSpec(spec, config) {
|
|
9268
|
-
if (spec === void 0) return void 0;
|
|
9269
|
-
if (typeof spec !== "function") return spec;
|
|
9270
|
-
try {
|
|
9271
|
-
const resolved = spec(config);
|
|
9272
|
-
return Array.isArray(resolved) ? resolved : void 0;
|
|
9273
|
-
} catch {
|
|
9274
|
-
return void 0;
|
|
9275
|
-
}
|
|
9276
|
-
}
|
|
9277
|
-
function nodeConfig(node) {
|
|
9278
|
-
return node.data?.config ?? {};
|
|
9279
|
-
}
|
|
9280
|
-
function resolveNodePorts(node, kind) {
|
|
9281
|
-
const config = nodeConfig(node);
|
|
9282
|
-
const data = node.data;
|
|
9283
|
-
return {
|
|
9284
|
-
inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
|
|
9285
|
-
outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
|
|
9286
|
-
};
|
|
9287
|
-
}
|
|
9288
|
-
|
|
9289
|
-
// src/runtime/run-identity.ts
|
|
9290
|
-
function escapeSegment(value) {
|
|
9291
|
-
return value.replace(/%/g, "%25").replace(/\//g, "%2F").replace(/#/g, "%23");
|
|
9292
|
-
}
|
|
9293
|
-
function renderSegment(value, occurrence) {
|
|
9294
|
-
const escaped = escapeSegment(value);
|
|
9295
|
-
return occurrence === void 0 || occurrence === null ? escaped : `${escaped}#${occurrence}`;
|
|
9296
|
-
}
|
|
9297
|
-
function instant(value) {
|
|
9298
|
-
const ms = Date.parse(value);
|
|
9299
|
-
if (Number.isNaN(ms)) {
|
|
9300
|
-
throw new Error(
|
|
9301
|
-
`RunIdentity: firstAttemptAt is not a parseable timestamp: ${JSON.stringify(value)}`
|
|
9302
|
-
);
|
|
9303
|
-
}
|
|
9304
|
-
return ms;
|
|
9305
|
-
}
|
|
9306
|
-
var RunIdentity = class _RunIdentity {
|
|
9307
|
-
constructor(runKey, path = [], attempt = 1, firstAttemptAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
9308
|
-
if (!runKey || runKey.trim() === "") {
|
|
9309
|
-
throw new Error("RunIdentity: runKey must be a non-empty string.");
|
|
9310
|
-
}
|
|
9311
|
-
this.runKey = runKey;
|
|
9312
|
-
this.path = Object.freeze([...path]);
|
|
9313
|
-
this.attempt = Math.max(1, Math.trunc(attempt));
|
|
9314
|
-
this.firstAttemptAt = firstAttemptAt;
|
|
9315
|
-
Object.freeze(this);
|
|
9316
|
-
}
|
|
9317
|
-
/**
|
|
9318
|
-
* The identity of one execution of one node — stable across retries of that
|
|
9319
|
-
* execution, distinct from every other execution of the same node.
|
|
9320
|
-
*
|
|
9321
|
-
* Pass `occurrence` when an executor runs the same node more than once at the
|
|
9322
|
-
* same level (a loop body, one item of a fan-out it drives itself).
|
|
9323
|
-
*/
|
|
9324
|
-
stepKey(nodeId, occurrence) {
|
|
9325
|
-
return `${this.runKey}:${[...this.path, renderSegment(nodeId, occurrence)].join("/")}`;
|
|
9326
|
-
}
|
|
9327
|
-
/**
|
|
9328
|
-
* A child identity for work nested inside this step.
|
|
9329
|
-
*
|
|
9330
|
-
* `subflow` pushes the invoking node's id, so a node inside the child graph
|
|
9331
|
-
* cannot collide with a same-named node in the parent. Attempt and
|
|
9332
|
-
* `firstAttemptAt` are carried down unchanged: the nested work happens inside
|
|
9333
|
-
* this step's attempt, and shares its clock.
|
|
9334
|
-
*/
|
|
9335
|
-
descend(segment, occurrence) {
|
|
9336
|
-
return new _RunIdentity(
|
|
9337
|
-
this.runKey,
|
|
9338
|
-
[...this.path, renderSegment(segment, occurrence)],
|
|
9339
|
-
this.attempt,
|
|
9340
|
-
this.firstAttemptAt
|
|
9341
|
-
);
|
|
9342
|
-
}
|
|
9343
|
-
/** A copy on a different attempt, first-attempt clock preserved. */
|
|
9344
|
-
withAttempt(attempt, firstAttemptAt) {
|
|
9345
|
-
return new _RunIdentity(
|
|
9346
|
-
this.runKey,
|
|
9347
|
-
this.path,
|
|
9348
|
-
attempt,
|
|
9349
|
-
firstAttemptAt ?? this.firstAttemptAt
|
|
9350
|
-
);
|
|
9351
|
-
}
|
|
9352
|
-
/**
|
|
9353
|
-
* May this attempt reuse the step key and still be deduplicated?
|
|
9354
|
-
*
|
|
9355
|
-
* Providers forget idempotency keys — Stripe after 24 hours. Past that
|
|
9356
|
-
* window, resending the key creates a second charge and sending a fresh one
|
|
9357
|
-
* creates a second charge, so **the caller must refuse rather than pick
|
|
9358
|
-
* between them**: a loud stuck run beats a silent double write.
|
|
9359
|
-
*
|
|
9360
|
-
* `true` on attempt 1 whatever the elapsed time — nothing was sent on an
|
|
9361
|
-
* earlier attempt, so there is nothing for the provider to have forgotten.
|
|
9362
|
-
* That is what lets a run park on a human gate for a week and then write.
|
|
9363
|
-
*
|
|
9364
|
-
* `windowSeconds: null` means the provider does not expire keys. `0` means
|
|
9365
|
-
* it does not dedupe at all, so no retry may reuse a key — it is a real
|
|
9366
|
-
* window, not an absent one, and the two must not be conflated: reading `0`
|
|
9367
|
-
* as `null` turns "this provider does not dedupe" into "this provider
|
|
9368
|
-
* dedupes forever", which is the more dangerous of the two by a distance.
|
|
9369
|
-
*/
|
|
9370
|
-
isReplaySafe(windowSeconds, now2 = /* @__PURE__ */ new Date()) {
|
|
9371
|
-
if (this.attempt <= 1) return true;
|
|
9372
|
-
if (windowSeconds === null || windowSeconds === void 0) return true;
|
|
9373
|
-
if (windowSeconds <= 0) return false;
|
|
9374
|
-
const nowMs = typeof now2 === "string" ? instant(now2) : now2.getTime();
|
|
9375
|
-
const elapsedSeconds = Math.max(0, (nowMs - instant(this.firstAttemptAt)) / 1e3);
|
|
9376
|
-
return elapsedSeconds <= windowSeconds;
|
|
9377
|
-
}
|
|
9378
|
-
toJSON() {
|
|
9379
|
-
return {
|
|
9380
|
-
runKey: this.runKey,
|
|
9381
|
-
path: [...this.path],
|
|
9382
|
-
attempt: this.attempt,
|
|
9383
|
-
firstAttemptAt: this.firstAttemptAt
|
|
9384
|
-
};
|
|
9385
|
-
}
|
|
9386
|
-
/** Rebuild from a queue payload. */
|
|
9387
|
-
static from(value) {
|
|
9388
|
-
if (value instanceof _RunIdentity) return value;
|
|
9389
|
-
if (typeof value === "string") return new _RunIdentity(value);
|
|
9390
|
-
return new _RunIdentity(
|
|
9391
|
-
value.runKey,
|
|
9392
|
-
value.path ?? [],
|
|
9393
|
-
value.attempt ?? 1,
|
|
9394
|
-
value.firstAttemptAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
9395
|
-
);
|
|
9396
|
-
}
|
|
9397
|
-
};
|
|
9398
|
-
|
|
9399
|
-
// src/runtime/workflow-props.ts
|
|
9400
|
-
function typeOf(value) {
|
|
9401
|
-
if (value === null) return "null";
|
|
9402
|
-
if (Array.isArray(value)) return "array";
|
|
9403
|
-
return typeof value;
|
|
9404
|
-
}
|
|
9405
|
-
function resolveWorkflowProps(declared, passed) {
|
|
9406
|
-
const inputs = declared ?? [];
|
|
9407
|
-
const given = passed ?? {};
|
|
9408
|
-
const byName = new Map(inputs.map((input) => [input.name, input]));
|
|
9409
|
-
for (const name of Object.keys(given)) {
|
|
9410
|
-
if (!byName.has(name)) {
|
|
9411
|
-
const known = inputs.map((input) => input.name);
|
|
9412
|
-
const suffix = known.length === 0 ? "this workflow declares no inputs" : `known inputs: ${known.join(", ")}`;
|
|
9413
|
-
return {
|
|
9414
|
-
ok: false,
|
|
9415
|
-
code: "unknown_input",
|
|
9416
|
-
error: `Unknown workflow input "${name}" \u2014 ${suffix}.`
|
|
9417
|
-
};
|
|
9418
|
-
}
|
|
9419
|
-
}
|
|
9420
|
-
const resolved = {};
|
|
9421
|
-
for (const input of inputs) {
|
|
9422
|
-
const supplied = Object.prototype.hasOwnProperty.call(given, input.name);
|
|
9423
|
-
const hasDefault = Object.prototype.hasOwnProperty.call(input, "default");
|
|
9424
|
-
if (!supplied) {
|
|
9425
|
-
if (hasDefault) {
|
|
9426
|
-
resolved[input.name] = input.default;
|
|
9427
|
-
continue;
|
|
9428
|
-
}
|
|
9429
|
-
if (input.required) {
|
|
9430
|
-
return {
|
|
9431
|
-
ok: false,
|
|
9432
|
-
code: "missing_required",
|
|
9433
|
-
error: `Missing required workflow input "${input.name}"${input.type ? ` (${input.type})` : ""}.`
|
|
9434
|
-
};
|
|
9435
|
-
}
|
|
9436
|
-
continue;
|
|
9437
|
-
}
|
|
9438
|
-
const value = given[input.name];
|
|
9439
|
-
if (input.type !== void 0) {
|
|
9440
|
-
const actual = typeOf(value);
|
|
9441
|
-
if (actual !== input.type) {
|
|
9442
|
-
return {
|
|
9443
|
-
ok: false,
|
|
9444
|
-
code: "type_mismatch",
|
|
9445
|
-
error: `Workflow input "${input.name}" expects ${input.type}, got ${actual}.`
|
|
9446
|
-
};
|
|
9447
|
-
}
|
|
9448
|
-
}
|
|
9449
|
-
resolved[input.name] = value;
|
|
9450
|
-
}
|
|
9451
|
-
return { ok: true, props: resolved };
|
|
9452
|
-
}
|
|
9453
|
-
|
|
9454
|
-
// src/runtime/run-flow.ts
|
|
9455
|
-
async function runFlow(graph, executors, onEvent = () => {
|
|
9456
|
-
}, options = {}) {
|
|
9457
|
-
const { signal, initialInputs = {}, timeoutMs, depth = 0, resumeOutputs = {}, entryNodes } = options;
|
|
9458
|
-
const run = options.run === void 0 ? void 0 : RunIdentity.from(options.run);
|
|
9459
|
-
const outputs = {};
|
|
9460
|
-
const portValues = /* @__PURE__ */ new Map();
|
|
9461
|
-
const completed = /* @__PURE__ */ new Set();
|
|
9462
|
-
const errors = [];
|
|
9463
|
-
const order = topoSort(graph);
|
|
9464
|
-
if (order === null) {
|
|
9465
|
-
const msg = "Cycle detected in flow graph \u2014 aborting.";
|
|
9466
|
-
onEvent({ type: "run-error", error: msg });
|
|
9467
|
-
return { ok: false, outputs, error: msg };
|
|
9468
|
-
}
|
|
9469
|
-
const propsCheck = resolveWorkflowProps(graph.inputs, options.props);
|
|
9470
|
-
if (!propsCheck.ok) {
|
|
9471
|
-
onEvent({ type: "run-error", error: propsCheck.error });
|
|
9472
|
-
return { ok: false, outputs, error: propsCheck.error };
|
|
9473
|
-
}
|
|
9474
|
-
const props = propsCheck.props;
|
|
9475
|
-
const declaresProps = (graph.inputs?.length ?? 0) > 0;
|
|
9476
|
-
const incomingByNode = indexIncoming(graph.edges);
|
|
9477
|
-
const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
|
|
9478
|
-
onEvent({ type: "run-start" });
|
|
9479
|
-
try {
|
|
9480
|
-
for (const node of order) {
|
|
9481
|
-
if (signal?.aborted) throw new Error("aborted");
|
|
9482
|
-
if (errors.length) break;
|
|
9483
|
-
if (Object.prototype.hasOwnProperty.call(resumeOutputs, node.id)) {
|
|
9484
|
-
const stored = resumeOutputs[node.id];
|
|
9485
|
-
outputs[node.id] = stored;
|
|
9486
|
-
const activated = activatedPorts(node, stored);
|
|
9487
|
-
for (const portId of activated.ports) {
|
|
9488
|
-
portValues.set(`${node.id}:${portId}`, activated.value);
|
|
9489
|
-
onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
|
|
9490
|
-
}
|
|
9491
|
-
completed.add(node.id);
|
|
9492
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "done", text: "resumed" });
|
|
9493
|
-
continue;
|
|
9494
|
-
}
|
|
9495
|
-
const incoming = incomingByNode.get(node.id) ?? [];
|
|
9496
|
-
if (incoming.length === 0 && entryNodes !== void 0 && !entryNodes.includes(node.id)) {
|
|
9497
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
|
|
9498
|
-
continue;
|
|
9499
|
-
}
|
|
9500
|
-
if (incoming.length > 0) {
|
|
9501
|
-
const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
|
|
9502
|
-
if (!anyActive) {
|
|
9503
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
|
|
9504
|
-
continue;
|
|
9505
|
-
}
|
|
9506
|
-
}
|
|
9507
|
-
const visualKind = getNodeKind(node.type ?? "");
|
|
9508
|
-
const isLayout = visualKind?.category === "layout";
|
|
9509
|
-
const isAnnotation = node.type === "note" || visualKind?.category === "annotation";
|
|
9510
|
-
if (isLayout || isAnnotation) {
|
|
9511
|
-
onEvent({
|
|
9512
|
-
type: "node-status",
|
|
9513
|
-
nodeId: node.id,
|
|
9514
|
-
status: "idle",
|
|
9515
|
-
text: isLayout ? "lane" : "annotation"
|
|
9516
|
-
});
|
|
9517
|
-
continue;
|
|
9518
|
-
}
|
|
9519
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "running" });
|
|
9520
|
-
announce(onEvent, node, "start");
|
|
9521
|
-
const inputs = collectInputs(node, incoming, portValues, initialInputs, props, declaresProps);
|
|
9522
|
-
const exec = pickExecutor(executors, node);
|
|
9523
|
-
if (!exec) {
|
|
9524
|
-
const tried = executorLookupIds(node);
|
|
9525
|
-
const msg = `No executor registered for kind=${node.type} \u2014 tried ${tried.map((id2) => `"${id2}"`).join(", ")}. Key your registry by one of those.`;
|
|
9526
|
-
errors.push(msg);
|
|
9527
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
|
|
9528
|
-
onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
|
|
9529
|
-
break;
|
|
9530
|
-
}
|
|
9531
|
-
try {
|
|
9532
|
-
const result = await Promise.resolve(
|
|
9533
|
-
exec({
|
|
9534
|
-
node,
|
|
9535
|
-
inputs,
|
|
9536
|
-
abort: (reason) => {
|
|
9537
|
-
throw new Error(reason ?? "aborted");
|
|
9538
|
-
},
|
|
9539
|
-
emit: onEvent,
|
|
9540
|
-
executors,
|
|
9541
|
-
depth,
|
|
9542
|
-
run
|
|
9543
|
-
})
|
|
9544
|
-
);
|
|
9545
|
-
outputs[node.id] = result;
|
|
9546
|
-
const activated = activatedPorts(node, result);
|
|
9547
|
-
for (const portId of activated.ports) {
|
|
9548
|
-
portValues.set(`${node.id}:${portId}`, activated.value);
|
|
9549
|
-
onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
|
|
9550
|
-
}
|
|
9551
|
-
completed.add(node.id);
|
|
9552
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "done" });
|
|
9553
|
-
announce(onEvent, node, "end");
|
|
9554
|
-
} catch (e) {
|
|
9555
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
9556
|
-
errors.push(msg);
|
|
9557
|
-
onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
|
|
9558
|
-
onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
|
|
9559
|
-
break;
|
|
9560
|
-
}
|
|
9561
|
-
}
|
|
9562
|
-
} finally {
|
|
9563
|
-
if (timer2) clearTimeout(timer2);
|
|
9564
|
-
}
|
|
9565
|
-
const ok = errors.length === 0;
|
|
9566
|
-
onEvent({ type: "run-end", ok });
|
|
9567
|
-
return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
|
|
9568
|
-
}
|
|
9569
|
-
function indexIncoming(edges) {
|
|
9570
|
-
const map = /* @__PURE__ */ new Map();
|
|
9571
|
-
for (const e of edges) {
|
|
9572
|
-
const list = map.get(e.target) ?? [];
|
|
9573
|
-
list.push(e);
|
|
9574
|
-
map.set(e.target, list);
|
|
9575
|
-
}
|
|
9576
|
-
return map;
|
|
9577
|
-
}
|
|
9578
|
-
function topoSort(graph) {
|
|
9579
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
9580
|
-
for (const n of graph.nodes) inDegree.set(n.id, 0);
|
|
9581
|
-
for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
|
|
9582
|
-
const queue = [];
|
|
9583
|
-
for (const [id2, d] of inDegree) if (d === 0) queue.push(id2);
|
|
9584
|
-
const ordered = [];
|
|
9585
|
-
while (queue.length) {
|
|
9586
|
-
const id2 = queue.shift();
|
|
9587
|
-
ordered.push(id2);
|
|
9588
|
-
for (const e of graph.edges) {
|
|
9589
|
-
if (e.source !== id2) continue;
|
|
9590
|
-
const next = (inDegree.get(e.target) ?? 0) - 1;
|
|
9591
|
-
inDegree.set(e.target, next);
|
|
9592
|
-
if (next === 0) queue.push(e.target);
|
|
9593
|
-
}
|
|
9594
|
-
}
|
|
9595
|
-
if (ordered.length !== graph.nodes.length) return null;
|
|
9596
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
9597
|
-
return ordered.map((id2) => byId.get(id2)).filter(Boolean);
|
|
9598
|
-
}
|
|
9599
|
-
function collectInputs(node, incoming, portValues, initial, props, declaresProps) {
|
|
9600
|
-
const inputs = { ...initial[node.id] ?? {} };
|
|
9601
|
-
if (incoming.length === 0) {
|
|
9602
|
-
for (const [name, value] of Object.entries(props)) {
|
|
9603
|
-
if (!(name in inputs)) inputs[name] = value;
|
|
9604
|
-
}
|
|
9605
|
-
}
|
|
9606
|
-
for (const e of incoming) {
|
|
9607
|
-
const portId = e.targetHandle ?? "in";
|
|
9608
|
-
const key = `${e.source}:${e.sourceHandle ?? "out"}`;
|
|
9609
|
-
if (!portValues.has(key)) continue;
|
|
9610
|
-
inputs[portId] = portValues.get(key);
|
|
9611
|
-
if (e.targetHandle == null && !(e.source in inputs)) {
|
|
9612
|
-
inputs[e.source] = portValues.get(key);
|
|
9613
|
-
}
|
|
9614
|
-
}
|
|
9615
|
-
if (declaresProps) inputs.$props = props;
|
|
9616
|
-
return inputs;
|
|
9617
|
-
}
|
|
9618
|
-
function executorLookupIds(node) {
|
|
9619
|
-
const ids = [node.id];
|
|
9620
|
-
if (node.type) ids.push(node.type);
|
|
9621
|
-
const typeKind = node.type ? getNodeKind(node.type) : null;
|
|
9622
|
-
if (typeKind) {
|
|
9623
|
-
for (const id2 of kindIds(typeKind)) ids.push(id2);
|
|
9624
|
-
} else {
|
|
9625
|
-
const declared = node.data?.kind;
|
|
9626
|
-
const kindName = typeof declared === "string" && declared !== "" ? declared : null;
|
|
9627
|
-
if (kindName) {
|
|
9628
|
-
ids.push(kindName);
|
|
9629
|
-
const dataKind = getNodeKind(kindName);
|
|
9630
|
-
if (dataKind) {
|
|
9631
|
-
for (const id2 of kindIds(dataKind)) ids.push(id2);
|
|
9632
|
-
}
|
|
9633
|
-
}
|
|
9634
|
-
}
|
|
9635
|
-
ids.push("*");
|
|
9636
|
-
return [...new Set(ids)];
|
|
9637
|
-
}
|
|
9638
|
-
function pickExecutor(executors, node) {
|
|
9639
|
-
for (const id2 of executorLookupIds(node)) {
|
|
9640
|
-
if (executors[id2]) return executors[id2];
|
|
9641
|
-
}
|
|
9642
|
-
return void 0;
|
|
9643
|
-
}
|
|
9644
|
-
function activatedPorts(node, result) {
|
|
9645
|
-
if (result && typeof result === "object") {
|
|
9646
|
-
const r = result;
|
|
9647
|
-
if (typeof r.__port === "string") {
|
|
9648
|
-
return { ports: [r.__port], value: r.value };
|
|
9649
|
-
}
|
|
9650
|
-
if (typeof r.branch === "string") {
|
|
9651
|
-
return { ports: [r.branch], value: Object.prototype.hasOwnProperty.call(r, "value") ? r.value : r };
|
|
9652
|
-
}
|
|
9653
|
-
}
|
|
9654
|
-
const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
|
|
9655
|
-
const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
|
|
9656
|
-
return { ports: declared?.length ? declared : ["out"], value: result };
|
|
9657
|
-
}
|
|
9658
|
-
function announce(onEvent, node, phase) {
|
|
9659
|
-
const data = node.data;
|
|
9660
|
-
const raw = phase === "start" ? data?.startingMsg : data?.stoppingMsg;
|
|
9661
|
-
if (typeof raw !== "string") return;
|
|
9662
|
-
const message = raw.trim();
|
|
9663
|
-
if (message === "") return;
|
|
9664
|
-
onEvent({ type: "node-message", nodeId: node.id, phase, message });
|
|
9665
|
-
}
|
|
9666
|
-
|
|
9667
|
-
// src/registry/subflow.ts
|
|
9668
|
-
var DEFAULT_MAX_DEPTH = 8;
|
|
9669
|
-
function subflowMode(config) {
|
|
9670
|
-
const mode = config.mode;
|
|
9671
|
-
return mode === "stream" || mode === "both" ? mode : "output";
|
|
9672
|
-
}
|
|
9673
|
-
function subflowPorts(config) {
|
|
9674
|
-
const mode = subflowMode(config);
|
|
9675
|
-
const ports = [{ id: "out", label: "result" }];
|
|
9676
|
-
if (mode === "stream" || mode === "both") ports.unshift({ id: "stream", label: "stream" });
|
|
9677
|
-
return ports;
|
|
9678
|
-
}
|
|
9679
|
-
var subflowExecutor = async (ctx) => {
|
|
9680
|
-
const config = ctx.node.data?.config ?? {};
|
|
9681
|
-
const ref = String(config.workflow ?? "").trim();
|
|
9682
|
-
if (!ref) ctx.abort("subflow has no workflow reference configured");
|
|
9683
|
-
const resolver = getWorkflowResolver();
|
|
9684
|
-
if (!resolver) {
|
|
9685
|
-
ctx.abort(
|
|
9686
|
-
"No workflow resolver registered. Call registerWorkflowResolver() so subflow can find the workflow it references."
|
|
9687
|
-
);
|
|
9688
|
-
}
|
|
9689
|
-
const maxDepth = Number.isFinite(config.maxDepth) ? Number(config.maxDepth) : DEFAULT_MAX_DEPTH;
|
|
9690
|
-
const depth = ctx.depth ?? 0;
|
|
9691
|
-
if (depth + 1 > maxDepth) {
|
|
9692
|
-
ctx.abort(
|
|
9693
|
-
`subflow depth limit reached (${maxDepth}) at "${ref}" \u2014 a workflow is referencing itself, directly or through a chain.`
|
|
9694
|
-
);
|
|
9695
|
-
}
|
|
9696
|
-
const pinned = config.version === void 0 || config.version === "" ? void 0 : Number(config.version);
|
|
9697
|
-
if (pinned !== void 0 && !Number.isInteger(pinned)) {
|
|
9698
|
-
ctx.abort(`subflow "${ref}" has a non-integer version pin (${String(config.version)}).`);
|
|
9699
|
-
}
|
|
9700
|
-
const resolved = await resolver(ref, pinned);
|
|
9701
|
-
const child = isResolutionFailure(resolved) ? ctx.abort(
|
|
9702
|
-
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}"`
|
|
9703
|
-
) : resolved;
|
|
9704
|
-
if (!child) ctx.abort(`subflow could not resolve workflow "${ref}"`);
|
|
9705
|
-
const mode = subflowMode(config);
|
|
9706
|
-
const streaming = mode === "stream" || mode === "both";
|
|
9707
|
-
const forward = (event) => {
|
|
9708
|
-
if (!streaming) return;
|
|
9709
|
-
const detail = event.type === "node-status" ? `${event.nodeId} ${event.status}` : event.type === "run-end" ? `finished (${event.ok ? "ok" : "failed"})` : event.type;
|
|
9710
|
-
ctx.emit({
|
|
9711
|
-
type: "log",
|
|
9712
|
-
nodeId: ctx.node.id,
|
|
9713
|
-
level: "info",
|
|
9714
|
-
message: `[${ref}] ${detail}`
|
|
9715
|
-
});
|
|
9716
|
-
};
|
|
9717
|
-
const result = await runFlow(
|
|
9718
|
-
child,
|
|
9719
|
-
// Inherit the parent's registry, then let the graph layer its own on top.
|
|
9720
|
-
// The inherited half is the fix; the layered half keeps a graph that
|
|
9721
|
-
// deliberately hands its child extra or different executors working.
|
|
9722
|
-
{ ...ctx.executors ?? {}, ...config.executors ?? {} },
|
|
9723
|
-
forward,
|
|
9724
|
-
{
|
|
9725
|
-
initialInputs: config.inputs ?? {
|
|
9726
|
-
// With no explicit mapping, hand the parent's inputs to the child's
|
|
9727
|
-
// entry points — the obvious default, and it makes the simple case
|
|
9728
|
-
// require no configuration at all.
|
|
9729
|
-
__parent: ctx.inputs
|
|
9730
|
-
},
|
|
9731
|
-
depth: depth + 1,
|
|
9732
|
-
// Push THIS node onto the identity path, so a node inside the child graph
|
|
9733
|
-
// cannot share an idempotency key with a same-named node in the parent —
|
|
9734
|
-
// or with the same child graph invoked from a different parent node.
|
|
9735
|
-
// Attempt and the first-attempt clock ride down unchanged: the child's
|
|
9736
|
-
// work happens inside this node's attempt.
|
|
9737
|
-
run: ctx.run?.descend(ctx.node.id)
|
|
9738
|
-
}
|
|
9739
|
-
);
|
|
9740
|
-
if (!result.ok) {
|
|
9741
|
-
ctx.abort(`subflow "${ref}" failed: ${result.error ?? "unknown error"}`);
|
|
9742
|
-
}
|
|
9743
|
-
if (mode === "stream") {
|
|
9744
|
-
return { __port: "stream", value: result.outputs };
|
|
9745
|
-
}
|
|
9746
|
-
if (mode === "both") {
|
|
9747
|
-
return result.outputs;
|
|
9748
|
-
}
|
|
9749
|
-
return { __port: "out", value: result.outputs };
|
|
9750
|
-
};
|
|
9751
|
-
|
|
9752
|
-
// src/registry/builtin.ts
|
|
9753
|
-
var HUMAN_FIELD_OUTPUT_TYPE = {
|
|
9754
|
-
text: "string",
|
|
9755
|
-
textarea: "string",
|
|
9756
|
-
select: "string",
|
|
9757
|
-
date: "string",
|
|
9758
|
-
datetime: "string",
|
|
9759
|
-
time: "string",
|
|
9760
|
-
email: "string",
|
|
9761
|
-
url: "string",
|
|
9762
|
-
tel: "string",
|
|
9763
|
-
password: "string",
|
|
9764
|
-
number: "number",
|
|
9765
|
-
switch: "boolean"
|
|
9766
|
-
};
|
|
9767
|
-
function casePorts(cases) {
|
|
9768
|
-
const byPort = /* @__PURE__ */ new Map();
|
|
9769
|
-
if (cases && typeof cases === "object" && !Array.isArray(cases)) {
|
|
9770
|
-
for (const [match, port] of Object.entries(cases)) {
|
|
9771
|
-
if (typeof port !== "string" || port === "" || port === "default") continue;
|
|
9772
|
-
const matches = byPort.get(port) ?? [];
|
|
9773
|
-
matches.push(match);
|
|
9774
|
-
byPort.set(port, matches);
|
|
9775
|
-
}
|
|
9776
|
-
}
|
|
9777
|
-
const ports = [...byPort].map(([id2, matches]) => ({
|
|
9778
|
-
id: id2,
|
|
9779
|
-
label: matches.join("|")
|
|
9780
|
-
}));
|
|
9781
|
-
return [...ports, { id: "default", label: "default" }];
|
|
9782
|
-
}
|
|
9783
|
-
function routePorts(routes, fallback) {
|
|
9784
|
-
const ports = [];
|
|
9785
|
-
const seen = /* @__PURE__ */ new Set();
|
|
9786
|
-
if (Array.isArray(routes)) {
|
|
9787
|
-
for (const route of routes) {
|
|
9788
|
-
const id2 = route?.port;
|
|
9789
|
-
if (typeof id2 !== "string" || id2.trim() === "" || seen.has(id2)) continue;
|
|
9790
|
-
seen.add(id2);
|
|
9791
|
-
ports.push({ id: id2, label: id2 });
|
|
9792
|
-
}
|
|
9793
|
-
}
|
|
9794
|
-
if (fallback !== false && !seen.has("fallback")) {
|
|
9795
|
-
ports.push({ id: "fallback", label: "fallback" });
|
|
9796
|
-
}
|
|
9797
|
-
if (ports.length === 0) ports.push({ id: "out" });
|
|
9798
|
-
return ports;
|
|
9799
|
-
}
|
|
9800
|
-
var HTTP_METHODS = [
|
|
9801
|
-
{ type: "select", key: "method", label: "Method", options: [
|
|
9802
|
-
{ value: "GET", label: "GET" },
|
|
9803
|
-
{ value: "POST", label: "POST" },
|
|
9804
|
-
{ value: "PUT", label: "PUT" },
|
|
9805
|
-
{ value: "PATCH", label: "PATCH" },
|
|
9806
|
-
{ value: "DELETE", label: "DELETE" }
|
|
9807
|
-
], default: "GET", required: true }
|
|
9808
|
-
];
|
|
9809
|
-
var KINDS = [
|
|
9810
|
-
// ───────────── Triggers ─────────────
|
|
9811
|
-
{
|
|
9812
|
-
name: "@particle-academy/manual_trigger",
|
|
9813
|
-
// Returns the raw inputs MAP, not the `in` port -- flat at an entry
|
|
9814
|
-
// point, port-keyed the moment the node has an inbound edge.
|
|
9815
|
-
emits: "input-map-merged",
|
|
9816
|
-
aliases: ["manual_trigger", "@fancy/manual_trigger"],
|
|
9817
|
-
category: "trigger",
|
|
9818
|
-
label: "Manual",
|
|
9819
|
-
description: "Entry point fired when the user clicks Run.",
|
|
9820
|
-
icon: "\u26A1",
|
|
9821
|
-
inputs: [],
|
|
9822
|
-
outputs: [{ id: "out" }]
|
|
9823
|
-
},
|
|
9824
|
-
{
|
|
9825
|
-
name: "@particle-academy/webhook_trigger",
|
|
9826
|
-
aliases: ["webhook_trigger", "@fancy/webhook_trigger"],
|
|
9827
|
-
category: "trigger",
|
|
9828
|
-
label: "Webhook",
|
|
9829
|
-
description: "Triggered by an inbound HTTP request to a host-provided URL.",
|
|
9830
|
-
icon: "\u{1F4E1}",
|
|
9831
|
-
inputs: [],
|
|
9832
|
-
outputs: [{ id: "out", label: "payload" }],
|
|
9833
|
-
configSchema: [
|
|
9834
|
-
{ type: "text", key: "path", label: "Path", placeholder: "/hooks/my-flow", required: true },
|
|
9835
|
-
{ type: "select", key: "method", label: "Method", options: [
|
|
9836
|
-
{ value: "POST", label: "POST" },
|
|
9837
|
-
{ value: "GET", label: "GET" }
|
|
9838
|
-
], default: "POST" },
|
|
9839
|
-
{ type: "credential", key: "secret", label: "Verifying secret", credentialType: "webhook_secret" }
|
|
9840
|
-
]
|
|
9841
|
-
},
|
|
9842
|
-
{
|
|
9843
|
-
name: "@particle-academy/schedule_trigger",
|
|
9844
|
-
outputShape: [
|
|
9845
|
-
{ path: "cron", type: "string", description: "The cron expression that fired." },
|
|
9846
|
-
{ path: "timezone", type: "string", description: "The timezone it was evaluated in." }
|
|
9847
|
-
],
|
|
9848
|
-
// Merges $ctx->inputs ITSELF beside cron/timezone -- the MAP, whose
|
|
9849
|
-
// shape depends on position, not each port's payload the way `merge` does.
|
|
9850
|
-
emits: "input-map-merged",
|
|
9851
|
-
aliases: ["schedule_trigger", "@fancy/schedule_trigger"],
|
|
9852
|
-
category: "trigger",
|
|
9853
|
-
label: "Schedule",
|
|
9854
|
-
description: "Fires on a cron schedule (host-implemented).",
|
|
9855
|
-
icon: "\u23F1",
|
|
9856
|
-
inputs: [],
|
|
9857
|
-
outputs: [{ id: "out" }],
|
|
9858
|
-
configSchema: [
|
|
9859
|
-
{
|
|
9860
|
-
type: "text",
|
|
9861
|
-
key: "cron",
|
|
9862
|
-
label: "Cron",
|
|
9863
|
-
placeholder: "*/5 * * * *",
|
|
9864
|
-
required: true,
|
|
9865
|
-
description: "Standard 5-field cron expression."
|
|
9866
|
-
},
|
|
9867
|
-
{ type: "text", key: "timezone", label: "Timezone", placeholder: "UTC", default: "UTC" }
|
|
9868
|
-
]
|
|
9869
|
-
},
|
|
9870
|
-
{
|
|
9871
|
-
name: "@particle-academy/user_input",
|
|
9872
|
-
// The keys an author declared on THIS node — the case a static list cannot
|
|
9873
|
-
// express, and the one issue #5 named.
|
|
9874
|
-
//
|
|
9875
|
-
// The TYPE comes from the same normalizer the form renders with, so the
|
|
9876
|
-
// variable picker agrees with what the run actually resolves. It used to say
|
|
9877
|
-
// `string` for every field, which was wrong the moment a field was a number
|
|
9878
|
-
// or a switch — the picker told an author `{{ $json.age }}` was text while
|
|
9879
|
-
// the form handed the next node a number.
|
|
9880
|
-
outputShape: (config) => (config.fields ?? []).filter((f) => typeof f.key === "string" && f.key !== "").map((f) => ({
|
|
9881
|
-
path: f.key,
|
|
9882
|
-
type: HUMAN_FIELD_OUTPUT_TYPE[humanFieldType(f.type)],
|
|
9883
|
-
description: f.label
|
|
9884
|
-
})),
|
|
9885
|
-
aliases: ["user_input", "@fancy/user_input"],
|
|
9886
|
-
pausesForHuman: "input",
|
|
9887
|
-
category: "human",
|
|
9888
|
-
label: "User Input",
|
|
9889
|
-
description: "Pause the flow until the user submits the configured form.",
|
|
9890
|
-
icon: "\u270E",
|
|
9891
|
-
inputs: [{ id: "in" }],
|
|
9892
|
-
outputs: [{ id: "out", label: "values" }],
|
|
9893
|
-
configSchema: [
|
|
9894
|
-
{ type: "text", key: "title", label: "Form title", default: "Need your input" },
|
|
9895
|
-
{
|
|
9896
|
-
type: "repeater",
|
|
9897
|
-
key: "fields",
|
|
9898
|
-
label: "Fields",
|
|
9899
|
-
description: "The form the run pauses on.",
|
|
9900
|
-
titleKey: "label",
|
|
9901
|
-
addLabel: "Add field",
|
|
9902
|
-
minItems: 1,
|
|
9903
|
-
fields: [
|
|
9904
|
-
{ type: "text", key: "key", label: "Key", required: true, placeholder: "answer" },
|
|
9905
|
-
{ type: "text", key: "label", label: "Label", required: true, placeholder: "Your answer" },
|
|
9906
|
-
{
|
|
9907
|
-
type: "select",
|
|
9908
|
-
key: "type",
|
|
9909
|
-
label: "Type",
|
|
9910
|
-
default: "text",
|
|
9911
|
-
// Every control `HumanPrompt` can render. A type the form supports
|
|
9912
|
-
// but the panel cannot select is reachable only by hand-editing the
|
|
9913
|
-
// workflow JSON — which is the audience this panel exists for.
|
|
9914
|
-
options: [
|
|
9915
|
-
{ value: "text", label: "Text" },
|
|
9916
|
-
{ value: "textarea", label: "Long text" },
|
|
9917
|
-
{ value: "number", label: "Number" },
|
|
9918
|
-
{ value: "select", label: "Select" },
|
|
9919
|
-
{ value: "switch", label: "Switch (yes / no)" },
|
|
9920
|
-
{ value: "date", label: "Date" },
|
|
9921
|
-
{ value: "datetime", label: "Date + time" },
|
|
9922
|
-
{ value: "time", label: "Time" },
|
|
9923
|
-
{ value: "email", label: "Email" },
|
|
9924
|
-
{ value: "url", label: "URL" },
|
|
9925
|
-
{ value: "tel", label: "Phone" },
|
|
9926
|
-
{ value: "password", label: "Password" }
|
|
9927
|
-
]
|
|
9928
|
-
},
|
|
9929
|
-
{
|
|
9930
|
-
// Without this a panel-authored select had nothing to choose from,
|
|
9931
|
-
// so it rendered an empty dropdown — the type was declarable and
|
|
9932
|
-
// unusable in the same breath.
|
|
9933
|
-
type: "keyvalue",
|
|
9934
|
-
key: "options",
|
|
9935
|
-
label: "Choices",
|
|
9936
|
-
description: "Select fields only. Left is the value the flow receives, right is what the person sees.",
|
|
9937
|
-
keyLabel: "Value",
|
|
9938
|
-
valueLabel: "Label",
|
|
9939
|
-
keyPlaceholder: "small",
|
|
9940
|
-
valuePlaceholder: "Small",
|
|
9941
|
-
addLabel: "Add choice"
|
|
9942
|
-
},
|
|
9943
|
-
{ type: "switch", key: "required", label: "Required", default: false }
|
|
9944
|
-
],
|
|
9945
|
-
default: [{ key: "answer", label: "Your answer", type: "textarea", required: true }]
|
|
9946
|
-
}
|
|
9947
|
-
]
|
|
9948
|
-
},
|
|
9949
|
-
{
|
|
9950
|
-
name: "@particle-academy/rich_user_input",
|
|
9951
|
-
aliases: ["rich_user_input", "@fancy/rich_user_input"],
|
|
9952
|
-
pausesForHuman: "input",
|
|
9953
|
-
category: "human",
|
|
9954
|
-
label: "Rich User Input",
|
|
9955
|
-
description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
|
|
9956
|
-
icon: "\u25A4",
|
|
9957
|
-
inputs: [{ id: "in" }],
|
|
9958
|
-
outputs: [{ id: "out", label: "values" }],
|
|
9959
|
-
configSchema: [
|
|
9960
|
-
{ type: "text", key: "title", label: "Step title", default: "Please review" },
|
|
9961
|
-
{
|
|
9962
|
-
type: "document",
|
|
9963
|
-
key: "document",
|
|
9964
|
-
label: "Page content",
|
|
9965
|
-
documentType: "stages",
|
|
9966
|
-
description: "Authored with the host's document editor (fancy-cms Stages)."
|
|
9967
|
-
},
|
|
9968
|
-
{ type: "switch", key: "requireConfirm", label: "Require explicit confirmation", default: true },
|
|
9969
|
-
{ type: "text", key: "submitLabel", label: "Submit button", default: "Continue" }
|
|
9970
|
-
],
|
|
9971
|
-
// Preview the authored page inside a FauxClient frame, so the canvas shows
|
|
9972
|
-
// what the person hitting this step will actually see.
|
|
9973
|
-
renderBody: (ctx) => createElement(RichInputPreview, { config: ctx.config ?? {} })
|
|
9974
|
-
},
|
|
9975
|
-
{
|
|
9976
|
-
name: "@particle-academy/subflow",
|
|
9977
|
-
aliases: ["subflow", "@fancy/subflow"],
|
|
9978
|
-
category: "logic",
|
|
9979
|
-
label: "SubFlow",
|
|
9980
|
-
description: "Run another workflow and bring its result \u2014 or its live progress \u2014 back into this one.",
|
|
9981
|
-
icon: "\u29C9",
|
|
9982
|
-
inputs: [{ id: "in" }],
|
|
9983
|
-
// The stream port only exists when something actually streams.
|
|
9984
|
-
outputs: (config) => subflowPorts(config ?? {}),
|
|
9985
|
-
// Core, not marketplace: it runs a child graph through this same engine and
|
|
9986
|
-
// needs nothing from outside except where workflows live.
|
|
9987
|
-
executor: subflowExecutor,
|
|
9988
|
-
configSchema: [
|
|
9989
|
-
{
|
|
9990
|
-
type: "text",
|
|
9991
|
-
key: "workflow",
|
|
9992
|
-
label: "Workflow",
|
|
9993
|
-
required: true,
|
|
9994
|
-
placeholder: "onboarding-v2",
|
|
9995
|
-
description: "Reference resolved by the host's registerWorkflowResolver()."
|
|
9996
|
-
},
|
|
9997
|
-
{
|
|
9998
|
-
type: "number",
|
|
9999
|
-
key: "version",
|
|
10000
|
-
label: "Pin to version",
|
|
10001
|
-
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."
|
|
10002
|
-
},
|
|
10003
|
-
{
|
|
10004
|
-
type: "select",
|
|
10005
|
-
key: "mode",
|
|
10006
|
-
label: "Return",
|
|
10007
|
-
default: "output",
|
|
10008
|
-
options: [
|
|
10009
|
-
{ value: "output", label: "Output when it finishes" },
|
|
10010
|
-
{ value: "stream", label: "Stream progress as it runs" },
|
|
10011
|
-
{ value: "both", label: "Both \u2014 stream, then output" }
|
|
10012
|
-
],
|
|
10013
|
-
description: "Streaming adds a second port so a parent can show progress instead of a spinner."
|
|
10014
|
-
},
|
|
10015
|
-
{
|
|
10016
|
-
type: "keyvalue",
|
|
10017
|
-
key: "inputs",
|
|
10018
|
-
label: "Input mapping",
|
|
10019
|
-
description: "Values handed to the child's entry points. Omit to pass this node's inputs straight through.",
|
|
10020
|
-
keyLabel: "Name",
|
|
10021
|
-
valueLabel: "Value",
|
|
10022
|
-
addLabel: "Add input"
|
|
10023
|
-
},
|
|
10024
|
-
{
|
|
10025
|
-
type: "number",
|
|
10026
|
-
key: "maxDepth",
|
|
10027
|
-
label: "Max nesting depth",
|
|
10028
|
-
default: DEFAULT_MAX_DEPTH,
|
|
10029
|
-
min: 1,
|
|
10030
|
-
max: 32,
|
|
10031
|
-
description: "Guards against a workflow referencing itself."
|
|
10032
|
-
}
|
|
10033
|
-
]
|
|
10034
|
-
},
|
|
10035
|
-
// ───────────── Logic ─────────────
|
|
10036
|
-
{
|
|
10037
|
-
name: "@particle-academy/branch",
|
|
10038
|
-
// returns its input on the chosen port
|
|
10039
|
-
emits: "input",
|
|
10040
|
-
aliases: ["branch", "@fancy/branch"],
|
|
10041
|
-
category: "logic",
|
|
10042
|
-
label: "Branch",
|
|
10043
|
-
description: "Multi-way branch on a condition or value.",
|
|
10044
|
-
icon: "\u25C7",
|
|
10045
|
-
inputs: [{ id: "in" }],
|
|
10046
|
-
outputs: [{ id: "true", label: "true" }, { id: "false", label: "false" }],
|
|
10047
|
-
configSchema: [
|
|
10048
|
-
{
|
|
10049
|
-
type: "select",
|
|
10050
|
-
key: "match",
|
|
10051
|
-
label: "Match",
|
|
10052
|
-
default: "all",
|
|
10053
|
-
options: [
|
|
10054
|
-
{ value: "all", label: "All conditions (AND)" },
|
|
10055
|
-
{ value: "any", label: "Any condition (OR)" }
|
|
10056
|
-
]
|
|
10057
|
-
},
|
|
10058
|
-
{
|
|
10059
|
-
type: "repeater",
|
|
10060
|
-
key: "conditions",
|
|
10061
|
-
label: "Conditions",
|
|
10062
|
-
description: "Routes to `true` when these match, otherwise `false`.",
|
|
10063
|
-
titleKey: "left",
|
|
10064
|
-
addLabel: "Add condition",
|
|
10065
|
-
minItems: 1,
|
|
10066
|
-
fields: [
|
|
10067
|
-
{ type: "expression", key: "left", label: "Value", example: "{{ $json.status }}", required: true },
|
|
10068
|
-
{
|
|
10069
|
-
type: "select",
|
|
10070
|
-
key: "operator",
|
|
10071
|
-
label: "Is",
|
|
10072
|
-
default: "eq",
|
|
10073
|
-
options: [
|
|
10074
|
-
{ value: "eq", label: "equal to" },
|
|
10075
|
-
{ value: "neq", label: "not equal to" },
|
|
10076
|
-
{ value: "contains", label: "contains" },
|
|
10077
|
-
{ value: "not_contains", label: "does not contain" },
|
|
10078
|
-
{ value: "gt", label: "greater than" },
|
|
10079
|
-
{ value: "gte", label: "greater than or equal to" },
|
|
10080
|
-
{ value: "lt", label: "less than" },
|
|
10081
|
-
{ value: "lte", label: "less than or equal to" },
|
|
10082
|
-
{ value: "truthy", label: "true" },
|
|
10083
|
-
{ value: "falsy", label: "false" },
|
|
10084
|
-
{ value: "empty", label: "empty" },
|
|
10085
|
-
{ value: "not_empty", label: "not empty" }
|
|
10086
|
-
]
|
|
10087
|
-
},
|
|
10088
|
-
{ type: "text", key: "right", label: "Compared to", placeholder: "active" }
|
|
10089
|
-
],
|
|
10090
|
-
default: [{ left: "", operator: "eq", right: "" }]
|
|
10091
|
-
},
|
|
10092
|
-
{
|
|
10093
|
-
type: "expression",
|
|
10094
|
-
key: "condition",
|
|
10095
|
-
label: "Raw expression (advanced)",
|
|
10096
|
-
example: "{{ $json.active && $json.score > 10 }}",
|
|
10097
|
-
description: "Escape hatch for logic the builder can't express. Overrides the conditions above when set."
|
|
10098
|
-
}
|
|
10099
|
-
]
|
|
10100
|
-
},
|
|
10101
|
-
{
|
|
10102
|
-
name: "@particle-academy/switch_case",
|
|
10103
|
-
// returns its input on the chosen port
|
|
10104
|
-
emits: "input",
|
|
10105
|
-
aliases: ["switch_case", "@fancy/switch_case"],
|
|
10106
|
-
category: "logic",
|
|
10107
|
-
label: "Switch",
|
|
10108
|
-
description: "Route to one of N labelled outputs based on a key.",
|
|
10109
|
-
icon: "\u2933",
|
|
10110
|
-
inputs: [{ id: "in" }],
|
|
10111
|
-
// Ports ARE the config: every distinct port a case routes to becomes an
|
|
10112
|
-
// output handle, plus the always-present `default`. Editing the cases map
|
|
10113
|
-
// moves the ports on the canvas and the ports the runtime activates.
|
|
10114
|
-
outputs: (config) => casePorts(config?.cases),
|
|
10115
|
-
configSchema: [
|
|
10116
|
-
{ type: "expression", key: "value", label: "Switch on", example: "{{ $json.kind }}", required: true },
|
|
10117
|
-
{
|
|
10118
|
-
type: "keyvalue",
|
|
10119
|
-
key: "cases",
|
|
10120
|
-
label: "Cases",
|
|
10121
|
-
description: "Match value \u2192 output port. Unmatched input takes `default`.",
|
|
10122
|
-
keyLabel: "When value is",
|
|
10123
|
-
valueLabel: "Route to port",
|
|
10124
|
-
keyPlaceholder: "a",
|
|
10125
|
-
valuePlaceholder: "case_a",
|
|
10126
|
-
addLabel: "Add case",
|
|
10127
|
-
default: { a: "case_a", b: "case_b" }
|
|
10128
|
-
}
|
|
10129
|
-
]
|
|
10130
|
-
},
|
|
10131
|
-
{
|
|
10132
|
-
name: "@particle-academy/for_each",
|
|
10133
|
-
outputShape: [
|
|
10134
|
-
{ path: "items", type: "array" },
|
|
10135
|
-
{ path: "count", type: "number" }
|
|
10136
|
-
],
|
|
10137
|
-
aliases: ["for_each", "@fancy/for_each"],
|
|
10138
|
-
category: "logic",
|
|
10139
|
-
label: "For Each",
|
|
10140
|
-
description: "Iterate over a list, emitting each item on `item`.",
|
|
10141
|
-
icon: "\u21BB",
|
|
10142
|
-
inputs: [{ id: "in" }],
|
|
10143
|
-
outputs: [{ id: "item", label: "item" }, { id: "done", label: "done" }],
|
|
10144
|
-
configSchema: [
|
|
10145
|
-
{ type: "expression", key: "source", label: "List", example: "{{ $json.users }}", required: true },
|
|
10146
|
-
{ type: "number", key: "concurrency", label: "Concurrency", default: 1, min: 1, max: 50 }
|
|
10147
|
-
]
|
|
10148
|
-
},
|
|
10149
|
-
{
|
|
10150
|
-
name: "@particle-academy/merge",
|
|
10151
|
-
// `merge` mode unions every object input at the TOP level; `concat` builds
|
|
10152
|
-
// a list instead, whose elements are not addressable as fields. So concat
|
|
10153
|
-
// declares NOTHING rather than an empty list -- `[]` would claim "emits no
|
|
10154
|
-
// fields" of a kind that emits a list, which is false and refuses every
|
|
10155
|
-
// reference.
|
|
10156
|
-
emits: (config) => (config?.mode ?? "merge") === "concat" ? null : "inputs-merged",
|
|
10157
|
-
aliases: ["merge", "@fancy/merge"],
|
|
10158
|
-
category: "logic",
|
|
10159
|
-
label: "Merge",
|
|
10160
|
-
description: "Combine multiple inputs into one object or array.",
|
|
10161
|
-
icon: "\u2295",
|
|
10162
|
-
inputs: [{ id: "a" }, { id: "b" }],
|
|
10163
|
-
outputs: [{ id: "out" }],
|
|
10164
|
-
configSchema: [
|
|
10165
|
-
{
|
|
10166
|
-
type: "select",
|
|
10167
|
-
key: "mode",
|
|
10168
|
-
label: "Mode",
|
|
10169
|
-
default: "merge",
|
|
10170
|
-
options: [{ value: "merge", label: "Object merge" }, { value: "concat", label: "Array concat" }]
|
|
10171
|
-
}
|
|
10172
|
-
]
|
|
10173
|
-
},
|
|
10174
|
-
{
|
|
10175
|
-
name: "@particle-academy/wait",
|
|
10176
|
-
outputShape: [
|
|
10177
|
-
{ path: "waited", type: "string", description: "Which wait mode ran." },
|
|
10178
|
-
{ path: "duration", type: "number", description: "How long it waited." },
|
|
10179
|
-
{ path: "input", type: "unknown", description: "The value that arrived, carried forward." }
|
|
10180
|
-
],
|
|
10181
|
-
aliases: ["wait", "@fancy/wait"],
|
|
10182
|
-
category: "logic",
|
|
10183
|
-
label: "Wait",
|
|
10184
|
-
description: "Sleep or wait for an external event.",
|
|
10185
|
-
icon: "\u23F8",
|
|
10186
|
-
configSchema: [
|
|
10187
|
-
{
|
|
10188
|
-
type: "select",
|
|
10189
|
-
key: "mode",
|
|
10190
|
-
label: "Mode",
|
|
10191
|
-
default: "duration",
|
|
10192
|
-
options: [{ value: "duration", label: "Duration" }, { value: "until", label: "Until timestamp" }, { value: "event", label: "External event" }]
|
|
10193
|
-
},
|
|
10194
|
-
{ type: "text", key: "duration", label: "Duration", placeholder: "5s, 10m, 1h", description: "Used when mode = duration." }
|
|
10195
|
-
]
|
|
10196
|
-
},
|
|
10197
|
-
{
|
|
10198
|
-
name: "@particle-academy/transform",
|
|
10199
|
-
// Two behaviours: the input unchanged when no expression is configured,
|
|
10200
|
-
// else the shape that expression names. So the RELATION is config-dependent.
|
|
10201
|
-
emits: (config) => (config?.expression ?? "") === "" ? "input" : "expression:expression",
|
|
10202
|
-
aliases: ["transform", "@fancy/transform"],
|
|
10203
|
-
category: "logic",
|
|
10204
|
-
label: "Transform",
|
|
10205
|
-
description: "Reshape data with an expression.",
|
|
10206
|
-
icon: "\u0192",
|
|
10207
|
-
configSchema: [
|
|
10208
|
-
{
|
|
10209
|
-
type: "select",
|
|
10210
|
-
key: "mode",
|
|
10211
|
-
label: "Build the output",
|
|
10212
|
-
default: "fields",
|
|
10213
|
-
options: [
|
|
10214
|
-
{ value: "fields", label: "Field by field" },
|
|
10215
|
-
{ value: "expression", label: "One expression" }
|
|
10216
|
-
]
|
|
10217
|
-
},
|
|
10218
|
-
{
|
|
10219
|
-
type: "repeater",
|
|
10220
|
-
key: "fields",
|
|
10221
|
-
label: "Output fields",
|
|
10222
|
-
description: "Each row becomes a key on the result.",
|
|
10223
|
-
titleKey: "key",
|
|
10224
|
-
addLabel: "Add field",
|
|
10225
|
-
fields: [
|
|
10226
|
-
{ type: "text", key: "key", label: "Key", required: true, placeholder: "name" },
|
|
10227
|
-
{ type: "expression", key: "value", label: "Value", example: "{{ $json.first }}", required: true }
|
|
10228
|
-
],
|
|
10229
|
-
default: [{ key: "", value: "" }]
|
|
10230
|
-
},
|
|
10231
|
-
{
|
|
10232
|
-
type: "expression",
|
|
10233
|
-
key: "expression",
|
|
10234
|
-
label: "Expression (advanced)",
|
|
10235
|
-
example: "{{ { id: $json.id, name: $json.first + ' ' + $json.last } }}",
|
|
10236
|
-
description: "Used when the mode above is set to one expression."
|
|
10237
|
-
}
|
|
10238
|
-
]
|
|
10239
|
-
},
|
|
10240
|
-
// ───────────── Data ─────────────
|
|
10241
|
-
{
|
|
10242
|
-
name: "@particle-academy/memory_store",
|
|
10243
|
-
aliases: ["memory_store", "@fancy/memory_store"],
|
|
10244
|
-
category: "data",
|
|
10245
|
-
label: "Memory Store",
|
|
10246
|
-
description: "Read or write per-conversation memory.",
|
|
10247
|
-
icon: "\u{1F9E0}",
|
|
10248
|
-
configSchema: [
|
|
10249
|
-
{
|
|
10250
|
-
type: "select",
|
|
10251
|
-
key: "operation",
|
|
10252
|
-
label: "Operation",
|
|
10253
|
-
required: true,
|
|
10254
|
-
default: "read",
|
|
10255
|
-
options: [{ value: "read", label: "Read" }, { value: "write", label: "Write" }, { value: "append", label: "Append" }]
|
|
10256
|
-
},
|
|
10257
|
-
{ type: "text", key: "key", label: "Key", placeholder: "user.preferences", required: true },
|
|
10258
|
-
{ type: "expression", key: "value", label: "Value (write/append only)", example: "{{ $json }}" },
|
|
10259
|
-
{ type: "credential", key: "store", label: "Memory store", credentialType: "memory_store" }
|
|
10260
|
-
]
|
|
10261
|
-
},
|
|
10262
|
-
{
|
|
10263
|
-
name: "@particle-academy/data_store",
|
|
10264
|
-
aliases: ["data_store", "@fancy/data_store"],
|
|
10265
|
-
category: "data",
|
|
10266
|
-
label: "Data Store",
|
|
10267
|
-
description: "Key-value or table read/write against a host store.",
|
|
10268
|
-
icon: "\u{1F5C3}",
|
|
10269
|
-
configSchema: [
|
|
10270
|
-
{
|
|
10271
|
-
type: "select",
|
|
10272
|
-
key: "operation",
|
|
10273
|
-
label: "Operation",
|
|
10274
|
-
required: true,
|
|
10275
|
-
default: "get",
|
|
10276
|
-
options: [
|
|
10277
|
-
{ value: "get", label: "Get" },
|
|
10278
|
-
{ value: "set", label: "Set" },
|
|
10279
|
-
{ value: "delete", label: "Delete" },
|
|
10280
|
-
{ value: "query", label: "Query" },
|
|
10281
|
-
{ value: "list", label: "List" }
|
|
10282
|
-
]
|
|
10283
|
-
},
|
|
10284
|
-
{ type: "text", key: "table", label: "Table / collection", required: true },
|
|
10285
|
-
{ type: "text", key: "key", label: "Key" },
|
|
10286
|
-
{
|
|
10287
|
-
type: "keyvalue",
|
|
10288
|
-
key: "where",
|
|
10289
|
-
label: "Where",
|
|
10290
|
-
description: "Field/value pairs to match. For query and list operations.",
|
|
10291
|
-
keyLabel: "Field",
|
|
10292
|
-
valueLabel: "Equals",
|
|
10293
|
-
addLabel: "Add filter"
|
|
10294
|
-
},
|
|
10295
|
-
{ type: "expression", key: "value", label: "Value (set only)", example: "{{ $json }}" },
|
|
10296
|
-
{ type: "credential", key: "store", label: "Data store", credentialType: "data_store" }
|
|
10297
|
-
]
|
|
10298
|
-
},
|
|
10299
|
-
{
|
|
10300
|
-
name: "@particle-academy/variable",
|
|
10301
|
-
// evaluates the expression in config.value
|
|
10302
|
-
emits: "expression:value",
|
|
10303
|
-
aliases: ["variable", "@fancy/variable"],
|
|
10304
|
-
category: "data",
|
|
10305
|
-
label: "Variable",
|
|
10306
|
-
description: "Workflow-scoped value used by other nodes.",
|
|
10307
|
-
icon: "\u{1D4CD}",
|
|
10308
|
-
configSchema: [
|
|
10309
|
-
{ type: "text", key: "name", label: "Name", required: true },
|
|
10310
|
-
{ type: "expression", key: "value", label: "Value", required: true }
|
|
10311
|
-
]
|
|
10312
|
-
},
|
|
10313
|
-
// ───────────── AI ─────────────
|
|
10314
|
-
{
|
|
10315
|
-
name: "@particle-academy/llm_call",
|
|
10316
|
-
// LlmClient::complete() -> array{text:string,data?:mixed,usage?:array,raw?:mixed}
|
|
10317
|
-
//
|
|
10318
|
-
// A FUNCTION of config, because `data` exists only when this node was asked
|
|
10319
|
-
// for a schema. Declaring it statically would offer `{{ $json.data }}` on
|
|
10320
|
-
// every llm_call in the kit, including the ones that emit nothing of the
|
|
10321
|
-
// sort — and a suggestion that resolves to null at runtime is worse than no
|
|
10322
|
-
// suggestion, since the picker is exactly what gave the author confidence.
|
|
10323
|
-
outputShape: (config) => {
|
|
10324
|
-
const asked = config?.response_schema != null && config.response_schema !== "" && !(Array.isArray(config.response_schema) && config.response_schema.length === 0);
|
|
10325
|
-
return [
|
|
10326
|
-
{ path: "text", type: "string", description: "The model's completion." },
|
|
10327
|
-
...asked ? [{ path: "data", type: "unknown", description: "The parsed, schema-checked result." }] : [],
|
|
10328
|
-
{ path: "usage", type: "object", description: "Token counts, when the provider reports them." },
|
|
10329
|
-
{ path: "raw", type: "unknown", description: "The provider's untouched response." }
|
|
10330
|
-
];
|
|
10331
|
-
},
|
|
10332
|
-
aliases: ["llm_call", "@fancy/llm_call"],
|
|
10333
|
-
category: "ai",
|
|
10334
|
-
label: "LLM Call",
|
|
10335
|
-
description: "Send a prompt + context to a model and receive a response.",
|
|
10336
|
-
icon: "\u2726",
|
|
10337
|
-
configSchema: [
|
|
10338
|
-
{
|
|
10339
|
-
type: "select",
|
|
10340
|
-
key: "provider",
|
|
10341
|
-
label: "Provider",
|
|
10342
|
-
default: "anthropic",
|
|
10343
|
-
options: [
|
|
10344
|
-
{ value: "anthropic", label: "Anthropic" },
|
|
10345
|
-
{ value: "openai", label: "OpenAI" },
|
|
10346
|
-
{ value: "custom", label: "Custom" }
|
|
10347
|
-
]
|
|
10348
|
-
},
|
|
10349
|
-
{ type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5", required: true },
|
|
10350
|
-
{ type: "textarea", key: "system", label: "System prompt", rows: 4 },
|
|
10351
|
-
{ type: "expression", key: "prompt", label: "User prompt", example: "{{ $json.question }}", required: true },
|
|
10352
|
-
{ type: "number", key: "temperature", label: "Temperature", min: 0, max: 2, step: 0.1, default: 0.7 },
|
|
10353
|
-
{ type: "number", key: "max_tokens", label: "Max tokens", min: 1, max: 8192, default: 1024 },
|
|
10354
|
-
{
|
|
10355
|
-
type: "repeater",
|
|
10356
|
-
key: "tools",
|
|
10357
|
-
label: "Tools",
|
|
10358
|
-
description: "Tools the model may call.",
|
|
10359
|
-
titleKey: "name",
|
|
10360
|
-
addLabel: "Add tool",
|
|
10361
|
-
fields: [
|
|
10362
|
-
{ type: "text", key: "name", label: "Name", required: true, placeholder: "search_index" },
|
|
10363
|
-
{ type: "text", key: "description", label: "When to use it" },
|
|
10364
|
-
{
|
|
10365
|
-
type: "json",
|
|
10366
|
-
key: "input_schema",
|
|
10367
|
-
label: "Input schema",
|
|
10368
|
-
description: "JSON Schema for the tool's arguments."
|
|
10369
|
-
}
|
|
10370
|
-
]
|
|
10371
|
-
},
|
|
10372
|
-
{
|
|
10373
|
-
type: "json",
|
|
10374
|
-
key: "response_schema",
|
|
10375
|
-
label: "Response schema",
|
|
10376
|
-
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."
|
|
10377
|
-
},
|
|
10378
|
-
{ type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
|
|
10379
|
-
]
|
|
10380
|
-
},
|
|
10381
|
-
{
|
|
10382
|
-
name: "@particle-academy/llm_router",
|
|
10383
|
-
outputShape: [
|
|
10384
|
-
{ path: "route", type: "string", description: "The port the model chose." },
|
|
10385
|
-
{ path: "reason", type: "string", description: "Why the model chose it." },
|
|
10386
|
-
{ path: "input", type: "unknown", description: "The value that arrived, carried forward." }
|
|
10387
|
-
],
|
|
10388
|
-
// Every id this node has ever shipped under keeps resolving — MOIC's saved
|
|
10389
|
-
// flows carry the bare `llm_branch`.
|
|
10390
|
-
aliases: ["llm_router", "llm_branch", "@fancy/llm_branch", "@fancy/llm_router"],
|
|
10391
|
-
category: "ai",
|
|
10392
|
-
label: "LLM Router",
|
|
10393
|
-
description: "Let a model choose which route the flow takes.",
|
|
10394
|
-
icon: "\u2727",
|
|
10395
|
-
inputs: [{ id: "in" }],
|
|
10396
|
-
// Each declared route is a port. The executor returns `{ __port: id }`
|
|
10397
|
-
// (or `Port.only(id)` on the PHP runtime) to pick one.
|
|
10398
|
-
outputs: (config) => routePorts(config?.routes, config?.fallback),
|
|
10399
|
-
// A shuttle, not an engine: it carries the routes out to whatever LLM
|
|
10400
|
-
// client the host registered and carries the choice back. No provider SDK
|
|
10401
|
-
// reaches core, so this stays a builtin without adding a dependency.
|
|
10402
|
-
executor: llmRouterExecutor,
|
|
10403
|
-
configSchema: [
|
|
10404
|
-
{
|
|
10405
|
-
type: "textarea",
|
|
10406
|
-
key: "system",
|
|
10407
|
-
label: "System prompt",
|
|
10408
|
-
rows: 3,
|
|
10409
|
-
description: "Optional framing for the routing decision."
|
|
10410
|
-
},
|
|
10411
|
-
{
|
|
10412
|
-
type: "expression",
|
|
10413
|
-
key: "prompt",
|
|
10414
|
-
label: "What to route on",
|
|
10415
|
-
example: "{{ $json.message }}",
|
|
10416
|
-
required: true
|
|
10417
|
-
},
|
|
10418
|
-
{
|
|
10419
|
-
type: "repeater",
|
|
10420
|
-
key: "routes",
|
|
10421
|
-
label: "Routes",
|
|
10422
|
-
description: "The model picks exactly one. Descriptions are what it chooses between \u2014 make them distinct.",
|
|
10423
|
-
titleKey: "port",
|
|
10424
|
-
addLabel: "Add route",
|
|
10425
|
-
minItems: 2,
|
|
10426
|
-
fields: [
|
|
10427
|
-
{ type: "text", key: "port", label: "Port", required: true, placeholder: "billing" },
|
|
10428
|
-
{
|
|
10429
|
-
type: "text",
|
|
10430
|
-
key: "description",
|
|
10431
|
-
label: "When to choose it",
|
|
10432
|
-
required: true,
|
|
10433
|
-
placeholder: "The user is asking about an invoice, refund, or payment."
|
|
10434
|
-
}
|
|
10435
|
-
],
|
|
10436
|
-
default: [
|
|
10437
|
-
{ port: "a", description: "Describe when the model should pick this route." },
|
|
10438
|
-
{ port: "b", description: "Describe when the model should pick this route." }
|
|
10439
|
-
]
|
|
10440
|
-
},
|
|
10441
|
-
{
|
|
10442
|
-
type: "select",
|
|
10443
|
-
key: "provider",
|
|
10444
|
-
label: "Provider",
|
|
10445
|
-
default: "anthropic",
|
|
10446
|
-
options: [
|
|
10447
|
-
{ value: "anthropic", label: "Anthropic" },
|
|
10448
|
-
{ value: "openai", label: "OpenAI" },
|
|
10449
|
-
{ value: "custom", label: "Custom" }
|
|
10450
|
-
]
|
|
10451
|
-
},
|
|
10452
|
-
{ type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5" },
|
|
10453
|
-
{
|
|
10454
|
-
type: "switch",
|
|
10455
|
-
key: "fallback",
|
|
10456
|
-
label: "Add a `fallback` port",
|
|
10457
|
-
default: true,
|
|
10458
|
-
description: "Where the flow goes if the model returns no usable route."
|
|
10459
|
-
},
|
|
10460
|
-
{ type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
|
|
10461
|
-
]
|
|
10462
|
-
},
|
|
10463
|
-
{
|
|
10464
|
-
name: "@particle-academy/tool_use",
|
|
10465
|
-
aliases: ["tool_use", "@fancy/tool_use"],
|
|
10466
|
-
category: "ai",
|
|
10467
|
-
label: "Tool Use",
|
|
10468
|
-
description: "Hand control to a host-registered tool by name.",
|
|
10469
|
-
icon: "\u{1F6E0}",
|
|
10470
|
-
configSchema: [
|
|
10471
|
-
{ type: "text", key: "tool", label: "Tool name", placeholder: "search_index", required: true },
|
|
10472
|
-
{ type: "expression", key: "args", label: "Arguments", example: "{{ { query: $json.q } }}" }
|
|
10473
|
-
]
|
|
10474
|
-
},
|
|
10475
|
-
{
|
|
10476
|
-
name: "@particle-academy/embed_search",
|
|
10477
|
-
outputShape: [
|
|
10478
|
-
{ path: "query", type: "string" },
|
|
10479
|
-
{ path: "matches", type: "array", description: "Ranked results from the vector store." }
|
|
10480
|
-
],
|
|
10481
|
-
aliases: ["embed_search", "@fancy/embed_search"],
|
|
10482
|
-
category: "ai",
|
|
10483
|
-
label: "Embed & Search",
|
|
10484
|
-
description: "Embed a query and search a vector store.",
|
|
10485
|
-
icon: "\u273A",
|
|
10486
|
-
configSchema: [
|
|
10487
|
-
{ type: "expression", key: "query", label: "Query", required: true, example: "{{ $json.question }}" },
|
|
10488
|
-
{ type: "number", key: "topK", label: "Top K", default: 5, min: 1, max: 50 },
|
|
10489
|
-
{ type: "credential", key: "vectorStore", label: "Vector store", credentialType: "vector_store" }
|
|
10490
|
-
]
|
|
10491
|
-
},
|
|
10492
|
-
// ───────────── IO ─────────────
|
|
10493
|
-
{
|
|
10494
|
-
name: "@particle-academy/api_request",
|
|
10495
|
-
// HttpClient::send() -> array{status:int,headers:array,body:mixed}
|
|
10496
|
-
outputShape: [
|
|
10497
|
-
{ path: "status", type: "number", description: "HTTP status code." },
|
|
10498
|
-
{ path: "headers", type: "object" },
|
|
10499
|
-
{ path: "body", type: "unknown", description: "Decoded JSON when the response is JSON, otherwise the raw body." }
|
|
10500
|
-
],
|
|
10501
|
-
aliases: ["api_request", "@fancy/api_request"],
|
|
10502
|
-
category: "io",
|
|
10503
|
-
label: "API Request",
|
|
10504
|
-
description: "HTTP request to any URL.",
|
|
10505
|
-
icon: "\u2194",
|
|
10506
|
-
configSchema: [
|
|
10507
|
-
...HTTP_METHODS,
|
|
10508
|
-
{ type: "text", key: "url", label: "URL", placeholder: "https://api.example.com/...", required: true },
|
|
10509
|
-
{
|
|
10510
|
-
type: "keyvalue",
|
|
10511
|
-
key: "headers",
|
|
10512
|
-
label: "Headers",
|
|
10513
|
-
keyLabel: "Header",
|
|
10514
|
-
valueLabel: "Value",
|
|
10515
|
-
keyPlaceholder: "content-type",
|
|
10516
|
-
valuePlaceholder: "application/json",
|
|
10517
|
-
addLabel: "Add header",
|
|
10518
|
-
default: { "content-type": "application/json" }
|
|
10519
|
-
},
|
|
10520
|
-
{ type: "json", key: "body", label: "Body" },
|
|
10521
|
-
{ type: "credential", key: "auth", label: "Auth", credentialType: "api_credential" }
|
|
10522
|
-
]
|
|
10523
|
-
},
|
|
10524
|
-
{
|
|
10525
|
-
name: "@particle-academy/webhook_out",
|
|
10526
|
-
outputShape: [
|
|
10527
|
-
{ path: "sent", type: "boolean", description: "True once the request was made." },
|
|
10528
|
-
{ path: "status", type: "number", description: "HTTP status, when the transport reported one." },
|
|
10529
|
-
{ path: "response", type: "unknown", description: "The response body, when there was one." }
|
|
10530
|
-
],
|
|
10531
|
-
aliases: ["webhook_out", "@fancy/webhook_out"],
|
|
10532
|
-
category: "io",
|
|
10533
|
-
label: "Send Webhook",
|
|
10534
|
-
description: "POST a payload to a configured URL.",
|
|
10535
|
-
icon: "\u2197",
|
|
10536
|
-
configSchema: [
|
|
10537
|
-
{ type: "text", key: "url", label: "URL", required: true },
|
|
10538
|
-
{
|
|
10539
|
-
type: "keyvalue",
|
|
10540
|
-
key: "headers",
|
|
10541
|
-
label: "Headers",
|
|
10542
|
-
keyLabel: "Header",
|
|
10543
|
-
valueLabel: "Value",
|
|
10544
|
-
addLabel: "Add header"
|
|
10545
|
-
},
|
|
10546
|
-
{ type: "expression", key: "payload", label: "Payload", required: true, example: "{{ $json }}" }
|
|
10547
|
-
]
|
|
10548
|
-
},
|
|
10549
|
-
// ───────────── Human ─────────────
|
|
10550
|
-
{
|
|
10551
|
-
name: "@particle-academy/human_approval",
|
|
10552
|
-
// returns its input on approved/denied
|
|
10553
|
-
emits: "input",
|
|
10554
|
-
aliases: ["human_approval", "@fancy/human_approval"],
|
|
10555
|
-
pausesForHuman: "approval",
|
|
10556
|
-
category: "human",
|
|
10557
|
-
label: "Human Approval",
|
|
10558
|
-
description: "Pause until a human approves or denies.",
|
|
10559
|
-
icon: "\u2713",
|
|
10560
|
-
inputs: [{ id: "in" }],
|
|
10561
|
-
outputs: [{ id: "approved", label: "approved" }, { id: "denied", label: "denied" }],
|
|
10562
|
-
configSchema: [
|
|
10563
|
-
{ type: "text", key: "title", label: "Approval title", default: "Approve action" },
|
|
10564
|
-
{ type: "textarea", key: "description", label: "Description for approver", rows: 3 },
|
|
10565
|
-
{ type: "credential", key: "channel", label: "Notify channel", credentialType: "notify_channel" }
|
|
10566
|
-
]
|
|
10567
|
-
},
|
|
10568
|
-
{
|
|
10569
|
-
name: "@particle-academy/notify",
|
|
10570
|
-
outputShape: [
|
|
10571
|
-
{ path: "sent", type: "boolean", description: "True once the message was handed to the channel." },
|
|
10572
|
-
{ path: "channel", type: "string", description: "The channel it went to." },
|
|
10573
|
-
{ path: "to", type: "string", description: "The recipient." },
|
|
10574
|
-
{ path: "message", type: "string", description: "The rendered message." }
|
|
10575
|
-
],
|
|
10576
|
-
aliases: ["notify", "@fancy/notify"],
|
|
10577
|
-
category: "human",
|
|
10578
|
-
label: "Notify",
|
|
10579
|
-
description: "Send a message via Slack / email / SMS / etc.",
|
|
10580
|
-
icon: "\u{1F514}",
|
|
10581
|
-
configSchema: [
|
|
10582
|
-
{
|
|
10583
|
-
type: "select",
|
|
10584
|
-
key: "channel",
|
|
10585
|
-
label: "Channel",
|
|
10586
|
-
default: "slack",
|
|
10587
|
-
options: [
|
|
10588
|
-
{ value: "slack", label: "Slack" },
|
|
10589
|
-
{ value: "email", label: "Email" },
|
|
10590
|
-
{ value: "sms", label: "SMS" },
|
|
10591
|
-
{ value: "discord", label: "Discord" }
|
|
10592
|
-
]
|
|
10593
|
-
},
|
|
10594
|
-
{ type: "text", key: "to", label: "To", required: true },
|
|
10595
|
-
{ type: "expression", key: "message", label: "Message", required: true, example: "{{ $json.summary }}" }
|
|
10596
|
-
]
|
|
10597
|
-
},
|
|
10598
|
-
// ───────────── Output ─────────────
|
|
10599
|
-
{
|
|
10600
|
-
name: "@particle-academy/output",
|
|
10601
|
-
// returns its input unchanged
|
|
10602
|
-
emits: "input",
|
|
10603
|
-
aliases: ["output", "@fancy/output"],
|
|
10604
|
-
category: "output",
|
|
10605
|
-
label: "Output",
|
|
10606
|
-
description: "Terminal node \u2014 captures the workflow's result.",
|
|
10607
|
-
icon: "\u25CF",
|
|
10608
|
-
inputs: [{ id: "in" }],
|
|
10609
|
-
outputs: []
|
|
10610
|
-
},
|
|
10611
|
-
{
|
|
10612
|
-
name: "@particle-academy/log",
|
|
10613
|
-
outputShape: [
|
|
10614
|
-
{ path: "logged", type: "string", description: "The message that was written." },
|
|
10615
|
-
{ path: "level", type: "string", description: "The level it was written at." }
|
|
10616
|
-
],
|
|
10617
|
-
aliases: ["log", "@fancy/log"],
|
|
10618
|
-
category: "output",
|
|
10619
|
-
label: "Log",
|
|
10620
|
-
description: "Send to the run feed.",
|
|
10621
|
-
icon: "\u2261",
|
|
10622
|
-
inputs: [{ id: "in" }],
|
|
10623
|
-
outputs: [],
|
|
10624
|
-
configSchema: [
|
|
10625
|
-
{
|
|
10626
|
-
type: "select",
|
|
10627
|
-
key: "level",
|
|
10628
|
-
label: "Level",
|
|
10629
|
-
default: "info",
|
|
10630
|
-
options: [{ value: "info", label: "info" }, { value: "warn", label: "warn" }, { value: "error", label: "error" }]
|
|
10631
|
-
},
|
|
10632
|
-
{ type: "expression", key: "message", label: "Message", required: true, example: "{{ $json }}" }
|
|
10633
|
-
]
|
|
10634
|
-
},
|
|
10635
|
-
{
|
|
10636
|
-
// Swimlane — a portless, resizable container. Child nodes are parented into
|
|
10637
|
-
// it; it never runs (the runtime skips the `layout` category). Uses its own
|
|
10638
|
-
// renderer (LaneNode) instead of the default card.
|
|
10639
|
-
name: "@particle-academy/lane",
|
|
10640
|
-
aliases: ["lane", "@fancy/lane"],
|
|
10641
|
-
category: "layout",
|
|
10642
|
-
label: "Lane",
|
|
10643
|
-
description: "A resizable swimlane \u2014 drop nodes into it to group them.",
|
|
10644
|
-
icon: "\u25A4",
|
|
10645
|
-
inputs: [],
|
|
10646
|
-
outputs: [],
|
|
10647
|
-
configSchema: [
|
|
10648
|
-
{ type: "text", key: "title", label: "Title", default: "Lane" },
|
|
10649
|
-
{
|
|
10650
|
-
type: "select",
|
|
10651
|
-
key: "orientation",
|
|
10652
|
-
label: "Orientation",
|
|
10653
|
-
default: "horizontal",
|
|
10654
|
-
options: [{ value: "horizontal", label: "Row" }, { value: "vertical", label: "Column" }]
|
|
10655
|
-
}
|
|
10656
|
-
],
|
|
10657
|
-
defaultConfig: { title: "Lane", orientation: "horizontal" },
|
|
10658
|
-
resizable: { minWidth: 160, minHeight: 72 },
|
|
10659
|
-
component: LaneNode
|
|
10660
|
-
},
|
|
10661
|
-
{
|
|
10662
|
-
// Note — a sticky-note annotation. Portless + visual-only: the runtime skips
|
|
10663
|
-
// the `annotation` category, so a note's text NEVER reaches a runner — it
|
|
10664
|
-
// rides in the document purely for people, editors, and MCP tools. Uses its
|
|
10665
|
-
// own renderer (NoteNode); double-click on the canvas to edit in place.
|
|
10666
|
-
name: "@particle-academy/note",
|
|
10667
|
-
aliases: ["note", "@fancy/note"],
|
|
10668
|
-
category: "annotation",
|
|
10669
|
-
label: "Note",
|
|
10670
|
-
description: "A sticky note that documents the canvas. Never runs \u2014 editor + agent only.",
|
|
10671
|
-
icon: "\u{1F5D2}",
|
|
10672
|
-
inputs: [],
|
|
10673
|
-
outputs: [],
|
|
10674
|
-
configSchema: [
|
|
10675
|
-
{ type: "text", key: "title", label: "Title", placeholder: "Optional heading" },
|
|
10676
|
-
{ type: "textarea", key: "text", label: "Note", rows: 5, placeholder: "What does this part of the flow do?" },
|
|
10677
|
-
{ type: "select", key: "color", label: "Color", default: "amber", options: [
|
|
10678
|
-
{ value: "amber", label: "Amber" },
|
|
10679
|
-
{ value: "sky", label: "Sky" },
|
|
10680
|
-
{ value: "violet", label: "Violet" },
|
|
10681
|
-
{ value: "emerald", label: "Emerald" },
|
|
10682
|
-
{ value: "rose", label: "Rose" },
|
|
10683
|
-
{ value: "slate", label: "Slate" }
|
|
10684
|
-
] }
|
|
10685
|
-
],
|
|
10686
|
-
defaultConfig: { text: "", color: "amber" },
|
|
10687
|
-
resizable: { minWidth: 140, minHeight: 80 },
|
|
10688
|
-
component: NoteNode
|
|
10689
|
-
}
|
|
10690
|
-
];
|
|
10691
|
-
function registerBuiltinKinds() {
|
|
10692
|
-
for (const k of KINDS) registerNodeKind(k);
|
|
10693
|
-
}
|
|
10694
|
-
var BUILTIN_KINDS = KINDS;
|
|
10695
|
-
|
|
10696
|
-
// src/registry/registry.ts
|
|
10697
|
-
var kinds = /* @__PURE__ */ new Map();
|
|
10698
|
-
var aliases = /* @__PURE__ */ new Map();
|
|
10699
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
10700
|
-
var overrides = /* @__PURE__ */ new Map();
|
|
10701
|
-
var builtinsEnsured = false;
|
|
10702
|
-
function ensureBuiltinKinds() {
|
|
10703
|
-
if (builtinsEnsured) return;
|
|
10704
|
-
builtinsEnsured = true;
|
|
10705
|
-
registerBuiltinKinds();
|
|
10706
|
-
}
|
|
10707
|
-
function registerNodeKind(definition) {
|
|
10708
|
-
ensureBuiltinKinds();
|
|
10709
|
-
kinds.set(definition.name, definition);
|
|
10710
|
-
for (const alias of definition.aliases ?? []) aliases.set(alias, definition.name);
|
|
10711
|
-
notify();
|
|
10712
|
-
return () => {
|
|
10713
|
-
if (kinds.get(definition.name) === definition) {
|
|
10714
|
-
kinds.delete(definition.name);
|
|
10715
|
-
for (const alias of definition.aliases ?? []) {
|
|
10716
|
-
if (aliases.get(alias) === definition.name) aliases.delete(alias);
|
|
10717
|
-
}
|
|
10718
|
-
notify();
|
|
10719
|
-
}
|
|
10720
|
-
};
|
|
10721
|
-
}
|
|
10722
|
-
function resolveKindId(id2) {
|
|
10723
|
-
ensureBuiltinKinds();
|
|
10724
|
-
if (kinds.has(id2)) return id2;
|
|
10725
|
-
const canonical = aliases.get(id2);
|
|
10726
|
-
return canonical && kinds.has(canonical) ? canonical : null;
|
|
10727
|
-
}
|
|
10728
|
-
function overrideNodeKind(name, patch) {
|
|
10729
|
-
const canonical = resolveKindId(name) ?? name;
|
|
10730
|
-
const previous = overrides.get(canonical);
|
|
10731
|
-
overrides.set(canonical, { ...previous, ...patch });
|
|
10732
|
-
notify();
|
|
10733
|
-
return () => {
|
|
10734
|
-
if (previous) {
|
|
10735
|
-
overrides.set(canonical, previous);
|
|
10736
|
-
} else {
|
|
10737
|
-
overrides.delete(canonical);
|
|
10738
|
-
}
|
|
10739
|
-
notify();
|
|
10740
|
-
};
|
|
10741
|
-
}
|
|
10742
|
-
function clearNodeKindOverrides() {
|
|
10743
|
-
if (overrides.size === 0) return;
|
|
10744
|
-
overrides.clear();
|
|
10745
|
-
notify();
|
|
10746
|
-
}
|
|
10747
|
-
function withOverride(kind) {
|
|
10748
|
-
if (!kind) return null;
|
|
10749
|
-
const patch = overrides.get(kind.name);
|
|
10750
|
-
return patch ? { ...kind, ...patch } : kind;
|
|
10751
|
-
}
|
|
10752
|
-
function getNodeKind(name) {
|
|
10753
|
-
const canonical = resolveKindId(name);
|
|
10754
|
-
return canonical ? withOverride(kinds.get(canonical)) : null;
|
|
10755
|
-
}
|
|
10756
|
-
function kindIds(kind) {
|
|
10757
|
-
return [kind.name, ...kind.aliases ?? []];
|
|
10758
|
-
}
|
|
10759
|
-
function listNodeKinds(category) {
|
|
10760
|
-
ensureBuiltinKinds();
|
|
10761
|
-
const all = Array.from(kinds.values()).map(
|
|
10762
|
-
(k) => withOverride(k)
|
|
10763
|
-
);
|
|
10764
|
-
return category ? all.filter((k) => k.category === category) : all;
|
|
10765
|
-
}
|
|
10766
|
-
function onNodeKindsChanged(listener) {
|
|
10767
|
-
listeners.add(listener);
|
|
10768
|
-
return () => listeners.delete(listener);
|
|
10769
|
-
}
|
|
10770
|
-
function notify() {
|
|
10771
|
-
for (const l of listeners) l();
|
|
10772
|
-
}
|
|
10773
|
-
function defaultConfigFor(kind) {
|
|
10774
|
-
const fromKind = kind.defaultConfig ? { ...kind.defaultConfig } : {};
|
|
10775
|
-
for (const field of kind.configSchema ?? []) {
|
|
10776
|
-
if (fromKind[field.key] !== void 0) continue;
|
|
10777
|
-
if ("default" in field && field.default !== void 0) {
|
|
10778
|
-
fromKind[field.key] = field.default;
|
|
10779
|
-
}
|
|
10780
|
-
}
|
|
10781
|
-
return fromKind;
|
|
10782
|
-
}
|
|
10783
|
-
function validateConfig(kind, config) {
|
|
10784
|
-
const issues = [];
|
|
10785
|
-
for (const field of kind.configSchema ?? []) {
|
|
10786
|
-
const value = config[field.key];
|
|
10787
|
-
if (field.required && (value === void 0 || value === null || value === "")) {
|
|
10788
|
-
issues.push({ key: field.key, message: `${field.label} is required` });
|
|
10789
|
-
continue;
|
|
10790
|
-
}
|
|
10791
|
-
if (value === void 0 || value === null) continue;
|
|
10792
|
-
const issue = validateField(field, value);
|
|
10793
|
-
if (issue) issues.push({ key: field.key, message: issue });
|
|
10794
|
-
}
|
|
10795
|
-
return issues;
|
|
10796
|
-
}
|
|
10797
|
-
function validateField(field, value) {
|
|
10798
|
-
switch (field.type) {
|
|
10799
|
-
case "text":
|
|
10800
|
-
case "textarea":
|
|
10801
|
-
case "expression":
|
|
10802
|
-
case "credential":
|
|
10803
|
-
return typeof value === "string" ? null : `${field.label} must be a string`;
|
|
10804
|
-
case "number": {
|
|
10805
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return `${field.label} must be a number`;
|
|
10806
|
-
if (field.min !== void 0 && value < field.min) return `${field.label} must be >= ${field.min}`;
|
|
10807
|
-
if (field.max !== void 0 && value > field.max) return `${field.label} must be <= ${field.max}`;
|
|
10808
|
-
return null;
|
|
10809
|
-
}
|
|
10810
|
-
case "switch":
|
|
10811
|
-
return typeof value === "boolean" ? null : `${field.label} must be a boolean`;
|
|
10812
|
-
case "select": {
|
|
10813
|
-
const allowed = field.options.map((o) => o.value);
|
|
10814
|
-
return allowed.includes(String(value)) ? null : `${field.label} must be one of ${allowed.join(", ")}`;
|
|
10815
|
-
}
|
|
10816
|
-
case "json":
|
|
10817
|
-
return null;
|
|
10818
|
-
// permissive — just JSON-shaped
|
|
10819
|
-
case "repeater": {
|
|
10820
|
-
if (!Array.isArray(value)) return `${field.label} must be a list`;
|
|
10821
|
-
if (field.minItems !== void 0 && value.length < field.minItems) {
|
|
10822
|
-
return `${field.label} needs at least ${field.minItems}`;
|
|
10823
|
-
}
|
|
10824
|
-
if (field.maxItems !== void 0 && value.length > field.maxItems) {
|
|
10825
|
-
return `${field.label} allows at most ${field.maxItems}`;
|
|
10826
|
-
}
|
|
10827
|
-
for (let i = 0; i < value.length; i++) {
|
|
10828
|
-
const row = value[i];
|
|
10829
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
10830
|
-
return `${field.label} item ${i + 1} must be an object`;
|
|
10831
|
-
}
|
|
10832
|
-
for (const sub of field.fields) {
|
|
10833
|
-
const cell = row[sub.key];
|
|
10834
|
-
if (sub.required && (cell === void 0 || cell === null || cell === "")) {
|
|
10835
|
-
return `${field.label} item ${i + 1}: ${sub.label} is required`;
|
|
10836
|
-
}
|
|
10837
|
-
if (cell === void 0 || cell === null) continue;
|
|
10838
|
-
const issue = validateField(sub, cell);
|
|
10839
|
-
if (issue) return `${field.label} item ${i + 1}: ${issue}`;
|
|
10840
|
-
}
|
|
10841
|
-
}
|
|
10842
|
-
return null;
|
|
10843
|
-
}
|
|
10844
|
-
case "keyvalue": {
|
|
10845
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
10846
|
-
return `${field.label} must be a key/value map`;
|
|
10847
|
-
}
|
|
10848
|
-
const allowed = field.valueOptions?.map((o) => o.value);
|
|
10849
|
-
for (const [k, v] of Object.entries(value)) {
|
|
10850
|
-
if (typeof v !== "string") return `${field.label}: "${k}" must be a string`;
|
|
10851
|
-
if (allowed && !allowed.includes(v)) {
|
|
10852
|
-
return `${field.label}: "${k}" must be one of ${allowed.join(", ")}`;
|
|
10853
|
-
}
|
|
10854
|
-
}
|
|
10855
|
-
return null;
|
|
10856
|
-
}
|
|
10857
|
-
case "document":
|
|
10858
|
-
return null;
|
|
10859
|
-
// opaque to fancy-flow — the host's editor owns its shape
|
|
10860
|
-
default:
|
|
10861
|
-
return null;
|
|
10862
|
-
}
|
|
10863
|
-
}
|
|
10864
|
-
function categoryAccent(category) {
|
|
10865
|
-
switch (category) {
|
|
10866
|
-
case "trigger":
|
|
10867
|
-
return "#10b981";
|
|
10868
|
-
case "logic":
|
|
10869
|
-
return "#f59e0b";
|
|
10870
|
-
case "data":
|
|
10871
|
-
return "#0ea5e9";
|
|
10872
|
-
case "ai":
|
|
10873
|
-
return "#8b5cf6";
|
|
10874
|
-
case "io":
|
|
10875
|
-
return "#3b82f6";
|
|
10876
|
-
case "human":
|
|
10877
|
-
return "#ec4899";
|
|
10878
|
-
case "output":
|
|
10879
|
-
return "#a855f7";
|
|
10880
|
-
case "layout":
|
|
10881
|
-
return "#64748b";
|
|
10882
|
-
case "annotation":
|
|
10883
|
-
return "#eab308";
|
|
10884
|
-
default:
|
|
10885
|
-
return "#71717a";
|
|
10886
|
-
}
|
|
10887
|
-
}
|
|
10888
|
-
function LaneNodeInner(props) {
|
|
10889
|
-
const data = props.data;
|
|
10890
|
-
const config = data.config ?? {};
|
|
10891
|
-
const title = config.title ?? data.label ?? "Lane";
|
|
10892
|
-
const orientation = config.orientation ?? "horizontal";
|
|
10893
|
-
const accent = config.color ?? data.color ?? categoryAccent("layout");
|
|
10894
|
-
return /* @__PURE__ */ jsxs(
|
|
10895
|
-
"div",
|
|
10896
|
-
{
|
|
10897
|
-
className: ["ff-lane", `ff-lane--${orientation}`, props.selected ? "ff-lane--selected" : ""].filter(Boolean).join(" "),
|
|
10898
|
-
style: { borderColor: props.selected ? accent : void 0 },
|
|
10899
|
-
children: [
|
|
10900
|
-
/* @__PURE__ */ jsx(NodeResizer, { isVisible: props.selected ?? false, color: accent, minWidth: 160, minHeight: 72 }),
|
|
10901
|
-
/* @__PURE__ */ jsx("div", { className: "ff-lane__header", style: { background: accent }, children: /* @__PURE__ */ jsx("span", { className: "ff-lane__title", children: title }) })
|
|
10902
|
-
]
|
|
10903
|
-
}
|
|
10904
|
-
);
|
|
10905
|
-
}
|
|
10906
|
-
var LaneNode = memo(LaneNodeInner);
|
|
10907
|
-
|
|
10908
|
-
export { BUILTIN_KINDS, Background, BackgroundVariant, Controls, DEFAULT_MAX_DEPTH, FlowEditorProvider, Handle, LaneNode, MiniMap, NodeResizer, NodeToolbar, NoteNode, Position, ReactFlowProvider, RunIdentity, ViewportPortal, addEdge2 as addEdge, applyEdgeChanges, applyNodeChanges, categoryAccent, clearNodeKindOverrides, declaredRoutes, defaultConfigFor, ensureBuiltinKinds, escapeSegment, getNodeKind, humanInputFields, index, kindIds, listNodeKinds, llmRouterExecutor, nodeConfig, onNodeKindsChanged, overrideNodeKind, reconnectEdge2 as reconnectEdge, registerBuiltinKinds, registerNodeKind, resolveFallbackPort, resolveKindId, resolveNodePorts, resolvePortSpec, runFlow, subflowExecutor, subflowMode, subflowPorts, useFlowEditor, useFlowEditorOptional, useReactFlow, validateConfig };
|
|
10909
|
-
//# sourceMappingURL=chunk-JF6WCRBU.js.map
|
|
10910
|
-
//# sourceMappingURL=chunk-JF6WCRBU.js.map
|
|
9039
|
+
export { Background, BackgroundVariant, Controls, Handle, MiniMap, NodeResizer, NodeToolbar, Position, ReactFlowProvider, ViewportPortal, addEdge2 as addEdge, applyEdgeChanges, applyNodeChanges, index, reconnectEdge2 as reconnectEdge, useReactFlow };
|
|
9040
|
+
//# sourceMappingURL=chunk-OWENS2H5.js.map
|
|
9041
|
+
//# sourceMappingURL=chunk-OWENS2H5.js.map
|