auto-save 2026-05-13 13:53 (+1, ~4)

This commit is contained in:
2026-05-13 13:54:10 +08:00
parent 2d297ecdc1
commit 41fe9a8b1a
5 changed files with 195 additions and 4 deletions

View File

@@ -1602,6 +1602,13 @@
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 3 项未提交变更 · 最近提交auto-save 2026-05-13 13:42 (+1, ~4)",
"files_changed": 3
},
{
"ts": "2026-05-13T13:48:39+08:00",
"type": "commit",
"message": "auto-save 2026-05-13 13:48 (~3)",
"hash": "2d297ec",
"files_changed": 3
}
]
}

View File

@@ -15,6 +15,7 @@ import {
import { ThemeToggle } from "@/components/theme-toggle"
import { Dashboard, type DashboardHandle } from "@/components/dashboard"
import { StoryboardBar } from "@/components/storyboard-bar"
import { StoryboardEditor } from "@/components/storyboard-editor"
import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, type Job } from "@/lib/api"
import { VideoLightbox } from "@/components/video-lightbox"
@@ -65,6 +66,7 @@ export default function Home() {
const [selectedFrames, setSelectedFrames] = useState<Set<number>>(new Set())
const [expandedFrame, setExpandedFrame] = useState<number | null>(null)
const [videoLightboxOpen, setVideoLightboxOpen] = useState(false)
const [storyboardFrame, setStoryboardFrame] = useState<number | null>(null)
const dashboardRef = useRef<DashboardHandle>(null)
// 把 setJob(prev=>...) 翻译成 setJobs 里更新当前 active
@@ -259,6 +261,7 @@ export default function Home() {
onOpenPanel: (key: string) => dashboardRef.current?.openPanel(key),
onDeleteFrame: handleDeleteFrame,
onDeleteGenerated: handleDeleteGenerated,
onOpenStoryboard: (idx: number) => setStoryboardFrame(idx),
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated])
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag
@@ -323,7 +326,7 @@ export default function Home() {
<StoryboardBar
job={job}
selectedFrames={selectedFrames}
onExpandFrame={setExpandedFrame}
onOpenStoryboard={setStoryboardFrame}
/>
<div className="relative flex-1 min-h-0">
<ReactFlow
@@ -357,6 +360,13 @@ export default function Home() {
onClose={() => setVideoLightboxOpen(false)}
onAddFrame={handleAddManualFrame}
/>
{/* 分镜头编排专属面板 — imagegen 节点 / storyboard bar 缩略图点击进入 */}
<StoryboardEditor
job={job}
frameIndex={storyboardFrame}
onClose={() => setStoryboardFrame(null)}
/>
</main>
</>
)

View File

@@ -29,6 +29,7 @@ export interface NodeData {
onOpenPanel?: (key: string) => void // 控制 sidebar 哪个 drawer 展开
onDeleteFrame?: (idx: number) => void // 删整张关键帧
onDeleteGenerated?: (frameIdx: number, genId: string) => void // 删单张生成图
onOpenStoryboard?: (frameIdx: number) => void // 打开分镜头编排专属面板
}
/* ---- 状态映射工具 ---- */

View File

@@ -7,10 +7,10 @@ import { type Job, type KeyFrame, effectiveFrameUrl } from "@/lib/api"
interface Props {
job: Job | null
selectedFrames: Set<number>
onExpandFrame: (idx: number) => void
onOpenStoryboard: (idx: number) => void
}
export function StoryboardBar({ job, selectedFrames, onExpandFrame }: Props) {
export function StoryboardBar({ job, selectedFrames, onOpenStoryboard }: Props) {
const [collapsed, setCollapsed] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
@@ -71,7 +71,7 @@ export function StoryboardBar({ job, selectedFrames, onExpandFrame }: Props) {
<button
key={f.index}
ref={(el) => { btnRefs.current[f.index] = el }}
onClick={() => onExpandFrame(f.index)}
onClick={() => onOpenStoryboard(f.index)}
onMouseEnter={() => {
const el = btnRefs.current[f.index]
if (el) setHover({ frame: f, seq: i + 1, rect: el.getBoundingClientRect() })

View File

@@ -0,0 +1,173 @@
"use client"
import { useEffect, useState } from "react"
import { createPortal } from "react-dom"
import { X, LayoutGrid, Sparkle, Wand2, Brush } from "lucide-react"
import { type Job, effectiveFrameUrl, cutoutUrl } from "@/lib/api"
interface Props {
job: Job | null
frameIndex: number | null
onClose: () => void
}
export function StoryboardEditor({ job, frameIndex, onClose }: Props) {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
useEffect(() => {
if (frameIndex === null) return
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose() }
window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [frameIndex, onClose])
if (!mounted || !job || frameIndex === null) return null
const frame = job.frames.find((f) => f.index === frameIndex)
if (!frame) return null
const elements = frame.elements ?? []
const elementsWithCutout = elements.filter((e) => e.cutout_id)
const seq = job.frames.filter((f) => f.timestamp <= frame.timestamp).length
return createPortal(
<div
onClick={onClose}
className="fixed inset-0 z-[150] bg-black/80 backdrop-blur-xl flex items-center justify-center p-6"
style={{ animation: "drawer-in 0.2s cubic-bezier(0.32, 0.72, 0, 1)" }}
>
<div
onClick={(e) => e.stopPropagation()}
className="w-full max-w-5xl bg-black/60 rounded-2xl border border-white/15 overflow-hidden flex flex-col"
style={{ maxHeight: "88vh" }}
>
{/* 顶部栏 — 分镜头编排用紫粉渐变 */}
<div
className="flex items-center justify-between px-4 py-2.5 text-white"
style={{ background: "linear-gradient(135deg, #d946ef, #ec4899)" }}
>
<div className="flex items-center gap-2">
<LayoutGrid className="h-4 w-4" />
<span className="text-[14px] font-semibold"></span>
<span className="text-[11px] text-white/70 font-mono ml-2">
{seq} · {frame.timestamp.toFixed(2)}s
</span>
</div>
<button
onClick={onClose}
className="h-7 w-7 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center justify-center"
title="关闭 (Esc)"
>
<X className="h-4 w-4" />
</button>
</div>
{/* 主体 — 左大图 + 右素材 / 操作 */}
<div className="flex gap-4 p-4 overflow-hidden flex-1 min-h-0">
{/* 左:分镜大图 */}
<div className="flex-shrink-0 flex flex-col gap-2" style={{ width: 360 }}>
<img
src={effectiveFrameUrl(job.id, frame)}
alt={`frame ${frame.index}`}
className="w-full rounded-lg object-contain"
style={{ maxHeight: "65vh" }}
/>
<div className="text-[10.5px] text-white/50 text-center">
{frame.cleaned_applied ? "✨ 已清洗版本" : "原图"}
</div>
</div>
{/* 右:元素 + Phase 2 操作占位 */}
<div className="flex-1 min-w-0 overflow-y-auto space-y-3">
<section>
<div className="text-[12.5px] font-semibold text-white mb-1.5 flex items-center gap-1.5">
<Sparkle className="h-3.5 w-3.5 text-violet-300" />
<span className="text-[10px] text-white/40 font-mono">
· {elementsWithCutout.length}/{elements.length}
</span>
</div>
{elements.length === 0 ? (
<div className="rounded-md border border-dashed border-white/15 p-3 text-[11px] text-white/40">
·
</div>
) : (
<div className="grid grid-cols-4 gap-2">
{elements.map((e) => (
<div key={e.id} className="rounded-md bg-white/[0.04] border border-white/10 p-1.5">
<div className="w-full aspect-square rounded bg-black/40 overflow-hidden mb-1">
{e.cutout_id ? (
<img
src={cutoutUrl(job.id, frame.index, e.id)}
alt={e.name_zh}
className="w-full h-full object-contain"
/>
) : (
<div className="w-full h-full inline-flex items-center justify-center">
<Sparkle className="h-4 w-4 text-white/20" />
</div>
)}
</div>
<div className="text-[10.5px] text-white truncate">{e.name_zh}</div>
<div className="text-[9px] text-white/40 font-mono truncate">{e.name_en || "(无英文)"}</div>
</div>
))}
</div>
)}
</section>
{/* 编排操作占位Phase 2 */}
<section className="rounded-lg border border-dashed border-violet-300/30 bg-violet-500/5 p-3">
<div className="text-[12.5px] font-semibold text-white mb-2 flex items-center gap-1.5">
<Wand2 className="h-3.5 w-3.5 text-violet-300" />
· Phase 2
</div>
<div className="grid grid-cols-3 gap-2">
<button
disabled
className="rounded-md bg-white/[0.04] border border-white/10 p-2.5 text-left disabled:opacity-50 cursor-not-allowed"
>
<div className="text-[11.5px] font-medium text-white mb-0.5 inline-flex items-center gap-1">
📐
</div>
<div className="text-[9.5px] text-white/45 leading-tight">
/ /
</div>
</button>
<button
disabled
className="rounded-md bg-white/[0.04] border border-white/10 p-2.5 text-left disabled:opacity-50 cursor-not-allowed"
>
<div className="text-[11.5px] font-medium text-white mb-0.5 inline-flex items-center gap-1">
<Brush className="h-3 w-3" />
</div>
<div className="text-[9.5px] text-white/45 leading-tight">
+ prompt
</div>
</button>
<button
disabled
className="rounded-md bg-white/[0.04] border border-white/10 p-2.5 text-left disabled:opacity-50 cursor-not-allowed"
>
<div className="text-[11.5px] font-medium text-white mb-0.5 inline-flex items-center gap-1">
🎬
</div>
<div className="text-[9.5px] text-white/45 leading-tight">
/
</div>
</button>
</div>
<div className="mt-2 text-[10px] text-white/35 leading-relaxed">
+
</div>
</section>
</div>
</div>
{/* 底部 */}
<div className="px-4 py-1.5 text-[10px] text-white/40 font-mono text-center border-t border-white/5 bg-white/[0.02]">
ESC
</div>
</div>
</div>,
document.body,
)
}