import type { FormEvent } from 'react'; import { useState, useEffect, useRef } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { useClient } from 'hooks/useClient'; import { useTranslation } from '@/lib/i18n'; type Provider = 'openai' | 'elevenlabs'; type TtsConfig = { provider: Provider; url: string; apiKey?: string; model: string; voice: string; }; const PROVIDER_DEFAULTS: Record> = { openai: { url: 'http://localhost:64202', model: 'kokoro', voice: 'af_heart' }, elevenlabs: { url: '', model: 'eleven_multilingual_v2', voice: 'Rachel' }, }; export const TTSSection = () => { const { t } = useTranslation(); const client = useClient(); const queryClient = useQueryClient(); const audioRef = useRef(null); const { data: config, isLoading } = useQuery({ queryKey: ['TTS_CONFIG'], queryFn: () => client.get('/server-settings/tts'), }); const [provider, setProvider] = useState('openai'); const [url, setUrl] = useState('http://localhost:64202'); const [apiKey, setApiKey] = useState(''); const [model, setModel] = useState('kokoro'); const [voice, setVoice] = useState('af_heart'); const [voices, setVoices] = useState([]); const [voiceGroups, setVoiceGroups] = useState<{ label: string; voices: string[] }[]>([]); const [voicesLoading, setVoicesLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isTesting, setIsTesting] = useState(false); const fetchVoices = async (p: Provider, u: string, key: string, m?: string) => { setVoicesLoading(true); try { const res = await client.post<{ voices?: string[]; groups?: { label: string; voices: string[] }[]; error?: string; }>('/server-settings/tts/voices', { provider: p, url: u, apiKey: key || undefined, model: m || undefined, }); if (res.voices) setVoices(res.voices); else setVoices([]); setVoiceGroups(res.groups ?? []); } catch { setVoices([]); } finally { setVoicesLoading(false); } }; useEffect(() => { if (!config) return; setProvider(config.provider); setUrl(config.url ?? ''); setApiKey(config.apiKey ?? ''); setModel(config.model); setVoice(config.voice); fetchVoices(config.provider, config.url ?? '', config.apiKey ?? '', config.model); }, [config]); const handleProviderChange = (v: Provider) => { setProvider(v); const defaults = PROVIDER_DEFAULTS[v]; if (defaults.url !== undefined) setUrl(defaults.url); if (defaults.model !== undefined) setModel(defaults.model); if (defaults.voice !== undefined) setVoice(defaults.voice); setApiKey(''); }; const buildConfig = (): TtsConfig => ({ provider, url, model, voice, ...(apiKey ? { apiKey } : {}), }); const handleSave = async (ev?: FormEvent) => { ev?.preventDefault(); if (isSaving) return; setIsSaving(true); try { await client.put('/server-settings/tts', buildConfig()); queryClient.invalidateQueries({ queryKey: ['TTS_CONFIG'] }); toast.success(t('settings.server.tts.saved')); } catch { toast.error(t('settings.server.tts.saveFailed')); } finally { setIsSaving(false); } }; const handleTest = async () => { if (isTesting) return; setIsTesting(true); try { const res = await fetch(`${client.baseUrl}/server-settings/tts/test`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${client.token}` }, body: JSON.stringify(buildConfig()), }); if (!res.ok || res.headers.get('content-type')?.includes('json')) { const json = await res.json().catch(() => ({ error: 'Test failed' })); toast.error(json.error ?? 'Test failed'); return; } const blob = await res.blob(); const audioUrl = URL.createObjectURL(blob); if (audioRef.current) { audioRef.current.pause(); URL.revokeObjectURL(audioRef.current.src); } const audio = new Audio(audioUrl); audioRef.current = audio; audio.play(); toast.success(t('settings.server.tts.playing')); } catch { toast.error(t('settings.server.tts.testFailed')); } finally { setIsTesting(false); } }; if (isLoading) return

{t('settings.server.loading')}

; return (
{provider === 'openai' && ( )}
); };