"use client" import { useEffect, useMemo, useRef, useState } from "react" import { ArrowDownToLine, CheckCircle2, CircleAlert, Film, ImagePlus, Link2, Loader2, Play, RotateCcw, TerminalSquare, Upload, } from "lucide-react" const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:4291" type AgentRunLog = { ts: number level: "info" | "warn" | "error" message: string } type AgentRun = { id: string job_id: string status: "draft" | "queued" | "executing" | "reviewing" | "completed" | "failed" stage: string progress: number logs: AgentRunLog[] video_ids: string[] final_video_url: string contact_sheet_url: string error: string created_at: number updated_at: number } const STAGES = [ { key: "download", label: "下载" }, { key: "assets", label: "素材" }, { key: "analyze", label: "拆解" }, { key: "plan", label: "规划" }, { key: "execute", label: "生成" }, { key: "review", label: "审片" }, { key: "compose", label: "合成" }, { key: "final", label: "成片" }, ] function formatClock(ts: number) { if (!ts) return "--:--:--" return new Date(ts * 1000).toLocaleTimeString("zh-CN", { hour12: false }) } function runVideoUrl(run: AgentRun | null) { if (!run?.final_video_url) return "" return `${API_BASE}${run.final_video_url}` } function runContactUrl(run: AgentRun | null) { if (!run?.contact_sheet_url) return "" return `${API_BASE}${run.contact_sheet_url}` } export default function AgentPage() { const [url, setUrl] = useState("") const [files, setFiles] = useState([]) const [run, setRun] = useState(null) const [recent, setRecent] = useState([]) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState("") const terminalRef = useRef(null) const previews = useMemo(() => files.map((file) => ({ file, url: URL.createObjectURL(file) })), [files]) useEffect(() => () => previews.forEach((item) => URL.revokeObjectURL(item.url)), [previews]) useEffect(() => { fetch(`${API_BASE}/agent-runs?limit=8`, { cache: "no-store" }) .then((res) => (res.ok ? res.json() : [])) .then((items: AgentRun[]) => { setRecent(items) const latest = items.find((item) => item.status === "executing" || item.status === "reviewing" || item.status === "completed") if (latest) setRun(latest) }) .catch(() => undefined) }, []) useEffect(() => { if (!run || run.status === "completed" || run.status === "failed") return const timer = window.setInterval(async () => { try { const res = await fetch(`${API_BASE}/agent-runs/${run.id}`, { cache: "no-store" }) if (!res.ok) return const next = await res.json() setRun(next) } catch { /* keep current state */ } }, 2000) return () => window.clearInterval(timer) }, [run?.id, run?.status]) useEffect(() => { const el = terminalRef.current if (el) el.scrollTop = el.scrollHeight }, [run?.logs.length]) async function submit() { setError("") if (!url.trim()) { setError("需要 TikTok 链接") return } setSubmitting(true) try { const form = new FormData() form.append("tk_url", url.trim()) files.slice(0, 6).forEach((file) => form.append("product_files", file)) const res = await fetch(`${API_BASE}/agent-runs`, { method: "POST", body: form }) if (!res.ok) { const text = await res.text().catch(() => "") throw new Error(text.slice(0, 260) || `HTTP ${res.status}`) } const created = await res.json() setRun(created) setRecent((prev) => [created, ...prev.filter((item) => item.id !== created.id)].slice(0, 8)) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setSubmitting(false) } } const activeStageIndex = run ? Math.max(0, STAGES.findIndex((item) => item.key === run.stage)) : -1 const canStart = !!url.trim() && !submitting const videoSrc = runVideoUrl(run) const contactSrc = runContactUrl(run) return (
SKG Agent Cut

一分钟二创出片终端

{run ? `${run.status} · ${run.progress}%` : "standby"}