From 26a43fc9a0a17a5ba7859ba1c1eb11df5b7a12dd Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 03:37:19 +0800 Subject: [PATCH] auto-save 2026-05-14 03:37 (~5) --- .memory/worklog.json | 13 ++++ docs/source-analysis.html | 26 ++++++- web/app/page.tsx | 11 ++- web/components/lightbox.tsx | 68 +++++++++-------- web/components/nodes/index.tsx | 129 ++++++++++++++++++++++----------- 5 files changed, 166 insertions(+), 81 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 897242a..95821e2 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -3063,6 +3063,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 4 项未提交变更 · 最近提交:auto-save 2026-05-14 03:26 (~3)", "files_changed": 4 + }, + { + "ts": "2026-05-14T03:31:49+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 03:31 (~4)", + "hash": "6ff4f59", + "files_changed": 4 + }, + { + "ts": "2026-05-13T19:33:12Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 03:31 (~4)", + "files_changed": 3 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 3637f7e..26ac2f1 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -817,6 +817,30 @@ api/main.py

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-14 · 画面工作台详情面板统一为 Canvas Panel 框架

+ Visual Lab + Canvas Panel +
+
+

问题:关键帧详情 / 元素提取面板虽然默认左侧吸附,但外层仍是旧橙色“钉住”框架,和视频抽帧面板的紫色标题栏、画布/左/右/底吸附按钮不一致。

+

改动:KeyframePanelNode 改为和 VideoFramePanelNode 同一套 Canvas Panel 外壳:紫色标题栏、画布模式、吸附左侧、吸附右侧、吸附底部、缩放、关闭和右下角拖拽缩放。面板定位状态从单一 pinned 语义升级为 framePanelDock

+

影响:web/app/page.tsxweb/components/nodes/index.tsxdocs/source-analysis.html。后续工作面板应复用这套 Canvas Panel 框架。

+
+
+
+
+

2026-05-14 · 关键帧详情嵌入态去掉双标题栏

+ Visual Lab + FrameLightbox +
+
+

问题:画面工作台详情面板外层已有“关键帧详情 · 元素提取”工具栏,内层 FrameLightbox 嵌入态又显示一条橙红分镜导航栏,视觉上像两个窗口叠在一起。

+

改动:FrameLightboxembedded 模式下不再渲染自己的顶部工具栏和外边框;上一帧 / 下一帧导航移动到 KeyframePanelNode 外层标题栏。

+

影响:web/components/lightbox.tsxweb/components/nodes/index.tsxdocs/source-analysis.html。非嵌入式 lightbox 保留原标题栏。

+
+

2026-05-14 · 画面工作台详情面板默认左侧吸附

@@ -825,7 +849,7 @@ api/main.py

问题:画面工作台点击关键帧后,关键帧详情 / 元素提取面板仍默认作为画布节点出现,还会触发 fitView 拉动画布;和视频抽帧面板默认左侧吸附的规则不一致。

-

改动:framePanelPinned 初始值改为 true;从关闭状态打开关键帧详情时默认吸附左侧。若面板已打开,用户取消钉住后继续切换关键帧,不会被强制吸回左侧;默认吸附状态下也不再触发画布 fitView

+

改动:framePanelDock 初始值改为 left;从关闭状态打开关键帧详情时默认吸附左侧。若面板已打开,用户切回画布模式后继续切换关键帧,不会被强制吸回左侧;默认吸附状态下也不再触发画布 fitView

影响:web/app/page.tsxdocs/source-analysis.html。画面工作台处理面板现在与视频抽帧面板保持同一默认策略:先贴左侧,需要时再切回画布。

diff --git a/web/app/page.tsx b/web/app/page.tsx index 6cae1d0..0df53d5 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -88,7 +88,8 @@ export default function Home() { const [selectedFrames, setSelectedFrames] = useState>(new Set()) const [expandedFrame, setExpandedFrame] = useState(null) const [framePanelScale, setFramePanelScale] = useState(1) - const [framePanelPinned, setFramePanelPinned] = useState(true) + const [framePanelDock, setFramePanelDock] = useState("left") + const framePanelPinned = framePanelDock !== "canvas" const [videoPanelJobId, setVideoPanelJobId] = useState(null) const [videoPanelScale, setVideoPanelScale] = useState(1) const [videoPanelDock, setVideoPanelDock] = useState("left") @@ -210,7 +211,7 @@ export default function Home() { }, []) const handleOpenFramePanel = useCallback((idx: number) => { - if (expandedFrame === null) setFramePanelPinned(true) + if (expandedFrame === null) setFramePanelDock("left") setExpandedFrame(idx) }, [expandedFrame]) @@ -499,6 +500,7 @@ export default function Home() { expandedFrame, framePanelScale, framePanelPinned, + framePanelDock, videoPanelJobId, videoPanelScale, videoPanelDock, @@ -509,7 +511,8 @@ export default function Home() { onExpandFrame: setExpandedFrame, onOpenFramePanel: handleOpenFramePanel, onFramePanelScaleChange: handleFramePanelScaleChange, - onFramePanelPinnedChange: setFramePanelPinned, + onFramePanelPinnedChange: (pinned: boolean) => setFramePanelDock(pinned ? "left" : "canvas"), + onFramePanelDockChange: setFramePanelDock, onCloseExpandedFrame: () => setExpandedFrame(null), onAddManualFrame: handleAddManualFrame, onAddManualFrameForJob: handleAddManualFrameForJob, @@ -533,7 +536,7 @@ export default function Home() { onCopyImage: handleCopyImage, pinnedNodes, onToggleNodePin: handleToggleNodePin, - }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, videoPanelJobId, videoPanelScale, videoPanelDock, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleAddManualFrameForJob, handleOpenVideoPanel, handleVideoPanelScaleChange, handleSwitchJob, setJob, handleDeleteJob, handleDeleteFrame, handleDeleteFrameForJob, handleDeleteGenerated, handleDeleteVideo, handleDeleteCutout, handleCopyImage, pinnedNodes, handleToggleNodePin]) + }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, framePanelDock, videoPanelJobId, videoPanelScale, videoPanelDock, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleAddManualFrameForJob, handleOpenVideoPanel, handleVideoPanelScaleChange, handleSwitchJob, setJob, handleDeleteJob, handleDeleteFrame, handleDeleteFrameForJob, handleDeleteGenerated, handleDeleteVideo, handleDeleteCutout, handleCopyImage, pinnedNodes, handleToggleNodePin]) // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) const savedSizes = useMemo(() => loadNodeSizes(), []) diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index ca91ac6..0914354 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -297,7 +297,9 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const content = (
e.stopPropagation()} - className={`rounded-2xl border border-white/15 overflow-hidden flex flex-col ${embedded ? "" : "fixed z-[100] bg-black/70 backdrop-blur-2xl"}`} + className={embedded + ? "h-full overflow-hidden flex flex-col" + : "fixed z-[100] rounded-2xl border border-white/15 bg-black/70 backdrop-blur-2xl overflow-hidden flex flex-col"} style={embedded ? { height: "100%", background: "transparent", @@ -311,38 +313,40 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o }} > {/* 顶部工具栏 */} -
-
- - - - 分镜 {String(arrayPos + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")} - · - {f.timestamp.toFixed(2)}s - -
- -
+
+ + + + 分镜 {String(arrayPos + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")} + · + {f.timestamp.toFixed(2)}s + +
+ +
+ )} {/* 主体 — 左:大图 + 清洗 / 选用;右:识别 + 元素清单 */}
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index d51a68b..8909e23 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -9,8 +9,8 @@ import { createPortal } from "react-dom" import { type NodeProps, useReactFlow } from "@xyflow/react" import { Link2, Upload, Download, Scissors, Image as ImageIcon, - Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Pin, Maximize2, - Copy, Trash2, Move, PanelLeft, PanelRight, PanelBottom, + Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Maximize2, + Copy, Trash2, Move, PanelLeft, PanelRight, PanelBottom, ChevronLeft, ChevronRight, } from "lucide-react" import { toast } from "sonner" import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell" @@ -33,6 +33,7 @@ export interface NodeData { expandedFrame: number | null framePanelScale?: number framePanelPinned?: boolean + framePanelDock?: CanvasPanelDock videoPanelJobId?: string | null videoPanelScale?: number videoPanelDock?: CanvasPanelDock @@ -44,6 +45,7 @@ export interface NodeData { onOpenFramePanel?: (idx: number) => void // 打开/找回画布内关键帧详情面板 onFramePanelScaleChange?: (scale: number) => void onFramePanelPinnedChange?: (pinned: boolean) => void + onFramePanelDockChange?: (dock: CanvasPanelDock) => void onCloseExpandedFrame: () => void onAddManualFrame: (t: number) => void onAddManualFrameForJob?: (jobId: string, t: number) => Promise | void @@ -1519,20 +1521,15 @@ export function KeyframePanelNode({ data }: any) { const d: NodeData = data const { getZoom } = useReactFlow() const panelRef = useRef(null) - const [pinRect, setPinRect] = useState<{ left: number; top: number }>({ - left: FLOATING_PANEL_EDGE_INSET, - top: FLOATING_PANEL_EDGE_INSET, - }) const scale = d.framePanelScale ?? 1 - const pinned = d.framePanelPinned ?? false - - useEffect(() => { - if (!pinned) return - setPinRect({ left: FLOATING_PANEL_EDGE_INSET, top: FLOATING_PANEL_EDGE_INSET }) - }, [pinned]) + const dock = d.framePanelDock ?? (d.framePanelPinned ? "left" : "canvas") + const docked = dock !== "canvas" if (!d.job || d.expandedFrame === null) return null const active = d.job.frames.find((f) => f.index === d.expandedFrame) + const arrayPos = active ? d.job.frames.findIndex((f) => f.index === active.index) : -1 + const prevFrame = arrayPos > 0 ? d.job.frames[arrayPos - 1] : null + const nextFrame = arrayPos >= 0 && arrayPos < d.job.frames.length - 1 ? d.job.frames[arrayPos + 1] : null const panelWidth = Math.round(760 * scale) const panelHeight = Math.round(746 * scale) const bodyHeight = Math.max(520, panelHeight - 27) @@ -1540,15 +1537,18 @@ export function KeyframePanelNode({ data }: any) { const clamped = Math.max(0.65, Math.min(1.6, Number(next.toFixed(2)))) d.onFramePanelScaleChange?.(clamped) } - - const togglePinned = () => { - if (!pinned) { - const zoom = getZoom() - setScale(scale * zoom) - setPinRect({ left: FLOATING_PANEL_EDGE_INSET, top: FLOATING_PANEL_EDGE_INSET }) - } - d.onFramePanelPinnedChange?.(!pinned) + const dockText: Record = { + canvas: "画布模式", + left: "吸附左侧", + right: "吸附右侧", + bottom: "吸附底部", } + const dockButtonClass = (value: CanvasPanelDock) => + `nodrag inline-flex h-6 w-6 items-center justify-center rounded transition ${ + dock === value + ? "bg-white text-violet-700 shadow" + : "bg-white/10 text-white/75 hover:bg-white/20 hover:text-white" + }` const startResize = (e: React.PointerEvent) => { e.preventDefault() @@ -1556,7 +1556,7 @@ export function KeyframePanelNode({ data }: any) { const startX = e.clientX const startY = e.clientY const startScale = scale - const zoom = pinned ? 1 : getZoom() + const zoom = docked ? 1 : getZoom() const onMove = (ev: PointerEvent) => { const dx = (ev.clientX - startX) / zoom const dy = (ev.clientY - startY) / zoom @@ -1574,37 +1574,72 @@ export function KeyframePanelNode({ data }: any) { const panel = (
-
+
关键帧详情 · 元素提取 {active ? `分镜 ${active.index + 1} · ${active.timestamp.toFixed(2)}s` : "未选分镜"} +
+ + + + {arrayPos >= 0 ? `${String(arrayPos + 1).padStart(2, "0")} / ${String(d.job.frames.length).padStart(2, "0")}` : ""} + +
- {pinned ? "已钉住左侧 · 不跟画布" : "拖动标题栏移动 · 可钉住"} + {dockText[dock]} - + + +
@@ -1652,7 +1687,7 @@ export function KeyframePanelNode({ data }: any) {
) - if (pinned && typeof document !== "undefined") { + if (docked && typeof document !== "undefined") { + const fixedStyle = + dock === "left" + ? { left: FLOATING_PANEL_EDGE_INSET, top: FLOATING_PANEL_EDGE_INSET } + : dock === "right" + ? { right: FLOATING_PANEL_EDGE_INSET, top: FLOATING_PANEL_EDGE_INSET } + : { left: "50%", bottom: FLOATING_PANEL_EDGE_INSET, transform: "translateX(-50%)" } return createPortal(
{panel}
,