From 3876a0c40367b3a02f464a2e2eaa4390d753bdc0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 5 Apr 2026 03:32:41 +0800 Subject: [PATCH] Add collapsible video workflow sidebar --- app/canvas/[id]/page.tsx | 262 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 261 insertions(+), 1 deletion(-) diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index e6a28fc7a..ca5411f19 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -7,6 +7,9 @@ import { AlertCircle, ArrowLeft, Bot, + ChevronDown, + ChevronLeft, + ChevronRight, Save, Undo2, Redo2, @@ -56,6 +59,11 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; @@ -243,6 +251,14 @@ interface VideoCanvasInsertMenuItem { accent?: "highlight"; } +interface VideoCanvasSidebarGroup { + id: string; + label: string; + description: string; + icon: LucideIcon; + nodeTypes: VideoWorkflowTemplateType[]; +} + const IMPORTABLE_CANVAS_NODE_TYPES = new Set([ "message", "image", @@ -355,6 +371,83 @@ function getVideoWorkflowQuickConnectIcon(type: VideoWorkflowTemplateType) { return VIDEO_WORKFLOW_QUICK_CONNECT_ICON_MAP[type] || Plus; } +const VIDEO_CANVAS_SIDEBAR_GROUPS: VideoCanvasSidebarGroup[] = [ + { + id: "input", + label: "输入层", + description: "文本、图片、视频、小说", + icon: FileImage, + nodeTypes: ["text-node", "input-image", "video-input", "novel-input"], + }, + { + id: "analysis", + label: "分析层", + description: "视频拆解、角色场景抽取", + icon: Scan, + nodeTypes: [ + "video-analyze", + "extract-characters-scenes", + "character-description", + "scene-description", + "create-character", + "create-scene", + ], + }, + { + id: "storyboard", + label: "分镜层", + description: "分镜节点、分镜图表、表格", + icon: GitBranch, + nodeTypes: [ + "storyboard-node", + "storyboard-chart-node", + "table-editor-node", + "camera-movement", + "professional-camera", + ], + }, + { + id: "generation", + label: "生成层", + description: "生图、生视频、音乐、超清", + icon: Sparkles, + nodeTypes: [ + "gen-image", + "gen-video", + "gen-music", + "jimeng-super-resolution", + "generate-character-image", + "generate-character-video", + "generate-scene-image", + "generate-scene-video", + "motion-control", + ], + }, + { + id: "tool", + label: "工具层", + description: "局部重绘、Comfy、Agent", + icon: Wand2, + nodeTypes: [ + "inpaint-crop", + "inpaint-stitch", + "rh-comfy", + "rh-app", + "custom-agent", + "doodle-canvas", + "canvas-node", + "image-compare", + ], + }, + { + id: "output", + label: "输出层", + description: "预览、保存", + icon: Save, + nodeTypes: ["preview", "local-save"], + }, +]; + function isPlainObject(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } @@ -862,6 +955,15 @@ export default function CanvasEditorPage({ params }: PageProps) { const [editTitle, setEditTitle] = useState(""); const [activeWorkflowExecutionTaskKey, setActiveWorkflowExecutionTaskKey] = useState(null); const [isPanelOpen, setIsPanelOpen] = useState(() => !isVideoMode); + const [isVideoModuleSidebarCollapsed, setIsVideoModuleSidebarCollapsed] = useState(false); + const [videoModuleSectionOpen, setVideoModuleSectionOpen] = useState>({ + input: true, + analysis: true, + storyboard: true, + generation: true, + tool: true, + output: true, + }); const [videoCanvasInsertMenu, setVideoCanvasInsertMenu] = useState(null); const [sceneFusionDialogOpen, setSceneFusionDialogOpen] = useState(false); @@ -890,6 +992,12 @@ export default function CanvasEditorPage({ params }: PageProps) { setVideoCanvasInsertMenu(null); } }, [isVideoMode]); + + useEffect(() => { + if (!isVideoMode) { + setIsVideoModuleSidebarCollapsed(false); + } + }, [isVideoMode]); const [appearanceRecognitionEnabled, setAppearanceRecognitionEnabled] = useState(true); const [selectedBrandModel, setSelectedBrandModel] = useState(null); @@ -1073,6 +1181,12 @@ export default function CanvasEditorPage({ params }: PageProps) { selectedWorkflowQuickConnectAnchor && selectedVideoWorkflowNode ? selectedVideoWorkflowNode : null; + const videoModuleSidebarWidth = isVideoMode + ? isVideoModuleSidebarCollapsed + ? 64 + : 296 + : 0; + const selectedWorkflowPanelLeft = isVideoMode ? videoModuleSidebarWidth + 28 : 16; const workflowGraph = useMemo( () => createWorkflowGraphContext(nodes, edges), [edges, nodes] @@ -7411,6 +7525,149 @@ export default function CanvasEditorPage({ params }: PageProps) { ) : null} + {isVideoMode ? ( +
+
+
+ {isVideoModuleSidebarCollapsed ? ( +
+ +
+ ) : ( +
+
视频模块栏
+
+ 按工作流层级添加模块 +
+
+ )} + +
+ + {!isVideoModuleSidebarCollapsed ? ( + +
+ {VIDEO_CANVAS_SIDEBAR_GROUPS.map((group) => { + const GroupIcon = group.icon; + return ( + + setVideoModuleSectionOpen((prev) => ({ + ...prev, + [group.id]: open, + })) + } + > +
+ +
+ + + + + + {group.label} + + + {group.description} + + +
+ +
+ +
+
+ {group.nodeTypes.map((type) => { + const definition = getVideoWorkflowNodeDefinition(type); + if (!definition) return null; + const Icon = getVideoWorkflowQuickConnectIcon(type); + return ( + + ); + })} +
+
+
+
+
+ ); + })} +
+
+ ) : ( +
+ {VIDEO_CANVAS_SIDEBAR_GROUPS.map((group) => { + const GroupIcon = group.icon; + return ( + + ); + })} +
+ )} +
+
+ ) : null} + {isVideoMode && selectedWorkflowQuickConnectAnchor && selectedWorkflowQuickConnectSourceNode && @@ -7502,7 +7759,10 @@ export default function CanvasEditorPage({ params }: PageProps) { {isVideoMode && selectedVideoWorkflowDefinition && selectedWorkflowIsActionable ? ( -
+
{selectedWorkflowIsActionable ? (