From 467e8f600b3b6c4d4f2a20c7e9333f225784afcd Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 16:23:35 +0800 Subject: [PATCH] auto-save 2026-05-13 16:23 (~6) --- .memory/worklog.json | 7 +++++ web/app/page.tsx | 11 ++++++-- web/components/dashboard.tsx | 1 + web/components/lightbox.tsx | 37 +++++++++++++++---------- web/components/nodes/index.tsx | 35 +++++++++++------------ web/components/storyboard-workbench.tsx | 7 +++-- 6 files changed, 62 insertions(+), 36 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 8e4b8d5..8c97666 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1881,6 +1881,13 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 3 项未提交变更 · 最近提交:auto-save 2026-05-13 16:12 (~1)", "files_changed": 3 + }, + { + "ts": "2026-05-13T16:18:05+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 16:17 (~3)", + "hash": "f891cbc", + "files_changed": 3 } ] } diff --git a/web/app/page.tsx b/web/app/page.tsx index 4c121ba..d69fe7a 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -16,7 +16,7 @@ import { ThemeToggle } from "@/components/theme-toggle" import { Dashboard, type DashboardHandle } from "@/components/dashboard" import { StoryboardBar } from "@/components/storyboard-bar" import { StoryboardWorkbench } from "@/components/storyboard-workbench" -import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, pushStoryboardImage, type Job } from "@/lib/api" +import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, pushStoryboardImage, type Job, type ImageRef } from "@/lib/api" import { VideoLightbox } from "@/components/video-lightbox" const NODE_TYPES = { @@ -68,6 +68,7 @@ export default function Home() { const [videoLightboxOpen, setVideoLightboxOpen] = useState(false) const [storyboardFrame, setStoryboardFrame] = useState(null) const [workbenchOpen, setWorkbenchOpen] = useState(false) + const [clipboard, setClipboard] = useState(null) const dashboardRef = useRef(null) // 把 setJob(prev=>...) 翻译成 setJobs 里更新当前 active @@ -193,6 +194,11 @@ export default function Home() { } }, [activeJobId, setJob]) + const handleCopyImage = useCallback((ref: ImageRef) => { + setClipboard(ref) + toast.success(`已复制:${ref.label || (ref.kind === "keyframe" ? "关键帧" : "元素")} · 到分镜头编排工作台粘贴`) + }, []) + const handlePushToStoryboard = useCallback(async (payload: { kind: "keyframe" | "cutout"; frameIdx: number; elementId?: string; cutoutId?: string; label?: string }) => { if (!activeJobId) return try { @@ -281,7 +287,8 @@ export default function Home() { onDeleteGenerated: handleDeleteGenerated, onOpenStoryboard: (idx: number) => setStoryboardFrame(idx), onPushToStoryboard: handlePushToStoryboard, - }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handlePushToStoryboard]) + onCopyImage: handleCopyImage, + }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handlePushToStoryboard, handleCopyImage]) // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) const [nodes, setNodes, onNodesChange] = useNodesState( diff --git a/web/components/dashboard.tsx b/web/components/dashboard.tsx index ef94c08..366b092 100644 --- a/web/components/dashboard.tsx +++ b/web/components/dashboard.tsx @@ -327,6 +327,7 @@ export const Dashboard = forwardRef(function Dashboard({ data.onCloseExpandedFrame() setExpanded(new Set([key])) }} + onCopyImage={data.onCopyImage} /> ) : ( renderSection(t.key) diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 2f8e856..d0fdd14 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -6,7 +6,7 @@ import { frameUrl, cleanedFrameUrl, cutoutUrl, describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, deleteElement, cutoutElement, deleteCutout, pushStoryboardImage, - type KeyFrame, type Job, + type KeyFrame, type Job, type ImageRef, } from "@/lib/api" import { toast } from "sonner" @@ -20,10 +20,11 @@ interface Props { onToggleSelect: (idx: number) => void onJobUpdate?: (job: Job) => void onSwitchPanel?: (key: string) => void + onCopyImage?: (ref: ImageRef) => void embedded?: boolean } -export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, onSwitchPanel, embedded = false }: Props) { +export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, onSwitchPanel, onCopyImage, embedded = false }: Props) { const [describing, setDescribing] = useState(false) const [cleaning, setCleaning] = useState(false) const [applying, setApplying] = useState(false) @@ -714,18 +715,26 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
#{ci + 1}
- {/* 上推按钮:常驻可见 */} - + {/* 复制按钮:常驻可见 */} + {onCopyImage && ( + + )} {/* 删除该张 — 仅 v2 多图支持,老 fallback 不显示 */} {e.cutouts && e.cutouts.length > 0 && ( - {/* 上推按钮:常驻可见 — 推送关键帧本身到分镜头编排 */} - {d.onPushToStoryboard && ( + {/* 复制按钮:常驻可见 — 复制该关键帧到剪贴板 */} + {d.onCopyImage && ( )} {/* 删除按钮:hover 时右上角浮出 */} @@ -647,23 +648,23 @@ export function ImageGenNode({ data, selected }: any) { className="absolute inset-0 w-full h-full object-contain" /> - {/* 上推按钮:常驻可见 */} - {d.onPushToStoryboard && ( + {/* 复制按钮:常驻可见 — 复制元素提取图到剪贴板 */} + {d.onCopyImage && ( )} {/* hover 预览 — absolute 浮在缩略图上方 */} diff --git a/web/components/storyboard-workbench.tsx b/web/components/storyboard-workbench.tsx index 8c05c2f..77ec62e 100644 --- a/web/components/storyboard-workbench.tsx +++ b/web/components/storyboard-workbench.tsx @@ -3,8 +3,8 @@ import { useEffect, useState, useRef, type ReactNode } from "react" import { createPortal } from "react-dom" import { X, LayoutGrid, Loader2, Check, Sparkle, Wand2 } from "lucide-react" import { - type Job, type StoryboardScene, - effectiveFrameUrl, cutoutUrl, updateStoryboard, + type Job, type StoryboardScene, type ImageRef, + effectiveFrameUrl, updateStoryboard, resolveImageRefUrl, } from "@/lib/api" import { toast } from "sonner" @@ -14,13 +14,14 @@ interface Props { open: boolean onClose: () => void onJobUpdate?: (j: Job) => void + clipboard: ImageRef | null // 全局剪贴板(page.tsx 提供) } const emptyScene = (): StoryboardScene => ({ subject: "", product: "", scene: "", action: "", duration: 0, reference_ids: [], }) -export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobUpdate }: Props) { +export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobUpdate, clipboard }: Props) { const [mounted, setMounted] = useState(false) useEffect(() => setMounted(true), [])