@particle-academy/fancy-flow 0.41.0 → 0.42.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.cjs CHANGED
@@ -11496,6 +11496,9 @@ var KINDS = [
11496
11496
  },
11497
11497
  {
11498
11498
  name: "@particle-academy/user_input",
11499
+ // The keys an author declared on THIS node — the case a static list cannot
11500
+ // express, and the one issue #5 named.
11501
+ outputShape: (config) => (config.fields ?? []).filter((f) => typeof f.key === "string" && f.key !== "").map((f) => ({ path: f.key, type: "string", description: f.label })),
11499
11502
  aliases: ["user_input", "@fancy/user_input"],
11500
11503
  pausesForHuman: "input",
11501
11504
  category: "human",
@@ -11716,6 +11719,10 @@ var KINDS = [
11716
11719
  },
11717
11720
  {
11718
11721
  name: "@particle-academy/for_each",
11722
+ outputShape: [
11723
+ { path: "items", type: "array" },
11724
+ { path: "count", type: "number" }
11725
+ ],
11719
11726
  aliases: ["for_each", "@fancy/for_each"],
11720
11727
  category: "logic",
11721
11728
  label: "For Each",
@@ -11879,6 +11886,12 @@ var KINDS = [
11879
11886
  // ───────────── AI ─────────────
11880
11887
  {
11881
11888
  name: "@particle-academy/llm_call",
11889
+ // LlmClient::complete() -> array{text:string,usage?:array,raw?:mixed}
11890
+ outputShape: [
11891
+ { path: "text", type: "string", description: "The model's completion." },
11892
+ { path: "usage", type: "object", description: "Token counts, when the provider reports them." },
11893
+ { path: "raw", type: "unknown", description: "The provider's untouched response." }
11894
+ ],
11882
11895
  aliases: ["llm_call", "@fancy/llm_call"],
11883
11896
  category: "ai",
11884
11897
  label: "LLM Call",
@@ -12013,6 +12026,10 @@ var KINDS = [
12013
12026
  },
12014
12027
  {
12015
12028
  name: "@particle-academy/embed_search",
12029
+ outputShape: [
12030
+ { path: "query", type: "string" },
12031
+ { path: "matches", type: "array", description: "Ranked results from the vector store." }
12032
+ ],
12016
12033
  aliases: ["embed_search", "@fancy/embed_search"],
12017
12034
  category: "ai",
12018
12035
  label: "Embed & Search",
@@ -12027,6 +12044,12 @@ var KINDS = [
12027
12044
  // ───────────── IO ─────────────
12028
12045
  {
12029
12046
  name: "@particle-academy/api_request",
12047
+ // HttpClient::send() -> array{status:int,headers:array,body:mixed}
12048
+ outputShape: [
12049
+ { path: "status", type: "number", description: "HTTP status code." },
12050
+ { path: "headers", type: "object" },
12051
+ { path: "body", type: "unknown", description: "Decoded JSON when the response is JSON, otherwise the raw body." }
12052
+ ],
12030
12053
  aliases: ["api_request", "@fancy/api_request"],
12031
12054
  category: "io",
12032
12055
  label: "API Request",
@@ -12811,6 +12834,230 @@ function paletteDropHandlers(onDrop) {
12811
12834
  }
12812
12835
  };
12813
12836
  }
12837
+
12838
+ // src/expressions/variables.ts
12839
+ var ROOT = "$json";
12840
+ function expr(path) {
12841
+ return `{{ ${path} }}`;
12842
+ }
12843
+ function outputFieldsFor(node) {
12844
+ const kindName = node.data?.kind ?? node.type;
12845
+ if (!kindName) return [];
12846
+ const kind = getNodeKind(kindName);
12847
+ const shape = kind?.outputShape;
12848
+ if (!shape) return [];
12849
+ if (typeof shape === "function") {
12850
+ try {
12851
+ const config = node.data?.config ?? {};
12852
+ return shape(config) ?? [];
12853
+ } catch {
12854
+ return [];
12855
+ }
12856
+ }
12857
+ return shape;
12858
+ }
12859
+ function baseVariables() {
12860
+ return [
12861
+ { expression: expr(ROOT), path: ROOT, type: "unknown", description: "The whole incoming value." }
12862
+ ];
12863
+ }
12864
+ function availableVariables(graph, nodeId) {
12865
+ const out = baseVariables();
12866
+ const seen = /* @__PURE__ */ new Set([ROOT]);
12867
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
12868
+ const upstreamIds = graph.edges.filter((e) => e.target === nodeId).map((e) => e.source);
12869
+ for (const id2 of upstreamIds) {
12870
+ if (id2 === nodeId) continue;
12871
+ const upstream = byId.get(id2);
12872
+ if (!upstream) continue;
12873
+ const kindName = upstream.data?.kind ?? upstream.type;
12874
+ const label = upstream.data?.label ?? (kindName ? getNodeKind(kindName)?.label : void 0);
12875
+ for (const field of outputFieldsFor(upstream)) {
12876
+ const path = `${ROOT}.${field.path}`;
12877
+ if (seen.has(path)) continue;
12878
+ seen.add(path);
12879
+ out.push({
12880
+ expression: expr(path),
12881
+ path,
12882
+ type: field.type,
12883
+ description: field.description,
12884
+ source: label
12885
+ });
12886
+ }
12887
+ }
12888
+ return out;
12889
+ }
12890
+ function describeExpressionGrammar() {
12891
+ return {
12892
+ forms: [
12893
+ { syntax: "{{ $json }}", meaning: "The whole value arriving on this node's input." },
12894
+ { syntax: "{{ $json.field }}", meaning: "One key of that value. Dot-paths nest: $json.user.email." },
12895
+ { syntax: "{{ $input.field }}", meaning: "An alias for $json \u2014 the same value, either spelling." },
12896
+ { syntax: "{{ in }}", meaning: "The input by its context key, rather than through the alias." }
12897
+ ],
12898
+ note: "Dot-paths only \u2014 no arithmetic, comparisons or function calls; anything else resolves to nothing. A field that is exactly one expression keeps the resolved value's type; mixed text interpolates as a string. fancy-flow's own runFlow does not interpolate: it hands config to your executor verbatim, and the fancy-flow-php runtime is what resolves these."
12899
+ };
12900
+ }
12901
+ function findTrigger(text, caret) {
12902
+ const before = text.slice(0, caret);
12903
+ const open = before.lastIndexOf("{{");
12904
+ if (open === -1) return null;
12905
+ if (before.indexOf("}}", open) !== -1) return null;
12906
+ return { open, query: before.slice(open + 2).trim() };
12907
+ }
12908
+ function filterVariables(vars, query) {
12909
+ const q2 = query.trim().toLowerCase();
12910
+ if (q2 === "") return vars;
12911
+ return vars.filter((v2) => v2.path.toLowerCase().includes(q2));
12912
+ }
12913
+ function applyCompletion(text, caret, trigger, variable) {
12914
+ const head = text.slice(0, trigger.open);
12915
+ const tail = text.slice(caret);
12916
+ return { value: `${head}${variable.expression}${tail}`, caret: head.length + variable.expression.length };
12917
+ }
12918
+ function ExpressionField({
12919
+ value,
12920
+ onChange,
12921
+ placeholder,
12922
+ rows = 2,
12923
+ handle,
12924
+ graph,
12925
+ nodeId
12926
+ }) {
12927
+ const ref = ReactExports.useRef(null);
12928
+ const [trigger, setTrigger] = ReactExports.useState(null);
12929
+ const [active, setActive] = ReactExports.useState(0);
12930
+ const [showHelp, setShowHelp] = ReactExports.useState(false);
12931
+ const variables = ReactExports.useMemo(
12932
+ () => graph && nodeId ? availableVariables(graph, nodeId) : baseVariables(),
12933
+ [graph, nodeId]
12934
+ );
12935
+ const matches = ReactExports.useMemo(
12936
+ () => trigger ? filterVariables(variables, trigger.query) : [],
12937
+ [trigger, variables]
12938
+ );
12939
+ const help = ReactExports.useMemo(() => describeExpressionGrammar(), []);
12940
+ const open = trigger !== null && matches.length > 0;
12941
+ const sync = (el) => {
12942
+ const next = findTrigger(el.value, el.selectionStart ?? el.value.length);
12943
+ setTrigger(next);
12944
+ setActive(0);
12945
+ };
12946
+ const choose = (variable) => {
12947
+ const el = ref.current;
12948
+ if (!el || !trigger) return;
12949
+ const { value: next, caret } = applyCompletion(
12950
+ el.value,
12951
+ el.selectionStart ?? el.value.length,
12952
+ trigger,
12953
+ variable
12954
+ );
12955
+ onChange(next);
12956
+ setTrigger(null);
12957
+ requestAnimationFrame(() => {
12958
+ el.focus();
12959
+ el.setSelectionRange(caret, caret);
12960
+ });
12961
+ };
12962
+ const onKeyDown = (e) => {
12963
+ if (!open) return;
12964
+ if (e.key === "ArrowDown") {
12965
+ e.preventDefault();
12966
+ setActive((i) => (i + 1) % matches.length);
12967
+ } else if (e.key === "ArrowUp") {
12968
+ e.preventDefault();
12969
+ setActive((i) => (i - 1 + matches.length) % matches.length);
12970
+ } else if (e.key === "Enter" || e.key === "Tab") {
12971
+ e.preventDefault();
12972
+ choose(matches[active] ?? matches[0]);
12973
+ } else if (e.key === "Escape") {
12974
+ e.preventDefault();
12975
+ setTrigger(null);
12976
+ }
12977
+ };
12978
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-expression", "data-ff-expression": handle["data-ff-field"], children: [
12979
+ /* @__PURE__ */ jsxRuntime.jsx(
12980
+ "textarea",
12981
+ {
12982
+ ...handle,
12983
+ ref,
12984
+ className: "ff-panel__input ff-panel__input--expression",
12985
+ rows,
12986
+ value: value ?? "",
12987
+ placeholder: placeholder ?? "{{ $json.field }}",
12988
+ spellCheck: false,
12989
+ role: "combobox",
12990
+ "aria-expanded": open,
12991
+ "aria-autocomplete": "list",
12992
+ "aria-controls": open ? `${handle.id ?? handle["data-ff-field"]}-vars` : void 0,
12993
+ onChange: (e) => {
12994
+ onChange(e.target.value);
12995
+ sync(e.target);
12996
+ },
12997
+ onKeyDown,
12998
+ onClick: (e) => sync(e.currentTarget),
12999
+ onBlur: () => {
13000
+ window.setTimeout(() => setTrigger(null), 120);
13001
+ }
13002
+ }
13003
+ ),
13004
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-expression__bar", children: [
13005
+ /* @__PURE__ */ jsxRuntime.jsxs(
13006
+ "button",
13007
+ {
13008
+ type: "button",
13009
+ className: "ff-expression__helpbtn",
13010
+ "aria-expanded": showHelp,
13011
+ onClick: () => setShowHelp((s) => !s),
13012
+ children: [
13013
+ "{{ }}",
13014
+ " reference"
13015
+ ]
13016
+ }
13017
+ ),
13018
+ variables.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-expression__hint", children: [
13019
+ "type ",
13020
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "{{" }),
13021
+ " for ",
13022
+ variables.length,
13023
+ " variables"
13024
+ ] })
13025
+ ] }),
13026
+ open && /* @__PURE__ */ jsxRuntime.jsx(
13027
+ "ul",
13028
+ {
13029
+ className: "ff-expression__menu",
13030
+ id: `${handle.id ?? handle["data-ff-field"]}-vars`,
13031
+ role: "listbox",
13032
+ "data-ff-expression-menu": handle["data-ff-field"],
13033
+ children: matches.map((v2, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { role: "option", "aria-selected": i === active, children: /* @__PURE__ */ jsxRuntime.jsxs(
13034
+ "button",
13035
+ {
13036
+ type: "button",
13037
+ className: `ff-expression__option${i === active ? " is-active" : ""}`,
13038
+ "data-ff-expression-option": v2.path,
13039
+ onMouseDown: (e) => {
13040
+ e.preventDefault();
13041
+ choose(v2);
13042
+ },
13043
+ children: [
13044
+ /* @__PURE__ */ jsxRuntime.jsx("code", { className: "ff-expression__path", children: v2.path }),
13045
+ v2.source && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-expression__source", children: v2.source }),
13046
+ v2.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-expression__desc", children: v2.description })
13047
+ ]
13048
+ }
13049
+ ) }, v2.expression))
13050
+ }
13051
+ ),
13052
+ showHelp && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-expression__help", role: "note", "data-ff-expression-help": true, children: [
13053
+ /* @__PURE__ */ jsxRuntime.jsx("dl", { children: help.forms.map((f) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
13054
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: f.syntax }) }),
13055
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: f.meaning })
13056
+ ] }, f.syntax)) }),
13057
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-expression__note", children: help.note })
13058
+ ] })
13059
+ ] });
13060
+ }
12814
13061
  function ConfigFieldRenderer({
12815
13062
  field,
12816
13063
  value,
@@ -12818,7 +13065,9 @@ function ConfigFieldRenderer({
12818
13065
  id: id2,
12819
13066
  renderCredentialField,
12820
13067
  renderDocumentField,
12821
- fieldRenderers
13068
+ fieldRenderers,
13069
+ graph,
13070
+ nodeId
12822
13071
  }) {
12823
13072
  const handle = { id: id2, "data-ff-field": field.key };
12824
13073
  const custom = fieldRenderers?.[field.type];
@@ -12900,15 +13149,14 @@ function ConfigFieldRenderer({
12900
13149
  return /* @__PURE__ */ jsxRuntime.jsx(JsonField, { value, onChange, rows: field.rows, handle });
12901
13150
  case "expression":
12902
13151
  return /* @__PURE__ */ jsxRuntime.jsx(
12903
- "textarea",
13152
+ ExpressionField,
12904
13153
  {
12905
- ...handle,
12906
- className: "ff-panel__input ff-panel__input--expression",
12907
- rows: 2,
13154
+ handle,
12908
13155
  value: value ?? "",
13156
+ onChange,
12909
13157
  placeholder: field.example ?? "{{ $json.field }}",
12910
- spellCheck: false,
12911
- onChange: (e) => onChange(e.target.value)
13158
+ graph,
13159
+ nodeId
12912
13160
  }
12913
13161
  );
12914
13162
  case "credential":
@@ -12935,7 +13183,9 @@ function ConfigFieldRenderer({
12935
13183
  onChange,
12936
13184
  renderCredentialField,
12937
13185
  renderDocumentField,
12938
- fieldRenderers
13186
+ fieldRenderers,
13187
+ graph,
13188
+ nodeId
12939
13189
  }
12940
13190
  );
12941
13191
  case "keyvalue":
@@ -12988,6 +13238,8 @@ function RepeaterField({
12988
13238
  field,
12989
13239
  value,
12990
13240
  onChange,
13241
+ graph,
13242
+ nodeId,
12991
13243
  renderCredentialField,
12992
13244
  renderDocumentField,
12993
13245
  fieldRenderers
@@ -13068,6 +13320,8 @@ function RepeaterField({
13068
13320
  /* @__PURE__ */ jsxRuntime.jsx(
13069
13321
  ConfigFieldRenderer,
13070
13322
  {
13323
+ graph,
13324
+ nodeId,
13071
13325
  field: sub,
13072
13326
  value: row[sub.key],
13073
13327
  onChange: (cell) => setCell(i, sub.key, cell),
@@ -13249,6 +13503,7 @@ function NodeConfigPanel({
13249
13503
  renderCredentialField,
13250
13504
  renderDocumentField,
13251
13505
  fieldRenderers,
13506
+ graph,
13252
13507
  className,
13253
13508
  style: style2
13254
13509
  }) {
@@ -13354,7 +13609,9 @@ function NodeConfigPanel({
13354
13609
  onChange: (v2) => setConfigValue(field.key, v2),
13355
13610
  renderCredentialField,
13356
13611
  renderDocumentField: documentField,
13357
- fieldRenderers
13612
+ fieldRenderers,
13613
+ graph,
13614
+ nodeId: node.id
13358
13615
  }
13359
13616
  )
13360
13617
  ] }, field.key))
@@ -14667,6 +14924,7 @@ function FlowEditorInner({
14667
14924
  node: api.selected,
14668
14925
  onChange: api.updateNode,
14669
14926
  fieldRenderers,
14927
+ graph: flow,
14670
14928
  onDelete: builtins.delete === false ? void 0 : (n) => api.deleteNodes([n.id])
14671
14929
  }
14672
14930
  ),
@@ -15026,6 +15284,7 @@ exports.ActionNode = ActionNode;
15026
15284
  exports.BUILTIN_KINDS = BUILTIN_KINDS;
15027
15285
  exports.ConfigFieldRenderer = ConfigFieldRenderer;
15028
15286
  exports.DecisionNode = DecisionNode;
15287
+ exports.ExpressionField = ExpressionField;
15029
15288
  exports.FlowCanvas = FlowCanvas;
15030
15289
  exports.FlowEditor = FlowEditor;
15031
15290
  exports.FlowRunControls = FlowRunControls;
@@ -15049,6 +15308,8 @@ exports.WORKFLOW_SCHEMA_VERSION = WORKFLOW_SCHEMA_VERSION;
15049
15308
  exports.alignNodes = alignNodes;
15050
15309
  exports.applyOutputsToNodes = applyOutputsToNodes;
15051
15310
  exports.applyStatusesToNodes = applyStatusesToNodes;
15311
+ exports.availableVariables = availableVariables;
15312
+ exports.baseVariables = baseVariables;
15052
15313
  exports.buildNodeTypes = buildNodeTypes;
15053
15314
  exports.categoryAccent = categoryAccent;
15054
15315
  exports.clearNodeKindOverrides = clearNodeKindOverrides;
@@ -15060,6 +15321,7 @@ exports.defaultConfigFor = defaultConfigFor;
15060
15321
  exports.defaultNodeTypes = defaultNodeTypes;
15061
15322
  exports.defaultPortCompatibility = defaultPortCompatibility;
15062
15323
  exports.defineNode = defineNode;
15324
+ exports.describeExpressionGrammar = describeExpressionGrammar;
15063
15325
  exports.distributeNodes = distributeNodes;
15064
15326
  exports.encodePause = encodePause;
15065
15327
  exports.exportWorkflow = exportWorkflow;
@@ -15074,6 +15336,7 @@ exports.listNodeKinds = listNodeKinds;
15074
15336
  exports.migrateSchema = migrateSchema;
15075
15337
  exports.onNodeKindsChanged = onNodeKindsChanged;
15076
15338
  exports.onRichInputAdapterChanged = onRichInputAdapterChanged;
15339
+ exports.outputFieldsFor = outputFieldsFor;
15077
15340
  exports.overrideNodeKind = overrideNodeKind;
15078
15341
  exports.paletteDropHandlers = paletteDropHandlers;
15079
15342
  exports.pauseForHuman = pauseForHuman;