From de1254ff10f9b29d2e8ed2f8a5998b077cac17a5 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 15:22:51 +0800 Subject: [PATCH] auto-save 2026-05-13 15:22 (~5) --- .memory/worklog.json | 13 +++++ web/app/page.tsx | 22 +++++++- web/components/lightbox.tsx | 30 ++++++++++ web/components/nodes/index.tsx | 48 ++++++++++++++-- web/components/storyboard-bar.tsx | 93 ++++++++++++++++++++----------- 5 files changed, 167 insertions(+), 39 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 47c3459..4935fff 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1762,6 +1762,19 @@ "message": "auto-save 2026-05-13 15:11 (~3)", "hash": "02df0c5", "files_changed": 3 + }, + { + "ts": "2026-05-13T15:17:18+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 15:17 (~6)", + "hash": "6390472", + "files_changed": 6 + }, + { + "ts": "2026-05-13T07:17:40Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 15:17 (~6)", + "files_changed": 1 } ] } diff --git a/web/app/page.tsx b/web/app/page.tsx index 78852d0..69e8fda 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -15,7 +15,7 @@ import { import { ThemeToggle } from "@/components/theme-toggle" import { Dashboard, type DashboardHandle } from "@/components/dashboard" import { StoryboardBar } from "@/components/storyboard-bar" -import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, type Job } from "@/lib/api" +import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, pushStoryboardImage, type Job } from "@/lib/api" import { VideoLightbox } from "@/components/video-lightbox" const NODE_TYPES = { @@ -191,6 +191,23 @@ export default function Home() { } }, [activeJobId, setJob]) + const handlePushToStoryboard = useCallback(async (payload: { kind: "keyframe" | "cutout"; frameIdx: number; elementId?: string; cutoutId?: string; label?: string }) => { + if (!activeJobId) return + try { + const updated = await pushStoryboardImage(activeJobId, { + kind: payload.kind, + frame_idx: payload.frameIdx, + element_id: payload.elementId, + cutout_id: payload.cutoutId, + label: payload.label, + }) + setJob(updated) + toast.success("已推送到分镜头编排") + } catch (e) { + toast.error("推送失败:" + (e instanceof Error ? e.message : String(e))) + } + }, [activeJobId, setJob]) + // URL ?job=xxx,yyy 自动恢复多个 job useEffect(() => { const params = new URLSearchParams(window.location.search) @@ -261,7 +278,8 @@ export default function Home() { onDeleteFrame: handleDeleteFrame, onDeleteGenerated: handleDeleteGenerated, onOpenStoryboard: (idx: number) => setStoryboardFrame(idx), - }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated]) + onPushToStoryboard: handlePushToStoryboard, + }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handlePushToStoryboard]) // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) const [nodes, setNodes, onNodesChange] = useNodesState( diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 6f9f6aa..b195f92 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -5,6 +5,7 @@ import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, Ref import { frameUrl, cleanedFrameUrl, cutoutUrl, describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, deleteElement, cutoutElement, deleteCutout, + pushStoryboardImage, type KeyFrame, type Job, } from "@/lib/api" import { toast } from "sonner" @@ -233,6 +234,23 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o } } + const handlePushCutout = async (elementId: string, cutoutId: string, label: string, isLegacy: boolean) => { + if (activeIndex === null) return + try { + const updated = await pushStoryboardImage(jobId, { + kind: "cutout", + frame_idx: activeIndex, + element_id: elementId, + cutout_id: isLegacy ? elementId : cutoutId, // legacy 兼容 + label, + }) + onJobUpdate?.(updated) + toast.success("已推送到分镜头编排") + } catch (e) { + toast.error("推送失败:" + (e instanceof Error ? e.message : String(e))) + } + } + const handleDeleteCutout = async (elementId: string, cutoutId: string) => { try { const updated = await deleteCutout(jobId, f.index, elementId, cutoutId) @@ -696,6 +714,18 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
#{ci + 1}
+ {/* 上推按钮:左上角 */} + {/* 删除该张 — 仅 v2 多图支持,老 fallback 不显示 */} {e.cutouts && e.cutouts.length > 0 && ( + {/* 上推按钮:hover 时浮出 — 推送关键帧本身到分镜头编排 */} + {d.onPushToStoryboard && ( + + )} {/* 删除按钮:hover 时右上角浮出 */} {d.onDeleteFrame && ( + {/* 上推按钮:hover 时浮出 */} + {d.onPushToStoryboard && ( + + )} ) })} diff --git a/web/components/storyboard-bar.tsx b/web/components/storyboard-bar.tsx index b86d80c..20da9c9 100644 --- a/web/components/storyboard-bar.tsx +++ b/web/components/storyboard-bar.tsx @@ -145,44 +145,71 @@ export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame, ) )} - {/* 所有提取图(按分镜顺序展平) */} - {!collapsed && allShots.length > 0 && ( + {/* 已推送到分镜头编排区的图片 */} + {!collapsed && (
- 分镜素材 · {allShots.length} 张提取图 + 已推送素材 · {pushedImages.length} 张 + (在各处图片右上角点 ⬆ 推送过来)
-
- {allShots.map((s) => { - const url = s.isLegacy - ? cutoutUrl(job.id, s.frameIdx, s.elementId) - : cutoutUrl(job.id, s.frameIdx, s.elementId, s.cid) - const isFocusFrame = focusedFrame === s.frameIdx - return ( - + {/* 右上:移除按钮(hover 显示) */} +
- {/* 底部:元素名 */} -
- {s.elementName} -
- - ) - })} -
+ ) + })} + + )} )}