"use client"; import React, { useCallback, useState, useEffect, useMemo } from "react"; import { Handle, Position, NodeProps, Node, useReactFlow } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { ProviderBadge } from "./ProviderBadge"; import { ModelParameters } from "./ModelParameters"; import { useWorkflowStore } from "@/store/workflowStore"; import { GenerateAudioNodeData, ProviderType, SelectedModel, ModelInputDef } from "@/types"; import { ProviderModel } from "@/lib/providers/types"; import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; import { useAudioVisualization } from "@/hooks/useAudioVisualization"; import { useAudioPlayback } from "@/hooks/useAudioPlayback"; import { useInlineParameters } from "@/hooks/useInlineParameters"; import { InlineParameterPanel } from "./InlineParameterPanel"; type GenerateAudioNodeType = Node; export function GenerateAudioNode({ id, data, selected }: NodeProps) { const nodeData = data; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const generationsPath = useWorkflowStore((state) => state.generationsPath); const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false); const [isLoadingCarouselAudio, setIsLoadingCarouselAudio] = useState(false); // Inline parameters infrastructure const { inlineParametersEnabled } = useInlineParameters(); // Get the current selected provider (default to fal) const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal"; // Convert base64 data URL to Blob for visualization const [audioBlob, setAudioBlob] = useState(null); const { waveformData, isLoading: isLoadingWaveform } = useAudioVisualization(audioBlob); useEffect(() => { if (nodeData.outputAudio) { fetch(nodeData.outputAudio) .then((r) => r.blob()) .then(setAudioBlob) .catch(() => setAudioBlob(null)); } else { setAudioBlob(null); } }, [nodeData.outputAudio]); const { audioRef, canvasRef, waveformContainerRef, isPlaying, currentTime, handlePlayPause, handleSeek, formatTime, } = useAudioPlayback({ audioSrc: nodeData.outputAudio ?? null, waveformData, isLoadingWaveform, }); const handleClearAudio = useCallback(() => { if (audioRef.current) { audioRef.current.pause(); audioRef.current = null; } setAudioBlob(null); updateNodeData(id, { outputAudio: null, status: "idle", error: null, duration: null, format: null }); }, [id, updateNodeData, audioRef]); const handleParametersChange = useCallback( (parameters: Record) => { updateNodeData(id, { parameters }); }, [id, updateNodeData] ); const handleInputsLoaded = useCallback( (inputs: ModelInputDef[]) => { updateNodeData(id, { inputSchema: inputs }); }, [id, updateNodeData] ); const { setNodes } = useReactFlow(); const handleParametersExpandChange = useCallback( (expanded: boolean, parameterCount: number) => { const parameterHeight = expanded ? Math.max(parameterCount * 28 + 16, 60) : 0; const baseHeight = 300; const newHeight = baseHeight + parameterHeight; setNodes((nodes) => nodes.map((node) => node.id === id ? { ...node, style: { ...node.style, height: newHeight } } : node ) ); }, [id, setNodes] ); const regenerateNode = useWorkflowStore((state) => state.regenerateNode); const isRunning = useWorkflowStore((state) => state.isRunning); const handleRegenerate = useCallback(() => { regenerateNode(id); }, [id, regenerateNode]); // Load audio by ID from generations folder const loadAudioById = useCallback(async (audioId: string) => { if (!generationsPath) { console.error("Generations path not configured"); return null; } try { const response = await fetch("/api/load-generation", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ directoryPath: generationsPath, imageId: audioId, }), }); const result = await response.json(); if (!result.success) { console.log(`Audio not found: ${audioId}`); return null; } return result.audio || result.image; } catch (error) { console.warn("Error loading audio:", error); return null; } }, [generationsPath]); // Carousel navigation handlers const handleCarouselPrevious = useCallback(async () => { const history = nodeData.audioHistory || []; if (history.length === 0 || isLoadingCarouselAudio) return; const currentIndex = nodeData.selectedAudioHistoryIndex || 0; const newIndex = currentIndex === 0 ? history.length - 1 : currentIndex - 1; const audioItem = history[newIndex]; setIsLoadingCarouselAudio(true); const audio = await loadAudioById(audioItem.id); setIsLoadingCarouselAudio(false); if (audio) { updateNodeData(id, { outputAudio: audio, selectedAudioHistoryIndex: newIndex, }); } }, [id, nodeData.audioHistory, nodeData.selectedAudioHistoryIndex, isLoadingCarouselAudio, loadAudioById, updateNodeData]); const handleCarouselNext = useCallback(async () => { const history = nodeData.audioHistory || []; if (history.length === 0 || isLoadingCarouselAudio) return; const currentIndex = nodeData.selectedAudioHistoryIndex || 0; const newIndex = (currentIndex + 1) % history.length; const audioItem = history[newIndex]; setIsLoadingCarouselAudio(true); const audio = await loadAudioById(audioItem.id); setIsLoadingCarouselAudio(false); if (audio) { updateNodeData(id, { outputAudio: audio, selectedAudioHistoryIndex: newIndex, }); } }, [id, nodeData.audioHistory, nodeData.selectedAudioHistoryIndex, isLoadingCarouselAudio, loadAudioById, updateNodeData]); const handleBrowseModelSelect = useCallback((model: ProviderModel) => { const newSelectedModel: SelectedModel = { provider: model.provider, modelId: model.id, displayName: model.name, }; updateNodeData(id, { selectedModel: newSelectedModel, parameters: {} }); setIsBrowseDialogOpen(false); }, [id, updateNodeData]); const displayTitle = useMemo(() => { if (nodeData.selectedModel?.displayName && nodeData.selectedModel.modelId) { return nodeData.selectedModel.displayName; } return "Generate Audio"; }, [nodeData.selectedModel?.displayName, nodeData.selectedModel?.modelId]); // Inline parameters: compute collapse state and toggle handler const isParamsExpanded = nodeData.parametersExpanded ?? true; // default expanded const handleToggleParams = useCallback(() => { const nodes = useWorkflowStore.getState().nodes; const node = nodes.find(n => n.id === id); const currentExpanded = (node?.data as GenerateAudioNodeData)?.parametersExpanded ?? true; updateNodeData(id, { parametersExpanded: !currentExpanded }); }, [id, updateNodeData]); // Dynamic handles based on inputSchema const dynamicHandles = useMemo(() => { if (!nodeData.inputSchema || nodeData.inputSchema.length === 0) return null; return nodeData.inputSchema.map((input, index) => { const handleType = input.type === "image" ? "image" : "text"; return ( ); }); }, [nodeData.inputSchema]); return ( <> {/* Model parameters (hidden when inline enabled - shown in panel below) */} {!inlineParametersEnabled && nodeData.selectedModel?.modelId && ( )} {/* Output audio player */} {nodeData.outputAudio && (
{/* Waveform visualization */} {isLoadingWaveform ? (
Loading waveform...
) : waveformData ? (
) : (
Processing...
)} {/* Controls */}
{/* Progress bar */}
{!!audioRef.current?.duration && isFinite(audioRef.current.duration) && (
)}
{/* Time */} {formatTime(currentTime)} {/* Carousel navigation */} {(nodeData.audioHistory?.length || 0) > 1 && ( <> {(nodeData.selectedAudioHistoryIndex || 0) + 1}/{nodeData.audioHistory?.length} )}
{/* Clear button */}
)} {/* Status indicators */} {nodeData.status === "loading" && (
Generating audio...
)} {nodeData.status === "error" && nodeData.error && (
{nodeData.error}
)} {/* Dynamic handles from schema */} {dynamicHandles} {/* Default prompt handle (if no dynamic schema) */} {(!nodeData.inputSchema || nodeData.inputSchema.length === 0) && ( )} {/* Output audio handle */} {/* Inline parameter panel */} {inlineParametersEnabled && ( {/* Model selector: Browse button + current model display */}
{displayTitle}
{currentProvider}
{/* External provider parameters - reuse ModelParameters component */} {nodeData.selectedModel?.modelId && ( )}
)} {/* Browse dialog */} {isBrowseDialogOpen && ( setIsBrowseDialogOpen(false)} onModelSelected={handleBrowseModelSelect} initialProvider={currentProvider} initialCapabilityFilter="audio" /> )} ); }