From 7db74cfc327ef500c01100bca383b600dc32c016 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 10:11:03 +0800 Subject: [PATCH] auto-save 2026-05-13 10:10 (~3) --- .memory/worklog.json | 13 ++++ web/components/dashboard.tsx | 99 +++++++++++++++++++---- web/components/nodes/index.tsx | 138 ++++++++++++++++++++++++++++----- 3 files changed, 216 insertions(+), 34 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 953a864..c4b9cb6 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1184,6 +1184,19 @@ "message": "auto-save 2026-05-13 09:59 (~4)", "hash": "ea31219", "files_changed": 4 + }, + { + "ts": "2026-05-13T10:05:29+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 10:05 (~3)", + "hash": "d734c08", + "files_changed": 3 + }, + { + "ts": "2026-05-13T02:07:36Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-13 10:05 (~3)", + "files_changed": 2 } ] } diff --git a/web/components/dashboard.tsx b/web/components/dashboard.tsx index 343b769..b3c89ac 100644 --- a/web/components/dashboard.tsx +++ b/web/components/dashboard.tsx @@ -601,6 +601,9 @@ function ImageGenCard({ job, frame, onJobUpdate }: { const basePrompt = frame.description?.suggested_prompt ?? "(尚未识别 · 点关键帧打开 lightbox 先识别)" const [editablePrompt, setEditablePrompt] = useState(basePrompt) const [showPrompt, setShowPrompt] = useState(false) + const [previewGenId, setPreviewGenId] = useState(null) + const [previewMounted, setPreviewMounted] = useState(false) + useEffect(() => setPreviewMounted(true), []) // 当 vision 识别完成后更新默认 prompt useEffect(() => { @@ -760,7 +763,7 @@ function ImageGenCard({ job, frame, onJobUpdate }: { {generating ? "生成中…(约 5-15 秒)" : `⚡ 生成 1 张${gens.length > 0 ? "(再来一张)" : ""}`} - {/* 生成结果网格 */} + {/* 生成结果网格 — 按视频原比例 + 点击放大 */} {gens.length > 0 && (
@@ -768,31 +771,95 @@ function ImageGenCard({ job, frame, onJobUpdate }: {
{gens.map((g) => ( - + + {/* 右上角独立选用按钮(总显示,selected=绿) */} + +
))}
)} + + {/* 大图预览 modal */} + {previewGenId && previewMounted && createPortal( + (() => { + const g = gens.find((x) => x.id === previewGenId) + if (!g) return null + return ( +
setPreviewGenId(null)} + className="fixed inset-0 z-[200] bg-black/85 backdrop-blur-xl flex items-center justify-center p-6 cursor-zoom-out" + style={{ animation: "drawer-in 0.18s cubic-bezier(0.32, 0.72, 0, 1)" }} + > +
e.stopPropagation()} + className="relative max-w-full max-h-full flex flex-col gap-2 cursor-default" + > + {`gen +
+
+ 分镜 {frame.index + 1} · {g.mode === "edit" ? "i2i" : "text"} · {g.model} +
+
+ + +
+
+
+
+ ) + })(), + document.body + )} ) } diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 7056a75..02d65c3 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -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>; 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 ( - } - title="生图 · Image Gen" - subtitle="STEP 8 · nano-banana / GPT" - selected={selected} - > -
-
- nano-banana-pro
Gemini 3 Image +
+ {/* 节点上方:每帧 1 张缩略图 */} + {previews.length > 0 && job && ( +
+ {previews.map((p) => ( + + ))}
-
- GPT Image
OpenAI -
-
- + )} + + } + title="生图 · Image Gen" + subtitle={`STEP 6 · nano-banana ${totalGens > 0 ? `· ${totalGens} 张` : ""}`} + width={IMAGEGEN_WIDTH} + selected={selected} + > + {totalGens > 0 ? ( +
+ 已生成 {totalGens} 张 · 选用 {selectedCount}/{previews.length} +
+ + 上方缩略图点击展开分镜 · 在 sidebar「生图」节点调参再生成 + +
+ ) : ( +
+
+ nano-banana-pro
Gemini 3 Image +
+
+ GPT Image
OpenAI +
+
+ )} +
+
) }