Browse Source

fix(quick-001): fix type signatures and store aggregation for multi-image dynamicInputs

- Update dynamicInputs type from Record<string, string> to Record<string, string | string[]> in 4 locations
- Fix getConnectedInputs to aggregate same-key values into arrays instead of overwriting
- Fix dynamicInputs.prompt access to handle array case
- Fix hasPrompt check in POST handler to handle array-typed prompt
- Update processedDynamicInputs type in both Replicate and fal blocks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
dc2966a9ad
  1. 16
      src/app/api/generate/route.ts
  2. 2
      src/lib/providers/types.ts
  3. 19
      src/store/workflowStore.ts

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

@ -33,7 +33,7 @@ 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) */ /** Dynamic inputs from schema-based connections (e.g., image_url, tail_image_url, prompt) */
dynamicInputs?: Record<string, string>; dynamicInputs?: Record<string, string | string[]>;
} }
/** /**
@ -1273,7 +1273,11 @@ export async function POST(request: NextRequest) {
// - Provided via dynamicInputs // - Provided via dynamicInputs
// - Images are provided (image-to-video/image-to-image models) // - Images are provided (image-to-video/image-to-image models)
// - Dynamic inputs contain image frames (first_frame, last_frame, etc.) // - Dynamic inputs contain image frames (first_frame, last_frame, etc.)
const hasPrompt = prompt || (dynamicInputs && dynamicInputs.prompt); const hasPrompt = prompt || (dynamicInputs && (
typeof dynamicInputs.prompt === 'string'
? dynamicInputs.prompt
: Array.isArray(dynamicInputs.prompt) && dynamicInputs.prompt.length > 0
));
const hasImages = (images && images.length > 0); const hasImages = (images && images.length > 0);
const hasImageInputs = dynamicInputs && Object.keys(dynamicInputs).some(key => const hasImageInputs = dynamicInputs && Object.keys(dynamicInputs).some(key =>
key.includes('frame') || key.includes('image') key.includes('frame') || key.includes('image')
@ -1311,14 +1315,14 @@ export async function POST(request: NextRequest) {
const processedImages: string[] = images ? [...images] : []; const processedImages: string[] = images ? [...images] : [];
// Process dynamicInputs: filter empty values, keep Data URIs // Process dynamicInputs: filter empty values, keep Data URIs
let processedDynamicInputs: Record<string, string> | undefined = undefined; let processedDynamicInputs: Record<string, string | string[]> | undefined = undefined;
if (dynamicInputs) { if (dynamicInputs) {
processedDynamicInputs = {}; processedDynamicInputs = {};
for (const key of Object.keys(dynamicInputs)) { for (const key of Object.keys(dynamicInputs)) {
const value = dynamicInputs[key]; const value = dynamicInputs[key];
// Skip empty/null/undefined values // Skip empty/null/undefined values (arrays pass through)
if (value === null || value === undefined || value === '') { if (value === null || value === undefined || value === '') {
continue; continue;
} }
@ -1395,14 +1399,14 @@ export async function POST(request: NextRequest) {
const processedImages: string[] = images ? [...images] : []; const processedImages: string[] = images ? [...images] : [];
// Process dynamicInputs: filter empty values // Process dynamicInputs: filter empty values
let processedDynamicInputs: Record<string, string> | undefined = undefined; let processedDynamicInputs: Record<string, string | string[]> | undefined = undefined;
if (dynamicInputs) { if (dynamicInputs) {
processedDynamicInputs = {}; processedDynamicInputs = {};
for (const key of Object.keys(dynamicInputs)) { for (const key of Object.keys(dynamicInputs)) {
const value = dynamicInputs[key]; const value = dynamicInputs[key];
// Skip empty/null/undefined values // Skip empty/null/undefined values (arrays pass through)
if (value === null || value === undefined || value === '') { if (value === null || value === undefined || value === '') {
continue; continue;
} }

2
src/lib/providers/types.ts

@ -87,7 +87,7 @@ export interface GenerationInput {
/** 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:..." }) */ /** Dynamic inputs mapped from schema (e.g., { "image_url": "data:...", "tail_image_url": "data:..." }) */
dynamicInputs?: Record<string, string>; dynamicInputs?: Record<string, string | string[]>;
} }
/** /**

19
src/store/workflowStore.ts

@ -132,7 +132,7 @@ interface WorkflowStore {
// Helpers // Helpers
getNodeById: (id: string) => WorkflowNode | undefined; getNodeById: (id: string) => WorkflowNode | undefined;
getConnectedInputs: (nodeId: string) => { images: string[]; videos: string[]; text: string | null; dynamicInputs: Record<string, string> }; getConnectedInputs: (nodeId: string) => { images: string[]; videos: string[]; text: string | null; dynamicInputs: Record<string, string | string[]> };
validateWorkflow: () => { valid: boolean; errors: string[] }; validateWorkflow: () => { valid: boolean; errors: string[] };
// Global Image History // Global Image History
@ -745,7 +745,7 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
const images: string[] = []; const images: string[] = [];
const videos: string[] = []; const videos: string[] = [];
let text: string | null = null; let text: string | null = null;
const dynamicInputs: Record<string, string> = {}; const dynamicInputs: Record<string, string | string[]> = {};
// Get the target node to check for inputSchema // Get the target node to check for inputSchema
const targetNode = nodes.find((n) => n.id === nodeId); const targetNode = nodes.find((n) => n.id === nodeId);
@ -825,7 +825,15 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
// Map normalized handle ID to schema name for dynamicInputs // Map normalized handle ID to schema name for dynamicInputs
// This allows API to receive schema-specific parameter names // This allows API to receive schema-specific parameter names
if (handleId && handleToSchemaName[handleId]) { if (handleId && handleToSchemaName[handleId]) {
dynamicInputs[handleToSchemaName[handleId]] = value; const schemaName = handleToSchemaName[handleId];
const existing = dynamicInputs[schemaName];
if (existing !== undefined) {
dynamicInputs[schemaName] = Array.isArray(existing)
? [...existing, value]
: [existing, value];
} else {
dynamicInputs[schemaName] = value;
}
} }
// Route to typed arrays based on source output type // Route to typed arrays based on source output type
@ -1051,7 +1059,10 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
const { images, text, dynamicInputs } = getConnectedInputs(node.id); const { images, text, dynamicInputs } = getConnectedInputs(node.id);
// For dynamic inputs, check if we have at least a prompt // For dynamic inputs, check if we have at least a prompt
const promptText = text || dynamicInputs.prompt || null; const promptFromDynamic = Array.isArray(dynamicInputs.prompt)
? dynamicInputs.prompt[0]
: dynamicInputs.prompt;
const promptText = text || promptFromDynamic || null;
if (!promptText) { 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,

Loading…
Cancel
Save