auto-save 2026-05-13 19:23 (~4)
This commit is contained in:
@@ -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<Set<number>>(new Set())
|
||||
const [expandedFrame, setExpandedFrame] = useState<number | null>(null)
|
||||
const [framePanelResetNonce, setFramePanelResetNonce] = useState(0)
|
||||
const [videoLightboxOpen, setVideoLightboxOpen] = useState(false)
|
||||
const [storyboardFrame, setStoryboardFrame] = useState<number | null>(null)
|
||||
const [workbenchOpen, setWorkbenchOpen] = useState(false)
|
||||
const [clipboard, setClipboard] = useState<ImageRef | null>(null)
|
||||
const flowRef = useRef<any>(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<Node>(
|
||||
@@ -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<string, boolean> = {
|
||||
@@ -351,6 +417,7 @@ export default function Home() {
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={(instance) => { flowRef.current = instance }}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={NODE_TYPES}
|
||||
|
||||
Reference in New Issue
Block a user