"use client"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import useSWR from "swr"; import { ArrowLeft, Clapperboard, FileText, Film, FolderOpen, Image as ImageIcon, LayoutGrid, MoreHorizontal, Music2, Plus, Scissors, Sparkles, Video, WandSparkles, Zap, } from "lucide-react"; import { toast } from "sonner"; import { filterCanvasModels, type CanvasModelDescriptor } from "@/lib/video-canvas/model-matching"; export type VideoCanvasHomeProject = { id: string; title: string; thumbnail: string | null; createdAt: string; updatedAt: string; }; type Section = "home" | "projects"; type Starter = "image" | "video" | "audio" | "script" | "smart-edit"; type BrandingSettings = { siteName?: string | null; siteLogo?: string | null; siteLogoDark?: string | null; }; const quickStarts: Array<{ label: string; icon: typeof Video; starter: Starter; mode?: string; model?: string }> = [ { label: "文生视频", icon: WandSparkles, starter: "video", mode: "text-to-video" }, { label: "首帧视频", icon: Video, starter: "video", mode: "first-frame" }, { label: "多参考视频", icon: Film, starter: "video", mode: "reference-images" }, { label: "视频生成", icon: Clapperboard, starter: "video" }, { label: "图片生成", icon: ImageIcon, starter: "image" }, { label: "音频生成", icon: Music2, starter: "audio" }, { label: "剧本生成", icon: FileText, starter: "script" }, { label: "智能剪辑", icon: Scissors, starter: "smart-edit" }, ]; function projectHref(id: string, starter?: Starter, mode?: string, model?: string) { const params = new URLSearchParams({ mode: "video", workspaceType: "video" }); if (starter) params.set("starter", starter); if (mode) params.set("starterMode", mode); if (model) params.set("starterModel", model); return `/canvas/${encodeURIComponent(id)}?${params.toString()}`; } function projectDate(value: string) { const parts = new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(new Date(value)); const get = (name: string) => parts.find(part => part.type === name)?.value || ""; return `${get("year")}-${get("month")}-${get("day")}`; } function projectTitle(value: string) { return !value || value === "未命名视频画布" ? "未命名" : value; } export function VideoCanvasHome({ initialHasMore, initialProjects, siteLogo, siteLogoDark, siteName, userName, }: { initialHasMore: boolean; initialProjects: VideoCanvasHomeProject[]; siteLogo: string | null; siteLogoDark: string | null; siteName: string; userName: string; }) { const router = useRouter(); const [section, setSection] = useState
("home"); const [projects, setProjects] = useState(initialProjects); const [hasMore, setHasMore] = useState(initialHasMore); const [loadingMore, setLoadingMore] = useState(false); const [creating, setCreating] = useState(false); const [menuId, setMenuId] = useState(null); const [editingId, setEditingId] = useState(null); const [draftTitle, setDraftTitle] = useState(""); const { data: branding } = useSWR( "/api/site-settings?scope=branding", async (url: string) => { const response = await fetch(url, { cache: "no-store" }); if (!response.ok) throw new Error("品牌信息不可用"); return response.json(); }, { refreshInterval: 30_000, revalidateOnFocus: true }, ); const brandName = String(branding?.siteName ?? siteName).trim() || "AI Chatbot"; const brandLogo = (branding ? branding.siteLogoDark : siteLogoDark)?.trim() || (branding ? branding.siteLogo : siteLogo)?.trim(); const { data: membership } = useSWR<{ summary?: { currentCredits?: number } }>( "/api/user/membership-summary", async (url: string) => { const response = await fetch(url); if (!response.ok) throw new Error("积分信息不可用"); return response.json(); }, ); const { data: modelData } = useSWR<{ models?: CanvasModelDescriptor[] }>("/api/models", async (url: string) => { const response = await fetch(url); if (!response.ok) throw new Error("模型目录不可用"); return response.json(); }); const availableVideoModels = filterCanvasModels(modelData?.models || [], "video", "text-to-video").slice(0, 3); const shortcuts = quickStarts.map((shortcut, index) => { const model = index < 3 ? availableVideoModels[index] : undefined; return model ? { ...shortcut, label: model.displayName || model.modelId || shortcut.label, model: model.modelId || model.id, mode: "text-to-video", } : shortcut; }); useEffect(() => { let active = true; async function refreshRecent() { try { const response = await fetch("/api/canvas?workspaceType=video&limit=24&page=1", { cache: "no-store" }); const result = await response.json(); if (!active || !response.ok || !result.success) return; setProjects(current => result.data.map((item: VideoCanvasHomeProject) => ({ ...item, thumbnail: item.thumbnail || current.find(existing => existing.id === item.id)?.thumbnail || null, }))); setHasMore(Boolean(result.pagination?.hasMore)); } catch { // The server-rendered project list remains available if refreshing fails. } } void refreshRecent(); return () => { active = false; }; }, []); async function createProject(starter?: Starter, mode?: string, model?: string) { if (creating) return; setCreating(true); try { const response = await fetch("/api/canvas", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ workspaceType: "video", title: "未命名视频画布" }), }); const result = await response.json(); if (!response.ok || !result.success || !result.data?.id) throw new Error(result.error || "创建项目失败"); setProjects(current => [{ id: result.data.id, title: result.data.title || "未命名视频画布", thumbnail: null, createdAt: result.data.createdAt || new Date().toISOString(), updatedAt: result.data.updatedAt || new Date().toISOString(), }, ...current]); router.push(projectHref(result.data.id, starter, mode, model)); } catch (error) { toast.error(error instanceof Error ? error.message : "创建项目失败,请重试"); setCreating(false); } } async function loadMore() { if (loadingMore || !hasMore) return; setLoadingMore(true); try { const page = Math.floor(projects.length / 24) + 1; const response = await fetch(`/api/canvas?workspaceType=video&limit=24&page=${page}`, { cache: "no-store" }); const result = await response.json(); if (!response.ok || !result.success) throw new Error(result.error || "加载项目失败"); setProjects(current => [...current, ...result.data.filter((item: VideoCanvasHomeProject) => !current.some(existing => existing.id === item.id))]); setHasMore(Boolean(result.pagination?.hasMore)); } catch (error) { toast.error(error instanceof Error ? error.message : "加载项目失败"); } finally { setLoadingMore(false); } } async function saveTitle(id: string) { const title = draftTitle.trim(); if (!title) { toast.error("项目名称不能为空"); return; } try { const response = await fetch(`/api/canvas/${encodeURIComponent(id)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, workspaceType: "video" }), }); const result = await response.json(); if (!response.ok || !result.success) throw new Error(result.error || "重命名失败"); setProjects(current => current.map(item => item.id === id ? { ...item, title, updatedAt: result.data.updatedAt || item.updatedAt } : item)); setEditingId(null); toast.success("项目已重命名"); } catch (error) { toast.error(error instanceof Error ? error.message : "重命名失败"); } } const visibleProjects = section === "home" ? projects.slice(0, 4) : projects; return (
{membership?.summary?.currentCredits ?? "—"} {(userName || "用").slice(0, 1)}
{section === "home" ? <>
{shortcuts.map(({ label, icon: Icon, starter, mode, model }, index) => )}
: null}

{section === "home" ? "最近项目" : "全部项目"}

{section === "home" && projects.length > 0 ? : null}
{visibleProjects.length ?
{visibleProjects.map(project =>
{menuId === project.id ?
: null} {editingId === project.id ?
{ event.preventDefault(); void saveTitle(project.id); }}> setDraftTitle(event.target.value)} value={draftTitle} />
: null}
)}
:
还没有视频项目,点击上方“新建画布创作”开始。
} {section === "projects" && hasMore ? : null}
); }