diff --git a/.memory/worklog.json b/.memory/worklog.json
index 0d18724..49672a9 100644
--- a/.memory/worklog.json
+++ b/.memory/worklog.json
@@ -2665,6 +2665,19 @@
"message": "auto-save 2026-05-14 00:25 (+6, ~5)",
"hash": "abeff42",
"files_changed": 11
+ },
+ {
+ "ts": "2026-05-14T00:31:52+08:00",
+ "type": "commit",
+ "message": "auto-save 2026-05-14 00:31 (~1)",
+ "hash": "5c9c80e",
+ "files_changed": 1
+ },
+ {
+ "ts": "2026-05-13T16:33:09Z",
+ "type": "session-heartbeat",
+ "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 00:31 (~1)",
+ "files_changed": 1
}
]
}
diff --git a/web/components/nodes/hover-preview.tsx b/web/components/nodes/hover-preview.tsx
new file mode 100644
index 0000000..852b618
--- /dev/null
+++ b/web/components/nodes/hover-preview.tsx
@@ -0,0 +1,57 @@
+"use client"
+
+/**
+ * 视觉类节点统一 hover 大预览:
+ * - 浮在缩略图正上方(bottom: calc(100% + 10px),居中),跟随 ReactFlow viewport 缩放
+ * - 固定 280px 宽,原比例高
+ * - 视频自动播放(muted loop),图片静态
+ * - pointer-events-none,不阻挡同行/邻近交互
+ * - 用法:
...
+ */
+interface Props {
+ imgSrc?: string
+ videoSrc?: string
+ poster?: string
+ aspect: string
+ label?: string
+ caption?: string
+ borderClass?: string
+ width?: number
+}
+
+export function HoverPreview({
+ imgSrc, videoSrc, poster, aspect,
+ label, caption,
+ borderClass = "border-violet-300/55",
+ width = 280,
+}: Props) {
+ return (
+
+
+
+ {videoSrc ? (
+
+ ) : imgSrc ? (
+

+ ) : (
+
+ )}
+
+ {(label || caption) && (
+
+ {label && {label}}
+ {caption && {caption}}
+
+ )}
+
+
+ )
+}
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx
index de75261..8697e6a 100644
--- a/web/components/nodes/index.tsx
+++ b/web/components/nodes/index.tsx
@@ -9,6 +9,7 @@ import {
} from "lucide-react"
import { toast } from "sonner"
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
+import { HoverPreview } from "./hover-preview"
import {
type Job, type ImageRef,
apiAssetUrl, effectiveFrameUrl, videoUrl, hasCutout, representativeCutoutUrl,
@@ -124,45 +125,54 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
{[...d.jobs].reverse().map((j) => {
const isActive = j.id === d.activeJobId
const ready = !!j.video_url
+ const aspectStr = ready ? `${j.width}/${j.height}` : "9/16"
return (
-