"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { Handle, Position, useReactFlow, type NodeProps, type Node } from "@xyflow/react"; import { useMutation, useAction } from "convex/react"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import BaseNodeWrapper from "./base-node-wrapper"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import { DEFAULT_MODEL_ID } from "@/lib/ai-models"; import { Sparkles, Loader2 } from "lucide-react"; type PromptNodeData = { prompt?: string; model?: string; canvasId?: string; _status?: string; _statusMessage?: string; }; export type PromptNode = Node; export default function PromptNode({ id, data, selected, }: NodeProps) { const nodeData = data as PromptNodeData; const { getEdges, getNode } = useReactFlow(); const [prompt, setPrompt] = useState(nodeData.prompt ?? ""); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); useEffect(() => { setPrompt(nodeData.prompt ?? ""); }, [nodeData.prompt]); const dataRef = useRef(data); dataRef.current = data; const updateData = useMutation(api.nodes.updateData); const createNode = useMutation(api.nodes.create); const createEdge = useMutation(api.edges.create); const generateImage = useAction(api.ai.generateImage); const debouncedSave = useDebouncedCallback((value: string) => { const raw = dataRef.current as Record; const { _status, _statusMessage, ...rest } = raw; void _status; void _statusMessage; updateData({ nodeId: id as Id<"nodes">, data: { ...rest, prompt: value }, }); }, 500); const handlePromptChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; setPrompt(value); debouncedSave(value); }, [debouncedSave] ); const handleGenerate = useCallback(async () => { if (!prompt.trim() || isGenerating) return; setError(null); setIsGenerating(true); try { const canvasId = nodeData.canvasId as Id<"canvases">; if (!canvasId) throw new Error("Missing canvasId on node"); const edges = getEdges(); const incomingEdges = edges.filter((e) => e.target === id); let referenceStorageId: Id<"_storage"> | undefined; for (const edge of incomingEdges) { const sourceNode = getNode(edge.source); if (sourceNode?.type === "image") { const srcData = sourceNode.data as { storageId?: string }; if (srcData.storageId) { referenceStorageId = srcData.storageId as Id<"_storage">; break; } } } const currentNode = getNode(id); const offsetX = (currentNode?.measured?.width ?? 280) + 32; const posX = (currentNode?.position?.x ?? 0) + offsetX; const posY = currentNode?.position?.y ?? 0; const aiNodeId = await createNode({ canvasId, type: "ai-image", positionX: posX, positionY: posY, width: 320, height: 320, data: { prompt, model: DEFAULT_MODEL_ID, modelTier: "standard", canvasId, }, }); await createEdge({ canvasId, sourceNodeId: id as Id<"nodes">, targetNodeId: aiNodeId, sourceHandle: "prompt-out", targetHandle: "prompt-in", }); await generateImage({ canvasId, nodeId: aiNodeId, prompt, referenceStorageId, model: DEFAULT_MODEL_ID, }); } catch (err) { setError(err instanceof Error ? err.message : "Generation failed"); } finally { setIsGenerating(false); } }, [ prompt, isGenerating, nodeData.canvasId, id, getEdges, getNode, createNode, createEdge, generateImage, ]); return (
✨ Prompt