diff --git a/src/components/nodes/GenerateVideoNode.tsx b/src/components/nodes/GenerateVideoNode.tsx index dbcd2d86..74424fb2 100644 --- a/src/components/nodes/GenerateVideoNode.tsx +++ b/src/components/nodes/GenerateVideoNode.tsx @@ -18,6 +18,17 @@ import { useVideoBlobUrl } from "@/hooks/useVideoBlobUrl"; // Video generation capabilities const VIDEO_CAPABILITIES: ModelCapability[] = ["text-to-video", "image-to-video"]; +// Hardcoded Veo parameter options (matches getGeminiVideoSchema in models/[modelId]/route.ts) +const VEO_ASPECT_RATIOS = ["16:9", "9:16"] as const; +const VEO_DURATIONS = ["4", "6", "8"] as const; +const VEO_RESOLUTIONS = ["720p", "1080p", "4k"] as const; + +/** Returns true for Gemini-native Veo video models */ +function isVeoModel(modelId: string | undefined): boolean { + if (!modelId) return false; + return modelId.startsWith("veo-"); +} + type GenerateVideoNodeType = Node; export function GenerateVideoNode({ id, data, selected }: NodeProps) { @@ -34,7 +45,6 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps { + const current = nodeData.parameters || {}; + // Remove the key if value is empty string (clear optional fields) + if (value === "") { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [key]: _, ...rest } = current; + updateNodeData(id, { parameters: rest }); + } else { + updateNodeData(id, { parameters: { ...current, [key]: value } }); + } + }, + [id, nodeData.parameters, updateNodeData] + ); + // Handle inputs loaded from schema const handleInputsLoaded = useCallback( (inputs: ModelInputDef[]) => { @@ -717,7 +743,58 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps + {/* Aspect ratio + Duration row */} +
+ + + +
+ {/* Negative prompt */} + updateVeoParam("negativePrompt", e.target.value)} + className="w-full text-[10px] py-1 px-1.5 border border-neutral-700 rounded bg-neutral-900/50 focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300 placeholder:text-neutral-600" + /> + {/* Seed */} + updateVeoParam("seed", e.target.value === "" ? "" : Number(e.target.value))} + min={0} + className="w-full text-[10px] py-1 px-1.5 border border-neutral-700 rounded bg-neutral-900/50 focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300 placeholder:text-neutral-600" + /> + + ) : nodeData.selectedModel?.modelId ? ( - )} + ) : null}