auto-save 2026-05-13 00:00 (~5)
This commit is contained in:
@@ -496,6 +496,13 @@
|
||||
"message": "auto-save 2026-05-12 23:49 (~2)",
|
||||
"hash": "25a1e63",
|
||||
"files_changed": 2
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-12T23:55:21+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-12 23:55 (~2)",
|
||||
"hash": "fd4c78f",
|
||||
"files_changed": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
import { ThemeToggle } from "@/components/theme-toggle"
|
||||
import { Dashboard } from "@/components/dashboard"
|
||||
import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, type Job } from "@/lib/api"
|
||||
import { FrameLightbox } from "@/components/lightbox"
|
||||
import { VideoLightbox } from "@/components/video-lightbox"
|
||||
|
||||
const NODE_TYPES = {
|
||||
@@ -215,15 +214,18 @@ export default function Home() {
|
||||
submitting,
|
||||
analyzing,
|
||||
selectedFrames,
|
||||
expandedFrame,
|
||||
onSubmitUrl: handleSubmit,
|
||||
onUploadFile: handleUpload,
|
||||
onAnalyze: handleAnalyze,
|
||||
onToggleFrame: handleToggleFrame,
|
||||
onExpandFrame: setExpandedFrame,
|
||||
onCloseExpandedFrame: () => setExpandedFrame(null),
|
||||
onAddManualFrame: handleAddManualFrame,
|
||||
onOpenVideoLightbox: () => setVideoLightboxOpen(true),
|
||||
onSwitchJob: handleSwitchJob,
|
||||
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob])
|
||||
onJobUpdate: setJob as any,
|
||||
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob])
|
||||
|
||||
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag)
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>(
|
||||
@@ -305,19 +307,7 @@ export default function Home() {
|
||||
|
||||
<Toaster theme="system" position="bottom-center" />
|
||||
|
||||
{/* Lightbox 看大图 */}
|
||||
{job && (
|
||||
<FrameLightbox
|
||||
jobId={job.id}
|
||||
frames={job.frames}
|
||||
activeIndex={expandedFrame}
|
||||
selected={selectedFrames}
|
||||
onClose={() => setExpandedFrame(null)}
|
||||
onChange={setExpandedFrame}
|
||||
onToggleSelect={handleToggleFrame}
|
||||
onJobUpdate={setJob}
|
||||
/>
|
||||
)}
|
||||
{/* FrameLightbox 已嵌入 dashboard 的 keyframe drawer(embedded mode),不再独立浮动 */}
|
||||
|
||||
{/* Video lightbox — InputNode 缩略图点击进入 */}
|
||||
<VideoLightbox
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "lucide-react"
|
||||
import { type Job, frameUrl, videoUrl } from "@/lib/api"
|
||||
import { type NodeData } from "@/components/nodes"
|
||||
import { FrameLightbox } from "@/components/lightbox"
|
||||
|
||||
type ColType = "input" | "process" | "ai" | "output"
|
||||
const TYPE_GRAD: Record<ColType, string> = {
|
||||
@@ -142,7 +143,17 @@ export function Dashboard({ data }: Props) {
|
||||
const toggleTile = (key: string) => {
|
||||
setExpanded((prev) => (prev.has(key) ? new Set() : new Set([key])))
|
||||
}
|
||||
const closeTile = (_key: string) => setExpanded(new Set())
|
||||
const closeTile = (_key: string) => {
|
||||
setExpanded(new Set())
|
||||
data.onCloseExpandedFrame()
|
||||
}
|
||||
|
||||
// 点关键帧缩略图时(onExpandFrame 触发),自动打开 keyframe drawer
|
||||
useEffect(() => {
|
||||
if (data.expandedFrame !== null && !expanded.has("keyframe")) {
|
||||
setExpanded(new Set(["keyframe"]))
|
||||
}
|
||||
}, [data.expandedFrame])
|
||||
|
||||
const Tile = ({ tkey }: { tkey: string }) => {
|
||||
const t = TILES.find((x) => x.key === tkey)!
|
||||
@@ -187,13 +198,20 @@ export function Dashboard({ data }: Props) {
|
||||
{/* 合流 */}
|
||||
<Tile tkey="compose" />
|
||||
|
||||
{/* 展开面板 — 用 portal 渲染到 body 避免 backdrop-filter 影响 fixed 定位 */}
|
||||
{/* 展开面板 — keyframe 有选中帧时变宽容纳 lightbox */}
|
||||
{expanded.size > 0 && mounted && createPortal(
|
||||
<div
|
||||
className="fixed z-[100]"
|
||||
style={{ left: 130, top: 16, bottom: 16, width: 400 }}
|
||||
style={{
|
||||
left: 130,
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
width: expanded.has("keyframe") && data.expandedFrame !== null ? 760 : 400,
|
||||
}}
|
||||
>
|
||||
{TILES.filter((t) => expanded.has(t.key)).map((t) => (
|
||||
{TILES.filter((t) => expanded.has(t.key)).map((t) => {
|
||||
const isKeyframeWithExpand = t.key === "keyframe" && data.expandedFrame !== null
|
||||
return (
|
||||
<section
|
||||
key={t.key}
|
||||
className="rounded-2xl border border-white/15 bg-black/60 backdrop-blur-2xl overflow-hidden flex flex-col h-full"
|
||||
@@ -218,10 +236,25 @@ export function Dashboard({ data }: Props) {
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-y-auto p-3 bg-black/20 flex-1">
|
||||
{renderSection(t.key)}
|
||||
{isKeyframeWithExpand && data.job ? (
|
||||
<FrameLightbox
|
||||
embedded
|
||||
jobId={data.job.id}
|
||||
frames={data.job.frames}
|
||||
activeIndex={data.expandedFrame}
|
||||
selected={data.selectedFrames}
|
||||
onClose={data.onCloseExpandedFrame}
|
||||
onChange={data.onExpandFrame}
|
||||
onToggleSelect={data.onToggleFrame}
|
||||
onJobUpdate={data.onJobUpdate}
|
||||
/>
|
||||
) : (
|
||||
renderSection(t.key)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
@@ -14,9 +14,10 @@ interface Props {
|
||||
onChange: (idx: number) => void
|
||||
onToggleSelect: (idx: number) => void
|
||||
onJobUpdate?: (job: Job) => void
|
||||
embedded?: boolean // true=嵌入到容器里(无 fixed),false=独立浮动卡(默认)
|
||||
}
|
||||
|
||||
export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate }: Props) {
|
||||
export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, embedded = false }: Props) {
|
||||
const [extractPrompt, setExtractPrompt] = useState("")
|
||||
const [describing, setDescribing] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
@@ -66,11 +67,14 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
navigator.clipboard.writeText(text).then(() => toast.success("已复制"))
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
const content = (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="fixed z-[100] rounded-2xl border border-white/15 bg-black/70 backdrop-blur-2xl overflow-hidden flex flex-col"
|
||||
style={{
|
||||
className={`rounded-2xl border border-white/15 overflow-hidden flex flex-col ${embedded ? "" : "fixed z-[100] bg-black/70 backdrop-blur-2xl"}`}
|
||||
style={embedded ? {
|
||||
height: "100%",
|
||||
background: "transparent",
|
||||
} : {
|
||||
top: 80,
|
||||
right: 16,
|
||||
width: 740,
|
||||
@@ -79,8 +83,11 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
animation: "drawer-in 0.24s cubic-bezier(0.32, 0.72, 0, 1)",
|
||||
}}
|
||||
>
|
||||
{/* 顶部工具栏 — 切换 / 关闭 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-white/10 bg-white/[0.03]">
|
||||
{/* 顶部工具栏 — 切换 / 关闭,用 keyframe 橙红配色 */}
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-2 text-white"
|
||||
style={{ background: "linear-gradient(135deg, #f59e0b, #ef4444)" }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); if (arrayPos > 0) onChange(frames[arrayPos - 1].index) }}
|
||||
@@ -262,9 +269,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-1.5 text-[10px] text-white/40 font-mono text-center border-t border-white/5 bg-white/[0.02]">
|
||||
←/→ 切换 · Space 选用 · ESC 关闭 · 可拖侧画布看背后
|
||||
←/→ 切换 · Space 选用 · ESC 关闭
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
</div>
|
||||
)
|
||||
|
||||
return embedded ? content : createPortal(content, document.body)
|
||||
}
|
||||
|
||||
@@ -15,14 +15,17 @@ export interface NodeData {
|
||||
submitting: boolean
|
||||
analyzing: boolean
|
||||
selectedFrames: Set<number>
|
||||
expandedFrame: number | null
|
||||
onSubmitUrl: (url: string) => void
|
||||
onUploadFile: (file: File) => void
|
||||
onAnalyze: () => void
|
||||
onToggleFrame: (idx: number) => void
|
||||
onExpandFrame: (idx: number) => void
|
||||
onCloseExpandedFrame: () => void
|
||||
onAddManualFrame: (t: number) => void
|
||||
onOpenVideoLightbox: () => void
|
||||
onSwitchJob: (id: string) => void
|
||||
onJobUpdate: (j: Job) => void
|
||||
}
|
||||
|
||||
/* ---- 状态映射工具 ---- */
|
||||
|
||||
Reference in New Issue
Block a user