@particle-academy/fancy-flow 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useFlowState, useFlowRun, useFlowHistory, applyOutputsToNodes, applyStatusesToNodes } from './chunk-OFW7GN3Y.js';
2
2
  export { applyOutputsToNodes, applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './chunk-OFW7GN3Y.js';
3
- import { buildNodeTypes, FlowEditorProvider, registerBuiltinKinds, NoteNode, createConnectionValidator } from './chunk-ATQXZEVD.js';
4
- export { ANY_PORT_TYPE, BUILTIN_KINDS, LaneNode, NoteNode, RegistryNode, buildNodeTypes, createConnectionValidator, defaultPortCompatibility, registerBuiltinKinds, useFlowEditor, useFlowEditorOptional } from './chunk-ATQXZEVD.js';
3
+ import { buildNodeTypes, FlowEditorProvider, registerBuiltinKinds, NoteNode, createConnectionValidator } from './chunk-YOFCVO2W.js';
4
+ export { ANY_PORT_TYPE, BUILTIN_KINDS, LaneNode, NoteNode, RegistryNode, buildNodeTypes, createConnectionValidator, defaultPortCompatibility, registerBuiltinKinds, useFlowEditor, useFlowEditorOptional } from './chunk-YOFCVO2W.js';
5
5
  import { ReactFlowProvider, useReactFlow, addEdge, applyEdgeChanges, applyNodeChanges, Position, Handle, reconnectEdge, index, BackgroundVariant, Background, Controls, MiniMap, ViewportPortal } from './chunk-OWENS2H5.js';
6
6
  export { LEGACY_PAUSE_PREFIXES, PAUSE_PREFIX, decodePause, encodePause, isPause, pauseForHuman } from './chunk-UEOE6B52.js';
7
7
  import './chunk-USL4FMFU.js';
@@ -13,7 +13,7 @@ import { onNodeKindsChanged, listNodeKinds, getNodeKind, defaultConfigFor, valid
13
13
  export { categoryAccent, defaultConfigFor, getNodeKind, listNodeKinds, onNodeKindsChanged, registerNodeKind, validateConfig } from './chunk-Q6IPK64P.js';
14
14
  import { getRichInputAdapter } from './chunk-F5RPRB7A.js';
15
15
  export { RichInputPreview, getRichInputAdapter, isRichInputEnabled, onRichInputAdapterChanged, registerRichInputAdapter } from './chunk-F5RPRB7A.js';
16
- import { memo, forwardRef, useState, useEffect, useMemo, useCallback, useRef, useImperativeHandle } from 'react';
16
+ import { memo, forwardRef, useState, useMemo, useEffect, useCallback, useRef, useImperativeHandle } from 'react';
17
17
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
18
18
 
19
19
  function NodeShellInner({
@@ -1062,6 +1062,133 @@ function formatTime(at) {
1062
1062
  const d = new Date(at);
1063
1063
  return `${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}.${Math.floor(d.getMilliseconds() / 100)}`;
1064
1064
  }
1065
+ function humanInputFields(config) {
1066
+ const raw = Array.isArray(config?.fields) ? config.fields : [];
1067
+ const fields = raw.filter((f) => f && typeof f === "object" && typeof f.key === "string" && f.key).map((f) => ({
1068
+ key: f.key,
1069
+ label: typeof f.label === "string" && f.label ? f.label : f.key,
1070
+ type: ["text", "textarea", "number", "select", "switch"].includes(f.type) ? f.type : "text",
1071
+ required: !!f.required,
1072
+ placeholder: typeof f.placeholder === "string" ? f.placeholder : void 0,
1073
+ options: Array.isArray(f.options) ? f.options : void 0,
1074
+ default: f.default
1075
+ }));
1076
+ if (fields.length) return fields;
1077
+ const title = typeof config?.title === "string" && config.title ? config.title : "Your answer";
1078
+ return [{ key: "value", label: title, type: "textarea", required: true }];
1079
+ }
1080
+ function initialValues(fields) {
1081
+ const v = {};
1082
+ for (const f of fields) v[f.key] = f.type === "switch" ? !!f.default : f.default ?? "";
1083
+ return v;
1084
+ }
1085
+ function HumanPrompt({ request, onCancel }) {
1086
+ const firstRef = useRef(null);
1087
+ useEffect(() => {
1088
+ firstRef.current?.focus();
1089
+ const onKey = (e) => {
1090
+ if (e.key === "Escape") onCancel();
1091
+ };
1092
+ window.addEventListener("keydown", onKey);
1093
+ return () => window.removeEventListener("keydown", onKey);
1094
+ }, [onCancel]);
1095
+ return /* @__PURE__ */ jsx("div", { className: "ff-prompt-overlay", role: "dialog", "aria-modal": "true", "aria-label": request.title, children: /* @__PURE__ */ jsxs("div", { className: "ff-prompt", onClick: (e) => e.stopPropagation(), children: [
1096
+ /* @__PURE__ */ jsx("div", { className: "ff-prompt__title", children: request.title }),
1097
+ request.kind === "approval" ? /* @__PURE__ */ jsx(ApprovalBody, { request, onCancel }) : /* @__PURE__ */ jsx(InputBody, { request, onCancel, firstRef })
1098
+ ] }) });
1099
+ }
1100
+ function ApprovalBody({ request, onCancel }) {
1101
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1102
+ request.description && /* @__PURE__ */ jsx("p", { className: "ff-prompt__desc", children: request.description }),
1103
+ /* @__PURE__ */ jsxs("div", { className: "ff-prompt__actions", children: [
1104
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ff-prompt__btn ff-prompt__btn--ghost", onClick: onCancel, children: "Cancel" }),
1105
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ff-prompt__btn ff-prompt__btn--danger", onClick: () => request.resolve(false), children: "Deny" }),
1106
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ff-prompt__btn ff-prompt__btn--primary", onClick: () => request.resolve(true), children: "Approve" })
1107
+ ] })
1108
+ ] });
1109
+ }
1110
+ function InputBody({
1111
+ request,
1112
+ onCancel,
1113
+ firstRef
1114
+ }) {
1115
+ const [values, setValues] = useState(() => initialValues(request.fields));
1116
+ const set = (k, v) => setValues((prev) => ({ ...prev, [k]: v }));
1117
+ const missing = request.fields.filter((f) => f.required && (values[f.key] === void 0 || values[f.key] === ""));
1118
+ const submit = () => {
1119
+ if (missing.length === 0) request.resolve(values);
1120
+ };
1121
+ return /* @__PURE__ */ jsxs(
1122
+ "form",
1123
+ {
1124
+ onSubmit: (e) => {
1125
+ e.preventDefault();
1126
+ submit();
1127
+ },
1128
+ onKeyDown: (e) => {
1129
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
1130
+ e.preventDefault();
1131
+ submit();
1132
+ }
1133
+ },
1134
+ children: [
1135
+ /* @__PURE__ */ jsx("div", { className: "ff-prompt__fields", children: request.fields.map((f, i) => /* @__PURE__ */ jsxs("label", { className: "ff-prompt__field", children: [
1136
+ /* @__PURE__ */ jsxs("span", { className: "ff-prompt__label", children: [
1137
+ f.label ?? f.key,
1138
+ f.required && /* @__PURE__ */ jsx("span", { className: "ff-prompt__req", children: " *" })
1139
+ ] }),
1140
+ renderControl(f, values[f.key], (v) => set(f.key, v), i === 0 ? firstRef : void 0)
1141
+ ] }, f.key)) }),
1142
+ /* @__PURE__ */ jsxs("div", { className: "ff-prompt__actions", children: [
1143
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ff-prompt__btn ff-prompt__btn--ghost", onClick: onCancel, children: "Cancel" }),
1144
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "ff-prompt__btn ff-prompt__btn--primary", disabled: missing.length > 0, children: request.submitLabel || "Continue" })
1145
+ ] })
1146
+ ]
1147
+ }
1148
+ );
1149
+ }
1150
+ function renderControl(f, value, onChange, ref) {
1151
+ const common = { className: "ff-prompt__input", placeholder: f.placeholder };
1152
+ if (f.type === "textarea") {
1153
+ return /* @__PURE__ */ jsx(
1154
+ "textarea",
1155
+ {
1156
+ ...common,
1157
+ ref,
1158
+ rows: 3,
1159
+ value: String(value ?? ""),
1160
+ onChange: (e) => onChange(e.target.value)
1161
+ }
1162
+ );
1163
+ }
1164
+ if (f.type === "switch") {
1165
+ return /* @__PURE__ */ jsx(
1166
+ "input",
1167
+ {
1168
+ type: "checkbox",
1169
+ className: "ff-prompt__switch",
1170
+ checked: !!value,
1171
+ onChange: (e) => onChange(e.target.checked)
1172
+ }
1173
+ );
1174
+ }
1175
+ if (f.type === "select" && f.options && f.options.length) {
1176
+ return /* @__PURE__ */ jsxs("select", { className: "ff-prompt__input", value: String(value ?? ""), onChange: (e) => onChange(e.target.value), children: [
1177
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Choose\u2026" }),
1178
+ f.options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
1179
+ ] });
1180
+ }
1181
+ return /* @__PURE__ */ jsx(
1182
+ "input",
1183
+ {
1184
+ ...common,
1185
+ ref,
1186
+ type: f.type === "number" ? "number" : "text",
1187
+ value: String(value ?? ""),
1188
+ onChange: (e) => onChange(f.type === "number" ? e.target.value === "" ? "" : Number(e.target.value) : e.target.value)
1189
+ }
1190
+ );
1191
+ }
1065
1192
  var FlowEditor = forwardRef(function FlowEditor2(props, ref) {
1066
1193
  return /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(
1067
1194
  "div",
@@ -1095,6 +1222,41 @@ function FlowEditorInner({
1095
1222
  const internal = useFlowState(initial);
1096
1223
  const runner = useFlowRun();
1097
1224
  const rf = useReactFlow();
1225
+ const [prompt, setPrompt] = useState(null);
1226
+ const humanExecutors = useMemo(
1227
+ () => ({
1228
+ user_input: ({ node }) => new Promise((resolve) => {
1229
+ const cfg = node.data?.config ?? {};
1230
+ setPrompt({
1231
+ kind: "input",
1232
+ title: typeof cfg.title === "string" && cfg.title || "Your input",
1233
+ submitLabel: typeof cfg.submitLabel === "string" ? cfg.submitLabel : void 0,
1234
+ fields: humanInputFields(cfg),
1235
+ resolve: (values) => {
1236
+ setPrompt(null);
1237
+ resolve(values);
1238
+ }
1239
+ });
1240
+ }),
1241
+ human_approval: ({ node }) => new Promise((resolve) => {
1242
+ const cfg = node.data?.config ?? {};
1243
+ setPrompt({
1244
+ kind: "approval",
1245
+ title: typeof cfg.title === "string" && cfg.title || "Approve action",
1246
+ description: typeof cfg.description === "string" ? cfg.description : void 0,
1247
+ resolve: (approved) => {
1248
+ setPrompt(null);
1249
+ resolve({ branch: approved ? "approved" : "denied" });
1250
+ }
1251
+ });
1252
+ })
1253
+ }),
1254
+ []
1255
+ );
1256
+ const runExecutors = useMemo(() => ({ ...humanExecutors, ...executors }), [humanExecutors, executors]);
1257
+ useEffect(() => {
1258
+ if (!runner.running) setPrompt(null);
1259
+ }, [runner.running]);
1098
1260
  const controlled = value !== void 0;
1099
1261
  const baseFlow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
1100
1262
  const hist = useFlowHistory(baseFlow);
@@ -1404,7 +1566,7 @@ function FlowEditorInner({
1404
1566
  ({ autoLayout }) => flow.setGraph({ nodes: autoLayout({ nodes: flow.nodes, edges: flow.edges }, { scope: laneId }), edges: flow.edges })
1405
1567
  );
1406
1568
  },
1407
- run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
1569
+ run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, runExecutors),
1408
1570
  cancel: runner.cancel,
1409
1571
  reset: runner.reset,
1410
1572
  toWorkflow,
@@ -1416,7 +1578,7 @@ function FlowEditorInner({
1416
1578
  canUndo: hist.canUndo,
1417
1579
  canRedo: hist.canRedo
1418
1580
  };
1419
- }, [flow, selectedId, selected, selectedEdgeId, selectedEdge, selectedIds, selectedNodes, runner, executors, metadata, addNode, deleteNodes, deleteEdges, duplicateSelected, alignSelected, distributeSelected, copySelection, pasteClipboard, addLane, hist, rf]);
1581
+ }, [flow, selectedId, selected, selectedEdgeId, selectedEdge, selectedIds, selectedNodes, runner, runExecutors, metadata, addNode, deleteNodes, deleteEdges, duplicateSelected, alignSelected, distributeSelected, copySelection, pasteClipboard, addLane, hist, rf]);
1420
1582
  useImperativeHandle(apiRef, () => api, [api]);
1421
1583
  const dropHandlers = paletteDropHandlers((kindName, evt) => {
1422
1584
  const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
@@ -1590,7 +1752,11 @@ function FlowEditorInner({
1590
1752
  onCancel: () => setLabelEdit(null)
1591
1753
  }
1592
1754
  ),
1593
- showFeed && (slots.feed ? slots.feed(api) : /* @__PURE__ */ jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" }))
1755
+ showFeed && (slots.feed ? slots.feed(api) : /* @__PURE__ */ jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" })),
1756
+ prompt && /* @__PURE__ */ jsx(HumanPrompt, { request: prompt, onCancel: () => {
1757
+ setPrompt(null);
1758
+ runner.cancel();
1759
+ } })
1594
1760
  ] }),
1595
1761
  showPanel && (slots.panel ? slots.panel(api) : /* @__PURE__ */ jsxs("div", { className: "ff-editor__panel-wrap", children: [
1596
1762
  /* @__PURE__ */ jsx(