"use client"; import { useCallback, useState, useEffect, useMemo } from "react"; import { Handle, Position, NodeProps, Node, useReactFlow } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { ModelParameters } from "./ModelParameters"; import { useWorkflowStore } from "@/store/workflowStore"; import { GenerateVideoNodeData, ProviderType, SelectedModel } from "@/types"; import { ProviderModel, ModelCapability } from "@/lib/providers/types"; // Video generation capabilities const VIDEO_CAPABILITIES: ModelCapability[] = ["text-to-video", "image-to-video"]; type GenerateVideoNodeType = Node; export function GenerateVideoNode({ id, data, selected }: NodeProps) { const nodeData = data; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const providerSettings = useWorkflowStore((state) => state.providerSettings); const [externalModels, setExternalModels] = useState([]); const [isLoadingModels, setIsLoadingModels] = useState(false); const [modelsFetchError, setModelsFetchError] = useState(null); // Get the current selected provider (default to fal since Gemini doesn't do video) const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal"; // Get enabled providers (exclude Gemini since it doesn't do video) const enabledProviders = useMemo(() => { const providers: { id: ProviderType; name: string }[] = []; // fal.ai is always available (works without key but rate limited) providers.push({ id: "fal", name: "fal.ai" }); // Add Replicate if configured if (providerSettings.providers.replicate?.enabled && providerSettings.providers.replicate?.apiKey) { providers.push({ id: "replicate", name: "Replicate" }); } return providers; }, [providerSettings]); // Fetch models from external providers when provider changes const fetchModels = useCallback(async () => { setIsLoadingModels(true); setModelsFetchError(null); try { const capabilities = VIDEO_CAPABILITIES.join(","); const headers: HeadersInit = {}; if (providerSettings.providers.replicate?.apiKey) { headers["X-Replicate-Key"] = providerSettings.providers.replicate.apiKey; } if (providerSettings.providers.fal?.apiKey) { headers["X-Fal-Key"] = providerSettings.providers.fal.apiKey; } const response = await fetch(`/api/models?provider=${currentProvider}&capabilities=${capabilities}`, { headers }); if (response.ok) { const data = await response.json(); setExternalModels(data.models || []); setModelsFetchError(null); } else { const errorData = await response.json().catch(() => ({})); const errorMsg = errorData.error || `Failed to load models (${response.status})`; setExternalModels([]); setModelsFetchError( currentProvider === "replicate" && response.status === 401 ? "Invalid Replicate API key. Check your settings." : errorMsg ); } } catch (error) { console.error("Failed to fetch video models:", error); setExternalModels([]); setModelsFetchError("Failed to load models. Check your connection."); } finally { setIsLoadingModels(false); } }, [currentProvider, providerSettings]); useEffect(() => { fetchModels(); }, [fetchModels]); // Handle provider change const handleProviderChange = useCallback( (e: React.ChangeEvent) => { const provider = e.target.value as ProviderType; // Set placeholder for the provider const newSelectedModel: SelectedModel = { provider, modelId: "", displayName: "Select model...", }; // Clear parameters when switching providers (different providers have different schemas) updateNodeData(id, { selectedModel: newSelectedModel, parameters: {} }); }, [id, updateNodeData] ); // Handle model change const handleModelChange = useCallback( (e: React.ChangeEvent) => { const modelId = e.target.value; const model = externalModels.find(m => m.id === modelId); if (model) { const newSelectedModel: SelectedModel = { provider: currentProvider, modelId: model.id, displayName: model.name, }; // Clear parameters when changing models (different models have different schemas) updateNodeData(id, { selectedModel: newSelectedModel, parameters: {} }); } }, [id, currentProvider, externalModels, updateNodeData] ); const handleClearVideo = useCallback(() => { updateNodeData(id, { outputVideo: null, status: "idle", error: null }); }, [id, updateNodeData]); const handleParametersChange = useCallback( (parameters: Record) => { updateNodeData(id, { parameters }); }, [id, updateNodeData] ); // Handle parameters expand/collapse - resize node height const { setNodes } = useReactFlow(); const handleParametersExpandChange = useCallback( (expanded: boolean, parameterCount: number) => { // Each parameter row is ~24px, plus some padding const parameterHeight = expanded ? Math.max(parameterCount * 28 + 16, 60) : 0; const baseHeight = 300; // Default node height const newHeight = baseHeight + parameterHeight; setNodes((nodes) => nodes.map((node) => node.id === id ? { ...node, style: { ...node.style, height: newHeight } } : node ) ); }, [id, setNodes] ); const regenerateNode = useWorkflowStore((state) => state.regenerateNode); const isRunning = useWorkflowStore((state) => state.isRunning); const handleRegenerate = useCallback(() => { regenerateNode(id); }, [id, regenerateNode]); return ( updateNodeData(id, { customTitle: title || undefined })} onCommentChange={(comment) => updateNodeData(id, { comment: comment || undefined })} onRun={handleRegenerate} selected={selected} isExecuting={isRunning} hasError={nodeData.status === "error"} > {/* Image input - accepts multiple connections */} {/* Text input - single connection */} {/* Video output - we use "image" handle type for now since the connection system expects image/text */}
{/* Preview area */} {nodeData.outputVideo ? (
) : (
{nodeData.status === "loading" ? ( ) : nodeData.status === "error" ? ( {nodeData.error || "Failed"} ) : ( Run to generate )}
)} {/* Provider selector - only show if multiple providers are enabled */} {enabledProviders.length > 1 && ( )} {/* Model selector */} {modelsFetchError ? (
{modelsFetchError}
) : ( )} {/* Model-specific parameters */} {nodeData.selectedModel?.modelId && !modelsFetchError && ( )}
); }