Browse Source

feat: support inline <var> tag parsing in PromptConstructor node

Allow upstream nodes to define variables using <var="name">value</var>
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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
b42ec262dc
  1. 56
      src/components/nodes/PromptConstructorNode.tsx
  2. 42
      src/store/execution/simpleNodeExecutors.ts
  3. 13
      src/utils/parseVarTags.ts

56
src/components/nodes/PromptConstructorNode.tsx

@ -7,8 +7,9 @@ import { BaseNode } from "./BaseNode";
import { useCommentNavigation } from "@/hooks/useCommentNavigation"; import { useCommentNavigation } from "@/hooks/useCommentNavigation";
import { usePromptAutocomplete } from "@/hooks/usePromptAutocomplete"; import { usePromptAutocomplete } from "@/hooks/usePromptAutocomplete";
import { useWorkflowStore } from "@/store/workflowStore"; 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 { PromptConstructorEditorModal } from "@/components/modals/PromptConstructorEditorModal";
import { parseVarTags } from "@/utils/parseVarTags";
type PromptConstructorNodeType = Node<PromptConstructorNodeData, "promptConstructor">; type PromptConstructorNodeType = Node<PromptConstructorNodeData, "promptConstructor">;
@ -35,21 +36,54 @@ export function PromptConstructorNode({ id, data, selected }: NodeProps<PromptCo
} }
}, [nodeData.template, isEditing]); }, [nodeData.template, isEditing]);
// Get available variables from connected prompt nodes // Get available variables from connected prompt nodes (named variables + inline <var> tags)
const availableVariables = useMemo((): AvailableVariable[] => { const availableVariables = useMemo((): AvailableVariable[] => {
const connectedPromptNodes = edges const connectedTextNodes = edges
.filter((e) => e.target === id && e.targetHandle === "text") .filter((e) => e.target === id && e.targetHandle === "text")
.map((e) => nodes.find((n) => n.id === e.source)) .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[] = []; const vars: AvailableVariable[] = [];
connectedPromptNodes.forEach((promptNode) => { const usedNames = new Set<string>();
const promptData = promptNode.data as PromptNodeData;
if (promptData.variableName) { // First pass: named variables from Prompt nodes (these take precedence)
vars.push({ connectedTextNodes.forEach((node) => {
name: promptData.variableName, if (node.type === "prompt") {
value: promptData.prompt || "", const promptData = node.data as PromptNodeData;
nodeId: promptNode.id, if (promptData.variableName) {
vars.push({
name: promptData.variableName,
value: promptData.prompt || "",
nodeId: node.id,
});
usedNames.add(promptData.variableName);
}
}
});
// Second pass: parse inline <var> 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);
}
}); });
} }
}); });

42
src/store/execution/simpleNodeExecutors.ts

@ -11,11 +11,13 @@ import type {
AnnotationNodeData, AnnotationNodeData,
PromptConstructorNodeData, PromptConstructorNodeData,
PromptNodeData, PromptNodeData,
LLMGenerateNodeData,
OutputNodeData, OutputNodeData,
OutputGalleryNodeData, OutputGalleryNodeData,
WorkflowNode, WorkflowNode,
} from "@/types"; } from "@/types";
import type { NodeExecutionContext } from "./types"; import type { NodeExecutionContext } from "./types";
import { parseVarTags } from "@/utils/parseVarTags";
/** /**
* Annotation node: receives upstream image as source, passes through if no annotations. * 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 edges = getEdges();
const nodes = getNodes(); const nodes = getNodes();
// Find connected prompt nodes via text edges // Find all connected text nodes
const connectedPromptNodes = edges const connectedTextNodes = edges
.filter((e) => e.target === node.id && e.targetHandle === "text") .filter((e) => e.target === node.id && e.targetHandle === "text")
.map((e) => nodes.find((n) => n.id === e.source)) .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<string, string> = {}; const variableMap: Record<string, string> = {};
connectedPromptNodes.forEach((promptNode) => { connectedTextNodes.forEach((srcNode) => {
const promptData = promptNode.data as PromptNodeData; if (srcNode.type === "prompt") {
if (promptData.variableName) { const promptData = srcNode.data as PromptNodeData;
variableMap[promptData.variableName] = promptData.prompt; if (promptData.variableName) {
variableMap[promptData.variableName] = promptData.prompt;
}
}
});
// Parse inline <var> 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;
}
});
} }
}); });

13
src/utils/parseVarTags.ts

@ -0,0 +1,13 @@
/**
* Parses inline <var="name">value</var> tags from text.
* Returns an array of { name, value } pairs.
*/
export function parseVarTags(text: string): Array<{ name: string; value: string }> {
const regex = /<var="(\w+)">([\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;
}
Loading…
Cancel
Save