"use client"; import { useCallback, useState } from "react"; import { Handle, Position, useReactFlow, type NodeProps, type Node } from "@xyflow/react"; import { useAction } from "convex/react"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import BaseNodeWrapper from "./base-node-wrapper"; import { DEFAULT_MODEL_ID, getModel } from "@/lib/ai-models"; import { cn, formatEurFromCents } from "@/lib/utils"; import { Loader2, AlertCircle, RefreshCw, ImageIcon, } from "lucide-react"; type AiImageNodeData = { storageId?: string; url?: string; prompt?: string; model?: string; modelTier?: string; generatedAt?: number; /** Gebuchte Credits in Euro-Cent (PRD: nach Commit) */ creditCost?: number; canvasId?: string; _status?: string; _statusMessage?: string; }; export type AiImageNode = Node; type NodeStatus = | "idle" | "analyzing" | "clarifying" | "executing" | "done" | "error"; export default function AiImageNode({ id, data, selected, }: NodeProps) { const nodeData = data as AiImageNodeData; const { getEdges, getNode } = useReactFlow(); const [isGenerating, setIsGenerating] = useState(false); const [localError, setLocalError] = useState(null); const generateImage = useAction(api.ai.generateImage); const status = (nodeData._status ?? "idle") as NodeStatus; const errorMessage = nodeData._statusMessage; const isLoading = status === "executing" || status === "analyzing" || status === "clarifying" || isGenerating; const handleRegenerate = useCallback(async () => { if (isLoading) return; setLocalError(null); setIsGenerating(true); try { const canvasId = nodeData.canvasId as Id<"canvases">; if (!canvasId) throw new Error("Missing canvasId"); const prompt = nodeData.prompt; if (!prompt) throw new Error("No prompt — use Generate from a Prompt node"); const edges = getEdges(); const incomingEdges = edges.filter((e) => e.target === id); let referenceStorageId: Id<"_storage"> | undefined; for (const edge of incomingEdges) { const src = getNode(edge.source); if (src?.type === "image") { const srcData = src.data as { storageId?: string }; if (srcData.storageId) { referenceStorageId = srcData.storageId as Id<"_storage">; break; } } } await generateImage({ canvasId, nodeId: id as Id<"nodes">, prompt, referenceStorageId, model: nodeData.model ?? DEFAULT_MODEL_ID, }); } catch (err) { setLocalError(err instanceof Error ? err.message : "Generation failed"); } finally { setIsGenerating(false); } }, [isLoading, nodeData, id, getEdges, getNode, generateImage]); const modelName = getModel(nodeData.model ?? DEFAULT_MODEL_ID)?.name ?? "AI"; return (
🖼️ AI Image
{status === "idle" && !nodeData.url && (

Connect a Prompt node and click Generate

)} {isLoading && (

{status === "analyzing" && "Analyzing…"} {status === "clarifying" && "Clarifying…"} {(status === "executing" || isGenerating) && "Generating…"}

{modelName}

)} {status === "error" && !isLoading && (

Generation failed

{errorMessage ?? localError ?? "Unknown error"} — Credits not charged

)} {nodeData.url && !isLoading && ( // eslint-disable-next-line @next/next/no-img-element {nodeData.prompt )} {nodeData.creditCost != null && nodeData.url && !isLoading && status !== "error" && (
{formatEurFromCents(nodeData.creditCost)}
)} {status === "done" && nodeData.url && !isLoading && (
)}
{nodeData.prompt && (

{nodeData.prompt}

{modelName}

)} ); }