"use client"; import { Bell, ChevronDown, Clock3, Copy, Crown, FileText, GitBranch, Headphones, HelpCircle, Image as ImageIcon, Layers, Loader2, Music2, Plus, Save, Search, Share2, Sparkles, UserRound, Video, X, type LucideIcon, } from "lucide-react"; import { useRouter } from "next/navigation"; import { type PointerEvent as ReactPointerEvent, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { toast } from "sonner"; import capturedCanvasModules from "@/artifacts/liblib-frontend-capture-ed9a2c12570a49b9a1186017b2c38b52/canvas-modules.json"; import { cn } from "@/lib/utils"; import { canvasService } from "@/services/canvas"; type ActivePanel = | "add" | "workflow" | "assets" | "history" | "help" | "support" | "skills" | "share" | "publish" | "member" | "notifications" | "account" | null; type ActiveStarterScene = "story-script" | "character-three-view" | "first-frame" | "audio-to-video" | null; type SceneToolbarMenu = "panorama" | "orbit" | "lighting" | "grid" | "hd" | "crop" | null; type ComposerMenu = | "style" | "mark" | "focus" | "model" | "ratio" | "resolution" | "camera" | "panorama" | "translate" | "settings" | "count" | null; type CapturedTool = { type?: string; toolKey?: string; modelKey?: string; modelName?: string; vendor?: string; scene?: string; modes?: string[]; settings?: string[]; advancedSettings?: string[]; propertyKeys?: string[]; generateTypes?: unknown; rules?: unknown; metadata?: { properties?: Record; config?: Record; generateTypes?: unknown; rules?: unknown; [key: string]: unknown; }; }; type WorkflowPreset = { id?: string | number; uuid?: string; name?: string; title?: string; description?: string; cover?: string; }; type CloneNodeKind = "input" | "model" | "text" | "audio" | "workflow" | "preview" | "save"; type CloneNode = { id: string; kind: CloneNodeKind; title: string; subtitle?: string; x: number; y: number; width: number; height: number; accent: string; tool?: CapturedTool; workflow?: WorkflowPreset; inputType?: "image" | "video" | "audio" | "text"; draftValues?: Record; }; type CloneEdge = { id: string; source: string; target: string; }; type DragState = | { kind: "node"; id: string; startX: number; startY: number; nodeX: number; nodeY: number; } | { kind: "canvas"; startX: number; startY: number; viewportX: number; viewportY: number; }; type LiblibVideoCanvasReplicaProps = { canvasId: string; isLoading?: boolean; }; const capturedTools = ((capturedCanvasModules as any).tools || []) as CapturedTool[]; const capturedCounts = (capturedCanvasModules as any).counts || {}; const capturedWorkflows = ((capturedCanvasModules as any).workflows || []) as WorkflowPreset[]; const TOOL_TYPE_META: Record< string, { label: string; icon: LucideIcon; accent: string; inputType?: CloneNode["inputType"] } > = { image: { label: "图片", icon: ImageIcon, accent: "#8b96ff", inputType: "image" }, video: { label: "视频", icon: Video, accent: "#8b96ff", inputType: "video" }, text: { label: "文本", icon: FileText, accent: "#f0d06b", inputType: "text" }, audio: { label: "音频", icon: Music2, accent: "#9ee6bb", inputType: "audio" }, compose: { label: "组合", icon: GitBranch, accent: "#ffb8c8" }, }; const PANEL_TABS = ["全部", "图片", "视频", "文本", "音频", "组合"] as const; const STARTER_CARDS = [ { title: "故事脚本生成", subtitle: "脚本到分镜", coverUrl: "https://liblibai-online.liblib.cloud/web/ef0097ae9da74189a0f5d604e98c77cd/f9d23335-4ec2-412d-9954-91c58f4b5eb3.png", steps: ["故事脚本", "多模态文本模型Pro", "可灵 3.0", "预览"], }, { title: "角色三视图", subtitle: "角色资产沉淀", coverUrl: "https://liblibai-online.liblib.cloud/web/ef0097ae9da74189a0f5d604e98c77cd/e3731aeb-a95b-46e9-99e7-3cd7f22a1e3f.png", steps: ["角色描述", "角色三视图", "全能图片模型V2", "保存"], }, { title: "首帧图生视频", subtitle: "参考图到成片", coverUrl: "https://liblibai-online.liblib.cloud/web/ef0097ae9da74189a0f5d604e98c77cd/f407cb9c-8dd7-436b-8e80-88da1cebf25d.png", steps: ["上传图片", "首帧图", "可灵 3.0", "预览"], }, { title: "音频生视频", subtitle: "音频驱动画面", coverUrl: "https://liblibai-online.liblib.cloud/web/ef0097ae9da74189a0f5d604e98c77cd/bd88df14-04bd-473d-9016-ec72a5273bfb.png", icon: "music", steps: ["上传音频", "音频主视频", "Wan 2.6", "预览"], }, ]; const TOOLBAR_ITEMS: Array<{ id: Exclude; label: string; icon: LucideIcon }> = [ { id: "add", label: "添加模块", icon: Plus }, { id: "workflow", label: "工作流", icon: GitBranch }, { id: "assets", label: "素材", icon: Layers }, { id: "history", label: "历史", icon: Clock3 }, { id: "help", label: "帮助", icon: HelpCircle }, { id: "support", label: "客服", icon: Headphones }, ]; const DEFAULT_VIEWPORT = { x: 0, y: 0, zoom: 1 }; const TARGET_AVATAR = "https://liblibai-online.liblib.cloud/web/1656e2f1b3c74f1ba823b610f1b2cbdf/0c3eb5e2-1dc0-405c-a823-07b2570237f8.jpg?x-oss-process=image/resize,w_100,m_lfit/format,webp"; const CHARACTER_IMAGE_URL = "https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/cca9e085-dccc-4f72-aef7-d68dc6688bcf.png"; const CHARACTER_THREE_VIEW_URL = "https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/7a32ba0b-7b5b-46b1-a86c-e0904482adf7.png"; const FIRST_FRAME_IMAGE_URL = "https://liblibai-online.liblib.cloud/web/28aa80df5944499487381983a8f9bbdd/da241fab-3569-49aa-ae4e-c20de9f718e0.png"; const AUDIO_SOURCE_URL = "https://liblibai-online.liblib.cloud/web/28aa80df5944499487381983a8f9bbdd/3ca0886d-d35f-4048-95bf-cc655e9c57fd.mp3"; const AUDIO_REFERENCE_IMAGE_URL = "https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/1342d344-2d95-41a1-a2aa-4d20869a318c.png"; const STORY_SCRIPT_PROMPT = "根据我上传的剧本生成一个完整的故事脚本"; const AUDIO_TO_VIDEO_PROMPT = "根据上传的音频生成对应场景画面,镜头语言、节奏、音乐匹配情绪变化,电影级质感。"; const STORY_SCRIPT_TEXT = `《我在盛唐写天下》 类型:古风 / 穿越 / 爽文漫剧 时长建议:60-90秒 基调:热血 x 盛唐史诗感 x 爽点节奏 【序幕】 【现代 · 深夜办公室】 键盘声急促。电脑屏幕蓝光刺眼。沈昭昭(女,28岁),面色苍白,伏案加班。手机提示音不断。她抬头,视线模糊,黑屏。 【第一幕】 【盛唐 · 金銮殿 · 日】 鼓声震天。沈昭昭猛然睁眼,已跪在大殿中央,身着男装官服,双手被押。百官列立,殿宇巍峨。 大臣厉声:“大逆不道!假称诗才惊世,欺君罔上,当斩!” 沈昭昭震惊低语:“这是……唐朝?” 龙椅之上,皇帝目光沉沉。 皇帝:“既言才华盖世,当殿作诗。若不能,斩。” 刀光映入她瞳孔。大殿死寂。 【第二幕】 沈昭昭缓缓起身。呼吸渐稳。她望向殿外,长安城阳光万里。 她低声:“既来盛唐……便与盛唐争一争。” 抬头,高声吟诵:“君不见黄河之水天上来。” 声音回荡大殿。群臣震动。皇帝缓缓站起。诗声激昂,如惊雷落殿。 【第三幕】 殿外风起。长安街市灯火连绵。 皇帝低声:“此诗,当传万世。” 沈昭昭目光坚定。她轻声自语:“既然来了……那便写尽三万里长安。” 镜头推远。盛唐山河展开。`; function normalizeCanvasTitle(title?: string | null) { const value = String(title || "").trim(); if (!value || value === "未命名视频画布") return "未命名"; return value; } function getToolType(tool?: CapturedTool) { return String(tool?.type || "video").trim() || "video"; } function getToolLabel(tool?: CapturedTool) { return String(tool?.modelName || tool?.scene || tool?.modelKey || tool?.toolKey || "未命名模块").trim(); } function getToolModes(tool?: CapturedTool) { return (tool?.modes || []).map(String).filter(Boolean).slice(0, 4); } function getToolProperties(tool?: CapturedTool) { const properties = tool?.metadata?.properties; if (!properties || typeof properties !== "object") return []; return Object.entries(properties).filter(([key]) => key !== "modeType"); } function getToolRules(tool?: CapturedTool) { const rules = tool?.rules || tool?.metadata?.rules; return Array.isArray(rules) ? rules : []; } function requiresMedia(tool?: CapturedTool) { return getToolRules(tool).some((rule) => { if (!rule || typeof rule !== "object") return false; const required = (rule as { require?: unknown }).require; return Array.isArray(required) && required.map(String).includes("media"); }); } function getPropertyLabel(key: string, config: unknown) { if (config && typeof config === "object" && "displayName" in config) { const value = (config as { displayName?: unknown }).displayName; if (typeof value === "string" && value.trim()) return value; } const labels: Record = { count: "生成数量", duration: "时长", enableSound: "生成音效", frameRate: "帧率", instrumental: "纯音乐", lora: "风格", prompt: "提示词", quality: "清晰度", ratio: "比例", resolution: "分辨率", searchable: "联网搜索", search_enabled: "联网搜索", slowMotion: "慢动作", slowmo: "减速强度", specifiedModel: "模型", stability: "稳定性", style: "模型", template: "风格", voice: "声音", }; return labels[key] || key; } function getOptions(config: unknown) { if (Array.isArray(config)) return config; if (config && typeof config === "object" && Array.isArray((config as { enum?: unknown[] }).enum)) { return (config as { enum: unknown[] }).enum; } return []; } function getOptionValue(option: unknown) { if (option && typeof option === "object" && "value" in option) { return String((option as { value?: unknown }).value ?? ""); } return String(option ?? ""); } function getOptionLabel(option: unknown) { if (option && typeof option === "object" && "displayName" in option) { const label = (option as { displayName?: unknown }).displayName; if (label) return String(label); } return getOptionValue(option); } function getDefaultValue(key: string, config: unknown) { if (config && typeof config === "object" && "default" in config) { return String((config as { default?: unknown }).default ?? ""); } const options = getOptions(config); if (options.length > 0) return getOptionValue(options[0]); if (typeof config === "boolean") return config ? "1" : "0"; if (key === "prompt") return ""; return ""; } function makeId(prefix: string) { return `${prefix}-${crypto.randomUUID()}`; } function serializeNodes(nodes: CloneNode[]) { return nodes.map((node) => ({ id: node.id, type: "workflow", position: { x: node.x, y: node.y }, size: { width: node.width, height: node.height }, data: { title: node.title, description: node.subtitle || "", status: "pending", templateType: `libtv-${node.kind}`, metadata: { liblibCloneNode: node, }, }, connections: [], createdAt: new Date(), updatedAt: new Date(), })); } function serializeEdges(edges: CloneEdge[]) { return edges.map((edge) => ({ id: edge.id, source: edge.source, target: edge.target, type: "default", })); } function restoreNodes(canvasData: unknown): CloneNode[] { const nodes = (canvasData as { nodes?: unknown[] } | null)?.nodes; if (!Array.isArray(nodes)) return []; return nodes .map((item) => { const metadata = ((item as any)?.data?.metadata || {}) as Record; const clone = metadata.liblibCloneNode as CloneNode | undefined; return clone && typeof clone.id === "string" ? clone : null; }) .filter(Boolean) as CloneNode[]; } function restoreEdges(canvasData: unknown): CloneEdge[] { const edges = (canvasData as { edges?: unknown[] } | null)?.edges; if (!Array.isArray(edges)) return []; return edges .map((edge) => ({ id: String((edge as any).id || makeId("edge")), source: String((edge as any).source || ""), target: String((edge as any).target || ""), })) .filter((edge) => edge.source && edge.target); } function createInputNode(inputType: CloneNode["inputType"], x: number, y: number): CloneNode { const title = inputType === "video" ? "上传视频" : inputType === "audio" ? "上传音频" : inputType === "text" ? "输入文本" : "上传图片"; return { id: makeId("input"), kind: "input", title, subtitle: inputType === "video" ? "添加视频素材、参考视频或首尾帧" : inputType === "audio" ? "添加音乐、语音或音效参考" : inputType === "text" ? "输入脚本、分镜或提示词" : "添加参考图、角色图或关键帧素材", inputType, x, y, width: 158, height: 158, accent: "#8790ff", }; } function createToolNode(tool: CapturedTool, x: number, y: number): CloneNode { const type = getToolType(tool); const meta = TOOL_TYPE_META[type] || TOOL_TYPE_META.video; const isAudio = type === "audio"; const isText = type === "text"; return { id: makeId("tool"), kind: isAudio ? "audio" : isText ? "text" : "model", title: getToolLabel(tool), subtitle: tool.vendor || meta.label, x, y, width: isText ? 210 : 194, height: isText ? 122 : 116, accent: meta.accent, tool, draftValues: {}, }; } function createWorkflowNode(workflow: WorkflowPreset, x: number, y: number): CloneNode { return { id: makeId("workflow"), kind: "workflow", title: String(workflow.name || workflow.title || "工作流预设"), subtitle: workflow.description || "LibTV workflow", x, y, width: 230, height: 116, accent: "#8b96ff", workflow, draftValues: {}, }; } function createSimpleNode(title: string, kind: CloneNodeKind, x: number, y: number): CloneNode { return { id: makeId(kind), kind, title, subtitle: kind === "preview" ? "预览结果" : kind === "save" ? "保存到本地" : "输入内容", x, y, width: kind === "text" ? 210 : 194, height: kind === "text" ? 122 : 116, accent: kind === "save" || kind === "preview" ? "#8b96ff" : "#f0d06b", inputType: kind === "text" ? "text" : undefined, draftValues: {}, }; } export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: LiblibVideoCanvasReplicaProps) { const router = useRouter(); const canvasRef = useRef(null); const dragRef = useRef(null); const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); const [selectedNodeId, setSelectedNodeId] = useState(null); const [viewport, setViewport] = useState(DEFAULT_VIEWPORT); const [projectTitle, setProjectTitle] = useState("未命名"); const [draftTitle, setDraftTitle] = useState("未命名"); const [isTitleEditing, setIsTitleEditing] = useState(false); const [activePanel, setActivePanel] = useState(null); const [activeStarterScene, setActiveStarterScene] = useState(null); const [toolTab, setToolTab] = useState<(typeof PANEL_TABS)[number]>("全部"); const [toolSearch, setToolSearch] = useState(""); const [isProjectLoading, setIsProjectLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [shareEnabled, setShareEnabled] = useState(false); const [publishPublic, setPublishPublic] = useState(false); const selectedNode = useMemo( () => nodes.find((node) => node.id === selectedNodeId) || null, [nodes, selectedNodeId] ); const filteredTools = useMemo(() => { const search = toolSearch.trim().toLowerCase(); return capturedTools.filter((tool) => { const type = getToolType(tool); const tabMatched = toolTab === "全部" || (toolTab === "图片" && type === "image") || (toolTab === "视频" && type === "video") || (toolTab === "文本" && type === "text") || (toolTab === "音频" && type === "audio") || (toolTab === "组合" && type === "compose"); if (!tabMatched) return false; if (!search) return true; return [tool.modelName, tool.vendor, tool.scene, tool.modelKey, tool.toolKey, ...(tool.modes || [])] .filter(Boolean) .join(" ") .toLowerCase() .includes(search); }); }, [toolSearch, toolTab]); useEffect(() => { let active = true; async function loadProject() { setIsProjectLoading(true); const result = await canvasService.getProject(canvasId); if (!active) return; if (result.success && result.data) { const title = normalizeCanvasTitle(result.data.title); setProjectTitle(title); setDraftTitle(title); const restoredNodes = restoreNodes(result.data.canvasData); const restoredEdges = restoreEdges(result.data.canvasData); setNodes(restoredNodes); setEdges(restoredEdges); } else { setProjectTitle("未命名"); setDraftTitle("未命名"); await canvasService.saveCanvasState( canvasId, { nodes: [], edges: [], selectedNodeIds: [], viewport: DEFAULT_VIEWPORT, tool: "select", }, "video" ); } setIsProjectLoading(false); } void loadProject(); return () => { active = false; }; }, [canvasId]); const saveCanvas = useCallback(async () => { setIsSaving(true); const result = await canvasService.saveCanvasState( canvasId, { nodes: serializeNodes(nodes) as any, edges: serializeEdges(edges) as any, selectedNodeIds: selectedNodeId ? [selectedNodeId] : [], viewport, tool: "select", }, "video" ); if (result.success) { toast.success("已保存"); } else { toast.error(result.error || "保存失败"); } setIsSaving(false); }, [canvasId, edges, nodes, selectedNodeId, viewport]); const commitTitle = useCallback(async () => { const nextTitle = draftTitle.trim() || "未命名"; setProjectTitle(nextTitle); setDraftTitle(nextTitle); setIsTitleEditing(false); await canvasService.updateProject(canvasId, { title: nextTitle }, "video"); }, [canvasId, draftTitle]); const addNodes = useCallback((nextNodes: CloneNode[], nextEdges: CloneEdge[]) => { setNodes((current) => [...current, ...nextNodes]); setEdges((current) => [...current, ...nextEdges]); const last = nextNodes[nextNodes.length - 1]; if (last) setSelectedNodeId(last.id); }, []); const addToolFlow = useCallback( (tool: CapturedTool) => { const type = getToolType(tool); const meta = TOOL_TYPE_META[type] || TOOL_TYPE_META.video; const origin = { x: 420 + nodes.length * 18, y: 330 + nodes.length * 10 }; const newNodes: CloneNode[] = []; const newEdges: CloneEdge[] = []; if (meta.inputType && type !== "text") { const input = createInputNode(meta.inputType, origin.x, origin.y); const model = createToolNode(tool, origin.x + 300, origin.y + 18); newNodes.push(input, model); newEdges.push({ id: makeId("edge"), source: input.id, target: model.id }); } else { newNodes.push(createToolNode(tool, origin.x + 120, origin.y + 12)); } addNodes(newNodes, newEdges); setActivePanel(null); toast.success(`已添加 ${getToolLabel(tool)}`); }, [addNodes, nodes.length] ); const addWorkflowPreset = useCallback( (workflow: WorkflowPreset, index = 0) => { const node = createWorkflowNode(workflow, 440 + index * 24, 350 + index * 18); addNodes([node], []); setActivePanel(null); toast.success("已添加工作流"); }, [addNodes] ); const createStarter = useCallback( (starter: (typeof STARTER_CARDS)[number]) => { const starterScenes: Record> = { 故事脚本生成: "story-script", 角色三视图: "character-three-view", 首帧图生视频: "first-frame", 音频生视频: "audio-to-video", }; const starterScene = starterScenes[starter.title]; if (starterScene) { setNodes([]); setEdges([]); setSelectedNodeId(null); setActivePanel(null); setActiveStarterScene(starterScene); return; } setActiveStarterScene(null); const baseX = 380; const baseY = 325; const newNodes = starter.steps.map((title, index) => { if (/上传图片|首帧图/.test(title)) return createInputNode("image", baseX + index * 270, baseY); if (/上传音频|音频/.test(title) && index === 0) return createInputNode("audio", baseX + index * 270, baseY); if (/脚本|描述/.test(title)) return createSimpleNode(title, "text", baseX + index * 270, baseY + 18); if (/预览/.test(title)) return createSimpleNode(title, "preview", baseX + index * 270, baseY + 18); if (/保存/.test(title)) return createSimpleNode(title, "save", baseX + index * 270, baseY + 18); const matchedTool = capturedTools.find((tool) => getToolLabel(tool).includes(title) || title.includes(getToolLabel(tool))); return matchedTool ? createToolNode(matchedTool, baseX + index * 270, baseY + 18) : createSimpleNode(title, "workflow", baseX + index * 270, baseY + 18); }); const newEdges = newNodes.slice(1).map((node, index) => ({ id: makeId("edge"), source: newNodes[index].id, target: node.id, })); addNodes(newNodes, newEdges); }, [addNodes] ); const updateNodeDraft = useCallback((key: string, value: unknown) => { setNodes((current) => current.map((node) => node.id === selectedNodeId ? { ...node, draftValues: { ...(node.draftValues || {}), [key]: value } } : node ) ); }, [selectedNodeId]); const startNodeDrag = useCallback((event: ReactPointerEvent, node: CloneNode) => { event.stopPropagation(); event.currentTarget.setPointerCapture(event.pointerId); setSelectedNodeId(node.id); dragRef.current = { kind: "node", id: node.id, startX: event.clientX, startY: event.clientY, nodeX: node.x, nodeY: node.y, }; }, []); const startCanvasDrag = useCallback((event: ReactPointerEvent) => { if ((event.target as HTMLElement).closest("[data-canvas-overlay]")) return; setSelectedNodeId(null); dragRef.current = { kind: "canvas", startX: event.clientX, startY: event.clientY, viewportX: viewport.x, viewportY: viewport.y, }; }, [viewport.x, viewport.y]); const onPointerMove = useCallback( (event: ReactPointerEvent) => { const drag = dragRef.current; if (!drag) return; if (drag.kind === "node") { const dx = (event.clientX - drag.startX) / viewport.zoom; const dy = (event.clientY - drag.startY) / viewport.zoom; setNodes((current) => current.map((node) => node.id === drag.id ? { ...node, x: Math.round(drag.nodeX + dx), y: Math.round(drag.nodeY + dy) } : node ) ); return; } setViewport((current) => ({ ...current, x: Math.round(drag.viewportX + event.clientX - drag.startX), y: Math.round(drag.viewportY + event.clientY - drag.startY), })); }, [viewport.zoom] ); const onPointerUp = useCallback(() => { dragRef.current = null; }, []); const zoomBy = useCallback((delta: number) => { setViewport((current) => ({ ...current, zoom: Math.min(2.4, Math.max(0.3, Number((current.zoom + delta).toFixed(2)))), })); }, []); const copyShareLink = useCallback(async () => { const url = typeof window === "undefined" ? `/canvas/${canvasId}?mode=video&workspaceType=video` : window.location.href; await navigator.clipboard?.writeText(url).catch(() => undefined); setShareEnabled(true); toast.success("分享链接已复制"); }, [canvasId]); const isBusy = isLoading || isProjectLoading; return (
{nodes.map((node) => ( startNodeDrag(event, node)} /> ))}
{activeStarterScene === "story-script" ? : null} {activeStarterScene === "character-three-view" ? : null} {activeStarterScene === "first-frame" ? : null} {activeStarterScene === "audio-to-video" ? : null} {nodes.length === 0 && !activePanel && !activeStarterScene ? (
双击画布 自由生成节点
{STARTER_CARDS.map((card) => { const cover = card.coverUrl || capturedWorkflows[0]?.cover || ""; return ( ); })}
) : null} {!activeStarterScene ? ( <> setActivePanel("account")} onBell={() => setActivePanel("notifications")} onCommitTitle={commitTitle} onDraftTitleChange={setDraftTitle} onMember={() => setActivePanel("member")} onSave={saveCanvas} onShare={() => setActivePanel("share")} onSkills={() => setActivePanel("skills")} onStartTitleEdit={() => setIsTitleEditing(true)} projectTitle={projectTitle} routerBack={() => router.push("/")} /> setActivePanel((current) => current === panel ? null : panel)} /> ) : null} setViewport(DEFAULT_VIEWPORT)} onZoomIn={() => zoomBy(0.1)} onZoomOut={() => zoomBy(-0.1)} /> {activePanel === "add" ? ( setActivePanel(null)} onSearchChange={setToolSearch} onTabChange={setToolTab} /> ) : null} {activePanel === "workflow" ? ( setActivePanel(null)} /> ) : null} {selectedNode ? ( setSelectedNodeId(null)} /> ) : null} {activePanel === "assets" ? ( setActivePanel(null)} title="素材库"> ) : null} {activePanel === "history" ? ( setActivePanel(null)} title="历史记录">
{nodes.length === 0 ? ( ) : ( nodes.slice(-10).reverse().map((node) => ( )) )}
) : null} {activePanel === "help" ? ( setActivePanel(null)} title="快捷键">
{[ ["V", "选择"], ["Space", "拖动画布"], ["双击画布", "自由生成节点"], ["连接点拖拽", "连接模块"], ["⌘ / Ctrl + S", "保存画布"], ].map(([key, value]) => (
{value} {key}
))}
) : null} {activePanel === "support" ? ( setActivePanel(null)} title="支持中心"> ) : null} {activePanel === "skills" ? ( setActivePanel(null)} title="LibTV Skills"> ) : null} {activePanel === "share" ? ( setActivePanel(null)} title="分享">
分享链接
复制当前画布链接,权限控制待接入。
setShareEnabled((value) => !value)} />
) : null} {activePanel === "publish" ? ( setActivePanel(null)} onTogglePublic={() => setPublishPublic((value) => !value)} /> ) : null} {activePanel === "member" ? ( setActivePanel(null)} title="会员中心"> ) : null} {activePanel === "notifications" ? ( setActivePanel(null)} title="通知"> ) : null} {activePanel === "account" ? ( setActivePanel(null)} title="账号"> ) : null} {isBusy ? (
项目加载中...
) : null}
); } function TopBar({ draftTitle, isSaving, isTitleEditing, onAccount, onBell, onCommitTitle, onDraftTitleChange, onMember, onSave, onShare, onSkills, onStartTitleEdit, projectTitle, routerBack, }: { draftTitle: string; isSaving: boolean; isTitleEditing: boolean; onAccount: () => void; onBell: () => void; onCommitTitle: () => void; onDraftTitleChange: (value: string) => void; onMember: () => void; onSave: () => void; onShare: () => void; onSkills: () => void; onStartTitleEdit: () => void; projectTitle: string; routerBack: () => void; }) { return (
{isTitleEditing ? ( onDraftTitleChange(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") onCommitTitle(); }} value={draftTitle} /> ) : ( )}
LibTV Skills {isSaving ? : } 会员超市
); } function TopButton({ children, className, onClick, title, }: { children: React.ReactNode; className?: string; onClick: () => void; title?: string; }) { return ( ); } function LeftToolbar({ activePanel, onOpen, }: { activePanel: ActivePanel; onOpen: (panel: Exclude) => void; }) { return ( ); } function ZoomDock({ compact = false, onCenter, onZoomIn, onZoomOut, zoom, }: { compact?: boolean; onCenter: () => void; onZoomIn: () => void; onZoomOut: () => void; zoom: number; }) { if (compact) { return (
); } return (
); } function LibTVLogo() { return ( ); } function PointerBadge() { return ( ); } function ArrowGlyph() { return ( ); } function MusicStarterIcon() { return ( ); } function StoreGlyph() { return ( ); } function DiamondGlyph() { return ( ); } function LightningGlyph() { return ( ); } function MemberDealPill({ onAccount, onMember }: { onAccount: () => void; onMember: () => void }) { return (
); } function AssetGlyph() { return ( ); } function WorkflowGlyph() { return ( ); } function GridTilesGlyph() { return ( ); } function MapPinGlyph() { return ( ); } function MagnetGlyph() { return ( ); } function StoryScriptScene() { return (
); } function FirstFrameVideoScene() { return (
); } function AudioToVideoScene() { return (
); } function PresetFloatingToolbar({ items }: { items: Array<[string, string]> }) { return (
{items.map(([label, message]) => ( ))}
); } function SceneConnector({ className }: { className?: string }) { return (
); } function StoryTextPanel() { return (
剧本
输入内容
{STORY_SCRIPT_TEXT}
); } function ScriptGeneratorPanel() { const rows = [ ["01", "现代办公室", "深夜加班,键盘声急促,人物体力透支", "环境键盘声"], ["02", "盛唐大殿", "女主惊醒跪于金銮殿,百官压迫", "鼓声与低频"], ["03", "长安街市", "诗声落下,盛唐山河展开", "史诗音乐起"], ]; return (
脚本生成器
故事脚本 待生成
{["镜号", "场景", "画面", "声音"].map((item) => (
{item}
))}
{rows.map((row) => (
{row.map((cell) => (
{cell}
))}
))}
); } function PresetMediaPanel({ imageUrl, label, small = false, wide = false, }: { imageUrl: string; label: string; small?: boolean; wide?: boolean; }) { return (
{label}
{label}
); } function VideoResultPanel({ label }: { label: string }) { return (
待生成视频
生成后将在这里预览
); } function AudioResourcePanel() { return (
音频
情绪配乐.mp3
音频生视频参考素材
试听
{Array.from({ length: 38 }).map((_, index) => ( ))}
); } function PresetComposerPanel({ credit, prompt, submitMessage, thumbnailUrl, title, }: { credit: string; prompt: string; submitMessage: string; thumbnailUrl?: string; title: string; }) { return (
{title} {thumbnailUrl ? (
) : null}
{prompt}
toast.info("模型选择接口待接入。")} /> toast.info("比例选择接口待接入。")} /> toast.info("清晰度选择接口待接入。")} /> toast.info("镜头配置接口待接入。")} /> toast.info("生成数量接口待接入。")} /> toast.info("积分估算接口待接入。")} />
); } function CharacterThreeViewScene() { const [toolbarMenu, setToolbarMenu] = useState(null); return (
setToolbarMenu((current) => current === "panorama" ? null : "panorama")} /> setToolbarMenu((current) => current === "orbit" ? null : "orbit")} /> setToolbarMenu((current) => current === "lighting" ? null : "lighting")} /> setToolbarMenu((current) => current === "grid" ? null : "grid")} /> setToolbarMenu((current) => current === "hd" ? null : "hd")} /> setToolbarMenu((current) => current === "crop" ? null : "crop")} />
{["⌁", "↔", "↓", "↗"].map((item, index) => ( ))}
{toolbarMenu ? setToolbarMenu(null)} /> : null}
); } function SceneTool({ active, badge, dropdown, kind, label, onClick, }: { active?: boolean; badge?: string; dropdown?: boolean; kind: "panorama" | "orbit" | "lighting" | "grid" | "hd" | "crop"; label: string; onClick: () => void; }) { return ( ); } function SceneToolbarPopover({ menu, onClose, }: { menu: Exclude; onClose: () => void; }) { const quickOptions: Record, string[]> = { crop: ["4 宫格切分", "9 宫格切分", "16 宫格切分", "25 宫格切分"], grid: ["2 x 2", "3 x 3", "4 x 4", "自定义宫格"], hd: ["2倍高清", "4倍高清"], orbit: ["正侧背三视角", "八方向参考", "环绕镜头"], panorama: ["全景", "半身", "近景"], }; if (menu === "lighting") { return (
打光