From d2199975a55a8f261f7fcd34183da08f63ffe344 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 18 Apr 2026 17:27:19 +0800 Subject: [PATCH] Match LibTV added module node cards --- .../canvas/LiblibVideoCanvasReplica.tsx | 206 +++++++++++++++++- 1 file changed, 195 insertions(+), 11 deletions(-) diff --git a/components/canvas/LiblibVideoCanvasReplica.tsx b/components/canvas/LiblibVideoCanvasReplica.tsx index 0d1987f16..91f7cdd8a 100644 --- a/components/canvas/LiblibVideoCanvasReplica.tsx +++ b/components/canvas/LiblibVideoCanvasReplica.tsx @@ -19,6 +19,7 @@ import { Scissors, Settings2, Share2, + Sparkles, Trash2, Upload, UserRound, @@ -216,6 +217,15 @@ const ADD_NODE_MODULES: Array<{ { kind: "script", label: "脚本", description: "创意脚本、生成故事板", icon: Layers, badge: "Beta" }, ]; +const TARGET_MODULE_CENTER_OFFSETS: Record = { + audio: { x: 705, y: -257 }, + image: { x: 53, y: -257 }, + script: { x: 556, y: 253 }, + text: { x: -742, y: -257 }, + video: { x: -574, y: 253 }, + "video-clip": { x: 80, y: 253 }, +}; + const STARTER_CARDS = [ { title: "故事脚本生成", @@ -655,13 +665,13 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index return { id: makeId("script"), kind: "script", - title: `脚本 ${index}`, + title: "脚本生成器", subtitle: "脚本生成", inputType: "text", x, y, - width: 520, - height: 360, + width: 438, + height: 470, accent: "#f0d06b", draftValues: { action: "SCRIPT_GENERATE", @@ -683,8 +693,8 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index inputType: "video", x, y, - width: 210, - height: 116, + width: 436, + height: 470, accent: "#ffb8c8", draftValues: { action: "VIDEO_CLIP_RESOURCE", @@ -737,6 +747,7 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index }, }; const item = meta[kind]; + const isWideModule = item.nodeType === "image" || item.nodeType === "video"; return { id: makeId(kind), kind: item.kind, @@ -745,8 +756,8 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index inputType: item.nodeType, x, y, - width: item.nodeType === "text" ? 210 : 194, - height: item.nodeType === "text" ? 122 : 116, + width: isWideModule ? 773 : 436, + height: 470, accent: item.accent, draftValues: { action: item.action, @@ -814,6 +825,22 @@ function getPresetMode(node: CloneNode | null | undefined): ActiveStarterScene { return null; } +function getTargetModuleKind(node: CloneNode): AddModuleKind | null { + if (node.kind === "media" || getPresetMode(node)) return null; + const nodeType = getDraftString(node, "nodeType"); + if ( + nodeType === "text" || + nodeType === "image" || + nodeType === "video" || + nodeType === "video-clip" || + nodeType === "audio" || + nodeType === "script" + ) { + return nodeType; + } + return null; +} + function createPresetMediaNode({ action, generated = false, @@ -1156,7 +1183,13 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib const addTargetModule = useCallback( (kind: AddModuleKind, point?: { x: number; y: number }) => { - const center = point || toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2); + const viewportCenter = toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2); + const offset = TARGET_MODULE_CENTER_OFFSETS[kind]; + const sameKindCount = nodes.filter((node) => getTargetModuleKind(node) === kind).length; + const center = point || { + x: viewportCenter.x + offset.x + sameKindCount * 28, + y: viewportCenter.y + offset.y + sameKindCount * 28, + }; const created = createTargetModuleNode(kind, 0, 0, nodes.length + 1); const node = { ...created, @@ -3376,6 +3409,15 @@ function CloneCanvasNode({ onPointerDown: (event: ReactPointerEvent) => void; selected: boolean; }) { + const targetModuleKind = getTargetModuleKind(node); + if (targetModuleKind) { + return ( + + + + ); + } + if (node.kind === "media") { return ( @@ -3451,6 +3493,133 @@ function CloneCanvasNode({ ); } +function TargetModuleCanvasNode({ + kind, + node, + selected, +}: { + kind: AddModuleKind; + node: CloneNode; + selected: boolean; +}) { + const HeaderIcon = kind === "video-clip" ? Scissors : kind === "audio" ? Music2 : kind === "video" ? Video : kind === "image" ? ImageIcon : FileText; + const actions: Record> = { + audio: [{ icon: Play, label: "音频生视频" }], + image: [ + { icon: Upload, label: "图生图" }, + { icon: "hd", label: "图片高清" }, + ], + script: [ + { icon: FileText, label: "脚本生成分镜脚本" }, + { icon: Video, label: "视频参考生成分镜脚本" }, + { icon: UserRound, label: "角色生成分镜脚本" }, + ], + text: [ + { icon: FileText, label: "自己编写内容" }, + { icon: Play, label: "文生视频" }, + { icon: ImageIcon, label: "图片反推提示词" }, + { icon: Music2, label: "文字生音乐" }, + ], + video: [ + { icon: Layers, label: "首尾帧生成视频" }, + { icon: Sparkles, label: "首帧生成视频" }, + ], + "video-clip": [], + }; + const actionTop = + kind === "text" + ? "top-[166px]" + : kind === "image" || kind === "video" + ? "top-[220px]" + : kind === "script" + ? "top-[190px]" + : "top-[190px]"; + + return ( +
+
+ + {node.title} +
+
+
+ +
+ {kind === "video-clip" ? ( +
+ 空空如也,请连接多个视频节点后操作 +
+ ) : ( +
+
尝试:
+
+ {actions[kind].map((action) => ( + + ))} +
+
+ )} +
+
+ ); +} + +function TargetModuleHeroGraphic({ kind }: { kind: AddModuleKind }) { + if (kind === "text" || kind === "script") { + return ( +
+ {[70, 70, 70, 70, 43].map((width, index) => ( + + ))} +
+ ); + } + + if (kind === "image") { + return ; + } + + if (kind === "audio") { + return ; + } + + if (kind === "video") { + return ( + + + + ); + } + + return ; +} + +function ModuleActionIcon({ icon }: { icon: LucideIcon | "hd" }) { + if (icon === "hd") { + return ( + + HD + + ); + } + const Icon = icon; + return ; +} + function CanvasNodeShell({ children, node, @@ -3464,10 +3633,12 @@ function CanvasNodeShell({ onPointerDown: (event: ReactPointerEvent) => void; selected: boolean; }) { + const targetModuleKind = getTargetModuleKind(node); return (
- - + {targetModuleKind ? ( + <> + + + + + + + + + + ) : ( + <> + + + + )} {children}
);