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 { 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<PromptConstructorNodeData, "promptConstructor">;
@ -35,21 +36,54 @@ export function PromptConstructorNode({ id, data, selected }: NodeProps<PromptCo
}
}, [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 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<string>();
// 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 <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,
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<string, string> = {};
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 <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