diff --git a/.memory/worklog.json b/.memory/worklog.json index 2baf947..51f517f 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -76,6 +76,13 @@ "message": "auto-save 2026-05-12 16:33 (~1)", "hash": "37bf7c9", "files_changed": 1 + }, + { + "ts": "2026-05-12T16:39:03+08:00", + "type": "commit", + "message": "auto-save 2026-05-12 16:38 (~1)", + "hash": "1b95cb2", + "files_changed": 1 } ] } diff --git a/web/app/globals.css b/web/app/globals.css index 39022c9..bf0d29c 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -9,6 +9,7 @@ ============================================================ */ :root { + color-scheme: light; /* ---- Light · 暖白底 ---- */ --bg-canvas-1: #f6f4ed; --bg-canvas-2: #ece6d8; @@ -59,6 +60,7 @@ } .dark { + color-scheme: dark; /* ---- Dark · 深蓝紫 ---- */ --bg-canvas-1: #0a0d1c; --bg-canvas-2: #14172e; diff --git a/web/app/page.tsx b/web/app/page.tsx index 303fdc1..3945b3f 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { ReactFlow, Background, BackgroundVariant, Controls, MiniMap, + useNodesState, useEdgesState, type Node, type Edge, } from "@xyflow/react" import { Toaster, toast } from "sonner" @@ -137,19 +138,29 @@ export default function Home() { onToggleFrame: handleToggleFrame, }), [job, submitting, selectedFrames, handleSubmit, handleUpload, handleToggleFrame]) - const nodes: Node[] = useMemo( - () => LAYOUT.map((n) => ({ + // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) + const [nodes, setNodes, onNodesChange] = useNodesState( + LAYOUT.map((n) => ({ id: n.id, type: n.type, position: { x: n.x, y: n.y }, data: nodeData, draggable: true, })), - [nodeData], + ) + const [edges, setEdges, onEdgesChange] = useEdgesState( + EDGES_RAW.map(([from, to], i) => ({ + id: `e${i}`, source: from, target: to, animated: false, type: "default", + })), ) - // 边状态:source 节点 done 时 animated - const edges: Edge[] = useMemo(() => { + // Job 数据变化时只更新节点 data 不动 position + useEffect(() => { + setNodes((prev) => prev.map((n) => ({ ...n, data: nodeData }))) + }, [nodeData, setNodes]) + + // 边的 animated 状态跟 Job 进度联动 + useEffect(() => { const doneOf: Record = { input: !!job, download: !!job?.video_url, @@ -158,14 +169,8 @@ export default function Home() { asr: !!job && job.transcript.length > 0, translate: !!job && (job.transcript.some((s) => s.zh) ?? false), } - return EDGES_RAW.map(([from, to], i) => ({ - id: `e${i}`, - source: from, - target: to, - animated: !!doneOf[from], - type: "default", - })) - }, [job]) + setEdges((prev) => prev.map((e) => ({ ...e, animated: !!doneOf[e.source] }))) + }, [job, setEdges]) return ( <> @@ -197,6 +202,8 @@ export default function Home() { | an onChange={(e) => setUrl(e.target.value)} placeholder="粘贴 TikTok 链接" disabled={isLocked} - className="w-full text-[12px] px-2.5 py-2 rounded-md bg-white/40 dark:bg-white/[0.04] border border-black/10 dark:border-white/10 outline-none placeholder:text-[var(--text-faint)] focus:ring-2 focus:ring-[var(--ring)] disabled:opacity-40" + className="w-full text-[12px] px-2.5 py-2 rounded-md bg-white/60 dark:bg-black/40 border border-black/10 dark:border-white/10 outline-none text-[var(--text-strong)] placeholder:text-[var(--text-faint)] focus:ring-2 focus:ring-[var(--ring)] disabled:opacity-40" />