auto-save 2026-05-13 09:42 (~2)

This commit is contained in:
2026-05-13 09:42:47 +08:00
parent 839a3f6d4b
commit 3958b51f54
2 changed files with 95 additions and 16 deletions

View File

@@ -1131,6 +1131,19 @@
"message": "auto-save 2026-05-13 09:31 (~3)",
"hash": "fdc3162",
"files_changed": 3
},
{
"ts": "2026-05-13T09:37:15+08:00",
"type": "commit",
"message": "auto-save 2026-05-13 09:37 (~4)",
"hash": "839a3f6",
"files_changed": 4
},
{
"ts": "2026-05-13T01:37:36Z",
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 1 项未提交变更 · 最近提交auto-save 2026-05-13 09:37 (~4)",
"files_changed": 1
}
]
}

View File

@@ -2,7 +2,7 @@
import { useEffect, useState } from "react"
import { createPortal } from "react-dom"
import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw, Plus } from "lucide-react"
import { frameUrl, describeFrame, translateText, type KeyFrame, type Job } from "@/lib/api"
import { frameUrl, describeFrame, translateText, generateImage, generatedImageUrl, type KeyFrame, type Job } from "@/lib/api"
import { toast } from "sonner"
type CustomItem = { id: string; zh: string; en: string; translating: boolean }
@@ -21,6 +21,7 @@ interface Props {
export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, embedded = false }: Props) {
const [describing, setDescribing] = useState(false)
const [generating, setGenerating] = useState(false)
const [mounted, setMounted] = useState(false)
// 自定义提取元素 — 按 frame 隔离,切换 frame 后回到同一帧时还能看到之前加的
const [customsByFrame, setCustomsByFrame] = useState<Record<number, CustomItem[]>>({})
@@ -79,6 +80,35 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
const isSelected = selected.has(f.index)
const desc = f.description
const handleGenerateMat = async () => {
if (activeIndex === null || !f) return
const base = f.description?.suggested_prompt?.trim()
if (!base) {
toast.error("请先识别此分镜(右上角『识别』按钮)")
return
}
// 自动选用此帧 → ImageGenCard 才会渲染
if (!selected.has(f.index)) onToggleSelect(f.index)
const extraEn = customs.filter((c) => c.en).map((c) => c.en).join(", ")
setGenerating(true)
try {
const updated = await generateImage(jobId, f.index, {
prompt: base,
extra_prompt: extraEn,
negative_prompt: "水印, @用户名, TikTok logo, 平台文字, 浮水印",
model: "gemini-3-pro-image-preview",
mode: "edit",
})
onJobUpdate?.(updated)
toast.success(`分镜 ${f.index + 1} 垫图生成完成 · 已加入生图卡`)
} catch (e) {
toast.error("生图失败:" + (e instanceof Error ? e.message : String(e)))
} finally {
setGenerating(false)
}
}
const handleDescribe = async () => {
setDescribing(true)
try {
@@ -310,25 +340,61 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</div>
<button
disabled
className="mt-2 w-full text-[11.5px] py-1.5 rounded-md bg-violet-500/40 text-white inline-flex items-center justify-center gap-1.5 cursor-not-allowed disabled:opacity-50"
title="即将上线:批量调 nano-banana-pro image edit"
onClick={handleGenerateMat}
disabled={generating || !desc?.suggested_prompt}
className="mt-2 w-full text-[12px] py-2 rounded-md bg-gradient-to-r from-rose-500 to-pink-500 text-white hover:opacity-95 disabled:opacity-40 disabled:cursor-not-allowed inline-flex items-center justify-center gap-1.5 font-semibold transition"
title={!desc?.suggested_prompt ? "先识别此分镜" : `合成元素到关键帧场景(${customs.length} 条自定义 + 识别结果)`}
>
<Wand2 className="h-3.5 w-3.5" />
{generating ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Wand2 className="h-3.5 w-3.5" />}
{generating ? "生成中…5-15 秒)" : "⚡ 生成垫图"}
</button>
{!desc?.suggested_prompt && (
<div className="mt-1 text-[10px] text-white/40 text-center"></div>
)}
{desc?.suggested_prompt && customs.length === 0 && (
<div className="mt-1 text-[10px] text-white/35 text-center"> · </div>
)}
</section>
{/* 已提取 */}
<section>
<div className="flex items-center gap-1.5 mb-2 text-white text-[12.5px] font-semibold">
<Check className="h-3.5 w-3.5" />
</div>
<div className="rounded-lg border border-dashed border-white/10 bg-white/[0.02] p-3 text-[11px] text-white/40 text-center">
·
</div>
</section>
{/* 已生成的垫图(与生图卡同源) */}
{f.generated_images && f.generated_images.length > 0 && (
<section>
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5 text-white text-[12.5px] font-semibold">
<Check className="h-3.5 w-3.5" />
<span className="text-[10px] text-white/35 font-mono">· {f.generated_images.length}</span>
</div>
<span className="text-[9.5px] text-white/35"> </span>
</div>
<div className="grid grid-cols-3 gap-1.5">
{f.generated_images.map((g) => (
<a
key={g.id}
href={generatedImageUrl(jobId, f.index, g.id)}
target="_blank"
rel="noreferrer"
title={g.prompt}
className={`relative aspect-square rounded-md overflow-hidden border-2 transition ${
g.selected ? "border-emerald-400 ring-2 ring-emerald-400/40" : "border-white/15 hover:border-white/40"
}`}
>
<img
src={generatedImageUrl(jobId, f.index, g.id)}
alt={`gen ${g.id}`}
className="absolute inset-0 w-full h-full object-cover"
/>
{g.selected && (
<div className="absolute top-1 right-1 bg-emerald-500 text-white rounded-full p-0.5">
<Check className="h-2.5 w-2.5" />
</div>
)}
</a>
))}
</div>
</section>
)}
</div>
</div>