diff --git a/.memory/worklog.json b/.memory/worklog.json index 28011a5..b2e34dc 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -2182,6 +2182,19 @@ "message": "auto-save 2026-05-13 19:12 (~3)", "hash": "61a4bec", "files_changed": 3 + }, + { + "ts": "2026-05-13T19:17:48+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 19:17 (~4)", + "hash": "fda2980", + "files_changed": 4 + }, + { + "ts": "2026-05-13T11:19:29Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 19:17 (~4)", + "files_changed": 1 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 2a746dd..5eab037 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -837,8 +837,8 @@ api/main.py

问题:关键帧详情 / 元素提取面板固定在左侧 drawer,和 ReactFlow 无限画布割裂,也不会跟随画布缩放。

-

改动:移除主页面隐藏渲染的 Dashboard drawer 承载方式,改为在 KeyframeNode 内部挂载 FrameLightbox,作为画布上的工作面板。

-

影响:web/app/page.tsxweb/components/nodes/index.tsx;点关键帧后面板会跟随 ReactFlow 平移和缩放。

+

改动:移除主页面隐藏渲染的 Dashboard drawer 承载方式,新增独立 keyframePanel ReactFlow 节点来挂载 FrameLightbox

+

影响:web/app/page.tsxweb/components/nodes/index.tsx;点关键帧后面板默认出现在流程左侧空白画布里,不遮挡 Input / Keyframe 主节点;标题栏可拖动,跟随 ReactFlow 平移和缩放。再次点击关键帧缩略图会把面板找回到默认位置,并自动把视野拉到“关键帧 + 面板”。

diff --git a/web/app/page.tsx b/web/app/page.tsx index 5d01f9f..5f321d4 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -9,7 +9,7 @@ import { import { Toaster, toast } from "sonner" import { InputNode, KeyframeNode, ASRNode, - TranslateNode, RewriteNode, StoryboardNode, VideoGenNode, ComposeNode, + TranslateNode, RewriteNode, StoryboardNode, VideoGenNode, ComposeNode, KeyframePanelNode, type NodeData, } from "@/components/nodes" import { ThemeToggle } from "@/components/theme-toggle" @@ -27,8 +27,11 @@ const NODE_TYPES = { storyboard: StoryboardNode, videogen: VideoGenNode, compose: ComposeNode, + keyframePanel: KeyframePanelNode, } +const KEYFRAME_PANEL_ID = "keyframe-detail-panel" + // 合并 input + download + split 为一个节点 // 分叉:上路 input → keyframe → storyboard → videogen ↘ // 下路 input → asr → translate → rewrite ──────→ storyboard / compose @@ -64,10 +67,12 @@ export default function Home() { const [analyzing, setAnalyzing] = useState(false) const [selectedFrames, setSelectedFrames] = useState>(new Set()) const [expandedFrame, setExpandedFrame] = useState(null) + const [framePanelResetNonce, setFramePanelResetNonce] = useState(0) const [videoLightboxOpen, setVideoLightboxOpen] = useState(false) const [storyboardFrame, setStoryboardFrame] = useState(null) const [workbenchOpen, setWorkbenchOpen] = useState(false) const [clipboard, setClipboard] = useState(null) + const flowRef = useRef(null) // 把 setJob(prev=>...) 翻译成 setJobs 里更新当前 active const setJob = useCallback((updater: Job | ((prev: Job | null) => Job | null) | null) => { @@ -163,6 +168,11 @@ export default function Home() { }) }, []) + const handleOpenFramePanel = useCallback((idx: number) => { + setExpandedFrame(idx) + setFramePanelResetNonce((n) => n + 1) + }, []) + const handleDeleteFrame = useCallback(async (idx: number) => { if (!activeJobId) return try { @@ -276,6 +286,7 @@ export default function Home() { onAnalyze: handleAnalyze, onToggleFrame: handleToggleFrame, onExpandFrame: setExpandedFrame, + onOpenFramePanel: handleOpenFramePanel, onCloseExpandedFrame: () => setExpandedFrame(null), onAddManualFrame: handleAddManualFrame, onOpenVideoLightbox: () => setVideoLightboxOpen(true), @@ -289,7 +300,7 @@ export default function Home() { setWorkbenchOpen(true) }, onCopyImage: handleCopyImage, - }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleCopyImage]) + }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleCopyImage]) // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) const [nodes, setNodes, onNodesChange] = useNodesState( @@ -312,6 +323,61 @@ export default function Home() { setNodes((prev) => prev.map((n) => ({ ...n, data: nodeData }))) }, [nodeData, setNodes]) + // 关键帧详情面板是独立 ReactFlow 节点:可拖动、跟随画布缩放。 + // 再次点击任意关键帧缩略图时,重新放回关键帧节点左侧附近,避免拖丢后找不到。 + const panelResetHandledRef = useRef(0) + useEffect(() => { + if (!job || expandedFrame === null) { + setNodes((prev) => prev.filter((n) => n.id !== KEYFRAME_PANEL_ID)) + return + } + + const shouldReset = panelResetHandledRef.current !== framePanelResetNonce + panelResetHandledRef.current = framePanelResetNonce + setNodes((prev) => { + const keyframeNode = prev.find((n) => n.id === "keyframe") + const inputNode = prev.find((n) => n.id === "input") + const defaultPosition = { + x: (inputNode?.position.x ?? 40) - 820, + y: (keyframeNode?.position.y ?? 60), + } + const exists = prev.some((n) => n.id === KEYFRAME_PANEL_ID) + if (exists) { + return prev.map((n) => n.id === KEYFRAME_PANEL_ID + ? { + ...n, + data: nodeData, + position: shouldReset ? defaultPosition : n.position, + draggable: true, + dragHandle: ".keyframe-panel-drag", + } + : n, + ) + } + return [ + ...prev, + { + id: KEYFRAME_PANEL_ID, + type: "keyframePanel", + position: defaultPosition, + data: nodeData, + draggable: true, + dragHandle: ".keyframe-panel-drag", + selectable: true, + }, + ] + }) + if (shouldReset) { + window.setTimeout(() => { + flowRef.current?.fitView?.({ + nodes: [{ id: KEYFRAME_PANEL_ID }, { id: "keyframe" }], + padding: 0.18, + duration: 260, + }) + }, 0) + } + }, [job?.id, expandedFrame, framePanelResetNonce, nodeData, setNodes]) + // 边的 animated 状态跟 Job 进度联动 useEffect(() => { const doneOf: Record = { @@ -351,6 +417,7 @@ export default function Home() { { flowRef.current = instance }} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} nodeTypes={NODE_TYPES} diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index b2f084b..ee5b52b 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -22,6 +22,7 @@ export interface NodeData { onAnalyze: () => void onToggleFrame: (idx: number) => void onExpandFrame: (idx: number) => void + onOpenFramePanel?: (idx: number) => void // 打开/找回画布内关键帧详情面板 onCloseExpandedFrame: () => void onAddManualFrame: (t: number) => void onOpenVideoLightbox: () => void @@ -375,8 +376,11 @@ export function KeyframeNode({ data, selected }: any) { }} > + + +
e.stopPropagation()}> + +
) }