"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTheme } from "next-themes"; import { ReactFlow, ReactFlowProvider, Background, Controls, MiniMap, applyNodeChanges, applyEdgeChanges, useReactFlow, type Node as RFNode, type Edge as RFEdge, type NodeChange, type EdgeChange, type Connection, BackgroundVariant, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { useConvexAuth, useMutation, useQuery } from "convex/react"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { nodeTypes } from "./node-types"; import { convexNodeToRF, convexEdgeToRF, NODE_DEFAULTS } from "@/lib/canvas-utils"; import CanvasToolbar from "@/components/canvas/canvas-toolbar"; interface CanvasInnerProps { canvasId: Id<"canvases">; } function CanvasInner({ canvasId }: CanvasInnerProps) { const { screenToFlowPosition } = useReactFlow(); const { resolvedTheme } = useTheme(); const { isLoading: isAuthLoading, isAuthenticated } = useConvexAuth(); const shouldSkipCanvasQueries = isAuthLoading || !isAuthenticated; useEffect(() => { if (process.env.NODE_ENV === "production") return; if (!isAuthLoading && !isAuthenticated) { console.warn("[Canvas debug] mounted without Convex auth", { canvasId }); } }, [canvasId, isAuthLoading, isAuthenticated]); // ─── Convex Realtime Queries ─────────────────────────────────── const convexNodes = useQuery( api.nodes.list, shouldSkipCanvasQueries ? "skip" : { canvasId }, ); const convexEdges = useQuery( api.edges.list, shouldSkipCanvasQueries ? "skip" : { canvasId }, ); // ─── Convex Mutations (exakte Signaturen aus nodes.ts / edges.ts) ── const moveNode = useMutation(api.nodes.move); const batchMoveNodes = useMutation(api.nodes.batchMove); const createNode = useMutation(api.nodes.create); const removeNode = useMutation(api.nodes.remove); const createEdge = useMutation(api.edges.create); const removeEdge = useMutation(api.edges.remove); // ─── Lokaler State (für flüssiges Dragging) ─────────────────── const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); // Drag-Lock: während des Drags kein Convex-Override const isDragging = useRef(false); // ─── Convex → Lokaler State Sync ────────────────────────────── useEffect(() => { if (!convexNodes || isDragging.current) return; // eslint-disable-next-line react-hooks/set-state-in-effect setNodes(convexNodes.map(convexNodeToRF)); }, [convexNodes]); useEffect(() => { if (!convexEdges) return; // eslint-disable-next-line react-hooks/set-state-in-effect setEdges(convexEdges.map(convexEdgeToRF)); }, [convexEdges]); // ─── Node Changes (Drag, Select, Remove) ───────────────────── const onNodesChange = useCallback((changes: NodeChange[]) => { setNodes((nds) => applyNodeChanges(changes, nds)); }, []); const onEdgesChange = useCallback((changes: EdgeChange[]) => { setEdges((eds) => applyEdgeChanges(changes, eds)); }, []); // ─── Drag Start → Lock ──────────────────────────────────────── const onNodeDragStart = useCallback(() => { isDragging.current = true; }, []); // ─── Drag Stop → Commit zu Convex ───────────────────────────── const onNodeDragStop = useCallback( (_: React.MouseEvent, node: RFNode, draggedNodes: RFNode[]) => { isDragging.current = false; // Wenn mehrere Nodes gleichzeitig gedraggt wurden → batchMove if (draggedNodes.length > 1) { batchMoveNodes({ moves: draggedNodes.map((n) => ({ nodeId: n.id as Id<"nodes">, positionX: n.position.x, positionY: n.position.y, })), }); } else { moveNode({ nodeId: node.id as Id<"nodes">, positionX: node.position.x, positionY: node.position.y, }); } }, [moveNode, batchMoveNodes], ); // ─── Neue Verbindung → Convex Edge ──────────────────────────── const onConnect = useCallback( (connection: Connection) => { if (connection.source && connection.target) { createEdge({ canvasId, sourceNodeId: connection.source as Id<"nodes">, targetNodeId: connection.target as Id<"nodes">, sourceHandle: connection.sourceHandle ?? undefined, targetHandle: connection.targetHandle ?? undefined, }); } }, [createEdge, canvasId], ); // ─── Node löschen → Convex ──────────────────────────────────── const onNodesDelete = useCallback( (deletedNodes: RFNode[]) => { for (const node of deletedNodes) { removeNode({ nodeId: node.id as Id<"nodes"> }); } }, [removeNode], ); // ─── Edge löschen → Convex ──────────────────────────────────── const onEdgesDelete = useCallback( (deletedEdges: RFEdge[]) => { for (const edge of deletedEdges) { removeEdge({ edgeId: edge.id as Id<"edges"> }); } }, [removeEdge], ); const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; }, []); const onDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); const nodeType = event.dataTransfer.getData( "application/lemonspace-node-type", ); if (!nodeType) { return; } const position = screenToFlowPosition({ x: event.clientX, y: event.clientY, }); const defaults = NODE_DEFAULTS[nodeType] ?? { width: 200, height: 100, data: {}, }; createNode({ canvasId, type: nodeType, positionX: position.x, positionY: position.y, width: defaults.width, height: defaults.height, data: defaults.data, }); }, [screenToFlowPosition, createNode, canvasId], ); // ─── Loading State ──────────────────────────────────────────── if (convexNodes === undefined || convexEdges === undefined) { return (
Canvas lädt…
); } return (
); } interface CanvasProps { canvasId: Id<"canvases">; } export default function Canvas({ canvasId }: CanvasProps) { return ( ); }