auto-save 2026-05-13 18:35 (~2)

This commit is contained in:
2026-05-13 18:35:30 +08:00
parent bc458b65f1
commit d4841ca64b
2 changed files with 80 additions and 22 deletions

View File

@@ -2102,6 +2102,13 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 2 项未提交变更 · 最近提交auto-save 2026-05-13 18:24 (~3)",
"files_changed": 2
},
{
"ts": "2026-05-13T18:29:59+08:00",
"type": "commit",
"message": "auto-save 2026-05-13 18:29 (~2)",
"hash": "bc458b6",
"files_changed": 2
}
]
}

View File

@@ -599,9 +599,17 @@ const IMAGEGEN_WIDTH = 360
export function StoryboardNode({ data, selected }: any) {
const d: NodeData = data
const job = d?.job
const [hover, setHover] = useState<{
rect: DOMRect
name: string
elementSrc: string
frameSrc: string
frameIdx: number
timestamp: number
} | null>(null)
// 上方浮条 = 所有 frame 的 elements 已提取图("分镜头编排"的输入素材)
type ElPreview = { frameIdx: number; elementId: string; name: string; src: string; cid: string }
type ElPreview = { frameIdx: number; elementId: string; name: string; src: string; cid: string; frameSrc: string; timestamp: number }
const elementCrops: ElPreview[] = job
? job.frames.flatMap((f) =>
(f.elements ?? [])
@@ -611,7 +619,15 @@ export function StoryboardNode({ data, selected }: any) {
const cid = (e.cutouts && e.cutouts.length > 0)
? e.cutouts[e.cutouts.length - 1]
: (e.cutout_id ?? "")
return { frameIdx: f.index, elementId: e.id, name: e.name_zh, src, cid }
return {
frameIdx: f.index,
elementId: e.id,
name: e.name_zh,
src,
cid,
frameSrc: effectiveFrameUrl(job.id, f),
timestamp: f.timestamp,
}
})
.filter((p) => p.src),
)
@@ -637,6 +653,17 @@ export function StoryboardNode({ data, selected }: any) {
key={key}
className="group relative rounded-md border border-violet-300/50 transition shadow-lg hover:-translate-y-0.5 bg-white overflow-hidden"
style={{ aspectRatio: aspect }}
onMouseEnter={(e) => {
setHover({
rect: e.currentTarget.getBoundingClientRect(),
name: p.name,
elementSrc: p.src,
frameSrc: p.frameSrc,
frameIdx: p.frameIdx,
timestamp: p.timestamp,
})
}}
onMouseLeave={() => setHover(null)}
>
<button
onClick={(e) => {
@@ -675,32 +702,56 @@ export function StoryboardNode({ data, selected }: any) {
📋
</button>
)}
{/* hover 预览 — absolute 浮在缩略图上方 */}
<div
className="pointer-events-none absolute opacity-0 group-hover:opacity-100 scale-95 group-hover:scale-100 transition-all duration-150 z-[60]"
style={{
bottom: "calc(100% + 10px)",
left: "50%",
transform: "translateX(-50%)",
transformOrigin: "bottom center",
}}
>
<div className="rounded-lg overflow-hidden border-2 border-violet-300/50 bg-white shadow-2xl" style={{ width: 280 }}>
<div style={{ aspectRatio: aspect }}>
<img src={p.src} alt="" className="w-full h-full object-contain" />
</div>
<div className="px-2 py-1 bg-black/80 text-white text-[10.5px] flex items-center justify-between">
<span className="truncate">{p.name}</span>
<span className="text-white/60 font-mono shrink-0 ml-2"> {p.frameIdx + 1}</span>
</div>
</div>
</div>
</div>
)
})}
</div>
)}
{job && hover && (() => {
const w = 420
const h = 300
const gap = 12
const centerX = hover.rect.left + hover.rect.width / 2
const left = Math.max(12, Math.min(window.innerWidth - w - 12, centerX - w / 2))
const top = hover.rect.top > h + gap
? hover.rect.top - h - gap
: Math.min(window.innerHeight - h - 12, hover.rect.bottom + gap)
return createPortal(
<div
className="fixed z-[180] pointer-events-none"
style={{
left,
top,
width: w,
animation: "drawer-in 0.18s cubic-bezier(0.32, 0.72, 0, 1)",
}}
>
<div className="overflow-hidden rounded-xl border-2 border-violet-300/60 bg-black shadow-2xl">
<div className="grid grid-cols-[1fr_128px] gap-0">
<div className="bg-black">
<div className="px-2 py-1 text-[10px] text-white/65"></div>
<div style={{ aspectRatio: aspect }}>
<img src={hover.frameSrc} alt="" className="h-full w-full object-cover" />
</div>
</div>
<div className="border-l border-white/10 bg-white">
<div className="bg-black px-2 py-1 text-[10px] text-white/65"></div>
<div className="flex h-[calc(100%-22px)] items-center justify-center p-2">
<img src={hover.elementSrc} alt="" className="max-h-full max-w-full object-contain" />
</div>
</div>
</div>
<div className="flex items-center justify-between gap-2 bg-black/90 px-2 py-1.5 text-[10.5px] text-white">
<span className="truncate">{hover.name}</span>
<span className="shrink-0 font-mono text-white/55"> {hover.frameIdx + 1} · {hover.timestamp.toFixed(2)}s</span>
</div>
</div>
</div>,
document.body,
)
})()}
<NodeShell
type="ai" status={status}
icon={<LayoutGrid className="h-4 w-4" />}