auto-save 2026-05-17 10:56 (+1, ~2)

This commit is contained in:
2026-05-17 10:56:31 +08:00
parent 9d1268bc42
commit a30a9de26e
3 changed files with 1231 additions and 705 deletions

View File

@@ -2,12 +2,11 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useTheme } from "next-themes"
import {
ReactFlow, Background, BackgroundVariant, Controls, MiniMap,
ReactFlow, Background, BackgroundVariant, Controls,
useNodesState, useEdgesState,
type Node, type Edge,
} from "@xyflow/react"
import { Toaster, toast } from "sonner"
import { LayoutGrid } from "lucide-react"
import {
InputNode, VisualLabNode, AudioNode,
ComposeNode, KeyframePanelNode,
@@ -17,6 +16,7 @@ import {
} from "@/components/nodes"
import { ThemeToggle } from "@/components/theme-toggle"
import { AudioStrip } from "@/components/audio-strip"
import { AdRecreationBoard } from "@/components/ad-recreation-board"
import {
addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteJob, deleteFrame, deleteGeneratedImage,
deleteGeneratedVideo, deleteCutout, generateStoryboardVideo, triggerTranscribe,
@@ -535,7 +535,7 @@ export default function Home() {
})
updateJobInList(updated)
void navigator.clipboard?.writeText(prompt).catch(() => {})
toast.success("视频任务已进入 Video Gen 节点")
toast.success("视频任务已进入候选片段")
} catch (e) {
toast.error("提交视频失败:" + (e instanceof Error ? e.message : String(e)))
}
@@ -666,7 +666,7 @@ export default function Home() {
if (jobs.length === 0) return
// 状态切到 downloaded 时提示用户点解析(仅一次)
if (job?.status === "downloaded" && prevStatusRef.current !== "downloaded") {
toast.info("📥 视频已就绪 — 请点 Input 节点里的「点这里开始解析」按钮", { duration: 6000 })
toast.info("视频已就绪,请在左侧看板开始抽帧", { duration: 6000 })
}
prevStatusRef.current = job?.status ?? null
@@ -767,7 +767,7 @@ export default function Home() {
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag
const savedSizes = useMemo(() => loadNodeSizes(), [])
const [nodes, setNodes, onNodesChange] = useNodesState<Node>(
const [nodes, setNodes] = useNodesState<Node>(
LAYOUT.map((n) => {
const s = savedSizes[n.id] ?? {}
const w = s.w ?? n.w
@@ -868,7 +868,7 @@ export default function Home() {
}
try { window.localStorage.setItem(NODE_SIZES_KEY, JSON.stringify(sizes)) } catch {}
}, [nodes])
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>(
const [, setEdges] = useEdgesState<Edge>(
EDGES_RAW.map(([from, to], i) => ({
id: `e${i}`, source: from, target: to, animated: false, type: "default",
})),
@@ -1002,45 +1002,32 @@ export default function Home() {
return (
<>
<div className="canvas-bg" />
<main className="relative h-screen w-screen overflow-hidden flex">
{/* 自动整理布局 — 主题切换上方,一键恢复默认位置和宽度 */}
<div className="absolute z-30 pointer-events-auto" style={{ bottom: 228, left: 12 }}>
<button
type="button"
onClick={handleResetLayout}
className="glass-node h-9 w-9 inline-flex items-center justify-center rounded-xl"
style={{ width: 36, height: 36, padding: 0, borderRadius: 12 }}
title="自动排版 · 保留每个节点的尺寸,重新排好间距和列布局"
>
<LayoutGrid className="h-4 w-4" />
</button>
</div>
{/* 主题切换 — 左下角 Controls 上方(错开) */}
<div className="absolute z-30 pointer-events-auto" style={{ bottom: 180, left: 12 }}>
<ThemeToggle />
</div>
<main className="relative flex h-screen w-screen overflow-hidden">
<AdRecreationBoard data={nodeData} onGenerateVideo={handleQuickGenerateVideo} />
{/* 右区:DAG 节点流图(原顶部 storyboard dock 已删除) */}
<section className="relative flex-1 min-h-0 flex flex-col">
{/* 右区:暂时清空,只保留无限画布能力,后续再定义要承载的内容。 */}
<section className="relative flex min-h-0 flex-1 flex-col">
<div className="absolute z-30 pointer-events-auto" style={{ bottom: 112, left: 12 }}>
<ThemeToggle />
</div>
<div className="relative flex-1 min-h-0">
{clientReady ? (
<ReactFlow
nodes={nodes}
edges={edges}
nodes={[]}
edges={[]}
onInit={(instance) => { flowRef.current = instance }}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={NODE_TYPES}
colorMode={resolvedTheme === "light" ? "light" : "dark"}
fitView
fitViewOptions={{ padding: 0.12 }}
minZoom={0.2}
maxZoom={1.5}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={false}
proOptions={{ hideAttribution: true }}
>
<Background variant={BackgroundVariant.Dots} gap={28} size={1.4} />
<Controls position="bottom-left" />
<MiniMap position="bottom-right" pannable zoomable nodeStrokeWidth={2} />
</ReactFlow>
) : (
<div className="h-full w-full" suppressHydrationWarning />