diff --git a/.memory/worklog.json b/.memory/worklog.json index 8a2db78..4766cd3 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -2771,6 +2771,19 @@ "message": "auto-save 2026-05-14 01:22 (+3, ~2)", "hash": "9fc2442", "files_changed": 5 + }, + { + "ts": "2026-05-14T01:29:00+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 01:28 (+5, ~3)", + "hash": "d054788", + "files_changed": 8 + }, + { + "ts": "2026-05-13T17:33:10Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 01:28 (+5, ~3)", + "files_changed": 1 } ] } diff --git a/.playwright-mcp/page-2026-05-13T17-34-21-918Z.yml b/.playwright-mcp/page-2026-05-13T17-34-21-918Z.yml new file mode 100644 index 0000000..c746b75 --- /dev/null +++ b/.playwright-mcp/page-2026-05-13T17-34-21-918Z.yml @@ -0,0 +1,19 @@ +- generic [active] [ref=e1]: + - main [ref=e3]: + - button "自动排版 · 保留每个节点的尺寸,重新排好间距和列布局" [ref=e5]: + - img [ref=e6] + - application [ref=e13]: + - img + - generic "Control Panel" [ref=e16]: + - button "Zoom In" [ref=e17] [cursor=pointer]: + - img [ref=e18] + - button "Zoom Out" [ref=e20] [cursor=pointer]: + - img [ref=e21] + - button "Fit View" [ref=e23] [cursor=pointer]: + - img [ref=e24] + - button "Toggle Interactivity" [ref=e26] [cursor=pointer]: + - img [ref=e27] + - img "Mini Map" [ref=e30] + - region "Notifications alt+T" + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-13T17-34-25-014Z.yml b/.playwright-mcp/page-2026-05-13T17-34-25-014Z.yml new file mode 100644 index 0000000..4b12fe4 --- /dev/null +++ b/.playwright-mcp/page-2026-05-13T17-34-25-014Z.yml @@ -0,0 +1,456 @@ +- generic [active] [ref=e1]: + - generic [ref=e36] [cursor=pointer]: + - button "Open Next.js Dev Tools" [ref=e37]: + - img [ref=e38] + - generic [ref=e43]: + - button "Open issues overlay" [ref=e44]: + - generic [ref=e45]: + - generic [ref=e46]: "0" + - generic [ref=e47]: "1" + - generic [ref=e48]: Issue + - button "Collapse issues badge" [ref=e49]: + - img [ref=e50] + - main [ref=e53]: + - button "自动排版 · 保留每个节点的尺寸,重新排好间距和列布局" [ref=e55]: + - img [ref=e56] + - button "切到明亮主题" [ref=e62]: + - img [ref=e63] + - generic [ref=e69]: + - generic [ref=e72]: + - generic [ref=e73]: + - img [ref=e74] + - generic [ref=e79]: 分镜头编排 + - generic [ref=e80]: 0 分镜 · 0 元素 + - generic [ref=e81]: · 组织分镜画面 → 为生成视频做准备 + - button "展开编排" [disabled] [ref=e83]: + - img [ref=e84] + - text: 展开编排 + - application [ref=e87]: + - generic [ref=e89]: + - generic: + - generic: + - img: + - group "Edge from input to keyframe" [ref=e90] [cursor=pointer] + - img: + - group "Edge from input to asr" [ref=e93] [cursor=pointer] + - img: + - group "Edge from asr to translate" [ref=e96] [cursor=pointer] + - img: + - group "Edge from translate to rewrite" [ref=e99] [cursor=pointer] + - img: + - group "Edge from keyframe to storyboard" [ref=e102] [cursor=pointer] + - img: + - group "Edge from rewrite to storyboard" [ref=e105] [cursor=pointer] + - img: + - group "Edge from storyboard to videogen" [ref=e108] [cursor=pointer] + - img: + - group "Edge from videogen to compose" [ref=e111] [cursor=pointer] + - img: + - group "Edge from rewrite to compose" [ref=e114] [cursor=pointer] + - generic: + - group [ref=e117]: + - generic [ref=e118]: + - generic [ref=e119]: + - button "再上传一个视频" [ref=e120]: + - img [ref=e121] + - generic [ref=e122]: + - button "64.5s" [ref=e123]: + - generic [ref=e125]: 64.5s + - generic: + - generic: + - generic: + - generic: 1080×1920 + - generic: 64.5s + - generic [ref=e126]: + - button "72.4s" [ref=e127]: + - generic [ref=e129]: 72.4s + - generic: + - generic: + - generic: + - generic: 576×1024 + - generic: 72.4s + - generic [ref=e130]: + - button "64.5s" [ref=e131]: + - generic [ref=e133]: 64.5s + - generic: + - generic: + - generic: + - generic: 1080×1920 + - generic: 64.5s + - generic [ref=e134]: + - button "71.4s" [ref=e135]: + - generic [ref=e137]: 71.4s + - generic: + - generic: + - generic: + - generic: 1080×1920 + - generic: 71.4s + - generic [ref=e138]: + - button "72.4s" [ref=e139]: + - generic [ref=e141]: 72.4s + - generic: + - generic: + - generic: + - generic: 576×1024 + - generic: 72.4s + - generic [ref=e142]: + - button "71.4s" [ref=e143]: + - generic [ref=e145]: 71.4s + - generic: + - generic: + - generic: + - generic: 1080×1920 + - generic: 71.4s + - generic [ref=e146]: + - button "71.4s" [ref=e147]: + - generic [ref=e149]: 71.4s + - generic: + - generic: + - generic: + - generic: 1080×1920 + - generic: 71.4s + - generic [ref=e150]: + - button "71.4s" [ref=e151]: + - generic [ref=e153]: 71.4s + - generic: + - generic: + - generic: + - generic: 1080×1920 + - generic: 71.4s + - generic [ref=e154]: + - button "71.4s" [ref=e155]: + - generic [ref=e157]: 71.4s + - generic: + - generic: + - generic: + - generic: 1080×1920 + - generic: 71.4s + - generic [ref=e158]: + - button "71.4s" [ref=e159]: + - generic [ref=e161]: 71.4s + - generic: + - generic: + - generic: + - generic: 1080×1920 + - generic: 71.4s + - generic [ref=e162]: + - button "8.0s" [ref=e163]: + - generic [ref=e165]: 8.0s + - generic: + - generic: + - generic: + - generic: 640×360 + - generic: 8.0s + - generic [ref=e166]: + - button "8.0s" [ref=e167]: + - generic [ref=e169]: 8.0s + - generic: + - generic: + - generic: + - generic: 640×360 + - generic: 8.0s + - generic [ref=e170]: + - button "8.0s" [ref=e171]: + - generic [ref=e173]: 8.0s + - generic: + - generic: + - generic: + - generic: 640×360 + - generic: 8.0s + - generic [ref=e174]: + - button "8.0s" [ref=e175]: + - generic [ref=e177]: 8.0s + - generic: + - generic: + - generic: + - generic: 640×360 + - generic: 8.0s + - button "…" [ref=e179]: + - img [ref=e181] + - generic [ref=e183]: … + - button "…" [ref=e185]: + - img [ref=e187] + - generic [ref=e189]: … + - button "…" [ref=e191]: + - img [ref=e193] + - generic [ref=e195]: … + - generic [ref=e196]: + - generic [ref=e197]: + - img [ref=e199] + - generic [ref=e202]: 输入 · Input + - generic [ref=e203]: + - img [ref=e204] + - button "钉住 · 锁定位置与尺寸" [ref=e208]: + - img [ref=e209] + - generic [ref=e212]: + - generic [ref=e213]: STEP 1 · 视频就绪 · 完成 + - textbox "再加一个 TK 链接" [ref=e214] + - generic [ref=e215]: + - button "+ 加链接" [disabled] [ref=e216] + - button "再传一个" [ref=e217]: + - img [ref=e218] + - text: 再传一个 + - generic [ref=e221]: + - generic [ref=e222]: 1080×1920 · 64.5s + - generic [ref=e223]: 🔗 链接 + - button "重新解析" [ref=e224] + - generic "拖动调整宽度" [ref=e226] + - generic "拖动调整大小(宽 × 高)" [ref=e227] + - group [ref=e228]: + - generic [ref=e229]: + - generic [ref=e230]: + - generic [ref=e231]: + - button "frame 9 1.7s" [ref=e232]: + - img "frame 9" [ref=e233] + - generic [ref=e234]: 1.7s + - button "📋" [ref=e235] + - button "删除该关键帧" [ref=e236]: + - img [ref=e237] + - generic: + - generic: + - generic: + - generic: 分镜 10 + - generic: 1.66s + - generic [ref=e240]: + - button "frame 0 ✨ 24.7s" [ref=e241]: + - img "frame 0" [ref=e242] + - generic "已清洗" [ref=e244]: ✨ + - generic [ref=e245]: 24.7s + - button "📋" [ref=e246] + - button "删除该关键帧" [ref=e247]: + - img [ref=e248] + - generic: + - generic: + - generic: + - generic: 分镜 1 + - generic: 24.73s + - generic [ref=e251]: + - button "frame 1 33.6s" [ref=e252]: + - img "frame 1" [ref=e253] + - generic [ref=e254]: 33.6s + - button "📋" [ref=e255] + - button "删除该关键帧" [ref=e256]: + - img [ref=e257] + - generic: + - generic: + - generic: + - generic: 分镜 2 + - generic: 33.61s + - generic [ref=e260]: + - button "frame 2 37.7s" [ref=e261]: + - img "frame 2" [ref=e262] + - generic [ref=e263]: 37.7s + - button "📋" [ref=e264] + - button "删除该关键帧" [ref=e265]: + - img [ref=e266] + - generic: + - generic: + - generic: + - generic: 分镜 3 + - generic: 37.70s + - generic [ref=e269]: + - button "frame 3 39.4s" [ref=e270]: + - img "frame 3" [ref=e271] + - generic [ref=e272]: 39.4s + - button "📋" [ref=e273] + - button "删除该关键帧" [ref=e274]: + - img [ref=e275] + - generic: + - generic: + - generic: + - generic: 分镜 4 + - generic: 39.42s + - generic [ref=e278]: + - button "frame 4 1 43.1s" [ref=e279]: + - img "frame 4" [ref=e280] + - generic "1 个元素已抠图" [ref=e282]: "1" + - generic [ref=e283]: 43.1s + - button "📋" [ref=e284] + - button "删除该关键帧" [ref=e285]: + - img [ref=e286] + - generic: + - generic: + - generic: + - generic: 分镜 5 + - generic: 43.13s + - generic [ref=e289]: + - button "frame 5 45.0s" [ref=e290]: + - img "frame 5" [ref=e291] + - generic [ref=e292]: 45.0s + - button "📋" [ref=e293] + - button "删除该关键帧" [ref=e294]: + - img [ref=e295] + - generic: + - generic: + - generic: + - generic: 分镜 6 + - generic: 45.05s + - generic [ref=e298]: + - button "frame 6 53.6s" [ref=e299]: + - img "frame 6" [ref=e300] + - generic [ref=e301]: 53.6s + - button "📋" [ref=e302] + - button "删除该关键帧" [ref=e303]: + - img [ref=e304] + - generic: + - generic: + - generic: + - generic: 分镜 7 + - generic: 53.60s + - generic [ref=e307]: + - button "frame 7 56.0s" [ref=e308]: + - img "frame 7" [ref=e309] + - generic [ref=e310]: 56.0s + - button "📋" [ref=e311] + - button "删除该关键帧" [ref=e312]: + - img [ref=e313] + - generic: + - generic: + - generic: + - generic: 分镜 8 + - generic: 55.96s + - generic [ref=e316]: + - button "frame 8 58.4s" [ref=e317]: + - img "frame 8" [ref=e318] + - generic [ref=e319]: 58.4s + - button "📋" [ref=e320] + - button "删除该关键帧" [ref=e321]: + - img [ref=e322] + - generic: + - generic: + - generic: + - generic: 分镜 9 + - generic: 58.39s + - generic [ref=e325]: + - generic [ref=e327]: + - img [ref=e329] + - generic [ref=e333]: 镜头拆解 · 元素提取 + - generic [ref=e334]: + - img [ref=e335] + - button "钉住 · 锁定位置与尺寸" [ref=e339]: + - img [ref=e340] + - generic [ref=e343]: + - generic [ref=e344]: STEP 2 · 0/10 入编排 · 完成 + - generic [ref=e345]: + - text: 自动 10 张 · + - generic [ref=e346]: 1 已清洗 + - text: · + - generic [ref=e347]: 1/2 已抠图 + - text: 点缩略图 → 清洗水印 / 提取可借鉴元素 → 改造成 SKG 画面素材 + - generic "拖动调整宽度" [ref=e349] + - generic "拖动调整大小(宽 × 高)" [ref=e350] + - group [ref=e351]: + - generic [ref=e352]: + - generic [ref=e354]: + - img [ref=e356] + - generic [ref=e359]: 声音文案 · ASR + - button "钉住 · 锁定位置与尺寸" [ref=e362]: + - img [ref=e363] + - generic [ref=e366]: + - generic [ref=e367]: STEP 3 · 可选文案轨 · 待运行 + - generic [ref=e368]: Gemini 2.5 · 英文带时间戳分段 + - generic "拖动调整宽度" [ref=e370] + - generic "拖动调整大小(宽 × 高)" [ref=e371] + - group [ref=e372]: + - generic [ref=e373]: + - generic [ref=e375]: + - img [ref=e377] + - generic [ref=e381]: 翻译理解 · Translate + - button "钉住 · 锁定位置与尺寸" [ref=e384]: + - img [ref=e385] + - generic [ref=e388]: + - generic [ref=e389]: STEP 4 · EN → ZH · 待运行 + - generic [ref=e390]: 中文翻译 · 段落级 · 实时输出 + - generic "拖动调整宽度" [ref=e392] + - generic "拖动调整大小(宽 × 高)" [ref=e393] + - group [ref=e394]: + - generic [ref=e395]: + - generic [ref=e397]: + - button "透明骷髅" [ref=e398]: + - img "透明骷髅" [ref=e399] + - button "📋" [ref=e400] + - button "删除该提取图" [ref=e401]: + - img [ref=e402] + - generic: + - generic: + - generic: + - generic: 分镜 5 · 透明骷髅 + - generic: 43.13s + - generic [ref=e405]: + - generic [ref=e407]: + - img [ref=e409] + - generic [ref=e414]: 元素改造 · Storyboard + - generic [ref=e415]: + - img [ref=e416] + - button "钉住 · 锁定位置与尺寸" [ref=e420]: + - img [ref=e421] + - generic [ref=e424]: + - generic [ref=e425]: STEP 6 · 参考元素 → SKG 画面 · 完成 + - generic [ref=e426]: + - text: 不是复刻原视频:先把参考图里的主体 / 场景 / 动作 / 道具拆出来,再替换成 SKG 产品画面。 + - generic [ref=e427]: 已有 1 个提取元素 · 0 个分镜进入编排 + - button "进入分镜编排" [disabled] [ref=e428] + - generic "拖动调整宽度" [ref=e430] + - generic "拖动调整大小(宽 × 高)" [ref=e431] + - group [ref=e432]: + - generic [ref=e433]: + - generic [ref=e435]: + - img [ref=e437] + - generic [ref=e441]: 产品文案 · Rewrite + - button "钉住 · 锁定位置与尺寸" [ref=e444]: + - img [ref=e445] + - generic [ref=e448]: + - generic [ref=e449]: STEP 5 · 接 SKG 卖点 · 待运行 + - textbox "粘贴 SKG 产品信息 / 关键卖点(可作为视频脚本和镜头动作参考)" [disabled] [ref=e450] + - generic [ref=e451]: 下一冲刺接入 + - generic "拖动调整宽度" [ref=e453] + - generic "拖动调整大小(宽 × 高)" [ref=e454] + - group [ref=e455]: + - generic [ref=e457]: + - generic [ref=e459]: + - img [ref=e461] + - generic [ref=e463]: 生成视频 · Video Gen + - button "钉住 · 锁定位置与尺寸" [ref=e466]: + - img [ref=e467] + - generic [ref=e470]: + - generic [ref=e471]: STEP 7 · 首帧 + 动作 prompt · 待运行 + - generic [ref=e472]: + - generic [ref=e473]: Seedance + - generic [ref=e474]: Kling + - generic [ref=e475]: Veo 3 + - generic "拖动调整宽度" [ref=e477] + - generic "拖动调整大小(宽 × 高)" [ref=e478] + - group [ref=e479]: + - generic [ref=e480]: + - generic [ref=e482]: + - img [ref=e484] + - generic [ref=e488]: 合成成品 · Compose + - button "钉住 · 锁定位置与尺寸" [ref=e491]: + - img [ref=e492] + - generic [ref=e495]: + - generic [ref=e496]: STEP 8 · ffmpeg + 字幕 · 待运行 + - generic [ref=e497]: + - text: 视频片段 + 字幕 / TTS + - text: → 最终 mp4 输出 + - generic "拖动调整宽度" [ref=e498] + - generic "拖动调整大小(宽 × 高)" [ref=e499] + - img + - generic "Control Panel" [ref=e500]: + - button "Zoom In" [ref=e501] [cursor=pointer]: + - img [ref=e502] + - button "Zoom Out" [ref=e504] [cursor=pointer]: + - img [ref=e505] + - button "Fit View" [ref=e507] [cursor=pointer]: + - img [ref=e508] + - button "Toggle Interactivity" [ref=e510] [cursor=pointer]: + - img [ref=e511] + - img "Mini Map" [ref=e514] + - region "Notifications alt+T": + - list: + - listitem [ref=e524]: + - img [ref=e526] + - generic [ref=e529]: 已自动排版 · 保留每个节点的尺寸 + - listitem [ref=e530]: + - img [ref=e532] + - generic [ref=e535]: 📥 视频已就绪 — 请点 Input 节点里的「点这里开始解析」按钮 + - alert [ref=e536] \ No newline at end of file diff --git a/web/components/nodes/hover-preview.tsx b/web/components/nodes/hover-preview.tsx index bd1692d..5d8e739 100644 --- a/web/components/nodes/hover-preview.tsx +++ b/web/components/nodes/hover-preview.tsx @@ -1,14 +1,14 @@ "use client" -import { useEffect, useLayoutEffect, useRef, useState } from "react" -import { createPortal } from "react-dom" import { X } from "lucide-react" /** * 视觉类节点统一大预览: - * - 用 portal 渲染到 document.body,**脱离 ReactFlow viewport transform**,图片/视频按真实像素显示不被画布缩放影响 - * - 自动锚定到 HoverPreview 在 DOM 树中的 parent(也就是 thumb 容器),mouseenter/leave 控制显示 - * - pinned=true:常驻显示,pointer-events 开启,可点 × 关闭;点击节点外由调用方负责 unpin - * - 图片/视频按 natural 像素尺寸渲染,max-w/max-h 限制不超出屏幕 + * - **在 ReactFlow 节点 DOM 内**作为 absolute 元素,贴 thumb 上方边缘(bottom: calc(100% + 10px) + 居中) + * - 跟随 ReactFlow 画布 pan/zoom 一起变化(属于"无限画布"的一部分) + * - 媒体按"自然像素分辨率"渲染 + max-w/max-h 限制,避免占满整个画布 + * - 不 pinned 时:pointer-events-none,依赖 group-hover 显示/隐藏 + * - pinned=true:强制 visible,pointer-events 开启,可点 × 关闭 + * - 用法:父级容器要带 `group` class,HoverPreview 直接作为子元素 */ interface Props { imgSrc?: string @@ -18,8 +18,8 @@ interface Props { label?: string caption?: string borderClass?: string - maxW?: string - maxH?: string + maxW?: string // 默认 "min(70vw, 1200px)" + maxH?: string // 默认 "min(70vh, 800px)" pinned?: boolean onClose?: () => void } @@ -28,128 +28,53 @@ export function HoverPreview({ imgSrc, videoSrc, poster, aspect, label, caption, borderClass = "border-violet-300/55", - maxW = "min(80vw, 1400px)", - maxH = "min(80vh, 880px)", + maxW = "min(70vw, 1200px)", + maxH = "min(70vh, 800px)", pinned = false, onClose, }: Props) { - const anchorFinderRef = useRef(null) - const portalRef = useRef(null) - const [hovered, setHovered] = useState(false) - const [anchorRect, setAnchorRect] = useState(null) - const [mounted, setMounted] = useState(false) - const [pos, setPos] = useState<{ top: number; left: number; placement: "above" | "below" } | null>(null) - - useEffect(() => { setMounted(true) }, []) - - useEffect(() => { - const finder = anchorFinderRef.current - const anchor = finder?.parentElement - if (!anchor) return - const update = () => setAnchorRect(anchor.getBoundingClientRect()) - const onEnter = () => { update(); setHovered(true) } - const onLeave = () => setHovered(false) - anchor.addEventListener("mouseenter", onEnter) - anchor.addEventListener("mouseleave", onLeave) - // 当 pinned 状态下,定时刷新位置(节点可能被拖动 / 缩放) - let pinnedRaf: number | undefined - if (pinned) { - const tick = () => { update(); pinnedRaf = requestAnimationFrame(tick) } - pinnedRaf = requestAnimationFrame(tick) - } - return () => { - anchor.removeEventListener("mouseenter", onEnter) - anchor.removeEventListener("mouseleave", onLeave) - if (pinnedRaf !== undefined) cancelAnimationFrame(pinnedRaf) - } - }, [pinned]) - - const visible = pinned || hovered - - // 测量 portal 实际尺寸 → 智能选 thumb 上方 / 下方,越界时 clamp 到 viewport 内 - useLayoutEffect(() => { - if (!visible || !anchorRect || !portalRef.current) return - const el = portalRef.current - const measure = () => { - const previewRect = el.getBoundingClientRect() - const ph = previewRect.height - const pw = previewRect.width - const vw = window.innerWidth - const vh = window.innerHeight - const spaceAbove = anchorRect.top - const spaceBelow = vh - anchorRect.bottom - const placement: "above" | "below" = spaceAbove >= ph + 20 ? "above" : (spaceBelow >= spaceAbove ? "below" : "above") - let top = placement === "above" - ? Math.max(10, anchorRect.top - 10 - ph) - : Math.min(vh - ph - 10, anchorRect.bottom + 10) - let left = anchorRect.left + anchorRect.width / 2 - pw / 2 - left = Math.max(10, Math.min(vw - pw - 10, left)) - setPos((prev) => { - if (prev && prev.top === top && prev.left === left && prev.placement === placement) return prev - return { top, left, placement } - }) - } - measure() - // 视频 / 图片 loaded 后实际尺寸可能变化,再测一次 - const media = el.querySelector("video, img") as HTMLVideoElement | HTMLImageElement | null - if (media) { - const reMeasure = () => measure() - media.addEventListener("loadedmetadata", reMeasure) - media.addEventListener("load", reMeasure) - return () => { - media.removeEventListener("loadedmetadata", reMeasure) - media.removeEventListener("load", reMeasure) - } - } - }, [visible, anchorRect, imgSrc, videoSrc]) - + const visibilityCls = pinned + ? "opacity-100 scale-100 pointer-events-auto" + : "pointer-events-none opacity-0 group-hover:opacity-100 scale-95 group-hover:scale-100" return ( - <> - - {mounted && visible && anchorRect && createPortal( -
-
- {videoSrc ? ( -