import { useEffect, useState } from 'react'; import { X, Settings, CheckCircle2, XCircle, Loader2, Mic, Volume2, RotateCcw, Play } from 'lucide-react'; import { bridge } from '../../lib/bridge'; import { HermesClient } from '../../services/hermes/client'; import { listSystemVoices } from '../../services/voice/tts'; import { speech } from '../../services/voice/speech'; import { listMicrophones } from '../../services/voice/capture'; import { transcribeWav } from '../../services/voice/stt'; import { encodeWav } from '../../services/voice/wav'; import { useHermes } from '../../state/hermes'; import { DEFAULT_SETTINGS, useSettings, type HudTheme } from '../../state/settings'; import { useVoice } from '../../state/voice'; import { installedModels, useVoiceModels } from '../../state/voiceModels'; import { ModelsSection } from './ModelsSection'; type Section = 'general' | 'hermes' | 'voice' | 'speech' | 'models' | 'webhook' | 'ui'; interface Props { onClose: () => void; } function Toggle({ on, onChange, label, hint }: { on: boolean; onChange: (v: boolean) => void; label: string; hint?: string }) { return (
onChange(!on)}>
{label} {hint && {hint}}
); } type TestState = { status: 'idle' | 'running' | 'ok' | 'fail'; message: string }; export function SettingsDrawer({ onClose }: Props) { const settings = useSettings((s) => s.settings); const update = useSettings((s) => s.update); const reset = useSettings((s) => s.reset); const hermesStore = useHermes(); const micDevices = useVoice((s) => s.micDevices); const voiceModels = useVoiceModels((s) => s.models); const refreshModels = useVoiceModels((s) => s.refresh); const [section, setSection] = useState
('hermes'); const [hermesTest, setHermesTest] = useState({ status: 'idle', message: '' }); const [sttTest, setSttTest] = useState({ status: 'idle', message: '' }); const [ttsTest, setTtsTest] = useState({ status: 'idle', message: '' }); const [voices, setVoices] = useState(listSystemVoices()); const [webhookSecretVisible, setWebhookSecretVisible] = useState(false); useEffect(() => { const refresh = () => setVoices(listSystemVoices()); refresh(); speechSynthesis.addEventListener?.('voiceschanged', refresh); void listMicrophones().then((d) => useVoice.getState().setMicDevices(d)); void refreshModels(); return () => speechSynthesis.removeEventListener?.('voiceschanged', refresh); }, [refreshModels]); const testHermes = async () => { setHermesTest({ status: 'running', message: 'connexion…' }); try { const client = new HermesClient(settings.hermes); const health = await client.health(); let caps = ''; try { const c = await client.capabilities(); caps = ` · modèle ${String(c.model ?? '?')} · ${Object.entries(c.features ?? {}).filter(([, v]) => v === true).length} fonctions`; } catch (err) { const message = (err as Error).message; caps = /404/.test(message) ? ' · /v1/capabilities absent (Hermes ancien : transport chat completions)' : ` · capabilities : ${message}`; } const via = bridge() ? 'via Electron' : 'via navigateur'; setHermesTest({ status: 'ok', message: `${health.status} (${via})${caps}` }); void hermesStore.connect(); } catch (err) { setHermesTest({ status: 'fail', message: (err as Error).message }); } }; const testStt = async () => { setSttTest({ status: 'running', message: 'envoi d’un WAV de test…' }); try { const samples = new Float32Array(16000); for (let i = 0; i < samples.length; i++) samples[i] = Math.sin(i / 20) * 0.05; const text = await transcribeWav(encodeWav(samples, 16000), settings.voice); setSttTest({ status: 'ok', message: `endpoint OK (réponse: "${text.slice(0, 40) || 'vide'}")` }); } catch (err) { setSttTest({ status: 'fail', message: (err as Error).message }); } }; const testTts = () => { setTtsTest({ status: 'running', message: 'synthèse…' }); speech.say(`Bonjour, je suis ${settings.assistantName}. Les systèmes sont opérationnels.`); setTimeout(() => setTtsTest({ status: 'ok', message: 'commande envoyée (écoutez le résultat)' }), 600); }; const applyWebhook = async () => { const api = bridge(); if (!api) return; await api.store.set('webhook', settings.webhook); const status = await api.hermes.webhookRestart(); useHermes.setState({ webhook: status }); }; const Result = ({ t }: { t: TestState }) => t.status === 'idle' ? null : (
{t.status === 'running' ? : t.status === 'ok' ? : } {t.message}
); const nav: Array<[Section, string]> = [ ['hermes', 'Hermes'], ['voice', 'Micro / STT'], ['speech', 'Voix / TTS'], ['models', 'Modèles locaux'], ['webhook', 'Webhook'], ['general', 'Général'], ['ui', 'Interface'] ]; return (