auto-save 2026-05-13 16:23 (~6)

This commit is contained in:
2026-05-13 16:23:35 +08:00
parent f891cbc2e2
commit 467e8f600b
6 changed files with 62 additions and 36 deletions

View File

@@ -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
}
]
}

View File

@@ -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<number | null>(null)
const [workbenchOpen, setWorkbenchOpen] = useState(false)
const [clipboard, setClipboard] = useState<ImageRef | null>(null)
const dashboardRef = useRef<DashboardHandle>(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<Node>(

View File

@@ -327,6 +327,7 @@ export const Dashboard = forwardRef<DashboardHandle, Props>(function Dashboard({
data.onCloseExpandedFrame()
setExpanded(new Set([key]))
}}
onCopyImage={data.onCopyImage}
/>
) : (
renderSection(t.key)

View File

@@ -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
<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>
{/* 上推按钮:常驻可见 */}
<button
onClick={(ev) => {
ev.preventDefault(); ev.stopPropagation()
const isLegacy = !(e.cutouts && e.cutouts.length > 0)
handlePushCutout(e.id, cid, `${e.name_zh} #${ci + 1}`, isLegacy)
}}
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-[11px] leading-none font-bold"
title="⬆ 上推到分镜头编排"
>
</button>
{/* 复制按钮:常驻可见 */}
{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

View File

@@ -7,7 +7,7 @@ import {
Mic, Languages, FileEdit, Sparkles, Film, FileVideo, Loader2, Plus, X, LayoutGrid,
} from "lucide-react"
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
import { type Job, frameUrl, effectiveFrameUrl, videoUrl, generatedImageUrl, cutoutUrl, hasCutout, representativeCutoutUrl } from "@/lib/api"
import { type Job, type ImageRef, frameUrl, effectiveFrameUrl, videoUrl, generatedImageUrl, cutoutUrl, hasCutout, representativeCutoutUrl } from "@/lib/api"
export interface NodeData {
job: Job | null // 当前 active job
@@ -32,6 +32,7 @@ export interface NodeData {
onDeleteGenerated?: (frameIdx: number, genId: string) => void // 删单张生成图
onOpenStoryboard?: (frameIdx: number) => void // 打开分镜头编排专属面板
onPushToStoryboard?: (payload: { kind: "keyframe" | "cutout"; frameIdx: number; elementId?: string; cutoutId?: string; label?: string }) => void
onCopyImage?: (ref: ImageRef) => void // 复制图片到全局剪贴板(粘贴到分镜头编排工作台插槽)
}
/* ---- 状态映射工具 ---- */
@@ -405,21 +406,21 @@ export function KeyframeNode({ data, selected }: any) {
{f.timestamp.toFixed(1)}s
</div>
</button>
{/* 上推按钮:常驻可见 — 推送关键帧本身到分镜头编排 */}
{d.onPushToStoryboard && (
{/* 复制按钮:常驻可见 — 复制该关键帧到剪贴板 */}
{d.onCopyImage && (
<button
onClick={(e) => {
e.stopPropagation()
d.onPushToStoryboard?.({
d.onCopyImage?.({
kind: "keyframe",
frameIdx: f.index,
frame_idx: f.index,
label: `分镜 ${f.index + 1} 关键帧`,
})
}}
title="⬆ 上推到分镜头编排"
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[12px] leading-none font-bold"
title="📋 复制此图(到分镜头编排工作台插槽粘贴)"
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[10px] leading-none"
>
📋
</button>
)}
{/* 删除按钮hover 时右上角浮出 */}
@@ -647,23 +648,23 @@ export function ImageGenNode({ data, selected }: any) {
className="absolute inset-0 w-full h-full object-contain"
/>
</button>
{/* 上推按钮:常驻可见 */}
{d.onPushToStoryboard && (
{/* 复制按钮:常驻可见 — 复制元素提取图到剪贴板 */}
{d.onCopyImage && (
<button
onClick={(e) => {
e.stopPropagation()
d.onPushToStoryboard?.({
d.onCopyImage?.({
kind: "cutout",
frameIdx: p.frameIdx,
elementId: p.elementId,
cutoutId: p.cid,
frame_idx: p.frameIdx,
element_id: p.elementId,
cutout_id: p.cid,
label: p.name,
})
}}
title="⬆ 上推到分镜头编排"
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[12px] leading-none font-bold"
title="📋 复制此图(到分镜头编排工作台插槽粘贴)"
className="absolute top-1 left-1 h-5 w-5 rounded-full bg-violet-500/90 backdrop-blur text-white shadow-md hover:bg-violet-400 hover:scale-110 inline-flex items-center justify-center transition z-[70] text-[10px] leading-none"
>
📋
</button>
)}
{/* hover 预览 — absolute 浮在缩略图上方 */}

View File

@@ -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), [])