auto-save 2026-05-13 15:17 (~6)

This commit is contained in:
2026-05-13 15:17:18 +08:00
parent 02df0c5a60
commit 6390472c27
6 changed files with 124 additions and 24 deletions

View File

@@ -304,11 +304,8 @@ export default function Home() {
<ThemeToggle />
</div>
{/* 左侧:竖向 tile 看板(极窄) */}
<aside
className="relative z-10 flex-shrink-0 border-r border-white/5 bg-black/20 backdrop-blur-xl overflow-y-auto"
style={{ width: 88 }}
>
{/* sidebar tile 列已去掉 · Dashboard 仍渲染hidden以保留 keyframe lightbox 等 drawer portal */}
<aside className="hidden">
<Dashboard ref={dashboardRef} data={nodeData} />
</aside>

View File

@@ -270,7 +270,7 @@ export const Dashboard = forwardRef<DashboardHandle, Props>(function Dashboard({
<div
className="fixed z-[100]"
style={{
left: 104,
left: 24,
top: 16,
bottom: 16,
width: drawerWidth,

View File

@@ -1,8 +1,9 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { LayoutGrid, ChevronDown, ChevronUp, Sparkle } from "lucide-react"
import { type Job, type KeyFrame, cutoutUrl, effectiveFrameUrl, hasCutout } from "@/lib/api"
import { LayoutGrid, ChevronDown, ChevronUp, Sparkle, X } from "lucide-react"
import { type Job, type KeyFrame, cutoutUrl, effectiveFrameUrl, hasCutout, removeStoryboardImage } from "@/lib/api"
import { toast } from "sonner"
interface Props {
job: Job | null
@@ -12,7 +13,7 @@ interface Props {
onJobUpdate?: (j: Job) => void
}
export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame }: Props) {
export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame, onJobUpdate }: Props) {
const [collapsed, setCollapsed] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
@@ -40,21 +41,19 @@ export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame
? frames.findIndex((f) => f.index === focusFrame.index) + 1
: 0
// 所有"已进入分镜阶段"的提取图(按分镜时间序展平)
type Shot = { frameIdx: number; seq: number; elementId: string; elementName: string; cid: string; isLegacy: boolean }
const allShots: Shot[] = []
frames.forEach((f, i) => {
const seq = i + 1
;(f.elements ?? []).forEach((e) => {
if (e.cutouts && e.cutouts.length > 0) {
e.cutouts.forEach((cid) => allShots.push({
frameIdx: f.index, seq, elementId: e.id, elementName: e.name_zh, cid, isLegacy: false,
}))
} else if (e.cutout_id) {
allShots.push({ frameIdx: f.index, seq, elementId: e.id, elementName: e.name_zh, cid: e.cutout_id, isLegacy: true })
}
})
})
// 用户已"上推"到分镜头编排区的图片
const pushedImages = job.storyboard_images ?? []
const frameSeqByIdx: Record<number, number> = {}
frames.forEach((f, i) => { frameSeqByIdx[f.index] = i + 1 })
const handleRemovePushed = async (refId: string) => {
try {
const updated = await removeStoryboardImage(job.id, refId)
onJobUpdate?.(updated)
} catch (e) {
toast.error("移除失败:" + (e instanceof Error ? e.message : String(e)))
}
}
return (
<div className="relative z-20 flex-shrink-0 border-b border-white/5 bg-black/30 backdrop-blur-xl">

View File

@@ -76,6 +76,16 @@ export interface TranscriptSegment {
zh: string
}
export interface StoryboardImage {
ref_id: string
kind: "keyframe" | "cutout"
frame_idx: number
element_id?: string | null
cutout_id?: string | null
label?: string
created_at?: number
}
export interface Job {
id: string
url: string
@@ -88,6 +98,7 @@ export interface Job {
height?: number
frames: KeyFrame[]
transcript: TranscriptSegment[]
storyboard_images?: StoryboardImage[]
error?: string
}
@@ -248,6 +259,31 @@ export function representativeCutoutUrl(
return null
}
export async function pushStoryboardImage(
jobId: string,
body: { kind: "keyframe" | "cutout"; frame_idx: number; element_id?: string | null; cutout_id?: string | null; label?: string },
): Promise<Job> {
const res = await fetch(`${API_BASE}/jobs/${jobId}/storyboard-images`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
if (!res.ok) {
const txt = await res.text().catch(() => "")
throw new Error(`pushStoryboardImage ${res.status} ${txt.slice(0, 300)}`)
}
return res.json()
}
export async function removeStoryboardImage(jobId: string, refId: string): Promise<Job> {
const res = await fetch(`${API_BASE}/jobs/${jobId}/storyboard-images/${refId}`, { method: "DELETE" })
if (!res.ok) {
const txt = await res.text().catch(() => "")
throw new Error(`removeStoryboardImage ${res.status} ${txt.slice(0, 300)}`)
}
return res.json()
}
export async function updateStoryboard(
jobId: string,
frameIdx: number,