feat: enhance canvas components with improved sidebar and toolbar functionality

- Updated CanvasSidebar to accept canvasId as a prop, enabling dynamic content based on the current canvas.
- Refactored CanvasToolbar to implement a dropdown menu for adding nodes, improving usability and organization.
- Introduced new node types and updated existing ones in the node template picker for better categorization and searchability.
- Enhanced AssetNode to utilize context for asset browser interactions, streamlining asset management on the canvas.
- Improved overall layout and styling for better user experience across canvas components.
This commit is contained in:
Matthias
2026-03-28 22:35:44 +01:00
parent e41d3c03b0
commit 4e55460792
14 changed files with 1104 additions and 115 deletions

284
lib/canvas-node-catalog.ts Normal file
View File

@@ -0,0 +1,284 @@
import type { Doc } from "@/convex/_generated/dataModel";
import { nodeTypes } from "@/components/canvas/node-types";
import {
CANVAS_NODE_TEMPLATES,
type CanvasNodeTemplate,
} from "@/lib/canvas-node-templates";
/** PRD-Kategorien (Reihenfolge für Sidebar / Dropdown). */
export type NodeCategoryId =
| "source"
| "ai-output"
| "transform"
| "control"
| "layout";
export const NODE_CATEGORY_META: Record<
NodeCategoryId,
{ label: string; order: number }
> = {
source: { label: "Quelle", order: 0 },
"ai-output": { label: "KI-Ausgabe", order: 1 },
transform: { label: "Transformation", order: 2 },
control: { label: "Steuerung & Flow", order: 3 },
layout: { label: "Canvas & Layout", order: 4 },
};
export const NODE_CATEGORIES_ORDERED: NodeCategoryId[] = (
Object.keys(NODE_CATEGORY_META) as NodeCategoryId[]
).sort((a, b) => NODE_CATEGORY_META[a].order - NODE_CATEGORY_META[b].order);
export type CatalogNodeType = Doc<"nodes">["type"];
export type NodeCatalogEntry = {
type: CatalogNodeType;
label: string;
category: NodeCategoryId;
phase: 1 | 2 | 3;
/** React-Flow-Komponente vorhanden. */
implemented: boolean;
/** Wird typischerweise vom KI-System erzeugt — nicht aus Palette/DnD anlegbar. */
systemOutput?: boolean;
/** Kurzer Hinweis für Tooltip (disabled). */
disabledHint?: string;
};
const REACT_FLOW_TYPES = new Set<string>(Object.keys(nodeTypes));
function entry(
partial: Omit<NodeCatalogEntry, "implemented"> & { implemented?: boolean },
): NodeCatalogEntry {
const implemented = partial.implemented ?? REACT_FLOW_TYPES.has(partial.type);
return { ...partial, implemented };
}
/**
* Vollständige Node-Taxonomie laut PRD / Convex `nodeType` (eine Zeile pro PRD-Node).
*/
export const NODE_CATALOG: readonly NodeCatalogEntry[] = [
// Quelle
entry({
type: "image",
label: "Bild",
category: "source",
phase: 1,
}),
entry({
type: "text",
label: "Text",
category: "source",
phase: 1,
}),
entry({
type: "color",
label: "Farbe / Palette",
category: "source",
phase: 2,
implemented: false,
disabledHint: "Folgt in Phase 2",
}),
entry({
type: "video",
label: "Video",
category: "source",
phase: 2,
}),
entry({
type: "asset",
label: "Asset (Stock)",
category: "source",
phase: 2,
}),
// KI-Ausgabe (Prompt-Knoten: steuert Generierung, ersetzt früheres „KI-Bild“ in der Palette)
entry({
type: "prompt",
label: "KI-Bild",
category: "ai-output",
phase: 1,
}),
entry({
type: "ai-text",
label: "KI-Text",
category: "ai-output",
phase: 2,
systemOutput: true,
disabledHint: "Wird von der KI erzeugt",
}),
entry({
type: "ai-video",
label: "KI-Video",
category: "ai-output",
phase: 2,
systemOutput: true,
disabledHint: "Wird von der KI erzeugt",
}),
entry({
type: "agent-output",
label: "Agent-Ausgabe",
category: "ai-output",
phase: 3,
systemOutput: true,
disabledHint: "Wird vom Agenten erzeugt",
}),
// Transformation
entry({
type: "crop",
label: "Crop / Resize",
category: "transform",
phase: 2,
implemented: false,
disabledHint: "Folgt in Phase 2",
}),
entry({
type: "bg-remove",
label: "BG entfernen",
category: "transform",
phase: 2,
implemented: false,
disabledHint: "Folgt in Phase 2",
}),
entry({
type: "upscale",
label: "Upscale",
category: "transform",
phase: 2,
implemented: false,
disabledHint: "Folgt in Phase 2",
}),
entry({
type: "style-transfer",
label: "Style Transfer",
category: "transform",
phase: 3,
implemented: false,
disabledHint: "Folgt in Phase 3",
}),
entry({
type: "face-restore",
label: "Gesicht",
category: "transform",
phase: 3,
implemented: false,
disabledHint: "Folgt in Phase 3",
}),
// Steuerung & Flow
entry({
type: "splitter",
label: "Splitter",
category: "control",
phase: 2,
implemented: false,
disabledHint: "Folgt in Phase 2",
}),
entry({
type: "loop",
label: "Loop",
category: "control",
phase: 2,
implemented: false,
disabledHint: "Folgt in Phase 2",
}),
entry({
type: "agent",
label: "Agent",
category: "control",
phase: 2,
implemented: false,
disabledHint: "Folgt in Phase 2",
}),
entry({
type: "mixer",
label: "Mixer / Merge",
category: "control",
phase: 3,
implemented: false,
disabledHint: "Folgt in Phase 3",
}),
entry({
type: "switch",
label: "Weiche",
category: "control",
phase: 3,
implemented: false,
disabledHint: "Folgt in Phase 3",
}),
// Canvas & Layout
entry({
type: "group",
label: "Gruppe",
category: "layout",
phase: 1,
}),
entry({
type: "frame",
label: "Frame",
category: "layout",
phase: 1,
}),
entry({
type: "note",
label: "Notiz",
category: "layout",
phase: 1,
}),
entry({
type: "text-overlay",
label: "Text-Overlay",
category: "layout",
phase: 2,
implemented: false,
disabledHint: "Folgt in Phase 2",
}),
entry({
type: "compare",
label: "Vergleich",
category: "layout",
phase: 1,
}),
entry({
type: "comment",
label: "Kommentar",
category: "layout",
phase: 3,
implemented: false,
disabledHint: "Folgt in Phase 3",
}),
entry({
type: "presentation",
label: "Präsentation",
category: "layout",
phase: 3,
implemented: false,
disabledHint: "Folgt in Phase 3",
}),
] as const;
const TEMPLATE_BY_TYPE = new Map<string, CanvasNodeTemplate>(
CANVAS_NODE_TEMPLATES.map((t) => [t.type, t]),
);
/** Sidebar / „+“: nur mit React-Flow-Typ, ohne systemOutput, mit Template. */
export function isNodePaletteEnabled(entry: NodeCatalogEntry): boolean {
if (!entry.implemented || entry.systemOutput) return false;
return TEMPLATE_BY_TYPE.has(entry.type);
}
export function getTemplateForCatalogType(
type: string,
): CanvasNodeTemplate | undefined {
return TEMPLATE_BY_TYPE.get(type);
}
export function catalogEntriesByCategory(): Map<
NodeCategoryId,
NodeCatalogEntry[]
> {
const map = new Map<NodeCategoryId, NodeCatalogEntry[]>();
for (const id of NODE_CATEGORIES_ORDERED) {
map.set(id, []);
}
for (const e of NODE_CATALOG) {
map.get(e.category)?.push(e);
}
return map;
}

View File

@@ -15,7 +15,7 @@ export const CANVAS_NODE_TEMPLATES = [
},
{
type: "prompt",
label: "KI-Bild",
label: "Prompt",
width: 320,
height: 220,
defaultData: { prompt: "", model: "", aspectRatio: "1:1" },
@@ -36,11 +36,32 @@ export const CANVAS_NODE_TEMPLATES = [
},
{
type: "compare",
label: "Compare",
label: "Vergleich",
width: 500,
height: 380,
defaultData: {},
},
{
type: "group",
label: "Gruppe",
width: 400,
height: 300,
defaultData: { label: "Gruppe" },
},
{
type: "asset",
label: "Asset (Stock)",
width: 260,
height: 240,
defaultData: {},
},
{
type: "video",
label: "Video",
width: 320,
height: 180,
defaultData: {},
},
] as const;
export type CanvasNodeTemplate = (typeof CANVAS_NODE_TEMPLATES)[number];