diff --git a/src/components/nodes/VideoFrameGrabNode.tsx b/src/components/nodes/VideoFrameGrabNode.tsx new file mode 100644 index 00000000..aa6509e5 --- /dev/null +++ b/src/components/nodes/VideoFrameGrabNode.tsx @@ -0,0 +1,175 @@ +"use client"; + +import React, { useMemo } from "react"; +import { Handle, Position, NodeProps, Node } from "@xyflow/react"; +import { BaseNode } from "./BaseNode"; +import { useCommentNavigation } from "@/hooks/useCommentNavigation"; +import { useWorkflowStore } from "@/store/workflowStore"; +import { VideoFrameGrabNodeData } from "@/types"; + +type VideoFrameGrabNodeType = Node; + +export function VideoFrameGrabNode({ id, data, selected }: NodeProps) { + const nodeData = data; + const commentNavigation = useCommentNavigation(id); + const updateNodeData = useWorkflowStore((state) => state.updateNodeData); + const regenerateNode = useWorkflowStore((state) => state.regenerateNode); + const isRunning = useWorkflowStore((state) => state.isRunning); + const edges = useWorkflowStore((state) => state.edges); + const nodes = useWorkflowStore((state) => state.nodes); + + // Find connected source video from incoming edges + const sourceVideoUrl = useMemo(() => { + const incomingEdge = edges.find((e) => e.target === id && e.targetHandle === "video"); + if (!incomingEdge) return null; + + const sourceNode = nodes.find((n) => n.id === incomingEdge.source); + if (!sourceNode) return null; + + const d = sourceNode.data as Record; + return (d.outputVideo as string | null) ?? null; + }, [edges, nodes, id]); + + const hasSourceVideo = Boolean(sourceVideoUrl); + const canExtract = hasSourceVideo && nodeData.status !== "loading" && !isRunning; + + const handleExtract = () => { + regenerateNode(id); + }; + + return ( + updateNodeData(id, { customTitle: title || undefined })} + onCommentChange={(comment) => updateNodeData(id, { comment: comment || undefined })} + onRun={canExtract ? handleExtract : undefined} + selected={selected} + isExecuting={isRunning} + hasError={nodeData.status === "error"} + commentNavigation={commentNavigation ?? undefined} + minWidth={320} + minHeight={320} + > + {/* Video In (target, left, 50%) */} + +
+ Video In +
+ + {/* Image Out (source, right, 50%) */} + +
+ Image Out +
+ +
+ {/* Image preview area */} +
+ {nodeData.outputImage ? ( + <> + Extracted frame + {/* Clear output button */} + + + ) : ( +
+ + Connect a video and extract a frame + +
+ )} +
+ + {/* Frame position toggle (only when source video connected) */} + {hasSourceVideo && ( +
+ + +
+ )} + + {/* Extract Frame button */} +
+ +
+ + {/* Processing overlay */} + {nodeData.status === "loading" && ( +
+ + + + + Extracting frame... +
+ )} + + {/* Error display */} + {nodeData.status === "error" && nodeData.error && ( +
+

{nodeData.error}

+
+ )} +
+
+ ); +}