auto-save 2026-05-13 18:29 (~2)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user