Browse Source

feat(ENHANCE-001): complete dynamic handles and execution pipeline

- Render dynamic input handles from model schema (GenerateImage/VideoNode)
- Update connection validation with getHandleType() helper
- Add dynamicInputs to getConnectedInputs() return value
- Pass dynamicInputs through generation pipeline (workflowStore -> API)
- Update generateWithFal/Replicate to use dynamic inputs
- Process large images in dynamicInputs to URLs for external providers
- Support prompt from dynamicInputs when top-level prompt is empty

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
dd2549f820
  1. 104
      src/app/api/generate/route.ts
  2. 22
      src/components/WorkflowCanvas.tsx
  3. 35
      src/components/nodes/GenerateImageNode.tsx
  4. 37
      src/components/nodes/GenerateVideoNode.tsx
  5. 2
      src/lib/providers/types.ts
  6. 96
      src/store/workflowStore.ts

104
src/app/api/generate/route.ts

@ -19,6 +19,8 @@ const MODEL_MAP: Record<ModelType, string> = {
interface MultiProviderGenerateRequest extends GenerateRequest { interface MultiProviderGenerateRequest extends GenerateRequest {
selectedModel?: SelectedModel; selectedModel?: SelectedModel;
parameters?: Record<string, unknown>; parameters?: Record<string, unknown>;
/** Dynamic inputs from schema-based connections (e.g., image_url, tail_image_url, prompt) */
dynamicInputs?: Record<string, string>;
} }
/** /**
@ -272,16 +274,20 @@ async function generateWithReplicate(
}; };
} }
const hasDynamicInputs = input.dynamicInputs && Object.keys(input.dynamicInputs).length > 0;
// Build input for the prediction // Build input for the prediction
const predictionInput: Record<string, unknown> = { const predictionInput: Record<string, unknown> = {
prompt: input.prompt,
...input.parameters, ...input.parameters,
}; };
// Log parameters being passed // Add dynamic inputs if provided (these come from schema-mapped connections)
if (input.parameters && Object.keys(input.parameters).length > 0) { if (hasDynamicInputs) {
console.log(`[API:${requestId}] Custom parameters:`, JSON.stringify(input.parameters)); Object.assign(predictionInput, input.dynamicInputs);
} console.log(`[API:${requestId}] Using dynamic inputs:`, Object.keys(input.dynamicInputs!).join(", "));
} else {
// Fallback to legacy behavior
predictionInput.prompt = input.prompt;
// Add image input if provided (for img2img workflows) // Add image input if provided (for img2img workflows)
// Note: Different Replicate models use different parameter names // Note: Different Replicate models use different parameter names
@ -290,6 +296,12 @@ async function generateWithReplicate(
predictionInput.image = input.images[0]; predictionInput.image = input.images[0];
console.log(`[API:${requestId}] Added image input to prediction (${input.images[0].substring(0, 50)}...)`); console.log(`[API:${requestId}] Added image input to prediction (${input.images[0].substring(0, 50)}...)`);
} }
}
// Log parameters being passed
if (input.parameters && Object.keys(input.parameters).length > 0) {
console.log(`[API:${requestId}] Custom parameters:`, JSON.stringify(input.parameters));
}
// Create a prediction // Create a prediction
console.log(`[API:${requestId}] Creating Replicate prediction...`); console.log(`[API:${requestId}] Creating Replicate prediction...`);
@ -474,20 +486,25 @@ async function generateWithFal(
console.log(`[API:${requestId}] - Model: ${input.model.id}`); console.log(`[API:${requestId}] - Model: ${input.model.id}`);
console.log(`[API:${requestId}] - Prompt length: ${input.prompt.length} chars`); console.log(`[API:${requestId}] - Prompt length: ${input.prompt.length} chars`);
console.log(`[API:${requestId}] - Images count: ${input.images?.length || 0}`); console.log(`[API:${requestId}] - Images count: ${input.images?.length || 0}`);
console.log(`[API:${requestId}] - Dynamic inputs: ${input.dynamicInputs ? Object.keys(input.dynamicInputs).join(", ") : "none"}`);
console.log(`[API:${requestId}] - API key: ${apiKey ? "provided" : "not provided (using rate-limited access)"}`); console.log(`[API:${requestId}] - API key: ${apiKey ? "provided" : "not provided (using rate-limited access)"}`);
const modelId = input.model.id; const modelId = input.model.id;
const hasDynamicInputs = input.dynamicInputs && Object.keys(input.dynamicInputs).length > 0;
// Build request body // Build request body
// If we have dynamic inputs, they take precedence (they already contain prompt, image_url, etc.)
const requestBody: Record<string, unknown> = { const requestBody: Record<string, unknown> = {
prompt: input.prompt,
...input.parameters, ...input.parameters,
}; };
// Log parameters being passed // Add dynamic inputs if provided (these come from schema-mapped connections)
if (input.parameters && Object.keys(input.parameters).length > 0) { if (hasDynamicInputs) {
console.log(`[API:${requestId}] Custom parameters:`, JSON.stringify(input.parameters)); Object.assign(requestBody, input.dynamicInputs);
} console.log(`[API:${requestId}] Using dynamic inputs:`, Object.keys(input.dynamicInputs!).join(", "));
} else {
// Fallback to legacy behavior: use prompt and images array
requestBody.prompt = input.prompt;
// Add image_url if provided (for img2img workflows) // Add image_url if provided (for img2img workflows)
// fal.ai accepts both URLs and data URIs in this field // fal.ai accepts both URLs and data URIs in this field
@ -495,6 +512,12 @@ async function generateWithFal(
requestBody.image_url = input.images[0]; requestBody.image_url = input.images[0];
console.log(`[API:${requestId}] Added image_url to request (${input.images[0].substring(0, 50)}...)`); console.log(`[API:${requestId}] Added image_url to request (${input.images[0].substring(0, 50)}...)`);
} }
}
// Log parameters being passed
if (input.parameters && Object.keys(input.parameters).length > 0) {
console.log(`[API:${requestId}] Custom parameters:`, JSON.stringify(input.parameters));
}
// Build headers // Build headers
const headers: Record<string, string> = { const headers: Record<string, string> = {
@ -639,9 +662,12 @@ export async function POST(request: NextRequest) {
useGoogleSearch, useGoogleSearch,
selectedModel, selectedModel,
parameters, parameters,
dynamicInputs,
} = body; } = body;
if (!prompt) { // Prompt is required unless provided via dynamicInputs
const hasPrompt = prompt || (dynamicInputs && dynamicInputs.prompt);
if (!hasPrompt) {
console.error(`[API:${requestId}] Validation failed: missing prompt`); console.error(`[API:${requestId}] Validation failed: missing prompt`);
return NextResponse.json<GenerateResponse>( return NextResponse.json<GenerateResponse>(
{ {
@ -689,6 +715,31 @@ export async function POST(request: NextRequest) {
} }
} }
// Process dynamicInputs: convert large images in image-type inputs to URLs
const processedDynamicInputs: Record<string, string> | undefined = dynamicInputs
? { ...dynamicInputs }
: undefined;
if (processedDynamicInputs) {
// Image input names that might contain base64 data
const imageInputNames = ["image", "image_url", "tail_image_url", "first_frame", "last_frame",
"start_image", "end_image", "reference_image", "init_image", "mask_image", "control_image"];
for (const key of Object.keys(processedDynamicInputs)) {
const value = processedDynamicInputs[key];
// Check if this is an image input with base64 data
if (imageInputNames.some(name => key.toLowerCase().includes(name.toLowerCase())) &&
value && value.startsWith("data:image")) {
if (shouldUseImageUrl(value)) {
const { url, id } = uploadImageForUrl(value, baseUrl);
uploadedImageIds.push(id);
processedDynamicInputs[key] = url;
console.log(`[API:${requestId}] Converted large dynamic input '${key}' to URL: ${url}`);
}
}
}
}
try { try {
// Build generation input // Build generation input
const genInput: GenerationInput = { const genInput: GenerationInput = {
@ -699,9 +750,10 @@ export async function POST(request: NextRequest) {
capabilities: ["text-to-image"], capabilities: ["text-to-image"],
description: null, description: null,
}, },
prompt, prompt: prompt || "",
images: processedImages, images: processedImages,
parameters, parameters,
dynamicInputs: processedDynamicInputs,
}; };
const result = await generateWithReplicate(requestId, replicateApiKey, genInput); const result = await generateWithReplicate(requestId, replicateApiKey, genInput);
@ -777,6 +829,31 @@ export async function POST(request: NextRequest) {
} }
} }
// Process dynamicInputs: convert large images in image-type inputs to URLs
const processedDynamicInputs: Record<string, string> | undefined = dynamicInputs
? { ...dynamicInputs }
: undefined;
if (processedDynamicInputs) {
// Image input names that might contain base64 data
const imageInputNames = ["image_url", "tail_image_url", "first_frame", "last_frame",
"start_image", "end_image", "reference_image", "init_image", "mask_image", "control_image"];
for (const key of Object.keys(processedDynamicInputs)) {
const value = processedDynamicInputs[key];
// Check if this is an image input with base64 data
if (imageInputNames.some(name => key.toLowerCase().includes(name.toLowerCase())) &&
value && value.startsWith("data:image")) {
if (shouldUseImageUrl(value)) {
const { url, id } = uploadImageForUrl(value, baseUrl);
uploadedImageIds.push(id);
processedDynamicInputs[key] = url;
console.log(`[API:${requestId}] Converted large dynamic input '${key}' to URL: ${url}`);
}
}
}
}
try { try {
// Build generation input // Build generation input
const genInput: GenerationInput = { const genInput: GenerationInput = {
@ -787,9 +864,10 @@ export async function POST(request: NextRequest) {
capabilities: ["text-to-image"], capabilities: ["text-to-image"],
description: null, description: null,
}, },
prompt, prompt: prompt || "",
images: processedImages, images: processedImages,
parameters, parameters,
dynamicInputs: processedDynamicInputs,
}; };
const result = await generateWithFal(requestId, falApiKey, genInput); const result = await generateWithFal(requestId, falApiKey, genInput);

22
src/components/WorkflowCanvas.tsx

@ -57,14 +57,32 @@ const edgeTypes: EdgeTypes = {
// Connection validation rules // Connection validation rules
// - Image handles (green) can only connect to image handles // - Image handles (green) can only connect to image handles
// - Text handles (blue) can only connect to text handles // - Text handles (blue) can only connect to text handles
// Helper to determine handle type from handle ID
// For dynamic handles, we use naming convention: image inputs contain "image", text inputs are "prompt" or "negative_prompt"
const getHandleType = (handleId: string | null | undefined): "image" | "text" | null => {
if (!handleId) return null;
// Standard handles
if (handleId === "image" || handleId === "text") return handleId;
// Dynamic handles - check naming patterns
if (handleId.includes("image") || handleId.includes("frame")) return "image";
if (handleId === "prompt" || handleId === "negative_prompt" || handleId.includes("prompt")) return "text";
return null;
};
// Connection validation: ensures type matching between source and target handles
// - Image handles can connect to image handles
// - Text handles can connect to text handles
// - NanoBanana image input accepts multiple connections // - NanoBanana image input accepts multiple connections
// - All other inputs accept only one connection // - All other inputs accept only one connection
const isValidConnection = (connection: Edge | Connection): boolean => { const isValidConnection = (connection: Edge | Connection): boolean => {
const sourceHandle = connection.sourceHandle; const sourceHandle = connection.sourceHandle;
const targetHandle = connection.targetHandle; const targetHandle = connection.targetHandle;
const sourceType = getHandleType(sourceHandle);
const targetType = getHandleType(targetHandle);
// Strict type matching: image <-> image, text <-> text // Strict type matching: image <-> image, text <-> text
if (sourceHandle === "image" && targetHandle !== "image") { if (sourceType === "image" && targetType !== "image") {
logger.warn('connection.validation', 'Connection validation failed: type mismatch', { logger.warn('connection.validation', 'Connection validation failed: type mismatch', {
source: connection.source, source: connection.source,
target: connection.target, target: connection.target,
@ -74,7 +92,7 @@ const isValidConnection = (connection: Edge | Connection): boolean => {
}); });
return false; return false;
} }
if (sourceHandle === "text" && targetHandle !== "text") { if (sourceType === "text" && targetType !== "text") {
logger.warn('connection.validation', 'Connection validation failed: type mismatch', { logger.warn('connection.validation', 'Connection validation failed: type mismatch', {
source: connection.source, source: connection.source,
target: connection.target, target: connection.target,

35
src/components/nodes/GenerateImageNode.tsx

@ -335,7 +335,37 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
isExecuting={isRunning} isExecuting={isRunning}
hasError={nodeData.status === "error"} hasError={nodeData.status === "error"}
> >
{/* Image input - accepts multiple connections */} {/* Dynamic input handles based on model schema (external providers only) */}
{!isGeminiProvider && nodeData.inputSchema && nodeData.inputSchema.length > 0 ? (
// Render handles from schema
nodeData.inputSchema.map((input, index) => {
const total = nodeData.inputSchema!.length;
const topPercent = ((index + 1) / (total + 1)) * 100;
return (
<div key={input.name}>
<Handle
type="target"
position={Position.Left}
id={input.name}
style={{ top: `${topPercent}%` }}
data-handletype={input.type}
isConnectable={true}
title={input.description || input.label}
/>
{/* Handle label */}
<div
className="absolute text-[8px] text-neutral-500 whitespace-nowrap pointer-events-none"
style={{ left: 12, top: `calc(${topPercent}% - 6px)` }}
>
{input.label}
{input.required && <span className="text-red-400">*</span>}
</div>
</div>
);
})
) : (
// Default handles for Gemini or when no schema
<>
<Handle <Handle
type="target" type="target"
position={Position.Left} position={Position.Left}
@ -344,7 +374,6 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
data-handletype="image" data-handletype="image"
isConnectable={true} isConnectable={true}
/> />
{/* Text input - single connection */}
<Handle <Handle
type="target" type="target"
position={Position.Left} position={Position.Left}
@ -352,6 +381,8 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
style={{ top: "65%" }} style={{ top: "65%" }}
data-handletype="text" data-handletype="text"
/> />
</>
)}
{/* Image output */} {/* Image output */}
<Handle <Handle
type="source" type="source"

37
src/components/nodes/GenerateVideoNode.tsx

@ -170,7 +170,37 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
isExecuting={isRunning} isExecuting={isRunning}
hasError={nodeData.status === "error"} hasError={nodeData.status === "error"}
> >
{/* Image input - accepts multiple connections */} {/* Dynamic input handles based on model schema */}
{nodeData.inputSchema && nodeData.inputSchema.length > 0 ? (
// Render handles from schema
nodeData.inputSchema.map((input, index) => {
const total = nodeData.inputSchema!.length;
const topPercent = ((index + 1) / (total + 1)) * 100;
return (
<div key={input.name}>
<Handle
type="target"
position={Position.Left}
id={input.name}
style={{ top: `${topPercent}%` }}
data-handletype={input.type}
isConnectable={true}
title={input.description || input.label}
/>
{/* Handle label */}
<div
className="absolute text-[8px] text-neutral-500 whitespace-nowrap pointer-events-none"
style={{ left: 12, top: `calc(${topPercent}% - 6px)` }}
>
{input.label}
{input.required && <span className="text-red-400">*</span>}
</div>
</div>
);
})
) : (
// Default handles when no schema
<>
<Handle <Handle
type="target" type="target"
position={Position.Left} position={Position.Left}
@ -179,7 +209,6 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
data-handletype="image" data-handletype="image"
isConnectable={true} isConnectable={true}
/> />
{/* Text input - single connection */}
<Handle <Handle
type="target" type="target"
position={Position.Left} position={Position.Left}
@ -187,7 +216,9 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
style={{ top: "65%" }} style={{ top: "65%" }}
data-handletype="text" data-handletype="text"
/> />
{/* Video output - we use "image" handle type for now since the connection system expects image/text */} </>
)}
{/* Video output */}
<Handle <Handle
type="source" type="source"
position={Position.Right} position={Position.Right}

2
src/lib/providers/types.ts

@ -84,6 +84,8 @@ export interface GenerationInput {
images?: string[]; images?: string[];
/** Model-specific parameters (varies by provider/model) */ /** Model-specific parameters (varies by provider/model) */
parameters?: Record<string, unknown>; parameters?: Record<string, unknown>;
/** Dynamic inputs mapped from schema (e.g., { "image_url": "data:...", "tail_image_url": "data:..." }) */
dynamicInputs?: Record<string, string>;
} }
/** /**

96
src/store/workflowStore.ts

@ -115,7 +115,7 @@ interface WorkflowStore {
// Helpers // Helpers
getNodeById: (id: string) => WorkflowNode | undefined; getNodeById: (id: string) => WorkflowNode | undefined;
getConnectedInputs: (nodeId: string) => { images: string[]; text: string | null }; getConnectedInputs: (nodeId: string) => { images: string[]; text: string | null; dynamicInputs: Record<string, string> };
validateWorkflow: () => { valid: boolean; errors: string[] }; validateWorkflow: () => { valid: boolean; errors: string[] };
// Global Image History // Global Image History
@ -816,6 +816,37 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
const { edges, nodes } = get(); const { edges, nodes } = get();
const images: string[] = []; const images: string[] = [];
let text: string | null = null; let text: string | null = null;
const dynamicInputs: Record<string, string> = {};
// Helper to determine if a handle ID is an image type
const isImageHandle = (handleId: string | null | undefined): boolean => {
if (!handleId) return false;
return handleId === "image" || handleId.includes("image") || handleId.includes("frame");
};
// Helper to determine if a handle ID is a text type
const isTextHandle = (handleId: string | null | undefined): boolean => {
if (!handleId) return false;
return handleId === "text" || handleId.includes("prompt");
};
// Helper to extract output from source node
const getSourceOutput = (sourceNode: WorkflowNode): { type: "image" | "text"; value: string | null } => {
if (sourceNode.type === "imageInput") {
return { type: "image", value: (sourceNode.data as ImageInputNodeData).image };
} else if (sourceNode.type === "annotation") {
return { type: "image", value: (sourceNode.data as AnnotationNodeData).outputImage };
} else if (sourceNode.type === "nanoBanana") {
return { type: "image", value: (sourceNode.data as NanoBananaNodeData).outputImage };
} else if (sourceNode.type === "generateVideo") {
return { type: "image", value: (sourceNode.data as GenerateVideoNodeData).outputVideo };
} else if (sourceNode.type === "prompt") {
return { type: "text", value: (sourceNode.data as PromptNodeData).prompt };
} else if (sourceNode.type === "llmGenerate") {
return { type: "text", value: (sourceNode.data as LLMGenerateNodeData).outputText };
}
return { type: "image", value: null };
};
edges edges
.filter((edge) => edge.target === nodeId) .filter((edge) => edge.target === nodeId)
@ -824,35 +855,24 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
if (!sourceNode) return; if (!sourceNode) return;
const handleId = edge.targetHandle; const handleId = edge.targetHandle;
const { value } = getSourceOutput(sourceNode);
if (handleId === "image" || !handleId) { if (!value) return;
// Get image from source node - collect all connected images
if (sourceNode.type === "imageInput") {
const sourceImage = (sourceNode.data as ImageInputNodeData).image;
if (sourceImage) images.push(sourceImage);
} else if (sourceNode.type === "annotation") {
const sourceImage = (sourceNode.data as AnnotationNodeData).outputImage;
if (sourceImage) images.push(sourceImage);
} else if (sourceNode.type === "nanoBanana") {
const sourceImage = (sourceNode.data as NanoBananaNodeData).outputImage;
if (sourceImage) images.push(sourceImage);
} else if (sourceNode.type === "generateVideo") {
// Video output can be used as input to downstream nodes
const sourceVideo = (sourceNode.data as GenerateVideoNodeData).outputVideo;
if (sourceVideo) images.push(sourceVideo);
}
}
if (handleId === "text") { // Store in dynamic inputs if handle ID is specific (not just "image" or "text")
if (sourceNode.type === "prompt") { if (handleId && handleId !== "image" && handleId !== "text") {
text = (sourceNode.data as PromptNodeData).prompt; dynamicInputs[handleId] = value;
} else if (sourceNode.type === "llmGenerate") {
text = (sourceNode.data as LLMGenerateNodeData).outputText;
} }
// Also populate legacy arrays for backward compatibility
if (isImageHandle(handleId) || !handleId) {
images.push(value);
} else if (isTextHandle(handleId)) {
text = value;
} }
}); });
return { images, text }; return { images, text, dynamicInputs };
}, },
validateWorkflow: () => { validateWorkflow: () => {
@ -1041,9 +1061,11 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
break; break;
case "nanoBanana": { case "nanoBanana": {
const { images, text } = getConnectedInputs(node.id); const { images, text, dynamicInputs } = getConnectedInputs(node.id);
if (!text) { // For dynamic inputs, check if we have at least a prompt
const promptText = text || dynamicInputs.prompt || null;
if (!promptText) {
logger.error('node.error', 'nanoBanana node missing text input', { logger.error('node.error', 'nanoBanana node missing text input', {
nodeId: node.id, nodeId: node.id,
}); });
@ -1058,7 +1080,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
updateNodeData(node.id, { updateNodeData(node.id, {
inputImages: images, inputImages: images,
inputPrompt: text, inputPrompt: promptText,
status: "loading", status: "loading",
error: null, error: null,
}); });
@ -1069,13 +1091,14 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
const requestPayload = { const requestPayload = {
images, images,
prompt: text, prompt: promptText,
aspectRatio: nodeData.aspectRatio, aspectRatio: nodeData.aspectRatio,
resolution: nodeData.resolution, resolution: nodeData.resolution,
model: nodeData.model, model: nodeData.model,
useGoogleSearch: nodeData.useGoogleSearch, useGoogleSearch: nodeData.useGoogleSearch,
selectedModel: nodeData.selectedModel, selectedModel: nodeData.selectedModel,
parameters: nodeData.parameters, parameters: nodeData.parameters,
dynamicInputs, // Pass dynamic inputs for schema-mapped connections
}; };
// Build headers with API keys for external providers // Build headers with API keys for external providers
@ -1102,7 +1125,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
aspectRatio: nodeData.aspectRatio, aspectRatio: nodeData.aspectRatio,
resolution: nodeData.resolution, resolution: nodeData.resolution,
imageCount: images.length, imageCount: images.length,
prompt: text, prompt: promptText,
}); });
const response = await fetch("/api/generate", { const response = await fetch("/api/generate", {
@ -1148,7 +1171,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
get().addToGlobalHistory({ get().addToGlobalHistory({
image: result.image, image: result.image,
timestamp, timestamp,
prompt: text, prompt: promptText,
aspectRatio: nodeData.aspectRatio, aspectRatio: nodeData.aspectRatio,
model: nodeData.model, model: nodeData.model,
}); });
@ -1157,7 +1180,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
const newHistoryItem = { const newHistoryItem = {
id: imageId, id: imageId,
timestamp, timestamp,
prompt: text, prompt: promptText,
aspectRatio: nodeData.aspectRatio, aspectRatio: nodeData.aspectRatio,
model: nodeData.model, model: nodeData.model,
}; };
@ -1237,15 +1260,17 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
} }
case "generateVideo": { case "generateVideo": {
const { images, text } = getConnectedInputs(node.id); const { images, text, dynamicInputs } = getConnectedInputs(node.id);
if (!text) { // For dynamic inputs, check if we have at least a prompt
logger.error('node.error', 'generateVideo node missing text input', { const hasPrompt = text || dynamicInputs.prompt || dynamicInputs.negative_prompt;
if (!hasPrompt && images.length === 0) {
logger.error('node.error', 'generateVideo node missing inputs', {
nodeId: node.id, nodeId: node.id,
}); });
updateNodeData(node.id, { updateNodeData(node.id, {
status: "error", status: "error",
error: "Missing text input", error: "Missing required inputs",
}); });
set({ isRunning: false, currentNodeId: null }); set({ isRunning: false, currentNodeId: null });
await logger.endSession(); await logger.endSession();
@ -1282,6 +1307,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
prompt: text, prompt: text,
selectedModel: nodeData.selectedModel, selectedModel: nodeData.selectedModel,
parameters: nodeData.parameters, parameters: nodeData.parameters,
dynamicInputs, // Pass dynamic inputs for schema-mapped connections
}; };
// Build headers with API keys for external providers // Build headers with API keys for external providers

Loading…
Cancel
Save