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:
284
lib/canvas-node-catalog.ts
Normal file
284
lib/canvas-node-catalog.ts
Normal 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;
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user