From d348be844b27d45ee1fe4d8c93c5c3954e5dd57f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 18 Apr 2026 16:38:21 +0800 Subject: [PATCH] Add LibTV composer trigger interactions --- .../canvas/LiblibVideoCanvasReplica.tsx | 252 ++++++++++++++++-- 1 file changed, 223 insertions(+), 29 deletions(-) diff --git a/components/canvas/LiblibVideoCanvasReplica.tsx b/components/canvas/LiblibVideoCanvasReplica.tsx index 51e9685ce..99d21ddf4 100644 --- a/components/canvas/LiblibVideoCanvasReplica.tsx +++ b/components/canvas/LiblibVideoCanvasReplica.tsx @@ -29,6 +29,7 @@ import { } from "lucide-react"; import { useRouter } from "next/navigation"; import { + type ChangeEvent as ReactChangeEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode, @@ -73,6 +74,8 @@ type ComposerMenu = | "translate" | "settings" | "count" + | "reference" + | "slash" | null; type MemberStoreTab = "vip" | "model" | "credits"; @@ -167,6 +170,15 @@ type PresetControlAnchor = { topY: number; }; +type ComposerSettings = { + camera: string; + count: string; + model: string; + panorama: string; + ratio: string; + resolution: string; +}; + type CanvasCreateKind = | "image-input" | "video-input" @@ -346,6 +358,14 @@ const CREDIT_PACKS = [ const STORY_SCRIPT_PROMPT = "根据我上传的剧本生成一个完整的故事脚本"; const AUDIO_TO_VIDEO_PROMPT = "根据上传的音频生成对应场景画面,镜头语言、节奏、音乐匹配情绪变化,电影级质感。"; +const SLASH_IMAGE_ITEMS = [ + "剧情推演四宫格", + "25宫格连贯分镜", + "电影级光影校正", + "角色三视图生成", + "画面推演-3秒后", + "画面推演-5秒前", +]; const STORY_SCRIPT_TEXT = `《我在盛唐写天下》 类型:古风 / 穿越 / 爽文漫剧 @@ -1218,6 +1238,8 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib if (event.key === "Escape") { setCanvasCommand(null); setNodeContextMenu(null); + setSelectedNodeId(null); + setActivePanel(null); } }; window.addEventListener("keydown", onKeyDown); @@ -1276,7 +1298,10 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib const onCanvasWheel = useCallback( (event: ReactWheelEvent) => { - if ((event.target as HTMLElement).closest("[data-canvas-overlay]")) return; + const target = event.target as HTMLElement; + const overlay = target.closest("[data-canvas-overlay]"); + const canvasNode = target.closest("[data-canvas-node]"); + if (overlay && !canvasNode) return; event.preventDefault(); if (event.ctrlKey || event.metaKey) { zoomAtPoint(event.deltaY > 0 ? -0.08 : 0.08, event.clientX, event.clientY); @@ -1418,7 +1443,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib ) : null} {selectedPresetMode && selectedPresetAnchor ? ( - + ) : null} ; + nodes: CloneNode[]; }) { if (mode === "character-three-view") { return (
- +
); } @@ -2650,9 +2677,66 @@ function CharacterImagePanel({ imageUrl, label, wide = false }: { imageUrl: stri ); } -function PromptComposer({ anchor }: { anchor?: PresetControlAnchor }) { +function PromptComposer({ + anchor, + nodes = [], +}: { + anchor?: PresetControlAnchor; + nodes?: CloneNode[]; +}) { const [menu, setMenu] = useState(null); const [prompt, setPrompt] = useState(""); + const [references, setReferences] = useState([]); + const [slashScene, setSlashScene] = useState(null); + const [settings, setSettings] = useState({ + camera: "摄像机", + count: "1张", + model: "Lib Nano Pro", + panorama: "全景", + ratio: "16:9", + resolution: "2K", + }); + const referenceNodes = useMemo( + () => nodes.filter((node) => node.kind !== "script" && node.inputType !== "audio"), + [nodes] + ); + + const insertReference = useCallback((node: CloneNode) => { + const label = node.title || "节点"; + setPrompt((current) => `${current.replace(/@$/, "").trimEnd()} @${label} `); + setReferences((current) => (current.includes(node.id) ? current : [...current, node.id])); + setMenu(null); + }, []); + + const insertSlashScene = useCallback((scene: string) => { + setSlashScene(scene); + setPrompt((current) => `${current.replace(/\/$/, "").trimEnd()} /${scene} `); + setMenu(null); + }, []); + + const applyComposerOption = useCallback((activeMenu: Exclude, option: string) => { + if (activeMenu === "model" || activeMenu === "ratio" || activeMenu === "resolution" || activeMenu === "camera" || activeMenu === "panorama" || activeMenu === "count") { + setSettings((current) => ({ ...current, [activeMenu]: option })); + } else if (activeMenu === "style" || activeMenu === "mark" || activeMenu === "focus" || activeMenu === "translate" || activeMenu === "settings") { + setPrompt((current) => `${current.trimEnd()} ${option}`.trimStart()); + } + setMenu(null); + toast.info(`${option} 已应用,生成接口待接入。`); + }, []); + + const onPromptChange = useCallback((event: ReactChangeEvent) => { + const value = event.target.value; + setPrompt(value); + const beforeCursor = value.slice(0, event.target.selectionStart ?? value.length); + if (beforeCursor.endsWith("@")) { + setMenu("reference"); + return; + } + if (beforeCursor.endsWith("/")) { + setMenu("slash"); + return; + } + }, []); return (
1
- {menu ? : null} + {references.length > 0 || slashScene ? ( +
+ {slashScene ? ( + + ) : null} + {references.map((nodeId) => { + const node = nodes.find((item) => item.id === nodeId); + if (!node) return null; + const previewUrl = getDraftString(node, "mediaUrl") || getDraftString(node, "thumbnailUrl") || getDraftString(node, "resultImage"); + return ( + + ); + })} +
+ ) : null} + {menu === "reference" ? ( + + ) : menu === "slash" ? ( + + ) : menu === "focus" ? ( + applyComposerOption("focus", value)} /> + ) : menu ? ( + applyComposerOption(menu, option)} /> + ) : null}