import { AnimatePresence, useReducedMotion } from "motion/react"; import { useEffect, useState, type CSSProperties, type ReactNode } from "react"; import { ArrowLeft, Check, ChevronRight, Clapperboard, Clipboard, Copy, FolderOpen, FolderPlus, Image as ImageIcon, Layers3, Link2, Maximize2, Music2, PanelTop, Pencil, Plus, Redo2, Tags, Trash2, Type, Undo2, Upload, UserRound, Video } from "lucide-react"; import { aceternityMotion } from "@/lib/aceternity-motion"; import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface"; import { CanvasCreateCommandGrid, type CanvasCreateCommand } from "@/components/canvas/canvas-create-command-grid"; import { canvasThemes } from "@/lib/canvas-theme"; import { canvasNodeAssetCategory } from "@/lib/canvas/canvas-node-asset"; import { useThemeStore } from "@/stores/use-theme-store"; import { CanvasNodeType, type CanvasNodeData, type CanvasWorkspaceMode, type ContextMenuState, type Position } from "@/types/canvas"; type CanvasAssetCategory = NonNullable["assetCategory"]>; const assetCategoryOptions: Array<{ value: CanvasAssetCategory; label: string }> = [ { value: "character", label: "角色" }, { value: "environment", label: "场景" }, { value: "wardrobe", label: "服饰" }, { value: "prop", label: "道具" }, { value: "weapon", label: "武器" }, { value: "style", label: "画风" }, { value: "other", label: "其他" }, ]; type CanvasNodeContextMenuProps = { menu: ContextMenuState; node?: CanvasNodeData | null; workspaceMode?: CanvasWorkspaceMode; isProjectLinked?: boolean; canUndo: boolean; canRedo: boolean; canPaste: boolean; onClose: () => void; onAddNode: (type: CanvasNodeType) => void; onOpenDirector: (position: Position) => void; onUpload: () => void; onOpenAssets: () => void; onOpenProjectCharacters: () => void; onUndo: () => void; onRedo: () => void; onPaste: () => void; onCopyNode: () => void; onDuplicate: () => void; onDelete: () => void; onSaveAsset: () => void; onViewMedia: () => void; onEditText: () => void; onOpenDrawing: () => void; onGenerateImage: () => void; onCopyContent: () => void; onCopyMediaUrl: () => void; onSetAssetCategory: (category: CanvasAssetCategory) => void; onToggleFrame: () => void; }; export function CanvasNodeContextMenu({ menu, node, workspaceMode = "professional", isProjectLinked = false, canUndo, canRedo, canPaste, onClose, onAddNode, onOpenDirector, onUpload, onOpenAssets, onOpenProjectCharacters, onUndo, onRedo, onPaste, onCopyNode, onDuplicate, onDelete, onSaveAsset, onViewMedia, onEditText, onOpenDrawing, onGenerateImage, onCopyContent, onCopyMediaUrl, onSetAssetCategory, onToggleFrame, }: CanvasNodeContextMenuProps) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const reducedMotion = useReducedMotion(); const [addOpen, setAddOpen] = useState(false); const [categoryOpen, setCategoryOpen] = useState(false); useEffect(() => { const close = (event: PointerEvent) => { const target = event.target; if (target instanceof Element && target.closest(".ant-popover")) return; onClose(); }; const closeOnEscape = (event: KeyboardEvent) => { if (event.key !== "Escape") return; if (categoryOpen) setCategoryOpen(false); else onClose(); }; window.addEventListener("pointerdown", close); window.addEventListener("keydown", closeOnEscape); return () => { window.removeEventListener("pointerdown", close); window.removeEventListener("keydown", closeOnEscape); }; }, [categoryOpen, onClose]); useEffect(() => { setAddOpen(false); setCategoryOpen(false); }, [menu.type, menu.x, menu.y]); const runAction = (action: () => void) => { action(); onClose(); }; const nodeContent = typeof node?.metadata?.content === "string" ? node.metadata.content : ""; const isImage = node?.type === CanvasNodeType.Image; const isText = node?.type === CanvasNodeType.Text; const isCharacterReference = Boolean(isText && node?.metadata?.workflowKind === "character" && node.metadata.characterAssetId); const isDrawing = node?.type === CanvasNodeType.Drawing; const isVideo = node?.type === CanvasNodeType.Video; const isMedia = isImage || isVideo; const isAudio = node?.type === CanvasNodeType.Audio; const isFrame = node?.type === CanvasNodeType.Frame; const hasNodeContent = isText ? Boolean(nodeContent.trim()) : Boolean(nodeContent); const canSaveAsset = Boolean(node && !isCharacterReference && (isText ? hasNodeContent : hasNodeContent && (isImage || isVideo || isAudio))); const canOpenPreview = Boolean(isMedia && hasNodeContent); const canGenerateFromText = Boolean(isText && !isCharacterReference && hasNodeContent); const canCopyMediaUrl = Boolean(isMedia && hasNodeContent); const assetCategory = node ? canvasNodeAssetCategory(node) : "other"; const position = getContextMenuPosition(menu); return ( <> event.preventDefault()} onPointerDown={(event) => event.stopPropagation()} >
{menu.type === "node" && isMedia && categoryOpen ? ( <> setCategoryOpen(false)} /> {assetCategoryOptions.map((option) => ( : } label={option.label} active={assetCategory === option.value} onClick={() => runAction(() => onSetAssetCategory(option.value))} /> ))} ) : menu.type === "canvas" ? ( <> } label="添加节点" chevron active={addOpen} onClick={() => setAddOpen((value) => !value)} /> } label="上传到这里" onClick={() => runAction(onUpload)} /> {!isProjectLinked ? } label="从素材库插入" onClick={() => runAction(onOpenAssets)} /> : null} } label="撤销" shortcut="⌘Z" disabled={!canUndo} onClick={() => runAction(onUndo)} /> } label="重做" shortcut="⇧⌘Z" disabled={!canRedo} onClick={() => runAction(onRedo)} /> } label="粘贴" shortcut="⌘V" disabled={!canPaste} onClick={() => runAction(onPaste)} /> ) : menu.type === "node" ? ( <> {isCharacterReference ? ( <> } label="查看角色详情" onClick={() => runAction(onEditText)} /> } label="复制角色引用" shortcut="⌘C" onClick={() => runAction(onCopyNode)} /> } label="创建引用副本" shortcut="⌘D" onClick={() => runAction(onDuplicate)} /> } label="删除节点" danger onClick={() => runAction(onDelete)} /> ) : isMedia ? ( <> } label="进入全景预览" disabled={!canOpenPreview} onClick={() => runAction(onViewMedia)} /> } label="设置资产分类" chevron onClick={() => setCategoryOpen(true)} /> } label="复制节点" shortcut="⌘C" onClick={() => runAction(onCopyNode)} /> } label={isImage ? "复制图片地址" : "复制视频地址"} disabled={!canCopyMediaUrl} onClick={() => runAction(onCopyMediaUrl)} /> } label="创建参数变体" shortcut="⌘D" onClick={() => runAction(onDuplicate)} /> } label="删除节点" danger onClick={() => runAction(onDelete)} /> ) : ( <> {isFrame ? } label={node?.metadata?.frame?.collapsed ? "展开背板" : "折叠背板"} onClick={() => runAction(onToggleFrame)} /> : } label="保存到我的素材" disabled={!canSaveAsset} onClick={() => runAction(onSaveAsset)} />} {isText ? } label="放大编辑" onClick={() => runAction(onEditText)} /> : null} {isDrawing ? } label="打开绘图" onClick={() => runAction(onOpenDrawing)} /> : null} {isText ? } label="用文本生图" disabled={!canGenerateFromText} onClick={() => runAction(onGenerateImage)} /> : null} } label={isFrame ? "复制背板及内容" : "复制节点"} shortcut="⌘C" onClick={() => runAction(onCopyNode)} /> {isText ? } label="复制文本" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null} } label={isFrame ? "创建背板副本" : "创建参数变体"} shortcut="⌘D" onClick={() => runAction(onDuplicate)} /> } label="粘贴" shortcut="⌘V" disabled={!canPaste} onClick={() => runAction(onPaste)} /> } label={isFrame ? "删除背板" : "删除节点"} danger onClick={() => runAction(onDelete)} /> )} ) : ( <> } label="删除连接" danger onClick={() => runAction(onDelete)} /> )}
{menu.type === "canvas" && addOpen ? ( runAction(() => onAddNode(type))} onOpenDirector={() => runAction(() => onOpenDirector(menu.position))} onUpload={() => runAction(onUpload)} onOpenAssets={() => runAction(onOpenAssets)} onOpenProjectCharacters={() => runAction(onOpenProjectCharacters)} /> ) : null} ); } function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, reducedMotion, onAddNode, onOpenDirector, onUpload, onOpenAssets, onOpenProjectCharacters }: { parentPosition: { left: number; top: number }; workspaceMode: CanvasWorkspaceMode; isProjectLinked: boolean; reducedMotion: boolean; onAddNode: (type: CanvasNodeType) => void; onOpenDirector: () => void; onUpload: () => void; onOpenAssets: () => void; onOpenProjectCharacters: () => void }) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const left = getSubmenuLeft(parentPosition.left); const simpleMode = workspaceMode === "simple"; const nodeCommands: CanvasCreateCommand[] = [ { id: "text", label: "文本", icon: , onClick: () => onAddNode(CanvasNodeType.Text) }, { id: "drawing", label: "绘图", icon: , onClick: () => onAddNode(CanvasNodeType.Drawing) }, { id: "script", label: "分镜脚本", icon: , badge: "核心", onClick: () => onAddNode(CanvasNodeType.Script) }, ...(!simpleMode ? [{ id: "frame", label: "背板", icon: , onClick: () => onAddNode(CanvasNodeType.Frame) }] : []), { id: "image", label: "图片", icon: , onClick: () => onAddNode(CanvasNodeType.Image) }, { id: "video", label: "视频", icon: