Browse Source

fix: add hardcoded Veo video parameters to GenerateVideoNode

Veo models (veo-3.1, veo-3.1-fast) showed no user-configurable
parameters in the UI. Follows the established pattern from
GenerateImageNode where Gemini model parameters are hardcoded in the
component rather than fetched via the schema API. Adds aspect ratio,
duration, resolution, negative prompt, and seed controls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
fc157f1d7b
  1. 83
      src/components/nodes/GenerateVideoNode.tsx

83
src/components/nodes/GenerateVideoNode.tsx

@ -18,6 +18,17 @@ import { useVideoBlobUrl } from "@/hooks/useVideoBlobUrl";
// Video generation capabilities // Video generation capabilities
const VIDEO_CAPABILITIES: ModelCapability[] = ["text-to-video", "image-to-video"]; 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<GenerateVideoNodeData, "generateVideo">; type GenerateVideoNodeType = Node<GenerateVideoNodeData, "generateVideo">;
export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVideoNodeType>) { export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVideoNodeType>) {
@ -34,7 +45,6 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
const [isLoadingCarouselVideo, setIsLoadingCarouselVideo] = useState(false); const [isLoadingCarouselVideo, setIsLoadingCarouselVideo] = useState(false);
const videoBlobUrl = useVideoBlobUrl(nodeData.outputVideo ?? null); const videoBlobUrl = useVideoBlobUrl(nodeData.outputVideo ?? null);
// Get the current selected provider (default to fal since Gemini doesn't do video)
const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal"; const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal";
// Get enabled providers // Get enabled providers
@ -149,6 +159,22 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
[id, updateNodeData] [id, updateNodeData]
); );
// Update a single key in the parameters bag (used by hardcoded Veo controls)
const updateVeoParam = useCallback(
(key: string, value: unknown) => {
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 // Handle inputs loaded from schema
const handleInputsLoaded = useCallback( const handleInputsLoaded = useCallback(
(inputs: ModelInputDef[]) => { (inputs: ModelInputDef[]) => {
@ -717,7 +743,58 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
)} )}
{/* Model-specific parameters */} {/* Model-specific parameters */}
{nodeData.selectedModel?.modelId && ( {nodeData.selectedModel?.modelId && isVeoModel(nodeData.selectedModel.modelId) ? (
// Hardcoded Veo parameters (matching GenerateImageNode pattern for Gemini models)
<div className="flex flex-col gap-1.5 shrink-0">
{/* Aspect ratio + Duration row */}
<div className="flex gap-1.5">
<select
value={(nodeData.parameters?.aspectRatio as string) || "16:9"}
onChange={(e) => updateVeoParam("aspectRatio", e.target.value)}
className="flex-1 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"
>
{VEO_ASPECT_RATIOS.map((r) => (
<option key={r} value={r}>{r}</option>
))}
</select>
<select
value={(nodeData.parameters?.durationSeconds as string) || "8"}
onChange={(e) => updateVeoParam("durationSeconds", e.target.value)}
className="w-12 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"
>
{VEO_DURATIONS.map((d) => (
<option key={d} value={d}>{d}s</option>
))}
</select>
<select
value={(nodeData.parameters?.resolution as string) || "720p"}
onChange={(e) => updateVeoParam("resolution", e.target.value)}
className="w-14 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"
>
{VEO_RESOLUTIONS.map((r) => (
<option key={r} value={r}>{r}</option>
))}
</select>
</div>
{/* Negative prompt */}
<input
type="text"
placeholder="Negative prompt..."
value={(nodeData.parameters?.negativePrompt as string) || ""}
onChange={(e) => 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 */}
<input
type="number"
placeholder="Seed (optional)"
value={(nodeData.parameters?.seed as string) ?? ""}
onChange={(e) => 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"
/>
</div>
) : nodeData.selectedModel?.modelId ? (
<ModelParameters <ModelParameters
modelId={nodeData.selectedModel.modelId} modelId={nodeData.selectedModel.modelId}
provider={currentProvider} provider={currentProvider}
@ -726,7 +803,7 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
onExpandChange={handleParametersExpandChange} onExpandChange={handleParametersExpandChange}
onInputsLoaded={handleInputsLoaded} onInputsLoaded={handleInputsLoaded}
/> />
)} ) : null}
</div> </div>
</BaseNode> </BaseNode>

Loading…
Cancel
Save