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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user