);
}
```
```tsx
// components/canvas/nodes/compare-node.tsx
'use client';
import { Handle, Position, type NodeProps, type Node } from '@xyflow/react';
import BaseNodeWrapper from './base-node-wrapper';
export type CompareNode = Node<{ leftUrl?: string; rightUrl?: string }, 'compare'>;
export default function CompareNode({ data, selected }: NodeProps) {
return (
đ Vergleich
{data.leftUrl ? : 'Bild A'}
{data.rightUrl ? : 'Bild B'}
);
}
```
---
## Schritt 3 â nodeTypes registrieren
**Kritisch:** Diese Map muss AUSSERHALB jeder React-Komponente definiert werden. Wenn sie innerhalb einer Komponente liegt, erstellt React bei jedem Render ein neues Objekt â React Flow re-rendert alle Nodes.
```tsx
// components/canvas/node-types.ts
import ImageNode from './nodes/image-node';
import TextNode from './nodes/text-node';
import PromptNode from './nodes/prompt-node';
import AiImageNode from './nodes/ai-image-node';
import GroupNode from './nodes/group-node';
import FrameNode from './nodes/frame-node';
import NoteNode from './nodes/note-node';
import CompareNode from './nodes/compare-node';
export const nodeTypes = {
image: ImageNode,
text: TextNode,
prompt: PromptNode,
'ai-image': AiImageNode,
group: GroupNode,
frame: FrameNode,
note: NoteNode,
compare: CompareNode,
} as const;
```
---
## Schritt 4 â Convex â React Flow Mapping
Das HerzstĂŒck: Convex-Daten in React Flow-Format transformieren und Ănderungen zurĂŒckschreiben.
```tsx
// lib/canvas-utils.ts
import type { Node as RFNode, Edge as RFEdge } from '@xyflow/react';
import type { Doc } from '@/convex/_generated/dataModel';
/**
* Transformiert einen Convex-Node in das React Flow-Format.
*/
export function convexNodeToRF(node: Doc<'nodes'>): RFNode {
return {
id: node._id,
type: node.type,
position: node.position,
data: node.data ?? {},
// parentId: node.parentNodeId ?? undefined, // â fĂŒr Group-Nodes, aktivieren wenn nötig
style: node.size
? { width: node.size.width, height: node.size.height }
: undefined,
};
}
/**
* Transformiert einen Convex-Edge in das React Flow-Format.
*/
export function convexEdgeToRF(edge: Doc<'edges'>): RFEdge {
return {
id: edge._id,
source: edge.sourceNodeId,
target: edge.targetNodeId,
// sourceHandle und targetHandle können spÀter ergÀnzt werden
};
}
```
---
## Schritt 5 â Haupt-Canvas-Komponente
Die zentrale Architekturentscheidung: **Lokaler State fĂŒr flĂŒssiges Interagieren, Convex als Sync-Layer.**
React Flow braucht `onNodesChange` fĂŒr jede Interaktion (Drag, Select, Remove). Wenn wir jede Drag-Bewegung direkt an Convex senden wĂŒrden, wĂ€re das zu viel Traffic und der Canvas wĂŒrde laggen. Stattdessen:
1. Convex-Daten kommen rein â werden in lokalen State geschrieben
2. Lokaler State wird von React Flow gesteuert (Drag, Select, etc.)
3. Bei `onNodeDragStop` wird die finale Position an Convex committed
4. Convex-Subscription aktualisiert den lokalen State bei Remote-Ănderungen
```tsx
// components/canvas/canvas.tsx
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
ReactFlow,
Background,
Controls,
MiniMap,
applyNodeChanges,
applyEdgeChanges,
type Node as RFNode,
type Edge as RFEdge,
type NodeChange,
type EdgeChange,
type Connection,
type ReactFlowInstance,
BackgroundVariant,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { 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 } from '@/lib/canvas-utils';
interface CanvasProps {
canvasId: Id<'canvases'>;
}
export default function Canvas({ canvasId }: CanvasProps) {
// âââ Convex Realtime Queries âââ
const convexNodes = useQuery(api.nodes.list, { canvasId });
const convexEdges = useQuery(api.edges.list, { canvasId });
// âââ Convex Mutations âââ
const moveNode = useMutation(api.nodes.move);
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([]);
// Track ob gerade gedraggt wird â dann kein Convex-Override
const isDragging = useRef(false);
// React Flow Instance Ref (fĂŒr screenToFlowPosition, etc.)
const [rfInstance, setRfInstance] = useState(null);
// âââ Convex â Lokaler State Sync âââ
useEffect(() => {
if (!convexNodes) return;
// Nur aktualisieren wenn NICHT gerade gedraggt wird
if (!isDragging.current) {
setNodes(convexNodes.map(convexNodeToRF));
}
}, [convexNodes]);
useEffect(() => {
if (!convexEdges) return;
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 Convex-Sync âââ
const onNodeDragStart = useCallback(() => {
isDragging.current = true;
}, []);
// âââ Drag Stop â Commit zu Convex âââ
const onNodeDragStop = useCallback(
(_: React.MouseEvent, node: RFNode) => {
isDragging.current = false;
moveNode({
nodeId: node.id as Id<'nodes'>,
position: { x: node.position.x, y: node.position.y },
});
},
[moveNode]
);
// âââ 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'>,
});
}
},
[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]
);
// âââ Loading State âââ
if (convexNodes === undefined || convexEdges === undefined) {
return (
Canvas lĂ€dtâŠ
);
}
return (
);
}
```
---
## Schritt 6 â Canvas Toolbar
Eine einfache Toolbar zum Anlegen neuer Nodes. In Phase 1 ist das der einfachste Weg, Nodes zu erstellen (Sidebar + Drag kommt danach).
```tsx
// components/canvas/canvas-toolbar.tsx
'use client';
import { useMutation } from 'convex/react';
import { api } from '@/convex/_generated/api';
import type { Id } from '@/convex/_generated/dataModel';
const nodeTemplates = [
{ type: 'image', label: 'đŒïž Bild', defaultData: {} },
{ type: 'text', label: 'đ Text', defaultData: { content: '' } },
{ type: 'prompt', label: 'âš Prompt', defaultData: { prompt: '' } },
{ type: 'note', label: 'đ Notiz', defaultData: { content: '' } },
{ type: 'frame', label: 'đ„ïž Frame', defaultData: { label: 'Untitled', resolution: '1080x1080' } },
] as const;
interface CanvasToolbarProps {
canvasId: Id<'canvases'>;
}
export default function CanvasToolbar({ canvasId }: CanvasToolbarProps) {
const createNode = useMutation(api.nodes.create);
const handleAddNode = async (type: string, data: Record) => {
// Platziere neue Nodes leicht versetzt, damit sie nicht ĂŒbereinander liegen
const offset = Math.random() * 200;
await createNode({
canvasId,
type,
position: { x: 100 + offset, y: 100 + offset },
data,
});
};
return (
{nodeTemplates.map((t) => (
))}
);
}
```
---
## Schritt 7 â Canvas Page (Next.js App Router)
```tsx
// app/(app)/canvas/[canvasId]/page.tsx
import Canvas from '@/components/canvas/canvas';
import CanvasToolbar from '@/components/canvas/canvas-toolbar';
import type { Id } from '@/convex/_generated/dataModel';
export default async function CanvasPage({
params,
}: {
params: Promise<{ canvasId: string }>;
}) {
const { canvasId } = await params;
const typedCanvasId = canvasId as Id<'canvases'>;
return (
);
}
```
---
## Schritt 8 â CSS Import nicht vergessen!
@xyflow/react braucht sein eigenes CSS. Importiere es entweder in der Canvas-Komponente (wie oben) oder global in `app/globals.css`:
```css
/* app/globals.css â NACH den Tailwind-Imports */
@import '@xyflow/react/dist/style.css';
```
> **Tailwind v4 Hinweis:** Falls die React Flow Styles von Tailwinds Reset ĂŒberschrieben werden, importiere sie NACH dem Tailwind-Import.
---
## Testing-Reihenfolge
Nachdem du alle Dateien erstellt hast, teste in dieser Reihenfolge:
### Test 1: Canvas rendert
- Navigiere zu `/canvas/` (du brauchst eine existierende Canvas-ID aus Convex)
- Erwartung: Leerer Canvas mit Dot-Background, Controls unten links, MiniMap unten rechts
- Falls 404: PrĂŒfe ob die Route `app/(app)/canvas/[canvasId]/page.tsx` korrekt liegt
### Test 2: Node hinzufĂŒgen
- Klicke auf "đ Notiz" in der Toolbar
- Erwartung: Note-Node erscheint auf dem Canvas
- PrĂŒfe im Convex Dashboard: neuer Eintrag in der `nodes`-Tabelle
### Test 3: Node verschieben
- Ziehe den Node an eine neue Position, lasse los
- Erwartung: Node bleibt an der neuen Position
- PrĂŒfe im Convex Dashboard: `position.x` und `position.y` haben sich aktualisiert
### Test 4: Verbindung erstellen
- Erstelle einen Prompt-Node und einen (leeren) AI-Image-Node
- Ziehe vom Source-Handle (rechts am Prompt) zum Target-Handle (links am AI-Image)
- Erwartung: Edge erscheint als Linie zwischen den Nodes
- PrĂŒfe im Convex Dashboard: neuer Eintrag in der `edges`-Tabelle
### Test 5: Node löschen
- Selektiere einen Node (Klick), drĂŒcke `Delete` oder `Backspace`
- Erwartung: Node verschwindet, zugehörige Edges werden ebenfalls entfernt
- PrĂŒfe im Convex Dashboard: Node und Edges sind gelöscht
---
## Bekannte Fallstricke
### 1. `nodeTypes` innerhalb der Komponente definiert
â React Flow re-rendert ALLE Nodes bei jedem State-Update. Die Map MUSS in einer eigenen Datei liegen.
### 2. React Flow CSS fehlt
â Nodes sind unsichtbar oder falsch positioniert. Import von `@xyflow/react/dist/style.css` ist Pflicht.
### 3. Convex-Sync wÀhrend Drag
â Wenn Convex einen neuen Wert pusht wĂ€hrend der User draggt, springt der Node zur alten Position zurĂŒck. Die `isDragging`-Ref verhindert das.
### 4. Handle-Styling
â Die Standard-Handles von React Flow sind winzig und dunkel. Die `!`-Klassen in Tailwind erzwingen Custom-Styling ĂŒber die React Flow Defaults.
### 5. Batch-Drag (mehrere Nodes selektiert)
â `onNodeDragStop` feuert nur fĂŒr den primĂ€r gedraggten Node. FĂŒr Batch-Moves nutze `onSelectionDragStop` oder `batchMove` Mutation.
---
## NĂ€chste Schritte (nach Schritt 1â3)
- **Schritt 4:** Sidebar mit Node-Palette + dnd-kit (Drag von Sidebar auf Canvas)
- **Schritt 5:** Inline-Editing (Text direkt im Node bearbeiten â `updateData` Mutation)
- **Schritt 6:** Bild-Upload (Convex File Storage + Image-Node)
- **Schritt 7:** OpenRouter-Integration (Prompt â KI-Bild-Generierung)
- **Schritt 8:** Node-Status-Modell visuell ausbauen (Shimmer, Progress, Error)