auto-save 2026-05-14 00:48 (+4, ~3)
This commit is contained in:
@@ -64,6 +64,18 @@ function loadNodeSizes(): Record<string, NodeSize> {
|
||||
}
|
||||
}
|
||||
|
||||
const NODE_PINS_KEY = "skg-tk:node-pins:v1"
|
||||
|
||||
function loadNodePins(): string[] {
|
||||
if (typeof window === "undefined") return []
|
||||
try {
|
||||
const raw = window.localStorage.getItem(NODE_PINS_KEY)
|
||||
return raw ? JSON.parse(raw) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const EDGES_RAW: Array<[string, string]> = [
|
||||
["input", "keyframe"],
|
||||
["input", "asr"],
|
||||
@@ -412,6 +424,16 @@ export default function Home() {
|
||||
return () => { if (pollRef.current) clearInterval(pollRef.current) }
|
||||
}, [job?.id, job?.status, job?.generated_videos?.map((v) => `${v.id}:${v.status}:${v.progress}`).join("|")])
|
||||
|
||||
const [pinnedNodes, setPinnedNodes] = useState<Set<string>>(() => new Set(loadNodePins()))
|
||||
const handleToggleNodePin = useCallback((id: string) => {
|
||||
setPinnedNodes((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id); else next.add(id)
|
||||
try { window.localStorage.setItem(NODE_PINS_KEY, JSON.stringify([...next])) } catch {}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const nodeData: NodeData = useMemo(() => ({
|
||||
job,
|
||||
jobs,
|
||||
@@ -444,7 +466,9 @@ export default function Home() {
|
||||
setWorkbenchOpen(true)
|
||||
},
|
||||
onCopyImage: handleCopyImage,
|
||||
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleDeleteVideo, handleCopyImage])
|
||||
pinnedNodes,
|
||||
onToggleNodePin: handleToggleNodePin,
|
||||
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleDeleteVideo, handleCopyImage, pinnedNodes, handleToggleNodePin])
|
||||
|
||||
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag)
|
||||
const savedSizes = useMemo(() => loadNodeSizes(), [])
|
||||
@@ -453,12 +477,13 @@ export default function Home() {
|
||||
const s = savedSizes[n.id] ?? {}
|
||||
const w = s.w ?? n.w
|
||||
const h = s.h
|
||||
const isPinned = pinnedNodes.has(n.id)
|
||||
return {
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
position: { x: n.x, y: n.y },
|
||||
data: nodeData,
|
||||
draggable: true,
|
||||
draggable: !isPinned,
|
||||
width: w,
|
||||
...(typeof h === "number" ? { height: h } : {}),
|
||||
style: { width: w, ...(typeof h === "number" ? { height: h } : {}) },
|
||||
@@ -466,6 +491,13 @@ export default function Home() {
|
||||
}),
|
||||
)
|
||||
|
||||
// pinned 变化时同步每个节点 draggable
|
||||
useEffect(() => {
|
||||
setNodes((prev) => prev.map((n) =>
|
||||
n.id === KEYFRAME_PANEL_ID ? n : { ...n, draggable: !pinnedNodes.has(n.id) },
|
||||
))
|
||||
}, [pinnedNodes, setNodes])
|
||||
|
||||
// 持久化每个节点宽 / 高到 localStorage(KeyframePanelNode 自己管尺寸,不写回)
|
||||
useEffect(() => {
|
||||
const sizes: Record<string, NodeSize> = {}
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface NodeData {
|
||||
onOpenStoryboard?: (frameIdx: number) => void // 打开分镜头编排专属面板
|
||||
onOpenWorkbench?: (frameIdx?: number) => void // 展开顶部分镜编排内嵌面板
|
||||
onCopyImage?: (ref: ImageRef) => void // 复制图片到全局剪贴板(粘贴到分镜头编排插槽)
|
||||
pinnedNodes?: Set<string> // 已钉住的节点 id 集合 — 钉住后位置 + 尺寸锁定
|
||||
onToggleNodePin?: (id: string) => void
|
||||
}
|
||||
|
||||
/* ---- 状态映射工具 ---- */
|
||||
@@ -234,6 +236,8 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
||||
subtitle={isDownloading ? "STEP 1 · 下载中" : hasVideo ? "STEP 1 · 视频就绪" : "STEP 1"}
|
||||
selected={selected}
|
||||
hasTarget={false}
|
||||
pinned={d.pinnedNodes?.has("input")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("input")}
|
||||
>
|
||||
{/* URL + 上传入口 — 一直显示(即使已有视频,也可以继续加新的) */}
|
||||
<>
|
||||
@@ -499,6 +503,8 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
title="镜头拆解 · 元素提取"
|
||||
subtitle={`STEP 2 · ${frames.length ? `${d.selectedFrames.size}/${frames.length} 入编排` : "等待抽取"}`}
|
||||
selected={selected}
|
||||
pinned={d.pinnedNodes?.has("keyframe")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("keyframe")}
|
||||
>
|
||||
{frames.length > 0 ? (() => {
|
||||
const cleanedCount = frames.filter((x) => x.cleaned_url).length
|
||||
@@ -726,6 +732,8 @@ export function ASRNode({ data, selected }: any) {
|
||||
title="声音文案 · ASR"
|
||||
subtitle="STEP 3 · 可选文案轨"
|
||||
selected={selected}
|
||||
pinned={d.pinnedNodes?.has("asr")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("asr")}
|
||||
>
|
||||
<div className="text-[11.5px] text-[var(--text-soft)]">
|
||||
Gemini 2.5 · 英文带时间戳分段
|
||||
@@ -767,6 +775,8 @@ export function TranslateNode({ data, selected }: any) {
|
||||
title="翻译理解 · Translate"
|
||||
subtitle="STEP 4 · EN → ZH"
|
||||
selected={selected}
|
||||
pinned={d.pinnedNodes?.has("translate")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("translate")}
|
||||
>
|
||||
<div className="text-[11.5px] text-[var(--text-soft)]">
|
||||
中文翻译 · 段落级 · 实时输出
|
||||
@@ -785,7 +795,8 @@ export function TranslateNode({ data, selected }: any) {
|
||||
/* ============================================================
|
||||
7. RewriteNode (placeholder)
|
||||
============================================================ */
|
||||
export function RewriteNode({ selected }: any) {
|
||||
export function RewriteNode({ data, selected }: any) {
|
||||
const d: NodeData = data
|
||||
return (
|
||||
<NodeShell
|
||||
type="ai" status="pending"
|
||||
@@ -793,6 +804,8 @@ export function RewriteNode({ selected }: any) {
|
||||
title="产品文案 · Rewrite"
|
||||
subtitle="STEP 5 · 接 SKG 卖点"
|
||||
selected={selected}
|
||||
pinned={d.pinnedNodes?.has("rewrite")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("rewrite")}
|
||||
>
|
||||
<textarea
|
||||
placeholder="粘贴 SKG 产品信息 / 关键卖点(可作为视频脚本和镜头动作参考)"
|
||||
@@ -929,6 +942,8 @@ export function StoryboardNode({ data, selected }: any) {
|
||||
title="元素改造 · Storyboard"
|
||||
subtitle={`STEP 6 · 参考元素 → SKG 画面${storyboardCount > 0 ? ` · ${storyboardCount} 分镜` : ""}`}
|
||||
selected={selected}
|
||||
pinned={d.pinnedNodes?.has("storyboard")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("storyboard")}
|
||||
>
|
||||
<div className="text-[11.5px] leading-relaxed text-[var(--text-soft)]">
|
||||
不是复刻原视频:先把参考图里的主体 / 场景 / 动作 / 道具拆出来,再替换成 SKG 产品画面。
|
||||
@@ -1103,6 +1118,8 @@ export function VideoGenNode({ data, selected }: any) {
|
||||
title="生成视频 · Video Gen"
|
||||
subtitle={`STEP 7 · 首帧 + 动作 prompt${videos.length > 0 ? ` · ${videos.length} 个视频任务` : ""}`}
|
||||
selected={selected}
|
||||
pinned={d.pinnedNodes?.has("videogen")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("videogen")}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-1.5 text-[10.5px]">
|
||||
{["Seedance", "Kling", "Veo 3"].map((m) => (
|
||||
@@ -1124,7 +1141,8 @@ export function VideoGenNode({ data, selected }: any) {
|
||||
/* ============================================================
|
||||
10. ComposeNode (placeholder)
|
||||
============================================================ */
|
||||
export function ComposeNode({ selected }: any) {
|
||||
export function ComposeNode({ data, selected }: any) {
|
||||
const d: NodeData = data
|
||||
return (
|
||||
<NodeShell
|
||||
type="output" status="pending"
|
||||
@@ -1133,6 +1151,8 @@ export function ComposeNode({ selected }: any) {
|
||||
subtitle="STEP 8 · ffmpeg + 字幕"
|
||||
selected={selected}
|
||||
hasSource={false}
|
||||
pinned={d.pinnedNodes?.has("compose")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("compose")}
|
||||
>
|
||||
<div className="text-[11.5px] text-[var(--text-soft)] leading-relaxed">
|
||||
视频片段 + 字幕 / TTS<br />→ 最终 mp4 输出
|
||||
|
||||
Reference in New Issue
Block a user