auto-save 2026-05-14 03:03 (~3)
This commit is contained in:
@@ -2979,6 +2979,25 @@
|
|||||||
"type": "session-heartbeat",
|
"type": "session-heartbeat",
|
||||||
"message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 02:52 (~4)",
|
"message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 02:52 (~4)",
|
||||||
"files_changed": 2
|
"files_changed": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-14T02:58:36+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-14 02:58 (~6)",
|
||||||
|
"hash": "bdbaf75",
|
||||||
|
"files_changed": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-13T18:58:48Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 02:58 (~6)",
|
||||||
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-13T19:03:11Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Claude 会话活跃 · 最近命令:claude · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 02:58 (~6)",
|
||||||
|
"files_changed": 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -816,6 +816,18 @@ api/main.py
|
|||||||
<h2>变更记录</h2>
|
<h2>变更记录</h2>
|
||||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||||
<div class="changelog">
|
<div class="changelog">
|
||||||
|
<article class="change">
|
||||||
|
<header>
|
||||||
|
<h3>2026-05-14 · 删除确认改为页面内分层交互</h3>
|
||||||
|
<span class="tag violet">Canvas</span>
|
||||||
|
<span class="tag blue">UX</span>
|
||||||
|
</header>
|
||||||
|
<div class="body">
|
||||||
|
<p><strong>问题:</strong>浏览器原生删除确认会突然出现在页面顶部,和无限画布的操作上下文割裂;图片类素材如果每次删除都确认,也会拖慢快速整理素材的节奏。</p>
|
||||||
|
<p><strong>改动:</strong>输入视频和生成视频任务删除改为画布内确认层,背景轻遮罩并支持点击背景 / Esc 取消;关键帧和元素提取图属于可快速整理的图片素材,点击删除后直接执行。</p>
|
||||||
|
<p><strong>影响:</strong><code>web/components/nodes/index.tsx</code>、<code>docs/source-analysis.html</code>。视频删除仍走既有删除接口;图片删除仍走原有回调,只调整确认策略。</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
<article class="change">
|
<article class="change">
|
||||||
<header>
|
<header>
|
||||||
<h3>2026-05-14 · 输入视频缩略图支持删除整个 job</h3>
|
<h3>2026-05-14 · 输入视频缩略图支持删除整个 job</h3>
|
||||||
|
|||||||
@@ -311,6 +311,73 @@ function FloatingThumbnailStrip({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DeleteConfirmDialog({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
confirmLabel = "删除",
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
confirmLabel?: string
|
||||||
|
onCancel: () => void
|
||||||
|
onConfirm: () => void
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onCancel()
|
||||||
|
}
|
||||||
|
document.addEventListener("keydown", onKeyDown)
|
||||||
|
return () => document.removeEventListener("keydown", onKeyDown)
|
||||||
|
}, [onCancel])
|
||||||
|
|
||||||
|
if (typeof document === "undefined") return null
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[1000] flex items-center justify-center bg-black/55 px-4 backdrop-blur-sm"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.target === e.currentTarget) onCancel()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={title}
|
||||||
|
className="w-full max-w-sm rounded-xl border border-white/14 bg-zinc-950/95 p-4 text-white shadow-2xl"
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-rose-500/16 text-rose-200">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold">{title}</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs leading-5 text-white/68">{description}</p>
|
||||||
|
<div className="mt-4 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onCancel() }}
|
||||||
|
className="rounded-md border border-white/12 bg-white/[0.04] px-3 py-1.5 text-xs font-medium text-white/78 transition hover:bg-white/[0.08] hover:text-white"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onConfirm() }}
|
||||||
|
className="rounded-md bg-rose-500 px-3 py-1.5 text-xs font-semibold text-white shadow-lg shadow-rose-950/35 transition hover:bg-rose-400"
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
1. InputNode — TK 链接 / 上传
|
1. InputNode — TK 链接 / 上传
|
||||||
============================================================ */
|
============================================================ */
|
||||||
@@ -322,6 +389,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
|||||||
const [videoExpanded, setVideoExpanded] = useState(false)
|
const [videoExpanded, setVideoExpanded] = useState(false)
|
||||||
const [hoverPreviewJob, setHoverPreviewJob] = useState<PreviewAnchor<string> | null>(null)
|
const [hoverPreviewJob, setHoverPreviewJob] = useState<PreviewAnchor<string> | null>(null)
|
||||||
const [pinnedPreviewJob, setPinnedPreviewJob] = useState<PreviewAnchor<string> | null>(null)
|
const [pinnedPreviewJob, setPinnedPreviewJob] = useState<PreviewAnchor<string> | null>(null)
|
||||||
|
const [deleteJobTarget, setDeleteJobTarget] = useState<Job | null>(null)
|
||||||
const rootRef = useRef<HTMLDivElement>(null)
|
const rootRef = useRef<HTMLDivElement>(null)
|
||||||
const fileRef = useRef<HTMLInputElement>(null)
|
const fileRef = useRef<HTMLInputElement>(null)
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
@@ -417,9 +485,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (confirm(`删除视频任务 ${j.id.slice(0, 8)}?源视频、关键帧、元素提取图和生成视频都会一并删除。`)) {
|
setDeleteJobTarget(j)
|
||||||
d.onDeleteJob?.(j.id)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
title="删除这个输入视频"
|
title="删除这个输入视频"
|
||||||
className="absolute right-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
className="absolute right-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||||
@@ -433,6 +499,20 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
|||||||
</FloatingThumbnailStrip>
|
</FloatingThumbnailStrip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{deleteJobTarget && (
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
title="删除这个输入视频?"
|
||||||
|
description={`会同时删除 ${deleteJobTarget.id.slice(0, 8)} 的源视频、关键帧、元素提取图、生成视频和本地任务目录。`}
|
||||||
|
confirmLabel="删除视频"
|
||||||
|
onCancel={() => setDeleteJobTarget(null)}
|
||||||
|
onConfirm={() => {
|
||||||
|
const id = deleteJobTarget.id
|
||||||
|
setDeleteJobTarget(null)
|
||||||
|
d.onDeleteJob?.(id)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const anchor = pinnedPreviewJob ?? hoverPreviewJob
|
const anchor = pinnedPreviewJob ?? hoverPreviewJob
|
||||||
if (!anchor || videoExpanded) return null
|
if (!anchor || videoExpanded) return null
|
||||||
@@ -714,6 +794,7 @@ export function VisualLabNode({ data, selected }: any) {
|
|||||||
|
|
||||||
const [hoverPreview, setHoverPreview] = useState<PreviewAnchor<string> | null>(null)
|
const [hoverPreview, setHoverPreview] = useState<PreviewAnchor<string> | null>(null)
|
||||||
const [pinnedPreview, setPinnedPreview] = useState<PreviewAnchor<string> | null>(null)
|
const [pinnedPreview, setPinnedPreview] = useState<PreviewAnchor<string> | null>(null)
|
||||||
|
const [deleteVideoTarget, setDeleteVideoTarget] = useState<{ id: string; label: string; caption: string } | null>(null)
|
||||||
const rootRef = useRef<HTMLDivElement>(null)
|
const rootRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const previews: VisualPreview[] = [
|
const previews: VisualPreview[] = [
|
||||||
@@ -885,7 +966,7 @@ export function VisualLabNode({ data, selected }: any) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (confirm(`删除${p.label}?相关清洗 / 抠图 / 生成图都会一并清除。`)) d.onDeleteFrame?.(p.frameIdx)
|
d.onDeleteFrame?.(p.frameIdx)
|
||||||
}}
|
}}
|
||||||
title="删除该关键帧"
|
title="删除该关键帧"
|
||||||
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||||
@@ -899,9 +980,7 @@ export function VisualLabNode({ data, selected }: any) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (confirm(`删除元素提取图「${p.label}」?该 cutout 文件会被移除。`)) {
|
d.onDeleteCutout?.(p.frameIdx, p.elementId, p.cutoutId)
|
||||||
d.onDeleteCutout?.(p.frameIdx, p.elementId, p.cutoutId)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
title="删除该提取图"
|
title="删除该提取图"
|
||||||
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||||
@@ -915,7 +994,7 @@ export function VisualLabNode({ data, selected }: any) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
d.onDeleteVideo?.(p.videoId)
|
setDeleteVideoTarget({ id: p.videoId, label: p.label, caption: p.caption })
|
||||||
}}
|
}}
|
||||||
className="absolute right-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
className="absolute right-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||||
title="删除这个视频任务"
|
title="删除这个视频任务"
|
||||||
@@ -929,6 +1008,20 @@ export function VisualLabNode({ data, selected }: any) {
|
|||||||
</FloatingThumbnailStrip>
|
</FloatingThumbnailStrip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{deleteVideoTarget && (
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
title="删除这个视频任务?"
|
||||||
|
description={`会删除${deleteVideoTarget.label}的生成任务和本地视频文件。${deleteVideoTarget.caption}`}
|
||||||
|
confirmLabel="删除视频任务"
|
||||||
|
onCancel={() => setDeleteVideoTarget(null)}
|
||||||
|
onConfirm={() => {
|
||||||
|
const id = deleteVideoTarget.id
|
||||||
|
setDeleteVideoTarget(null)
|
||||||
|
d.onDeleteVideo?.(id)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const anchor = pinnedPreview ?? hoverPreview
|
const anchor = pinnedPreview ?? hoverPreview
|
||||||
if (!anchor) return null
|
if (!anchor) return null
|
||||||
@@ -1107,9 +1200,7 @@ export function KeyframeNode({ data, selected }: any) {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (confirm(`删除分镜 ${f.index + 1}(${f.timestamp.toFixed(1)}s)?相关清洗 / 抠图 / 生成图都会一并清除。`)) {
|
d.onDeleteFrame?.(f.index)
|
||||||
d.onDeleteFrame?.(f.index)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
title="删除该关键帧"
|
title="删除该关键帧"
|
||||||
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||||
@@ -1608,9 +1699,7 @@ export function StoryboardNode({ data, selected }: any) {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (confirm(`删除元素提取图「${p.name}」?该 cutout 文件会被移除。`)) {
|
d.onDeleteCutout?.(p.frameIdx, p.elementId, p.cid)
|
||||||
d.onDeleteCutout?.(p.frameIdx, p.elementId, p.cid)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
title="删除该提取图"
|
title="删除该提取图"
|
||||||
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||||
@@ -1684,6 +1773,7 @@ export function VideoGenNode({ data, selected }: any) {
|
|||||||
const videos = d.job?.generated_videos ?? []
|
const videos = d.job?.generated_videos ?? []
|
||||||
const rootRef = useRef<HTMLDivElement>(null)
|
const rootRef = useRef<HTMLDivElement>(null)
|
||||||
const [hoverPreviewVideo, setHoverPreviewVideo] = useState<PreviewAnchor<string> | null>(null)
|
const [hoverPreviewVideo, setHoverPreviewVideo] = useState<PreviewAnchor<string> | null>(null)
|
||||||
|
const [deleteVideoTarget, setDeleteVideoTarget] = useState<{ id: string; label: string; caption: string } | null>(null)
|
||||||
const running = videos.some((v) => v.status === "queued" || v.status === "in_progress")
|
const running = videos.some((v) => v.status === "queued" || v.status === "in_progress")
|
||||||
const completed = videos.filter((v) => v.status === "completed" && v.url)
|
const completed = videos.filter((v) => v.status === "completed" && v.url)
|
||||||
const failed = videos.some((v) => v.status === "failed")
|
const failed = videos.some((v) => v.status === "failed")
|
||||||
@@ -1774,7 +1864,11 @@ export function VideoGenNode({ data, selected }: any) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
d.onDeleteVideo?.(v.id)
|
setDeleteVideoTarget({
|
||||||
|
id: v.id,
|
||||||
|
label: `视频 ${i + 1}`,
|
||||||
|
caption: `分镜 ${v.frame_idx + 1} · ${modelLabel(v.model)} · ${v.status}`,
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
className="absolute right-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
className="absolute right-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||||
title="删除这个视频任务"
|
title="删除这个视频任务"
|
||||||
@@ -1785,6 +1879,19 @@ export function VideoGenNode({ data, selected }: any) {
|
|||||||
)})}
|
)})}
|
||||||
</FloatingThumbnailStrip>
|
</FloatingThumbnailStrip>
|
||||||
)}
|
)}
|
||||||
|
{deleteVideoTarget && (
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
title="删除这个视频任务?"
|
||||||
|
description={`会删除${deleteVideoTarget.label}的生成任务和本地视频文件。${deleteVideoTarget.caption}`}
|
||||||
|
confirmLabel="删除视频任务"
|
||||||
|
onCancel={() => setDeleteVideoTarget(null)}
|
||||||
|
onConfirm={() => {
|
||||||
|
const id = deleteVideoTarget.id
|
||||||
|
setDeleteVideoTarget(null)
|
||||||
|
d.onDeleteVideo?.(id)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{(() => {
|
{(() => {
|
||||||
if (!hoverPreviewVideo) return null
|
if (!hoverPreviewVideo) return null
|
||||||
const item = videos.find((v) => v.id === hoverPreviewVideo.id)
|
const item = videos.find((v) => v.id === hoverPreviewVideo.id)
|
||||||
|
|||||||
Reference in New Issue
Block a user