auto-save 2026-05-13 16:28 (~3)

This commit is contained in:
2026-05-13 16:30:04 +08:00
parent 467e8f600b
commit f18aedf59c
3 changed files with 105 additions and 108 deletions

View File

@@ -1888,6 +1888,19 @@
"message": "auto-save 2026-05-13 16:17 (~3)", "message": "auto-save 2026-05-13 16:17 (~3)",
"hash": "f891cbc", "hash": "f891cbc",
"files_changed": 3 "files_changed": 3
},
{
"ts": "2026-05-13T16:23:35+08:00",
"type": "commit",
"message": "auto-save 2026-05-13 16:23 (~6)",
"hash": "467e8f6",
"files_changed": 6
},
{
"ts": "2026-05-13T08:27:40Z",
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 3 项未提交变更 · 最近提交auto-save 2026-05-13 16:23 (~6)",
"files_changed": 3
} }
] ]
} }

View File

@@ -386,6 +386,7 @@ export default function Home() {
open={workbenchOpen} open={workbenchOpen}
onClose={() => setWorkbenchOpen(false)} onClose={() => setWorkbenchOpen(false)}
onJobUpdate={setJob as any} onJobUpdate={setJob as any}
clipboard={clipboard}
/> />
</main> </main>

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import { useEffect, useState, useRef, type ReactNode } from "react" import { useEffect, useState, useRef, type ReactNode } from "react"
import { createPortal } from "react-dom" import { createPortal } from "react-dom"
import { X, LayoutGrid, Loader2, Check, Sparkle, Wand2 } from "lucide-react" import { X, LayoutGrid, Loader2, Check, Wand2 } from "lucide-react"
import { import {
type Job, type StoryboardScene, type ImageRef, type Job, type StoryboardScene, type ImageRef,
effectiveFrameUrl, updateStoryboard, resolveImageRefUrl, effectiveFrameUrl, updateStoryboard, resolveImageRefUrl,
@@ -83,21 +83,6 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
}, 600) }, 600)
} }
// 参考图候选 = 全部已推送图
const pushedImages = job.storyboard_images ?? []
const refOptions = pushedImages
.map((p) => {
const url = p.kind === "keyframe"
? effectiveFrameUrl(job.id, { index: p.frame_idx, cleaned_applied: false })
: (p.element_id && p.cutout_id
? (p.cutout_id === p.element_id
? cutoutUrl(job.id, p.frame_idx, p.element_id)
: cutoutUrl(job.id, p.frame_idx, p.element_id, p.cutout_id))
: "")
return { ref_id: p.ref_id, url, label: p.label || "", frame_idx: p.frame_idx, kind: p.kind }
})
.filter((r) => r.url)
const aspect = job.height > 0 ? `${job.width}/${job.height}` : "9/16" const aspect = job.height > 0 ? `${job.width}/${job.height}` : "9/16"
return createPortal( return createPortal(
@@ -111,7 +96,7 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
<LayoutGrid className="h-4 w-4 text-violet-300" /> <LayoutGrid className="h-4 w-4 text-violet-300" />
<span className="text-[14px] font-semibold"></span> <span className="text-[14px] font-semibold"></span>
<span className="text-[11px] text-white/40 font-mono ml-2"> <span className="text-[11px] text-white/40 font-mono ml-2">
{frames.length} · {pushedImages.length} {frames.length}
</span> </span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -184,106 +169,104 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
)} )}
</aside> </aside>
{/* 右侧详情 */} {/* 右侧详情 — 4 图槽 + 时长 */}
<main className="flex-1 overflow-y-auto"> <main className="flex-1 overflow-y-auto">
{!focusFrame ? ( {!focusFrame ? (
<div className="p-8 text-[13px] text-white/40"></div> <div className="p-8 text-[13px] text-white/40"></div>
) : ( ) : (
<div className="max-w-5xl mx-auto p-6 space-y-5"> <div className="max-w-5xl mx-auto p-6 space-y-5">
{/* 顶栏 */} {/* 顶栏:分镜信息 + 剪贴板提示 + 时长 */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-4">
<div className="text-[15.5px] font-semibold text-white"> <div className="text-[15.5px] font-semibold text-white">
{focusSeq} <span className="text-white/40 text-[12px] font-mono ml-2">{focusFrame.timestamp.toFixed(2)}s</span> {focusSeq}
<span className="text-white/40 text-[12px] font-mono ml-2">{focusFrame.timestamp.toFixed(2)}s</span>
</div>
<div className="flex items-center gap-3">
{clipboard ? (
<div className="text-[11px] text-emerald-300 inline-flex items-center gap-1">
📋 <span className="text-white font-medium truncate max-w-[180px]">{clipboard.label || (clipboard.kind === "keyframe" ? "关键帧" : "元素")}</span>
</div>
) : (
<div className="text-[11px] text-white/40"> · DAG / lightbox 📋 </div>
)}
<label className="inline-flex items-center gap-1.5 text-[11px] text-white/55">
<input
type="number"
step="0.1"
min="0"
value={form.duration || ""}
onChange={(e) => queueSave({ ...form, duration: parseFloat(e.target.value) || 0 })}
placeholder="3.5"
className="w-16 text-[12.5px] px-2 py-1 rounded-md bg-black/40 border border-white/15 outline-none text-white placeholder:text-white/30 focus:ring-2 focus:ring-violet-400/40 focus:border-violet-400/40 text-center"
/>
</label>
</div> </div>
</div> </div>
{/* 左大图 + 右字段 */} {/* 4 图槽 grid */}
<div className="flex gap-5"> <div className="grid grid-cols-4 gap-4">
<div className="flex-shrink-0"> {([
<img { key: "subject_image" as const, label: "主体", placeholder: "戴头带的骨架人" },
src={effectiveFrameUrl(job.id, focusFrame)} { key: "scene_image" as const, label: "场景", placeholder: "药店柜台 / 卧室" },
alt="" { key: "product_image" as const, label: "产品", placeholder: "Goli 营养软糖" },
className="rounded-lg bg-black object-contain" { key: "action_image" as const, label: "在干什么", placeholder: "递糖给顾客" },
style={{ width: 320, maxHeight: "52vh" }} ]).map(({ key, label, placeholder }) => {
/> const ref = form[key]
<div className="mt-1.5 text-[10.5px] text-white/50 text-center"> const url = ref ? resolveImageRefUrl(job.id, ref) : ""
{focusFrame.cleaned_applied ? "✨ 已清洗版" : "原图"} return (
</div> <div key={key} className="rounded-lg bg-white/[0.04] border border-white/10 p-2.5">
</div> <div className="text-[12px] text-white font-semibold mb-2 flex items-center justify-between">
<span>{label}</span>
<div className="flex-1 min-w-0 space-y-3"> {ref && (
<div className="grid grid-cols-2 gap-3"> <button
<FieldText label="主体" placeholder="如:戴头带的骨架人" onClick={() => queueSave({ ...form, [key]: null })}
value={form.subject} onChange={(v) => queueSave({ ...form, subject: v })} /> className="text-[10px] text-white/40 hover:text-rose-300"
<FieldText label="产品" placeholder="如Goli 营养软糖" title="清空"
value={form.product} onChange={(v) => queueSave({ ...form, product: v })} /> >
<FieldText label="场景" placeholder="如:药店柜台 / 夜晚卧室"
value={form.scene} onChange={(v) => queueSave({ ...form, scene: v })} /> </button>
<FieldNum label="时长 (秒)" placeholder="3.5" )}
value={form.duration} onChange={(v) => queueSave({ ...form, duration: v })} /> </div>
</div> <div
<FieldTextarea className="relative rounded-md overflow-hidden border-2 border-dashed border-white/15 bg-white/[0.03] flex items-center justify-center mb-2"
label="在干什么" style={{ aspectRatio: "1/1" }}
placeholder="如:骨架人递给顾客一瓶 Goli · 顾客接过并查看 · 灯光柔和" >
value={form.action} {ref && url ? (
onChange={(v) => queueSave({ ...form, action: v })} <img src={url} alt={label} className="absolute inset-0 w-full h-full object-contain bg-white" />
rows={4} ) : (
/> <div className="text-center text-white/30 text-[10.5px] p-3 leading-relaxed">
</div> · <br /><br />
</div>
{/* 参考图选择 */}
<section>
<div className="flex items-center justify-between mb-2">
<div className="text-[12.5px] font-semibold text-white inline-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">
· {form.reference_ids.length} / {refOptions.length}
</span>
</div>
</div>
{refOptions.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-8 gap-1.5">
{refOptions.map((r) => {
const checked = form.reference_ids.includes(r.ref_id)
return (
<button
key={r.ref_id}
onClick={() => {
const next = checked
? form.reference_ids.filter((x) => x !== r.ref_id)
: [...form.reference_ids, r.ref_id]
queueSave({ ...form, reference_ids: next })
}}
title={r.label}
className={`relative rounded-md overflow-hidden border-2 transition bg-white ${
checked ? "border-emerald-400 ring-2 ring-emerald-400/40" : "border-white/15 hover:border-white/40"
}`}
style={{ aspectRatio: "1/1" }}
>
<img src={r.url} alt={r.label} className="absolute inset-0 w-full h-full object-contain" />
{checked && (
<div className="absolute top-0.5 right-0.5 h-4 w-4 rounded-full bg-emerald-500 text-white inline-flex items-center justify-center">
<Check className="h-2.5 w-2.5" />
</div>
)}
{r.kind === "keyframe" && (
<div className="absolute top-0.5 left-0.5 text-[8px] text-white bg-amber-500/85 px-1 py-0.5 rounded font-bold leading-none">KF</div>
)}
<div className="absolute bottom-0 left-0 right-0 px-1 py-0.5 text-[8px] text-white bg-black/70 truncate leading-tight">
{r.label}
</div> </div>
</button> )}
) </div>
})} <button
</div> onClick={() => {
)} if (!clipboard) {
</section> toast.error("先在某张图上点 📋 复制")
return
}
queueSave({ ...form, [key]: clipboard })
toast.success(`已粘贴到「${label}`)
}}
disabled={!clipboard}
className={`w-full text-[11.5px] py-1.5 rounded-md inline-flex items-center justify-center gap-1 transition font-medium ${
clipboard
? "bg-violet-500 hover:bg-violet-400 text-white"
: "bg-white/[0.04] text-white/30 cursor-not-allowed"
}`}
title={clipboard ? `粘贴剪贴板的图:${clipboard.label || ""}` : "剪贴板为空"}
>
📋
</button>
<div className="mt-1 text-[9.5px] text-white/30 truncate text-center">
{ref?.label || placeholder}
</div>
</div>
)
})}
</div>
{/* 生成按钮Phase 2 占位) */} {/* 生成按钮Phase 2 占位) */}
<section> <section>
@@ -296,7 +279,7 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
Phase 2 Phase 2
</button> </button>
<div className="mt-2 text-[10.5px] text-white/35 leading-relaxed"> <div className="mt-2 text-[10.5px] text-white/35 leading-relaxed">
+ Seedance / Kling / Veo3 4 / / / + Seedance / Kling / Veo3
</div> </div>
</section> </section>
</div> </div>