diff --git a/.memory/worklog.json b/.memory/worklog.json index 85b703f..6d79137 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -279,6 +279,13 @@ "message": "auto-save 2026-05-12 19:47 (~2)", "hash": "07766e0", "files_changed": 2 + }, + { + "ts": "2026-05-12T19:53:40+08:00", + "type": "commit", + "message": "auto-save 2026-05-12 19:53 (~2)", + "hash": "c481da4", + "files_changed": 2 } ] } diff --git a/web/app/page.tsx b/web/app/page.tsx index add7fae..6c7b389 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -16,6 +16,7 @@ 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 = { input: InputNode, @@ -61,6 +62,7 @@ export default function Home() { const [analyzing, setAnalyzing] = useState(false) const [selectedFrames, setSelectedFrames] = useState>(new Set()) const [expandedFrame, setExpandedFrame] = useState(null) + const [videoLightboxOpen, setVideoLightboxOpen] = useState(false) const pollRef = useRef | null>(null) const handleSubmit = useCallback(async (url: string) => { @@ -181,6 +183,7 @@ export default function Home() { onToggleFrame: handleToggleFrame, onExpandFrame: setExpandedFrame, onAddManualFrame: handleAddManualFrame, + onOpenVideoLightbox: () => setVideoLightboxOpen(true), }), [job, submitting, analyzing, selectedFrames, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame]) // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) @@ -275,6 +278,14 @@ export default function Home() { onToggleSelect={handleToggleFrame} /> )} + + {/* Video lightbox — InputNode 缩略图点击进入 */} + setVideoLightboxOpen(false)} + onAddFrame={handleAddManualFrame} + /> ) diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 8fc4dd0..d86ae62 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect } from "react" import { X, ChevronLeft, ChevronRight, Check } from "lucide-react" -import { frameUrl, type KeyFrame } from "@/lib/api" +import { frameUrl, videoUrl, type KeyFrame } from "@/lib/api" interface Props { jobId: string @@ -65,12 +65,19 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o )} - {/* 大图 */} + {/* 大视频 — 可拖时间轴播放(默认 seek 到该帧时间点) */}
e.stopPropagation()} className="flex flex-col items-center gap-4 max-w-[92vw] max-h-[92vh]"> - {`frame { + (e.target as HTMLVideoElement).currentTime = f.timestamp + }} />
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 0a3a241..aa387c0 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -19,6 +19,7 @@ export interface NodeData { onToggleFrame: (idx: number) => void onExpandFrame: (idx: number) => void onAddManualFrame: (t: number) => void + onOpenVideoLightbox: () => void } /* ---- 状态映射工具 ---- */ @@ -76,6 +77,45 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an const inputLocked = isDownloading || d.submitting return ( +
+ {/* 视频缩略图浮于节点上方 — hover 自动播放,点击进大 lightbox */} + {hasVideo && job && ( +
+ +
+ )} + } @@ -128,37 +168,10 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an )} - {/* 已下载:视频播放器 + 加帧 + 元数据 + 解析按钮 */} + {/* 已下载:仅元数据(视频缩略图浮在节点上方,点击进 lightbox) */} {hasVideo && job && ( <> - {/* 视频播放器(拖时间轴选帧) */} -
- {/* Hover 大图预览 — 大尺寸(跟整排缩略图同宽 + 高至屏幕顶) */} + {/* Hover 视频片段预览 — 从该时间点静音 loop 播放 */}
- {`preview { + const v = e.target as HTMLVideoElement + v.currentTime = f.timestamp + v.play().catch(() => {}) + }} />
- 分镜 {f.index + 1} - {f.timestamp.toFixed(2)}s + 分镜 {f.index + 1} · 自动播放 + 从 {f.timestamp.toFixed(2)}s
diff --git a/web/components/video-lightbox.tsx b/web/components/video-lightbox.tsx new file mode 100644 index 0000000..d09bf04 --- /dev/null +++ b/web/components/video-lightbox.tsx @@ -0,0 +1,74 @@ +"use client" +import { useEffect, useRef, useState } from "react" +import { X, Plus, Loader2 } from "lucide-react" +import { videoUrl } from "@/lib/api" + +interface Props { + jobId: string | null + open: boolean + onClose: () => void + onAddFrame: (t: number) => Promise +} + +export function VideoLightbox({ jobId, open, onClose, onAddFrame }: Props) { + const videoRef = useRef(null) + const [currentT, setCurrentT] = useState(0) + const [adding, setAdding] = useState(false) + + useEffect(() => { + if (!open) return + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose() + } + window.addEventListener("keydown", onKey) + return () => window.removeEventListener("keydown", onKey) + }, [open, onClose]) + + if (!open || !jobId) return null + + return ( +
+ + +
e.stopPropagation()} className="flex flex-col items-center gap-4 max-w-[92vw] max-h-[92vh]"> +
+
+ ) +}