From b42ec262dc1bb5461954b69e2ccecbeb1de3a45a Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 18 Feb 2026 19:46:02 +1300 Subject: [PATCH] feat: support inline tag parsing in PromptConstructor node Allow upstream nodes to define variables using value syntax in their text content. These are parsed and made available as @name variables in the template, coexisting with the existing named variable system (which takes precedence on name conflicts). Co-Authored-By: Claude Opus 4.6 --- .../nodes/PromptConstructorNode.tsx | 56 +++++++++++++++---- src/store/execution/simpleNodeExecutors.ts | 42 +++++++++++--- src/utils/parseVarTags.ts | 13 +++++ 3 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 src/utils/parseVarTags.ts diff --git a/src/components/nodes/PromptConstructorNode.tsx b/src/components/nodes/PromptConstructorNode.tsx index 443807b6..b0a4a271 100644 --- a/src/components/nodes/PromptConstructorNode.tsx +++ b/src/components/nodes/PromptConstructorNode.tsx @@ -7,8 +7,9 @@ import { BaseNode } from "./BaseNode"; import { useCommentNavigation } from "@/hooks/useCommentNavigation"; import { usePromptAutocomplete } from "@/hooks/usePromptAutocomplete"; import { useWorkflowStore } from "@/store/workflowStore"; -import { PromptConstructorNodeData, PromptNodeData, AvailableVariable } from "@/types"; +import { PromptConstructorNodeData, PromptNodeData, LLMGenerateNodeData, AvailableVariable } from "@/types"; import { PromptConstructorEditorModal } from "@/components/modals/PromptConstructorEditorModal"; +import { parseVarTags } from "@/utils/parseVarTags"; type PromptConstructorNodeType = Node; @@ -35,21 +36,54 @@ export function PromptConstructorNode({ id, data, selected }: NodeProps tags) const availableVariables = useMemo((): AvailableVariable[] => { - const connectedPromptNodes = edges + const connectedTextNodes = edges .filter((e) => e.target === id && e.targetHandle === "text") .map((e) => nodes.find((n) => n.id === e.source)) - .filter((n): n is typeof nodes[0] => n !== undefined && n.type === "prompt"); + .filter((n): n is typeof nodes[0] => n !== undefined); const vars: AvailableVariable[] = []; - connectedPromptNodes.forEach((promptNode) => { - const promptData = promptNode.data as PromptNodeData; - if (promptData.variableName) { - vars.push({ - name: promptData.variableName, - value: promptData.prompt || "", - nodeId: promptNode.id, + const usedNames = new Set(); + + // First pass: named variables from Prompt nodes (these take precedence) + connectedTextNodes.forEach((node) => { + if (node.type === "prompt") { + const promptData = node.data as PromptNodeData; + if (promptData.variableName) { + vars.push({ + name: promptData.variableName, + value: promptData.prompt || "", + nodeId: node.id, + }); + usedNames.add(promptData.variableName); + } + } + }); + + // Second pass: parse inline tags from all connected text nodes + connectedTextNodes.forEach((node) => { + let text: string | null = null; + if (node.type === "prompt") { + text = (node.data as PromptNodeData).prompt || null; + } else if (node.type === "llmGenerate") { + text = (node.data as LLMGenerateNodeData).outputText || null; + } else if (node.type === "promptConstructor") { + const pcData = node.data as PromptConstructorNodeData; + text = pcData.outputText ?? pcData.template ?? null; + } + + if (text) { + const parsed = parseVarTags(text); + parsed.forEach(({ name, value }) => { + if (!usedNames.has(name)) { + vars.push({ + name, + value, + nodeId: `${node.id}-var-${name}`, + }); + usedNames.add(name); + } }); } }); diff --git a/src/store/execution/simpleNodeExecutors.ts b/src/store/execution/simpleNodeExecutors.ts index f782c62a..0cfcd24d 100644 --- a/src/store/execution/simpleNodeExecutors.ts +++ b/src/store/execution/simpleNodeExecutors.ts @@ -11,11 +11,13 @@ import type { AnnotationNodeData, PromptConstructorNodeData, PromptNodeData, + LLMGenerateNodeData, OutputNodeData, OutputGalleryNodeData, WorkflowNode, } from "@/types"; import type { NodeExecutionContext } from "./types"; +import { parseVarTags } from "@/utils/parseVarTags"; /** * Annotation node: receives upstream image as source, passes through if no annotations. @@ -72,18 +74,42 @@ export async function executePromptConstructor(ctx: NodeExecutionContext): Promi const edges = getEdges(); const nodes = getNodes(); - // Find connected prompt nodes via text edges - const connectedPromptNodes = edges + // Find all connected text nodes + const connectedTextNodes = edges .filter((e) => e.target === node.id && e.targetHandle === "text") .map((e) => nodes.find((n) => n.id === e.source)) - .filter((n): n is WorkflowNode => n !== undefined && n.type === "prompt"); + .filter((n): n is WorkflowNode => n !== undefined); - // Build variable map from connected prompt nodes + // Build variable map: named variables from Prompt nodes take precedence const variableMap: Record = {}; - connectedPromptNodes.forEach((promptNode) => { - const promptData = promptNode.data as PromptNodeData; - if (promptData.variableName) { - variableMap[promptData.variableName] = promptData.prompt; + connectedTextNodes.forEach((srcNode) => { + if (srcNode.type === "prompt") { + const promptData = srcNode.data as PromptNodeData; + if (promptData.variableName) { + variableMap[promptData.variableName] = promptData.prompt; + } + } + }); + + // Parse inline tags from all connected text nodes + connectedTextNodes.forEach((srcNode) => { + let text: string | null = null; + if (srcNode.type === "prompt") { + text = (srcNode.data as PromptNodeData).prompt || null; + } else if (srcNode.type === "llmGenerate") { + text = (srcNode.data as LLMGenerateNodeData).outputText || null; + } else if (srcNode.type === "promptConstructor") { + const pcData = srcNode.data as PromptConstructorNodeData; + text = pcData.outputText ?? pcData.template ?? null; + } + + if (text) { + const parsed = parseVarTags(text); + parsed.forEach(({ name, value }) => { + if (variableMap[name] === undefined) { + variableMap[name] = value; + } + }); } }); diff --git a/src/utils/parseVarTags.ts b/src/utils/parseVarTags.ts new file mode 100644 index 00000000..7a737e63 --- /dev/null +++ b/src/utils/parseVarTags.ts @@ -0,0 +1,13 @@ +/** + * Parses inline value tags from text. + * Returns an array of { name, value } pairs. + */ +export function parseVarTags(text: string): Array<{ name: string; value: string }> { + const regex = /([\s\S]*?)<\/var>/g; + const vars: Array<{ name: string; value: string }> = []; + let match; + while ((match = regex.exec(text)) !== null) { + vars.push({ name: match[1], value: match[2] }); + } + return vars; +}