"use client"; import { useCallback, useRef, useState, useEffect } from "react"; import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { useWorkflowStore } from "@/store/workflowStore"; import { AudioInputNodeData } from "@/types"; import { useAudioVisualization } from "@/hooks/useAudioVisualization"; import { useAudioPlayback } from "@/hooks/useAudioPlayback"; import { downloadMedia } from "@/utils/downloadMedia"; type AudioInputNodeType = Node; export function AudioInputNode({ id, data, selected }: NodeProps) { const nodeData = data; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const fileInputRef = useRef(null); const [audioBlob, setAudioBlob] = useState(null); // Convert base64 data URL to Blob for the hook useEffect(() => { if (nodeData.audioFile) { fetch(nodeData.audioFile) .then((r) => r.blob()) .then(setAudioBlob) .catch(() => setAudioBlob(null)); } else { setAudioBlob(null); } }, [nodeData.audioFile]); const { waveformData, isLoading } = useAudioVisualization(audioBlob); const { audioRef, canvasRef, waveformContainerRef, isPlaying, currentTime, handlePlayPause, handleSeek, formatTime, } = useAudioPlayback({ audioSrc: nodeData.audioFile ?? null, waveformData, isLoadingWaveform: isLoading, }); const handleFileChange = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (!file.type.match(/^audio\//)) { alert("Unsupported format. Use MP3, WAV, OGG, AAC, or other audio formats."); return; } if (file.size > 50 * 1024 * 1024) { alert("Audio file too large. Maximum size is 50MB."); return; } const reader = new FileReader(); reader.onload = (event) => { const base64 = event.target?.result as string; // Extract duration using HTML Audio element const audio = new Audio(base64); audio.onloadedmetadata = () => { updateNodeData(id, { audioFile: base64, filename: file.name, format: file.type, duration: audio.duration, }); }; audio.onerror = () => { // Still load the file even if metadata extraction fails updateNodeData(id, { audioFile: base64, filename: file.name, format: file.type, duration: null, }); }; }; reader.readAsDataURL(file); }, [id, updateNodeData] ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); const file = e.dataTransfer.files?.[0]; if (!file) return; const dt = new DataTransfer(); dt.items.add(file); if (fileInputRef.current) { fileInputRef.current.files = dt.files; fileInputRef.current.dispatchEvent(new Event("change", { bubbles: true })); } }, [] ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }, []); const handleRemove = useCallback(() => { if (audioRef.current) { audioRef.current.pause(); audioRef.current = null; } setAudioBlob(null); updateNodeData(id, { audioFile: null, filename: null, duration: null, format: null, }); }, [id, updateNodeData, audioRef]); return ( {nodeData.audioFile ? (
{nodeData.isOptional && ( Optional )} {/* Filename and duration */}
{nodeData.filename} {nodeData.duration && ( {formatTime(nodeData.duration)} )}
{/* Waveform visualization */} {isLoading ? (
Loading waveform...
) : waveformData ? (
) : (
Processing...
)} {/* Play/pause controls */}
{/* Progress bar / scrubber */}
{audioRef.current?.duration && isFinite(audioRef.current.duration) && (
)}
{/* Current time */} {formatTime(currentTime)}
{/* Download button */} {/* Remove button */}
) : (
fileInputRef.current?.click()} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fileInputRef.current?.click(); } }} onDrop={handleDrop} onDragOver={handleDragOver} className={`w-full h-full bg-neutral-900/40 flex flex-col items-center justify-center cursor-pointer hover:bg-neutral-800/60 transition-colors ${nodeData.isOptional ? "border-2 border-dashed border-neutral-600" : ""}`} > {nodeData.isOptional ? "Optional" : "Drop audio or click"}
)} ); }