"use client"; import { useCallback, useState } from "react"; import { useAction } from "convex/react"; import type { FunctionReference } from "convex/server"; import { useTranslations } from "next-intl"; import { AlertCircle, Download, Loader2, RefreshCw, Video } from "lucide-react"; import { Position, useReactFlow, type Node, type NodeProps } from "@xyflow/react"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { useCanvasSync } from "@/components/canvas/canvas-sync-context"; import { classifyError } from "@/lib/ai-errors"; import { getVideoModel, type VideoModelDurationSeconds } from "@/lib/ai-video-models"; import { toast } from "@/lib/toast"; import BaseNodeWrapper from "./base-node-wrapper"; import CanvasHandle from "@/components/canvas/canvas-handle"; type AiVideoNodeData = { prompt?: string; modelId?: string; durationSeconds?: VideoModelDurationSeconds; creditCost?: number; canvasId?: string; url?: string; _status?: string; _statusMessage?: string; }; type NodeStatus = | "idle" | "analyzing" | "clarifying" | "executing" | "done" | "error"; export type AiVideoNodeType = Node; export default function AiVideoNode({ id, data, selected }: NodeProps) { const t = useTranslations("aiVideoNode"); const tToast = useTranslations("toasts"); const nodeData = data as AiVideoNodeData; const { getEdges, getNode } = useReactFlow(); const { status: syncStatus } = useCanvasSync(); const generateVideo = useAction( (api as unknown as { ai: { generateVideo: FunctionReference< "action", "public", { canvasId: Id<"canvases">; sourceNodeId: Id<"nodes">; outputNodeId: Id<"nodes">; prompt: string; modelId: string; durationSeconds: 5 | 10; }, { queued: true; outputNodeId: Id<"nodes"> } >; }; }).ai.generateVideo, ); const status = (nodeData._status ?? "idle") as NodeStatus; const [isRetrying, setIsRetrying] = useState(false); const [localError, setLocalError] = useState(null); const classifiedError = classifyError(nodeData._statusMessage ?? localError); const isLoading = status === "executing" || status === "analyzing" || status === "clarifying" || isRetrying; const modelLabel = typeof nodeData.modelId === "string" ? getVideoModel(nodeData.modelId)?.label ?? nodeData.modelId : "-"; const handleRetry = useCallback(async () => { if (isRetrying) return; if (syncStatus.isOffline) { toast.warning( "Offline aktuell nicht unterstuetzt", "KI-Generierung benoetigt eine aktive Verbindung.", ); return; } const prompt = nodeData.prompt?.trim(); const modelId = nodeData.modelId; const durationSeconds = nodeData.durationSeconds; if (!prompt || !modelId || !durationSeconds) { setLocalError(t("errorFallback")); return; } const incomingEdge = getEdges().find((edge) => edge.target === id); if (!incomingEdge) { setLocalError(t("errorFallback")); return; } const sourceNode = getNode(incomingEdge.source); if (!sourceNode || sourceNode.type !== "video-prompt") { setLocalError(t("errorFallback")); return; } const sourceData = sourceNode.data as { canvasId?: string } | undefined; const canvasId = (nodeData.canvasId ?? sourceData?.canvasId) as Id<"canvases"> | undefined; if (!canvasId) { setLocalError(t("errorFallback")); return; } setLocalError(null); setIsRetrying(true); try { await toast.promise( generateVideo({ canvasId, sourceNodeId: incomingEdge.source as Id<"nodes">, outputNodeId: id as Id<"nodes">, prompt, modelId, durationSeconds, }), { loading: tToast("ai.generating"), success: tToast("ai.generationQueued"), error: tToast("ai.generationFailed"), }, ); } catch (error) { const classified = classifyError(error); setLocalError(classified.rawMessage ?? tToast("ai.generationFailed")); } finally { setIsRetrying(false); } }, [ generateVideo, getEdges, getNode, id, isRetrying, nodeData.canvasId, nodeData.durationSeconds, nodeData.modelId, nodeData.prompt, syncStatus.isOffline, t, tToast, ]); return (
{status === "idle" && !nodeData.url ? (
{t("idleHint")}
) : null} {isLoading ? (

{t("generating")}

) : null} {status === "error" && !isLoading ? (

{classifiedError.rawMessage ?? t("errorFallback")}

) : null} {nodeData.url && !isLoading ? (

{t("modelMeta", { model: modelLabel })}

{typeof nodeData.durationSeconds === "number" ? (

{t("durationMeta", { duration: nodeData.durationSeconds })}

) : null} {typeof nodeData.creditCost === "number" ? (

{t("creditMeta", { credits: nodeData.creditCost })}

) : null} {nodeData.prompt ?

{nodeData.prompt}

: null} {nodeData.url ? ( {t("downloadButton")} ) : null}
); }