import React, { useEffect, useRef, useState } from 'react'; import { AlertCircle, Check, Loader2, Mic, RefreshCw, Square } from 'lucide-react'; import { api } from '../../lib/api'; import { VoiceAdminStatus } from '../../lib/types'; const TEST_SECONDS = 6; // Admin: which speech recognition is active. Configured only by environment (STT_*), so this card // just shows the state and offers a short test recording through the own Whisper server. export const VoiceStatusCard: React.FC = () => { const [info, setInfo] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [recording, setRecording] = useState(false); const [testing, setTesting] = useState(false); const [result, setResult] = useState<{ text: string; ms: number } | null>(null); const recorderRef = useRef(null); const stopTimerRef = useRef(null); const load = async () => { setLoading(true); try { setInfo(await api.adminVoiceStatus()); setError(null); } catch (err: any) { setError(err.message || 'Status konnte nicht geladen werden'); } finally { setLoading(false); } }; useEffect(() => { void load(); return () => { if (stopTimerRef.current !== null) window.clearTimeout(stopTimerRef.current); recorderRef.current?.stream.getTracks().forEach((t) => t.stop()); }; }, []); const stopRecording = () => { if (stopTimerRef.current !== null) window.clearTimeout(stopTimerRef.current); stopTimerRef.current = null; if (recorderRef.current?.state === 'recording') recorderRef.current.stop(); }; const startTest = async () => { setResult(null); setError(null); let stream: MediaStream; try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch { setError('Kein Zugriff auf das Mikrofon (Berechtigung, oder die Seite läuft nicht über HTTPS)'); return; } const recorder = new MediaRecorder(stream); const chunks: Blob[] = []; recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); }; recorder.onstop = async () => { stream.getTracks().forEach((t) => t.stop()); setRecording(false); setTesting(true); const started = performance.now(); try { const text = await api.transcribe(new Blob(chunks, { type: recorder.mimeType })); setResult({ text, ms: Math.round(performance.now() - started) }); } catch (err: any) { setError(err.message || 'Spracherkennung fehlgeschlagen'); } finally { setTesting(false); } }; recorderRef.current = recorder; recorder.start(); setRecording(true); stopTimerRef.current = window.setTimeout(stopRecording, TEST_SECONDS * 1000); }; const status = info?.status; const serverOk = info?.mode === 'server' && status?.reachable && status?.modelReady; return (

Spracherkennung

Wird nur über Umgebungsvariablen eingestellt (STT_URL,{' '} STT_MODEL …); Fehler stehen im Container-Log.

{info && (
Aktiv
{info.mode === 'server' ? 'Eigener Server (Whisper)' : info.mode === 'off' ? 'Ausgeschaltet' : info.demo && info.url ? 'Browser (in der Demo immer)' : 'Browser (Spracherkennung des Browsers)'}
{info.url && ( <>
Server
{info.url}
Modell
{info.model}
Sprache
{info.language || 'automatisch'}
)} {info.mode === 'server' && ( <>
Zustand
{serverOk ? ( Erreichbar, Modell geladen ) : ( {status?.reachable ? 'Modell fehlt noch (wird heruntergeladen)' : 'Nicht erreichbar'} {status?.error && !status.reachable ? `: ${status.error}` : ''} )}
Grenzen
{Math.round(info.maxSeconds / 60)} Min. bzw. {info.maxUploadMb} MB pro Aufnahme, {info.maxParallel}{' '} gleichzeitig, {info.perUserPerHour} pro Person und Stunde, {info.timeoutSeconds} s Zeitlimit
)}
)} {info?.mode === 'server' && (
{recording ? ( ) : ( )} {recording && Sprich jetzt …} {result && ( „{result.text || '(nichts erkannt)'}“ · {(result.ms / 1000).toFixed(1)} s )}
)} {error &&

{error}

}
); };