auto-save 2026-05-13 10:10 (~3)

This commit is contained in:
2026-05-13 10:11:03 +08:00
parent d734c08dda
commit 7db74cfc32
3 changed files with 216 additions and 34 deletions

View File

@@ -6,7 +6,7 @@ import {
Mic, Languages, FileEdit, Sparkles, Film, FileVideo, Loader2, Plus,
} from "lucide-react"
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
import { type Job, frameUrl, videoUrl } from "@/lib/api"
import { type Job, frameUrl, videoUrl, generatedImageUrl } from "@/lib/api"
export interface NodeData {
job: Job | null // 当前 active job
@@ -534,26 +534,128 @@ export function RewriteNode({ selected }: any) {
}
/* ============================================================
8. ImageGenNode (placeholder)
8. ImageGenNode — 显示 selected frames 的代表生成图
============================================================ */
export function ImageGenNode({ selected }: any) {
const IMAGEGEN_WIDTH = 320
export function ImageGenNode({ data, selected }: any) {
const d: NodeData = data
const job = d?.job
const selectedIdxs = Array.from(d?.selectedFrames ?? []).sort((a: number, b: number) => a - b) as number[]
// 每个 selected frame 取一张代表图:优先 selected gen否则最新一张
const previews = selectedIdxs
.map((idx) => {
const f = job?.frames.find((x) => x.index === idx)
if (!f || !f.generated_images || f.generated_images.length === 0) return null
const sel = f.generated_images.find((g) => g.selected)
const pick = sel ?? f.generated_images[f.generated_images.length - 1]
return { frameIdx: idx, gen: pick, hasSelected: !!sel, total: f.generated_images.length }
})
.filter((p): p is { frameIdx: number; gen: NonNullable<ReturnType<typeof Object>>; hasSelected: boolean; total: number } => p !== null)
const totalGens = job?.frames.reduce((sum, f) => sum + (f.generated_images?.length ?? 0), 0) ?? 0
const selectedCount = previews.filter((p) => p.hasSelected).length
const status: NodeStatus = !job ? "pending" : totalGens > 0 ? "done" : "pending"
const aspect = job && job.height > 0 ? `${job.width}/${job.height}` : "9/16"
return (
<NodeShell
type="ai" status="pending"
icon={<Sparkles className="h-4 w-4" />}
title="生图 · Image Gen"
subtitle="STEP 8 · nano-banana / GPT"
selected={selected}
>
<div className="grid grid-cols-2 gap-1.5">
<div className="rounded-md border border-dashed border-black/15 dark:border-white/10 px-2 py-1.5 text-[10.5px] text-[var(--text-faint)]">
nano-banana-pro<br /><span className="text-[var(--text-strong)] text-[11px]">Gemini 3 Image</span>
<div className="relative" style={{ width: IMAGEGEN_WIDTH }}>
{/* 节点上方:每帧 1 张缩略图 */}
{previews.length > 0 && job && (
<div
className="absolute left-0 right-0 grid grid-cols-5 gap-1.5"
style={{ bottom: "calc(100% + 12px)" }}
>
{previews.map((p) => (
<button
key={p.frameIdx}
onClick={(e) => { e.stopPropagation(); d.onExpandFrame(p.frameIdx) }}
title={`分镜 ${p.frameIdx + 1} · ${p.total}${p.hasSelected ? " · 已选用" : ""}`}
className={`group relative rounded-md border transition shadow-lg hover:-translate-y-0.5 ${
p.hasSelected
? "border-emerald-400 ring-2 ring-emerald-400/60"
: "border-pink-300/40 dark:border-pink-300/30"
}`}
style={{ aspectRatio: aspect }}
>
<img
src={generatedImageUrl(job.id, p.frameIdx, p.gen.id)}
alt={`gen ${p.gen.id}`}
className="absolute inset-0 w-full h-full object-cover rounded-md"
/>
{p.total > 1 && (
<div className="absolute top-0 left-0 bg-black/70 text-white text-[8.5px] font-mono px-1 py-0.5 leading-none rounded-tl-md rounded-br">
{p.total}
</div>
)}
{p.hasSelected && (
<div className="absolute inset-0 bg-emerald-400/15 rounded-md pointer-events-none" />
)}
{/* Hover 大图预览 */}
<div
className="pointer-events-none absolute opacity-0 group-hover:opacity-100 scale-95 group-hover:scale-100 transition-all duration-200 z-[60]"
style={{
bottom: "calc(100% + 10px)",
left: "50%",
transform: "translateX(-50%)",
transformOrigin: "bottom center",
}}
>
<div className="rounded-2xl overflow-hidden border border-white/25 bg-black" style={{ boxShadow: "0 40px 100px -20px rgba(0,0,0,0.85), 0 0 0 1px rgba(255,255,255,0.06)" }}>
<img
src={generatedImageUrl(job.id, p.frameIdx, p.gen.id)}
alt={`preview ${p.gen.id}`}
className="block"
style={{
width: IMAGEGEN_WIDTH * 2,
maxWidth: "min(720px, 80vw)",
height: "auto",
maxHeight: "82vh",
objectFit: "contain",
}}
/>
<div className="flex items-center justify-between px-3 py-2 bg-black/70 backdrop-blur-md">
<span className="text-white text-[12.5px] font-medium"> {p.frameIdx + 1} · </span>
<span className="text-white/60 text-[11px] font-mono">{p.gen.mode === "edit" ? "i2i" : "text"} · {p.total} </span>
</div>
</div>
</div>
</button>
))}
</div>
<div className="rounded-md border border-dashed border-black/15 dark:border-white/10 px-2 py-1.5 text-[10.5px] text-[var(--text-faint)]">
GPT Image<br /><span className="text-[var(--text-strong)] text-[11px]">OpenAI</span>
</div>
</div>
</NodeShell>
)}
<NodeShell
type="ai" status={status}
icon={<Sparkles className="h-4 w-4" />}
title="生图 · Image Gen"
subtitle={`STEP 6 · nano-banana ${totalGens > 0 ? `· ${totalGens}` : ""}`}
width={IMAGEGEN_WIDTH}
selected={selected}
>
{totalGens > 0 ? (
<div className="text-[11.5px] leading-relaxed text-[var(--text-soft)]">
<span className="text-[var(--text-strong)] font-medium">{totalGens}</span> · {selectedCount}/{previews.length}
<br />
<span className="text-[10.5px] text-[var(--text-faint)]">
· sidebar
</span>
</div>
) : (
<div className="grid grid-cols-2 gap-1.5">
<div className="rounded-md border border-dashed border-black/15 dark:border-white/10 px-2 py-1.5 text-[10.5px] text-[var(--text-faint)]">
nano-banana-pro<br /><span className="text-[var(--text-strong)] text-[11px]">Gemini 3 Image</span>
</div>
<div className="rounded-md border border-dashed border-black/15 dark:border-white/10 px-2 py-1.5 text-[10.5px] text-[var(--text-faint)]">
GPT Image<br /><span className="text-[var(--text-strong)] text-[11px]">OpenAI</span>
</div>
</div>
)}
</NodeShell>
</div>
)
}