"use client"; import { Bell, Box, Clock3, Copy, Crown, Eye, FileImage, FileText, FolderOpen, GitBranch, Headphones, HelpCircle, KeyRound, Layers, Loader2, Lock, Minus, MousePointer2, Music2, Plus, Save, Search, Share2, UserRound, Video, X, Zap, type LucideIcon, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import capturedCanvasModules from "@/artifacts/liblib-frontend-capture-ed9a2c12570a49b9a1186017b2c38b52/canvas-modules.json"; import { CanvasWorkspace } from "@/components/canvas/CanvasWorkspace"; import { canvasService } from "@/services/canvas"; import { useCanvasStore, canvasSelectors } from "@/store/canvas"; import { cn } from "@/lib/utils"; import type { CanvasNode, Position, WorkflowNodeData } from "@/types/canvas"; type ActivePanel = | "add" | "workflow" | "assets" | "history" | "help" | "support" | "skills" | "share" | "publish" | "member" | "notifications" | "account" | 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 TargetNodeKind = "image" | "video" | "text" | "audio" | "compose" | "workflow" | "preview" | "save"; interface 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 Array<{ id?: string | number; name?: string; title?: string; description?: string; }>; const TOOL_TYPE_META: Record< string, { label: string; icon: LucideIcon; color: string; nodeKind: TargetNodeKind } > = { image: { label: "图片", icon: FileImage, color: "#70d6ff", nodeKind: "image" }, video: { label: "视频", icon: Video, color: "#9b8cff", nodeKind: "video" }, text: { label: "文本", icon: FileText, color: "#ffd166", nodeKind: "text" }, audio: { label: "音频", icon: Music2, color: "#80ed99", nodeKind: "audio" }, compose: { label: "组合", icon: GitBranch, color: "#ff8fab", nodeKind: "compose" }, }; const STARTER_CARDS: Array<{ title: string; subtitle: string; nodes: Array<{ label: string; kind: TargetNodeKind }>; tone: string; }> = [ { title: "故事脚本生成", subtitle: "脚本到分镜", nodes: [ { label: "故事脚本", kind: "text" }, { label: "分镜拆解", kind: "compose" }, { label: "视频生成", kind: "video" }, { label: "预览", kind: "preview" }, ], tone: "from-slate-800 via-zinc-900 to-black", }, { title: "角色三视图", subtitle: "角色资产沉淀", nodes: [ { label: "角色描述", kind: "text" }, { label: "角色三视图", kind: "image" }, { label: "角色视频", kind: "video" }, { label: "保存", kind: "save" }, ], tone: "from-red-950 via-zinc-900 to-black", }, { title: "首帧图生视频", subtitle: "参考图到成片", nodes: [ { label: "首帧图", kind: "image" }, { label: "镜头提示词", kind: "text" }, { label: "图生视频", kind: "video" }, { label: "预览", kind: "preview" }, ], tone: "from-stone-800 via-zinc-900 to-black", }, { title: "音频生视频", subtitle: "音频驱动画面", nodes: [ { label: "音频输入", kind: "audio" }, { label: "画面描述", kind: "text" }, { label: "音频生视频", kind: "video" }, { label: "预览", kind: "preview" }, ], tone: "from-emerald-950 via-zinc-900 to-black", }, ]; 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 PANEL_TABS = ["全部", "图片", "视频", "文本", "音频", "组合"] as const; 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 inferNodeKindForTool(tool: CapturedTool): TargetNodeKind { const type = getToolType(tool); const modes = (tool.modes || []).map((mode) => String(mode).toLowerCase()); const name = `${tool.modelName || ""} ${tool.scene || ""} ${tool.toolKey || ""}`.toLowerCase(); if (type === "audio") return "audio"; if (type === "text") return "text"; if (type === "compose") return "compose"; if (name.includes("保存")) return "save"; if (name.includes("预览")) return "preview"; if (type === "image" && modes.some((mode) => mode.includes("video"))) return "video"; if (type === "image") return "image"; return "video"; } function getToolModes(tool: CapturedTool) { return (tool.modes || []).map(String).filter(Boolean).slice(0, 3); } function getVisibleOrigin(viewport: { x: number; y: number; zoom: number }) { return { x: (-viewport.x + 320) / viewport.zoom, y: (-viewport.y + 220) / viewport.zoom, }; } function patchNodeForCapturedTool(node: CanvasNode, tool: CapturedTool): Partial { const label = getToolLabel(tool); const metadata = { ...((node.data as any)?.metadata || {}), source: "liblib-capture", backendStatus: "placeholder", liblibTool: { type: tool.type || "", toolKey: tool.toolKey || "", modelKey: tool.modelKey || "", modelName: tool.modelName || "", vendor: tool.vendor || "", scene: tool.scene || "", modes: tool.modes || [], settings: tool.settings || [], advancedSettings: tool.advancedSettings || [], propertyKeys: tool.propertyKeys || [], properties: tool.metadata?.properties || {}, config: tool.metadata?.config || {}, generateTypes: tool.generateTypes || tool.metadata?.generateTypes || {}, rules: tool.rules || tool.metadata?.rules || [], }, }; if (node.type === "workflow") { const data = node.data as WorkflowNodeData; return { data: { ...data, title: label, description: `${tool.vendor || "LibLib"} / ${tool.scene || tool.modelKey || "模型模块"}`, summary: "前端模块已迁移,真实生成接口待后续接入。", metadata, }, }; } if (node.type === "image") { return { data: { ...(node.data as any), title: label, prompt: "上传或连接上游图片后可接入生成。", status: "pending", metadata, }, }; } if (node.type === "video") { return { data: { ...(node.data as any), prompt: "上传或连接上游视频后可接入生成。", status: "pending", metadata, }, }; } if (node.type === "text") { return { data: { ...(node.data as any), content: `${label}\n\n在这里输入提示词、脚本或约束。`, metadata, }, }; } return { data: { ...(node.data as any), metadata, }, }; } function buildTargetCanvasNode({ description, kind, metadata, position, title, }: { description?: string; kind: TargetNodeKind; metadata?: Record; position: Position; title: string; }): Omit { const baseMetadata = { source: "liblib-target-replica", backendStatus: "placeholder", ...metadata, }; if (kind === "image") { return { type: "image", position, size: { width: 260, height: 220 }, data: { src: "", status: "pending", title, prompt: description || "图片模块前端已就绪,生成接口待接入。", note: "上传、参考图、结果图能力后续接入。", metadata: baseMetadata, }, connections: [], }; } if (kind === "video") { return { type: "video", position, size: { width: 300, height: 220 }, data: { src: "", status: "pending", prompt: description || "视频模块前端已就绪,生成接口待接入。", metadata: baseMetadata, }, connections: [], }; } if (kind === "text") { return { type: "text", position, size: { width: 300, height: 170 }, data: { content: `${title}\n\n输入提示词、脚本、镜头描述或参数。`, fontSize: 15, color: "#f4f4f5", lineHeight: 1.55, borderWidth: 1, borderColor: "rgba(255,255,255,0.12)", borderRadius: 8, padding: 18, backgroundColor: "rgba(31,31,31,0.96)", metadata: baseMetadata, }, connections: [], }; } const iconTitle = kind === "audio" ? "音频模块" : kind === "preview" ? "预览模块" : kind === "save" ? "保存模块" : "工作流模块"; return { type: "workflow", position, size: { width: kind === "compose" ? 340 : 280, height: 170 }, data: { templateType: `liblib-${kind}-${title}`, title, description: description || iconTitle, summary: "前端节点已迁移,后端能力留空待补。", status: "pending", group: kind === "audio" ? "generation" : kind === "save" || kind === "preview" ? "output" : "tool", accentColor: kind === "audio" ? "#80ed99" : kind === "save" || kind === "preview" ? "#70d6ff" : "#ff8fab", metadata: baseMetadata, }, connections: [], }; } function getNodeLiblibTool(node: CanvasNode | null) { const metadata = ((node?.data as any)?.metadata || {}) as Record; return (metadata.liblibTool || null) as | (CapturedTool & { properties?: Record; config?: Record; generateTypes?: unknown; rules?: unknown; }) | null; } function getNodeDraftValues(node: CanvasNode | null) { const metadata = ((node?.data as any)?.metadata || {}) as Record; return (metadata.draftValues || {}) as Record; } function getPropertyEntries(tool: ReturnType) { const properties = tool?.properties; if (!properties || typeof properties !== "object") return []; return Object.entries(properties).filter(([key]) => key !== "modeType"); } function getPropertyLabel(key: string, config: unknown) { if (config && typeof config === "object" && "displayName" in config) { const displayName = (config as { displayName?: unknown }).displayName; if (typeof displayName === "string" && displayName.trim()) return displayName; } const labels: Record = { count: "生成数量", frameRate: "帧率", lora: "风格", media: "素材", prompt: "提示词", quality: "分辨率", ratio: "比例", resolution: "分辨率", searchable: "联网搜索", seed: "种子", style: "模型", template: "风格", }; return labels[key] || key; } 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 normalizeEnumOptions(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 getDefaultPropertyValue(key: string, config: unknown) { if (config && typeof config === "object" && "default" in config) { return String((config as { default?: unknown }).default ?? ""); } const options = normalizeEnumOptions(config); if (options.length > 0) return getOptionValue(options[0]); if (typeof config === "boolean") return config ? "true" : "false"; if (key === "prompt") return ""; return ""; } function rulesRequireMedia(tool: ReturnType) { const rules = tool?.rules; if (!Array.isArray(rules)) return false; return rules.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 GlassButton({ active, children, className, disabled, onClick, title, }: { active?: boolean; children: React.ReactNode; className?: string; disabled?: boolean; onClick?: () => void; title?: string; }) { return ( ); } function FloatingModal({ children, onClose, title, width = 520, }: { children: React.ReactNode; onClose: () => void; title: string; width?: number; }) { return (

{title}

{children}
); } export function LiblibVideoCanvasReplica({ canvasId, isLoading = false, }: LiblibVideoCanvasReplicaProps) { const router = useRouter(); const nodes = useCanvasStore(canvasSelectors.nodes); const selectedNodeIds = useCanvasStore(canvasSelectors.selectedNodeIds); const viewport = useCanvasStore(canvasSelectors.viewport); const tool = useCanvasStore(canvasSelectors.tool); const projectTitle = useCanvasStore((state) => state.projectTitle); const setProject = useCanvasStore((state) => state.setProject); const setProjectTitle = useCanvasStore((state) => state.setProjectTitle); const setTool = useCanvasStore((state) => state.setTool); const setViewport = useCanvasStore((state) => state.setViewport); const addNode = useCanvasStore((state) => state.addNode); const addEdge = useCanvasStore((state) => state.addEdge); const updateNode = useCanvasStore((state) => state.updateNode); const selectNodes = useCanvasStore((state) => state.selectNodes); const clearSelection = useCanvasStore((state) => state.clearSelection); const loadCanvas = useCanvasStore((state) => state.loadCanvas); const clearCanvas = useCanvasStore((state) => state.clearCanvas); const getCanvasState = useCanvasStore((state) => state.getCanvasState); const [activePanel, setActivePanel] = useState(null); const [toolSearch, setToolSearch] = useState(""); const [toolTab, setToolTab] = useState<(typeof PANEL_TABS)[number]>("全部"); const [isTitleEditing, setIsTitleEditing] = useState(false); const [draftTitle, setDraftTitle] = useState(projectTitle || "未命名"); 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) => selectedNodeIds.includes(node.id)) || null, [nodes, selectedNodeIds] ); const isBusy = isLoading || isProjectLoading; useEffect(() => { if (!isTitleEditing) { setDraftTitle(projectTitle || "未命名视频画布"); } }, [isTitleEditing, projectTitle]); useEffect(() => { let active = true; async function loadTargetCanvas() { setIsProjectLoading(true); clearCanvas(); const result = await canvasService.getProject(canvasId); if (!active) return; if (result.success && result.data) { setProject(canvasId, result.data.title || "未命名视频画布"); if (result.data.canvasData) { loadCanvas(result.data.canvasData); } setIsProjectLoading(false); return; } setProject(canvasId, "未命名视频画布"); await canvasService .saveCanvasState( canvasId, { nodes: [], edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, tool: "select", }, "video" ) .catch(() => undefined); if (active) { setIsProjectLoading(false); } } void loadTargetCanvas(); return () => { active = false; }; }, [canvasId, clearCanvas, loadCanvas, setProject]); const filteredTools = useMemo(() => { const normalizedSearch = toolSearch.trim().toLowerCase(); return capturedTools.filter((item) => { const type = getToolType(item); const tabMatched = toolTab === "全部" || (toolTab === "图片" && type === "image") || (toolTab === "视频" && type === "video") || (toolTab === "文本" && type === "text") || (toolTab === "音频" && type === "audio") || (toolTab === "组合" && type === "compose"); if (!tabMatched) return false; if (!normalizedSearch) return true; return [ item.modelName, item.vendor, item.scene, item.toolKey, item.modelKey, ...(item.modes || []), ] .filter(Boolean) .join(" ") .toLowerCase() .includes(normalizedSearch); }); }, [toolSearch, toolTab]); const saveCanvas = useCallback(async () => { setIsSaving(true); try { const result = await canvasService.saveCanvasState(canvasId, getCanvasState(), "video"); if (!result.success) { toast.error(result.error || "保存失败"); return; } toast.success("视频画布已保存"); } finally { setIsSaving(false); } }, [canvasId, getCanvasState]); const commitTitle = useCallback(async () => { const nextTitle = draftTitle.trim() || "未命名"; setProjectTitle(nextTitle); setIsTitleEditing(false); await canvasService.updateProject(canvasId, { title: nextTitle }, "video"); }, [canvasId, draftTitle, setProjectTitle]); const getPlacement = useCallback( (size: { width: number; height: number }, index = 0): Position => { const origin = getVisibleOrigin(viewport); const selected = selectedNode; if (selected) { return { x: selected.position.x + selected.size.width + 180 + index * 48, y: selected.position.y + selected.size.height / 2 - size.height / 2 + index * 36, }; } return { x: origin.x + index * 110, y: origin.y + index * 24, }; }, [selectedNode, viewport] ); const createTargetNode = useCallback( ( payload: { description?: string; kind: TargetNodeKind; metadata?: Record; title: string; }, options?: { position?: Position; connectFromSelection?: boolean; patchTool?: CapturedTool; index?: number; } ) => { const size = payload.kind === "video" ? { width: 300, height: 220 } : payload.kind === "image" ? { width: 260, height: 220 } : payload.kind === "text" ? { width: 300, height: 170 } : { width: payload.kind === "compose" ? 340 : 280, height: 170 }; const nodeConfig = buildTargetCanvasNode({ ...payload, position: options?.position || getPlacement(size, options?.index || 0), }); const nodeId = addNode(nodeConfig); const sourceNodeId = options?.connectFromSelection !== false && selectedNode ? selectedNode.id : null; if (sourceNodeId && sourceNodeId !== nodeId) { addEdge({ source: sourceNodeId, target: nodeId, type: "default" }); } if (options?.patchTool) { const createdNode = useCanvasStore.getState().nodes.find((node) => node.id === nodeId); if (createdNode) { updateNode(nodeId, patchNodeForCapturedTool(createdNode, options.patchTool)); } } selectNodes([nodeId]); return nodeId; }, [addEdge, addNode, getPlacement, selectNodes, selectedNode, updateNode] ); const createToolNode = useCallback( (capturedTool: CapturedTool) => { const kind = inferNodeKindForTool(capturedTool); createTargetNode( { kind, title: getToolLabel(capturedTool), description: `${capturedTool.vendor || "LibLib"} / ${capturedTool.scene || capturedTool.modelKey || "目标模块"}`, metadata: { source: "liblib-tool-spec", }, }, { patchTool: capturedTool } ); toast.success(`已添加 ${getToolLabel(capturedTool)}`); }, [createTargetNode] ); const createStarter = useCallback( (starter: (typeof STARTER_CARDS)[number]) => { const origin = getVisibleOrigin(viewport); const nodeIds = starter.nodes.map((item, index) => { const position = { x: origin.x + index * 390, y: origin.y + (index % 2) * 18, }; return addNode( buildTargetCanvasNode({ kind: item.kind, title: item.label, description: starter.title, metadata: { source: "liblib-starter-card", starterTitle: starter.title, }, position, }) ); }); nodeIds.slice(1).forEach((nodeId, index) => { const source = nodeIds[index]; if (source && nodeId) { addEdge({ source, target: nodeId, type: "default" }); } }); if (nodeIds.length > 0) { selectNodes([nodeIds[nodeIds.length - 1]]); } toast.success(`已创建 ${starter.title}`); }, [addEdge, addNode, selectNodes, viewport] ); const createWorkflowPresetNode = useCallback( (workflow: (typeof capturedWorkflows)[number]) => { createTargetNode( { kind: "workflow", title: String(workflow.name || workflow.title || "工作流预设"), description: String(workflow.description || "目标前端预设,后端执行待接入。"), metadata: { source: "liblib-workflow-preset", workflowId: workflow.id || "", workflowUuid: (workflow as any).uuid || "", cover: (workflow as any).cover || "", }, }, { connectFromSelection: true } ); toast.success("已添加目标工作流预设"); }, [createTargetNode] ); const updateSelectedDraftValue = useCallback( (key: string, value: unknown) => { if (!selectedNode) return; const data = selectedNode.data as Record; const metadata = (data.metadata || {}) as Record; updateNode(selectedNode.id, { data: { ...data, metadata: { ...metadata, draftValues: { ...((metadata.draftValues || {}) as Record), [key]: value, }, }, } as unknown as CanvasNode["data"], }); }, [selectedNode, updateNode] ); 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 openPanel = useCallback((panel: Exclude) => { setActivePanel((current) => (current === panel ? null : panel)); }, []); return (
{ clearSelection(); setActivePanel("add"); setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom, }); }} showEmptyState={false} showZoomControls={false} workspaceLabel="视频画布" workspaceVariant="video" />
{isBusy ? (
正在打开视频画布...
) : null}
{isTitleEditing ? ( void commitTitle()} onChange={(event) => setDraftTitle(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") void commitTitle(); if (event.key === "Escape") setIsTitleEditing(false); }} value={draftTitle} /> ) : ( )}
openPanel("skills")}>LibTV Skills void saveCanvas()} title="保存"> {isSaving ? : } openPanel("share")} title="分享"> openPanel("notifications")} title="通知"> openPanel("member")} > 会员超市 openPanel("member")}> 会员特惠 openPanel("account")} title="账号">
{activePanel === "add" ? (
添加功能模块
已迁移 {capturedCounts.tools || capturedTools.length} 个工具配置
setToolSearch(event.target.value)} placeholder="搜索模型、厂商、模式" value={toolSearch} />
{PANEL_TABS.map((tab) => ( ))}
{filteredTools.map((capturedTool) => { const type = getToolType(capturedTool); const meta = TOOL_TYPE_META[type] || TOOL_TYPE_META.video; const Icon = meta.icon; const modes = getToolModes(capturedTool); return ( ); })}
) : null} {activePanel === "workflow" ? (
目标工作流预设
抓取到 {capturedWorkflows.length || 25} 个预设,后端执行先留空
{capturedWorkflows.map((workflow) => { const cover = String((workflow as any).cover || "").trim(); return ( ); })}
) : null} {nodes.length === 0 && activePanel !== "add" && activePanel !== "workflow" ? (
双击画布 自由生成节点
{STARTER_CARDS.map((card) => ( ))}
) : null}
{selectedNode ? ( ) : null} {activePanel === "assets" ? ( setActivePanel(null)} title="素材库"> ) : null} {activePanel === "history" ? ( setActivePanel(null)} title="历史记录">
{nodes.length === 0 ? ( ) : ( nodes.slice(-12).reverse().map((node) => ( )) )}
) : null} {activePanel === "help" ? ( setActivePanel(null)} title="快捷键">
{[ ["V", "选择工具"], ["H / 空格拖动", "平移画布"], ["L", "工作流连线"], ["⌘ / Ctrl + S", "保存"], ["双击空白", "打开添加模块"], ["双击连线", "删除连线"], ].map(([key, label]) => (
{label} {key}
))}
) : null} {activePanel === "support" ? ( setActivePanel(null)} title="支持中心"> ) : null} {activePanel === "skills" ? ( setActivePanel(null)} title="LibTV Skills">
能力覆盖
{Object.entries(capturedCounts.byType || {}).map(([type, count]) => { const meta = TOOL_TYPE_META[type] || TOOL_TYPE_META.video; return (
{String(count)}
{meta.label}
); })}
) : null} {activePanel === "share" ? ( setActivePanel(null)} title="发布与分享">
分享链接
打开后可查看当前视频画布,权限控制后续接入。
启用分享
) : null} {activePanel === "publish" ? ( setActivePanel(null)} title="发布作品到视频画布广场" width={560}>