import { useMemo, useState } from "react"; import { Empty, Input, Select, Tag } from "antd"; import { ChevronRight, FileText, Image as ImageIcon, Music2, Search, Settings2, Square, Type, Video } from "lucide-react"; import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme"; import { getNodeDefinition } from "@/lib/canvas/node-registry"; import { cn } from "@/lib/utils"; import { useAssetStore, type Asset, type AssetKind } from "@/stores/use-asset-store"; import { useThemeStore } from "@/stores/use-theme-store"; import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas"; import type { InsertAssetPayload } from "./asset-picker-modal"; type PanelTab = "canvas" | "assets"; type Props = { nodes: CanvasNodeData[]; selectedNodeIds: Set; onFocusNode: (nodeId: string) => void; onInsertAsset: (payload: InsertAssetPayload) => void; }; const NODE_TYPE_ICON: Record = { [CanvasNodeType.Image]: ImageIcon, [CanvasNodeType.Video]: Video, [CanvasNodeType.Audio]: Music2, [CanvasNodeType.Text]: Type, [CanvasNodeType.Config]: Settings2, [CanvasNodeType.Group]: Square, }; const STATUS_COLOR: Record = { success: "#22c55e", loading: "#f59e0b", error: "#ef4444", idle: "transparent", }; export function CanvasSidePanel({ nodes, selectedNodeIds, onFocusNode, onInsertAsset }: Props) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const [tab, setTab] = useState("canvas"); return ( ); } function TabButton({ label, active, theme, onClick }: { label: string; active: boolean; theme: CanvasTheme; onClick: () => void }) { return ( ); } // --------------------------------------------------------------------------- // 画布 Tab —— 列出节点,点击居中放大并选中 // --------------------------------------------------------------------------- const NODE_FILTER_OPTIONS = [ { label: "全部", value: "all" }, { label: "图片", value: CanvasNodeType.Image }, { label: "视频", value: CanvasNodeType.Video }, { label: "文本", value: CanvasNodeType.Text }, { label: "音频", value: CanvasNodeType.Audio }, { label: "配置", value: CanvasNodeType.Config }, { label: "分组", value: CanvasNodeType.Group }, ]; function nodePreviewText(node: CanvasNodeData) { if (node.type === CanvasNodeType.Text) return node.metadata?.content || node.metadata?.prompt || ""; return getNodeDefinition(node.type)?.title || node.type; } function CanvasNodesTab({ nodes, selectedNodeIds, onFocusNode, theme }: { nodes: CanvasNodeData[]; selectedNodeIds: Set; onFocusNode: (nodeId: string) => void; theme: CanvasTheme }) { const [keyword, setKeyword] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const filtered = useMemo(() => { const query = keyword.trim().toLowerCase(); return nodes.filter((node) => (typeFilter === "all" || node.type === typeFilter) && (!query || [node.title, node.metadata?.content, node.metadata?.prompt].filter(Boolean).join(" ").toLowerCase().includes(query))); }, [nodes, keyword, typeFilter]); return (
画布元素 {filtered.length ? {filtered.length} : null} } placeholder="搜索节点" value={keyword} onChange={(e) => setKeyword(e.target.value)} />
{filtered.length ? (
{filtered.map((node) => { const Icon = NODE_TYPE_ICON[node.type] || FileText; const isImage = node.type === CanvasNodeType.Image && node.metadata?.content; const active = selectedNodeIds.has(node.id); return ( ); })}
) : (
画布暂无节点
)}
); } // --------------------------------------------------------------------------- // 资产 Tab —— 按类型折叠分组 + 标签筛选,点击插入画布 // --------------------------------------------------------------------------- const ASSET_GROUPS: { kind: AssetKind; label: string; icon: typeof Square }[] = [ { kind: "image", label: "图片", icon: ImageIcon }, { kind: "video", label: "视频", icon: Video }, { kind: "text", label: "文本", icon: FileText }, ]; function buildInsertPayload(asset: Asset): InsertAssetPayload { if (asset.kind === "text") return { kind: "text", content: asset.data.content, title: asset.title }; if (asset.kind === "video") return { kind: "video", url: asset.data.url, storageKey: asset.data.storageKey, title: asset.title, width: asset.data.width, height: asset.data.height }; return { kind: "image", dataUrl: asset.data.dataUrl, storageKey: asset.data.storageKey, title: asset.title }; } function CanvasAssetsTab({ onInsert, theme }: { onInsert: (payload: InsertAssetPayload) => void; theme: CanvasTheme }) { const assets = useAssetStore((state) => state.assets); const [keyword, setKeyword] = useState(""); const [tagFilter, setTagFilter] = useState("all"); const [collapsed, setCollapsed] = useState>({}); const allTags = useMemo(() => Array.from(new Set(assets.flatMap((asset) => asset.tags || []))).slice(0, 20), [assets]); const filtered = useMemo(() => { const query = keyword.trim().toLowerCase(); return assets.filter((asset) => (tagFilter === "all" || (asset.tags || []).includes(tagFilter)) && (!query || [asset.title, ...(asset.tags || [])].join(" ").toLowerCase().includes(query))); }, [assets, keyword, tagFilter]); const groups = useMemo(() => ASSET_GROUPS.map((group) => ({ ...group, items: filtered.filter((asset) => asset.kind === group.kind) })).filter((group) => group.items.length > 0), [filtered]); return (
} placeholder="搜索资产" value={keyword} onChange={(e) => setKeyword(e.target.value)} />
{allTags.length ? (
setTagFilter("all")}> 全部 {allTags.map((tag) => ( setTagFilter((prev) => (prev === tag ? "all" : tag))}> {tag} ))}
) : null}
{groups.length ? (
{groups.map((group) => { const isCollapsed = collapsed[group.kind]; return (
{isCollapsed ? null : (
{group.items.map((asset) => ( onInsert(buildInsertPayload(asset))} /> ))}
)}
); })}
) : ( )}
); } function AssetCard({ asset, theme, onClick }: { asset: Asset; theme: CanvasTheme; onClick: () => void }) { const cover = asset.coverUrl || (asset.kind === "image" ? asset.data.dataUrl : ""); return ( ); }