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

This commit is contained in:
2026-05-13 18:29:59 +08:00
parent 9a9c0cca9c
commit bc458b65f1
2 changed files with 180 additions and 79 deletions

View File

@@ -2089,6 +2089,19 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-13 18:18 (~5)",
"files_changed": 1
},
{
"ts": "2026-05-13T18:24:27+08:00",
"type": "commit",
"message": "auto-save 2026-05-13 18:24 (~3)",
"hash": "9a9c0cc",
"files_changed": 3
},
{
"ts": "2026-05-13T10:29:28Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 2 项未提交变更 · 最近提交auto-save 2026-05-13 18:24 (~3)",
"files_changed": 2
}
]
}

View File

@@ -1,10 +1,10 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw, Plus, Sparkle, Crop } from "lucide-react"
import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw, Plus, Sparkle, Crop, Copy, PencilLine, Trash2, Save } from "lucide-react"
import {
frameUrl, cleanedFrameUrl, cutoutUrl,
describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, deleteElement, cutoutElement, deleteCutout,
describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, updateElement, deleteElement, cutoutElement, deleteCutout,
pushStoryboardImage,
type KeyFrame, type Job, type ImageRef,
} from "@/lib/api"
@@ -31,6 +31,12 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
const [cuttingId, setCuttingId] = useState<string | null>(null)
const [addingZh, setAddingZh] = useState(false)
const [addInput, setAddInput] = useState("")
const [editingElement, setEditingElement] = useState<{
id: string
name_zh: string
name_en: string
position: string
} | null>(null)
const [mounted, setMounted] = useState(false)
// 画框模式 + 多选区(相对坐标 0-1
type Region = { x: number; y: number; w: number; h: number }
@@ -217,11 +223,28 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
try {
const updated = await deleteElement(jobId, f.index, id)
onJobUpdate?.(updated)
if (editingElement?.id === id) setEditingElement(null)
} catch (e) {
toast.error("删除失败:" + (e instanceof Error ? e.message : String(e)))
}
}
const handleUpdateElement = async () => {
if (!editingElement || !editingElement.name_zh.trim()) return
try {
const updated = await updateElement(jobId, f.index, editingElement.id, {
name_zh: editingElement.name_zh,
name_en: editingElement.name_en,
position: editingElement.position,
})
onJobUpdate?.(updated)
setEditingElement(null)
toast.success("元素已更新")
} catch (e) {
toast.error("更新失败:" + (e instanceof Error ? e.message : String(e)))
}
}
const handleCutout = async (id: string) => {
setCuttingId(id)
try {
@@ -629,7 +652,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
{elements.length > 0 && (
<span className="text-[10px] text-white/35 font-mono ml-0.5">· {elements.length}</span>
)}
<span className="text-[9.5px] text-white/35 font-normal ml-auto"> </span>
<span className="text-[9.5px] text-white/35 font-normal ml-auto"> </span>
</div>
<div className="mb-2 rounded-md border border-white/10 bg-white/[0.035] px-2.5 py-1.5 text-[10.5px] leading-relaxed text-white/45">
Vision
</div>
{elements.length > 0 && (
@@ -642,50 +668,116 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
: (e.cutout_id ? [e.cutout_id] : [])
const hasAny = cutouts.length > 0
const hasRegion = !!e.region
const isEditing = editingElement?.id === e.id
return (
<div
key={e.id}
className="group/c relative rounded-md bg-white/[0.04] border border-white/10 p-2"
className={`relative rounded-md border p-2 ${
isEditing
? "bg-violet-500/10 border-violet-300/45"
: "bg-white/[0.04] border-white/10"
}`}
>
{/* 顶部:名字 + 操作 */}
<div className="flex items-start gap-2 mb-2">
<div className="flex-1 min-w-0 pr-5">
<div className="flex items-center gap-1 text-white text-[12px] font-medium leading-tight truncate">
<span className="truncate">{e.name_zh}</span>
{e.source === "auto" && (
<span className="text-[8.5px] text-pink-300/70 font-mono uppercase">auto</span>
)}
{e.source === "region" && (
<span className="text-[8.5px] text-cyan-300/80 font-mono uppercase">box</span>
)}
{cutouts.length > 0 && (
<span className="text-[8.5px] text-white/40 font-mono">· {cutouts.length} </span>
<div className="mb-2 space-y-2">
{isEditing ? (
<div className="space-y-1.5">
<input
value={editingElement.name_zh}
onChange={(ev) => setEditingElement({ ...editingElement, name_zh: ev.target.value })}
placeholder="元素中文名"
className="w-full rounded-md border border-white/15 bg-black/35 px-2 py-1.5 text-[12px] text-white outline-none placeholder:text-white/30 focus:border-violet-300/60"
/>
<input
value={editingElement.name_en}
onChange={(ev) => setEditingElement({ ...editingElement, name_en: ev.target.value })}
placeholder="英文提取提示,可手动修正"
className="w-full rounded-md border border-white/15 bg-black/35 px-2 py-1.5 text-[11px] font-mono text-white outline-none placeholder:text-white/30 focus:border-violet-300/60"
/>
<input
value={editingElement.position}
onChange={(ev) => setEditingElement({ ...editingElement, position: ev.target.value })}
placeholder="位置 / 备注,例如:画面左下角、手里拿着"
className="w-full rounded-md border border-white/15 bg-black/35 px-2 py-1.5 text-[11px] text-white outline-none placeholder:text-white/30 focus:border-violet-300/60"
/>
</div>
) : (
<div className="min-w-0">
<div className="flex items-center gap-1 text-white text-[12px] font-medium leading-tight">
<span className="truncate">{e.name_zh}</span>
{e.source === "auto" && (
<span className="text-[8.5px] text-pink-300/70 font-mono uppercase">auto</span>
)}
{e.source === "region" && (
<span className="text-[8.5px] text-cyan-300/80 font-mono uppercase">box</span>
)}
{cutouts.length > 0 && (
<span className="text-[8.5px] text-white/40 font-mono">· {cutouts.length} </span>
)}
</div>
<div className="mt-0.5 text-[10px] font-mono leading-tight truncate text-white/45">
{e.name_en || <span className="text-white/30">()</span>}
</div>
{e.position && (
<div className="mt-0.5 text-[9.5px] leading-tight truncate text-white/35">
{e.position}
</div>
)}
</div>
<div className="mt-0.5 text-[10px] font-mono leading-tight truncate text-white/45">
{e.name_en || <span className="text-white/30">()</span>}
</div>
)}
<div className="flex items-center gap-1">
{isEditing ? (
<>
<button
onClick={handleUpdateElement}
disabled={!editingElement.name_zh.trim()}
className="flex-1 rounded px-2 py-1 text-[10.5px] font-medium inline-flex items-center justify-center gap-1 bg-violet-500 hover:bg-violet-400 text-white disabled:opacity-40"
>
<Save className="h-3 w-3" />
</button>
<button
onClick={() => setEditingElement(null)}
className="rounded px-2 py-1 text-[10.5px] bg-white/10 hover:bg-white/20 text-white"
>
</button>
</>
) : (
<>
<button
onClick={() => setEditingElement({
id: e.id,
name_zh: e.name_zh,
name_en: e.name_en || "",
position: e.position || "",
})}
className="rounded px-2 py-1 text-[10.5px] inline-flex items-center gap-1 bg-white/8 hover:bg-white/15 text-white/75 hover:text-white border border-white/10"
>
<PencilLine className="h-3 w-3" />
</button>
<button
onClick={() => handleCutout(e.id)}
disabled={isCutting}
title={`${hasAny ? "再提取一张" : "AI 提取"} · 模型识别 + 补全缺失部分(如缺手脚 / 半个台灯)→ 完整清晰白底图5-15s`}
className="flex-1 rounded px-2 py-1 text-[10.5px] font-medium inline-flex items-center justify-center gap-1 transition disabled:opacity-50 disabled:cursor-not-allowed bg-violet-500/30 text-white/90 hover:bg-violet-500/50"
>
{isCutting ? <Loader2 className="h-3 w-3 animate-spin" /> : <Sparkle className="h-3 w-3" />}
{isCutting ? "提取中…" : hasAny ? "再提取" : "AI 提取"}
</button>
<button
onClick={() => handleDeleteElement(e.id)}
className="rounded px-2 py-1 text-[10.5px] inline-flex items-center gap-1 bg-rose-500/15 hover:bg-rose-500/30 text-rose-100 border border-rose-300/20"
title="删除整个元素和它的提取图"
>
<Trash2 className="h-3 w-3" />
</button>
</>
)}
</div>
{/* 提取按钮 — AI 补全完整元素(每次累积一张) */}
<button
onClick={() => handleCutout(e.id)}
disabled={isCutting}
title={`${hasAny ? "再提取一张" : "AI 提取"} · 模型识别 + 补全缺失部分(如缺手脚 / 半个台灯)→ 完整清晰白底图5-15s`}
className="shrink-0 text-[10.5px] px-2 py-1 rounded inline-flex items-center gap-1 transition disabled:opacity-50 disabled:cursor-not-allowed font-medium bg-violet-500/30 text-white/90 hover:bg-violet-500/50"
>
{isCutting ? <Loader2 className="h-3 w-3 animate-spin" /> : <Sparkle className="h-3 w-3" />}
{isCutting ? "提取中…" : hasAny ? "再提取" : "AI 提取"}
</button>
{/* 删除整条元素 */}
<button
onClick={() => handleDeleteElement(e.id)}
className="absolute right-1 top-1 h-4 w-4 rounded-sm text-white/40 hover:text-white hover:bg-white/10 inline-flex items-center justify-center opacity-0 group-hover/c:opacity-100 transition"
title="删除该元素"
>
<X className="h-2.5 w-2.5" />
</button>
</div>
{/* 多张提取图横向 grid */}
@@ -699,52 +791,48 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
return (
<div
key={cid}
className="group/img relative rounded-md border border-white/10 bg-white overflow-hidden"
style={{ width: 80, height: 80 }}
className="relative rounded-md border border-white/10 bg-white overflow-hidden"
style={{ width: 92, height: 112 }}
>
<a
href={url}
target="_blank"
rel="noreferrer"
title="点击新窗口查看原图"
className="block w-full h-full"
>
<div className="h-[80px] w-full bg-white">
<img src={url} alt={`${e.name_zh} #${ci + 1}`} className="w-full h-full object-contain" />
</a>
</div>
{/* 序号 */}
<div className="absolute bottom-0 left-0 text-[8.5px] font-mono text-white bg-black/70 backdrop-blur px-1 rounded-tr">
#{ci + 1}
</div>
{/* 复制按钮:常驻可见 */}
{onCopyImage && (
<button
onClick={(ev) => {
ev.preventDefault(); ev.stopPropagation()
const isLegacy = !(e.cutouts && e.cutouts.length > 0)
onCopyImage({
kind: "cutout",
frame_idx: f.index,
element_id: e.id,
cutout_id: isLegacy ? e.id : cid,
label: `${e.name_zh} #${ci + 1}`,
})
}}
className="absolute left-0.5 top-0.5 h-4 w-4 rounded-sm bg-violet-500/90 text-white shadow hover:bg-violet-400 inline-flex items-center justify-center transition text-[9px] leading-none"
title="📋 复制此图(到分镜编排工作台插槽粘贴)"
>
📋
</button>
)}
{/* 删除该张 — 仅 v2 多图支持,老 fallback 不显示 */}
{e.cutouts && e.cutouts.length > 0 && (
<button
onClick={(ev) => { ev.preventDefault(); handleDeleteCutout(e.id, cid) }}
className="absolute right-0.5 top-0.5 h-4 w-4 rounded-sm bg-black/70 text-white/80 hover:bg-rose-500 hover:text-white inline-flex items-center justify-center opacity-0 group-hover/img:opacity-100 transition"
title="删除该张"
>
<X className="h-2.5 w-2.5" />
</button>
)}
<div className="flex h-8 border-t border-black/10 bg-black text-white">
{onCopyImage && (
<button
onClick={(ev) => {
ev.preventDefault(); ev.stopPropagation()
const isLegacy = !(e.cutouts && e.cutouts.length > 0)
onCopyImage({
kind: "cutout",
frame_idx: f.index,
element_id: e.id,
cutout_id: isLegacy ? e.id : cid,
label: `${e.name_zh} #${ci + 1}`,
})
}}
className="flex-1 inline-flex items-center justify-center gap-1 text-[9.5px] hover:bg-violet-500/70 transition"
title="复制到分镜编排剪贴板"
>
<Copy className="h-2.5 w-2.5" />
</button>
)}
{e.cutouts && e.cutouts.length > 0 && (
<button
onClick={(ev) => { ev.preventDefault(); handleDeleteCutout(e.id, cid) }}
className="flex-1 inline-flex items-center justify-center gap-1 text-[9.5px] hover:bg-rose-500/80 transition border-l border-white/10"
title="删除这张错误提取图"
>
<Trash2 className="h-2.5 w-2.5" />
</button>
)}
</div>
</div>
)
})}