import { useState, useEffect, useRef } from 'react'; import { RefreshCw, Play, Square, Loader2 } from 'lucide-react'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select'; import { useClient } from 'hooks/useClient'; import { useSettings } from 'state/useSettings'; type VoiceGroup = { label: string; voices: string[] }; type TtsConfig = { provider: string; url: string; apiKey?: string; model: string; voice: string; }; const SERVER_DEFAULT = '__server_default__'; export const VoicePreference = () => { const client = useClient(); const { settings, saveSettings } = useSettings(); const [voices, setVoices] = useState([]); const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(false); const [serverConfig, setServerConfig] = useState(null); const [listening, setListening] = useState<'idle' | 'loading' | 'playing'>('idle'); const audioRef = useRef(null); const fetchConfig = async () => { try { const config = await client.get('/server-settings/tts'); setServerConfig(config); return config; } catch { return null; } }; const fetchVoices = async (config: TtsConfig) => { setLoading(true); try { const res = await client.post<{ voices?: string[]; groups?: VoiceGroup[] }>('/server-settings/tts/voices', { provider: config.provider, url: config.url, apiKey: config.apiKey || undefined, model: config.model || undefined, }); setVoices(res.voices ?? []); setGroups(res.groups ?? []); } catch { setVoices([]); setGroups([]); } finally { setLoading(false); } }; useEffect(() => { fetchConfig().then((config) => { if (config) fetchVoices(config); }); }, []); const handleChange = (value: string) => { const voice = value === SERVER_DEFAULT ? null : value; saveSettings({ ...settings, tts: { voice } }); }; const handleRefresh = () => { if (serverConfig) fetchVoices(serverConfig); }; const handleListen = async () => { if (listening === 'loading') return; if (listening === 'playing') { audioRef.current?.pause(); audioRef.current = null; setListening('idle'); return; } if (!serverConfig) return; setListening('loading'); try { const effectiveVoice = settings.tts.voice ?? serverConfig.voice; const res = await fetch(`${client.baseUrl}/server-settings/tts/test`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${client.token}` }, body: JSON.stringify({ ...serverConfig, voice: effectiveVoice }), }); if (!res.ok || res.headers.get('content-type')?.includes('json')) { setListening('idle'); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const audio = new Audio(url); audioRef.current = audio; audio.onended = () => { audioRef.current = null; setListening('idle'); URL.revokeObjectURL(url); }; audio.onerror = () => { audioRef.current = null; setListening('idle'); URL.revokeObjectURL(url); }; await audio.play(); setListening('playing'); } catch { setListening('idle'); } }; const selectedValue = settings.tts.voice ?? SERVER_DEFAULT; const prettify = (v: string) => v.replace(/^[a-z]{2}_/, '').replace(/^\w/, (c) => c.toUpperCase()); return (
); };