"use client"; import { useCallback, useState, useMemo, useEffect, useRef } 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 { OutputNodeData } from "@/types"; import { useVideoBlobUrl } from "@/hooks/useVideoBlobUrl"; type OutputNodeType = Node; export function OutputNode({ id, data, selected }: NodeProps) { const nodeData = data; const commentNavigation = useCommentNavigation(id); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const regenerateNode = useWorkflowStore((state) => state.regenerateNode); const connectedEdgeCount = useWorkflowStore( (state) => state.edges.filter((edge) => edge.target === id).length ); const isRunning = useWorkflowStore((state) => state.isRunning); const [showLightbox, setShowLightbox] = useState(false); const previousEdgeCountRef = useRef(null); // Determine if content is audio const isAudio = useMemo(() => { if (nodeData.audio) return true; if (nodeData.contentType === "audio") return true; if (nodeData.image?.startsWith("data:audio/")) return true; return false; }, [nodeData.audio, nodeData.contentType, nodeData.image]); // Determine if content is video const isVideo = useMemo(() => { if (isAudio) return false; if (nodeData.video) return true; if (nodeData.contentType === "video") return true; if (nodeData.image?.startsWith("data:video/")) return true; if (nodeData.image?.includes(".mp4") || nodeData.image?.includes(".webm")) return true; return false; }, [isAudio, nodeData.video, nodeData.contentType, nodeData.image]); // Get the content source (audio, video, or image) const contentSrc = useMemo(() => { if (nodeData.audio) return nodeData.audio; if (nodeData.video) return nodeData.video; return nodeData.image; }, [nodeData.audio, nodeData.video, nodeData.image]); const videoBlobUrl = useVideoBlobUrl(isVideo ? contentSrc ?? null : null); // Auto-trigger execution when a new connection is made useEffect(() => { if (previousEdgeCountRef.current === null) { // First run — just record the baseline, don't trigger previousEdgeCountRef.current = connectedEdgeCount; return; } if (connectedEdgeCount > previousEdgeCountRef.current) { regenerateNode(id); } previousEdgeCountRef.current = connectedEdgeCount; }, [connectedEdgeCount, id, regenerateNode]); // Handle Run button click const handleRun = useCallback(() => { regenerateNode(id); }, [id, regenerateNode]); const handleDownload = useCallback(async () => { if (!contentSrc) return; const timestamp = Date.now(); const extension = isAudio ? "mp3" : isVideo ? "mp4" : "png"; // Use custom filename if provided, otherwise use timestamp const filename = nodeData.outputFilename ? `${nodeData.outputFilename}.${extension}` : `generated-${timestamp}.${extension}`; // Handle URL-based content (needs fetch + blob conversion) if (contentSrc.startsWith("http://") || contentSrc.startsWith("https://")) { try { const response = await fetch(contentSrc); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = blobUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(blobUrl); } catch (error) { console.error("Failed to download:", error); } return; } // Handle data URL content (direct download) const link = document.createElement("a"); link.href = contentSrc; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); }, [contentSrc, isAudio, isVideo, nodeData.outputFilename]); return ( <>
{contentSrc ? ( <> {isAudio ? (
) : (
setShowLightbox(true)} > {isVideo ? (
)} ) : (
Connect input
)}
{/* Lightbox Modal (skip for audio) */} {showLightbox && contentSrc && !isAudio && (
setShowLightbox(false)} >
{isVideo ? (
)} ); }