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

View File

@@ -92,7 +92,7 @@ export default function AiImageNode({
if (!canvasId) throw new Error("Missing canvasId");
const prompt = nodeData.prompt;
if (!prompt) throw new Error("No prompt — use Generate from a KI-Bild node");
if (!prompt) throw new Error("No prompt — Generierung vom Prompt-Knoten aus starten");
const edges = getEdges();
const incomingEdges = edges.filter((e) => e.target === id);
@@ -178,7 +178,7 @@ export default function AiImageNode({
<div className="shrink-0 border-b border-border px-3 py-2">
<div className="flex items-center gap-1.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
<ImageIcon className="h-3.5 w-3.5" />
AI Image
Bildausgabe
</div>
</div>
@@ -187,7 +187,7 @@ export default function AiImageNode({
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-muted-foreground">
<ImageIcon className="h-10 w-10 opacity-30" />
<p className="px-6 text-center text-xs opacity-60">
Verbinde einen KI-Bild-Knoten und starte die Generierung dort.
Verbinde einen Prompt-Knoten und starte die Generierung dort.
</p>
</div>
)}

View File

@@ -1,17 +1,20 @@
"use client";
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type MouseEvent,
} from "react";
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { Handle, Position, useStore, type Node, type NodeProps } from "@xyflow/react";
import { useMutation } from "convex/react";
import { ExternalLink, ImageIcon } from "lucide-react";
import BaseNodeWrapper from "./base-node-wrapper";
import {
AssetBrowserPanel,
useAssetBrowserTarget,
type AssetBrowserSessionState,
} from "@/components/canvas/asset-browser-panel";
import { api } from "@/convex/_generated/api";
@@ -40,7 +43,9 @@ type AssetNodeData = {
export type AssetNodeType = Node<AssetNodeData, "asset">;
export default function AssetNode({ id, data, selected, width, height }: NodeProps<AssetNodeType>) {
const [panelOpen, setPanelOpen] = useState(false);
const { targetNodeId, openForNode, close: closeAssetBrowser } =
useAssetBrowserTarget();
const panelOpen = targetNodeId === id;
const [loadedPreviewUrl, setLoadedPreviewUrl] = useState<string | null>(null);
const [failedPreviewUrl, setFailedPreviewUrl] = useState<string | null>(null);
const [browserState, setBrowserState] = useState<AssetBrowserSessionState>({
@@ -52,6 +57,31 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
});
const resizeNode = useMutation(api.nodes.resize);
const edges = useStore((s) => s.edges);
const nodes = useStore((s) => s.nodes);
const linkedSearchTerm = useMemo(() => {
const incoming = edges.filter((e) => e.target === id);
for (const edge of incoming) {
const sourceNode = nodes.find((n) => n.id === edge.source);
if (sourceNode?.type !== "text") continue;
const content = (sourceNode.data as { content?: string }).content;
if (typeof content === "string" && content.trim().length > 0) {
return content.trim();
}
}
return "";
}, [edges, id, nodes]);
const openAssetBrowser = useCallback(() => {
setBrowserState((s) =>
linkedSearchTerm
? { ...s, term: linkedSearchTerm, results: [], page: 1, totalPages: 1 }
: s,
);
openForNode(id);
}, [id, linkedSearchTerm, openForNode]);
const hasAsset = typeof data.assetId === "number";
const previewUrl = data.url ?? data.previewUrl;
const isPreviewLoading = Boolean(
@@ -143,8 +173,9 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
<Button
size="sm"
variant={hasAsset ? "ghost" : "default"}
className="h-6 px-2 text-xs"
onClick={() => setPanelOpen(true)}
className="nodrag h-6 px-2 text-xs"
onClick={openAssetBrowser}
onPointerDown={(e) => e.stopPropagation()}
type="button"
>
{hasAsset ? "Change" : "Browse Assets"}
@@ -239,7 +270,7 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
canvasId={data.canvasId}
initialState={browserState}
onStateChange={setBrowserState}
onClose={() => setPanelOpen(false)}
onClose={closeAssetBrowser}
/>
) : null}

View File

@@ -311,7 +311,7 @@ export default function PromptNode({
<div className="flex h-full flex-col gap-2 p-3">
<div className="flex items-center gap-1.5 text-xs font-medium text-violet-600 dark:text-violet-400">
<Sparkles className="h-3.5 w-3.5" />
Eingabe
KI-Bild
</div>
{inputMeta.hasTextInput ? (
<div className="flex-1 overflow-auto rounded-md border border-violet-500/30 bg-violet-500/5 px-3 py-2">