diff --git a/src/components/__tests__/GenerateAudioNode.test.tsx b/src/components/__tests__/GenerateAudioNode.test.tsx index c4b801ca..db137ff2 100644 --- a/src/components/__tests__/GenerateAudioNode.test.tsx +++ b/src/components/__tests__/GenerateAudioNode.test.tsx @@ -195,32 +195,30 @@ describe("GenerateAudioNode", () => { }); }); - describe("Provider Selection", () => { - it("should show fal.ai as default provider", () => { + describe("Provider Badge", () => { + it("should show provider badge for default provider (fal)", () => { const { container } = render( ); - // fal.ai badge should be the default (check via select value) - const select = container.querySelector('select') as HTMLSelectElement; - expect(select).toBeInTheDocument(); - expect(select.value).toBe("fal"); + // Provider badge should be rendered with fal.ai title + const badge = container.querySelector('[title="fal.ai"]'); + expect(badge).toBeInTheDocument(); }); - it("should update provider on change", () => { - render( + it("should show provider badge matching selected model provider", () => { + const { container } = render( ); - // Provider select should be present - const selects = screen.getAllByRole("combobox"); - expect(selects.length).toBeGreaterThan(0); + const badge = container.querySelector('[title="Kie.ai"]'); + expect(badge).toBeInTheDocument(); }); }); @@ -449,28 +447,14 @@ describe("GenerateAudioNode", () => { }); }); - describe("Fetch Models on Mount", () => { - it("should fetch models with audio capabilities", async () => { - render( - - - - ); - - await waitFor(() => { - const fetchCalls = mockFetch.mock.calls.filter(call => - typeof call[0] === 'string' && call[0].includes('text-to-audio') - ); - expect(fetchCalls.length).toBeGreaterThan(0); + describe("ModelParameters Component", () => { + it("should render ModelParameters when model is selected", () => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ parameters: [], inputs: [] }), }); - }); - }); - describe("ModelParameters Component", () => { - it("should render ModelParameters when model is selected", async () => { - render( + const { container } = render( { ); - // ModelParameters should attempt to load schema - await waitFor(() => { - expect(mockFetch).toHaveBeenCalled(); - }); + // ModelParameters component should be rendered (it contains the parameters section) + // The node should show the model name as its title + expect(screen.getByText("ElevenLabs Turbo v2.5")).toBeInTheDocument(); }); }); diff --git a/src/components/nodes/GenerateAudioNode.tsx b/src/components/nodes/GenerateAudioNode.tsx index 5bf5d90d..f6416ee1 100644 --- a/src/components/nodes/GenerateAudioNode.tsx +++ b/src/components/nodes/GenerateAudioNode.tsx @@ -3,29 +3,22 @@ import React, { useCallback, useState, useEffect, useMemo, useRef } from "react"; import { Handle, Position, NodeProps, Node, useReactFlow } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; +import { ProviderBadge } from "./ProviderBadge"; import { useCommentNavigation } from "@/hooks/useCommentNavigation"; import { ModelParameters } from "./ModelParameters"; -import { useWorkflowStore, useProviderApiKeys } from "@/store/workflowStore"; -import { deduplicatedFetch } from "@/utils/deduplicatedFetch"; +import { useWorkflowStore } from "@/store/workflowStore"; import { GenerateAudioNodeData, ProviderType, SelectedModel, ModelInputDef } from "@/types"; -import { ProviderModel, ModelCapability } from "@/lib/providers/types"; +import { ProviderModel } from "@/lib/providers/types"; import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; import { useAudioVisualization } from "@/hooks/useAudioVisualization"; -// Audio generation capabilities -const AUDIO_CAPABILITIES: ModelCapability[] = ["text-to-audio"]; - type GenerateAudioNodeType = Node; export function GenerateAudioNode({ id, data, selected }: NodeProps) { const nodeData = data; const commentNavigation = useCommentNavigation(id); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); - const { replicateApiKey, falApiKey, kieApiKey, wavespeedApiKey, replicateEnabled, kieEnabled } = useProviderApiKeys(); const generationsPath = useWorkflowStore((state) => state.generationsPath); - const [externalModels, setExternalModels] = useState([]); - const [isLoadingModels, setIsLoadingModels] = useState(false); - const [modelsFetchError, setModelsFetchError] = useState(null); const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false); const [isLoadingCarouselAudio, setIsLoadingCarouselAudio] = useState(false); const [isPlaying, setIsPlaying] = useState(false); @@ -37,26 +30,6 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps { - const providers: { id: ProviderType; name: string }[] = []; - // fal.ai is always available - providers.push({ id: "fal", name: "fal.ai" }); - // Add Replicate if configured - if (replicateEnabled && replicateApiKey) { - providers.push({ id: "replicate", name: "Replicate" }); - } - // Add Kie.ai if configured - if (kieEnabled && kieApiKey) { - providers.push({ id: "kie", name: "Kie.ai" }); - } - // Add WaveSpeed if configured (always check for key since enabled flag not in hook) - if (wavespeedApiKey) { - providers.push({ id: "wavespeed", name: "WaveSpeed" }); - } - return providers; - }, [replicateEnabled, replicateApiKey, kieEnabled, kieApiKey, wavespeedApiKey]); - // Convert base64 data URL to Blob for visualization const [audioBlob, setAudioBlob] = useState(null); const { waveformData, isLoading: isLoadingWaveform } = useAudioVisualization(audioBlob); @@ -191,80 +164,6 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps { - setIsLoadingModels(true); - setModelsFetchError(null); - try { - const capabilities = AUDIO_CAPABILITIES.join(","); - const headers: HeadersInit = {}; - if (replicateApiKey) { - headers["X-Replicate-Key"] = replicateApiKey; - } - if (falApiKey) { - headers["X-Fal-Key"] = falApiKey; - } - if (kieApiKey) { - headers["X-Kie-Key"] = kieApiKey; - } - if (wavespeedApiKey) { - headers["X-WaveSpeed-Key"] = wavespeedApiKey; - } - const response = await deduplicatedFetch(`/api/models?provider=${currentProvider}&capabilities=${capabilities}`, { headers }); - if (response.ok) { - const data = await response.json(); - setExternalModels(data.models || []); - setModelsFetchError(null); - } else { - const errorData = await response.json().catch(() => ({})); - const errorMsg = errorData.error || `Failed to load models (${response.status})`; - setExternalModels([]); - setModelsFetchError(errorMsg); - } - } catch (error) { - console.error("Failed to fetch audio models:", error); - setExternalModels([]); - setModelsFetchError("Failed to load models. Check your connection."); - } finally { - setIsLoadingModels(false); - } - }, [currentProvider, replicateApiKey, falApiKey, kieApiKey, wavespeedApiKey]); - - useEffect(() => { - fetchModels(); - }, [fetchModels]); - - // Handle provider change - const handleProviderChange = useCallback( - (e: React.ChangeEvent) => { - const provider = e.target.value as ProviderType; - const newSelectedModel: SelectedModel = { - provider, - modelId: "", - displayName: "Select model...", - }; - updateNodeData(id, { selectedModel: newSelectedModel, parameters: {} }); - }, - [id, updateNodeData] - ); - - // Handle model change - const handleModelChange = useCallback( - (e: React.ChangeEvent) => { - const modelId = e.target.value; - const model = externalModels.find(m => m.id === modelId); - if (model) { - const newSelectedModel: SelectedModel = { - provider: currentProvider, - modelId: model.id, - displayName: model.name, - }; - updateNodeData(id, { selectedModel: newSelectedModel, parameters: {} }); - } - }, - [id, currentProvider, externalModels, updateNodeData] - ); - const handleClearAudio = useCallback(() => { if (audioRef.current) { audioRef.current.pause(); @@ -420,6 +319,21 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps ( + + ), [currentProvider]); + + // Header action element - browse button + const headerAction = useMemo(() => ( + setIsBrowseDialogOpen(true)} + className="nodrag nopan text-[10px] py-0.5 px-1.5 bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors" + > + Browse + + ), []); + // Dynamic handles based on inputSchema const dynamicHandles = useMemo(() => { if (!nodeData.inputSchema || nodeData.inputSchema.length === 0) return null; @@ -448,6 +362,8 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps updateNodeData(id, { customTitle: title || undefined })} @@ -457,50 +373,6 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps - {/* Provider and model selection */} - - - - {enabledProviders.map(provider => ( - - {provider.name} - - ))} - - setIsBrowseDialogOpen(true)} - className="px-2 py-1 text-xs bg-violet-600 hover:bg-violet-500 rounded transition-colors" - title="Browse all models" - > - Browse - - - - {isLoadingModels ? ( - Loading models... - ) : modelsFetchError ? ( - {modelsFetchError} - ) : ( - - Select model... - {externalModels.map(model => ( - - {model.name} - - ))} - - )} - - {/* Model parameters */} {nodeData.selectedModel?.modelId && (