/* Screen components: Home (with inline recording), List, Detail */ const { useState, useEffect, useRef, useCallback, useMemo } = React; /* ---------- Home ---------- */ // Recording happens inline — no screen change. Button toggles idle → recording // (red, blinking) → transcribing (gray, spinner). When a transcript is ready, // onDone fires and the app navigates to Detail for the letter. function HomeScreen({ onDone, onList, recordingsCount }) { const [phase, setPhase] = useState("idle"); // idle | recording | transcribing | error const [elapsed, setElapsed] = useState(0); const [error, setError] = useState(""); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); const startTimeRef = useRef(0); const streamRef = useRef(null); const audioCtxRef = useRef(null); const durationRef = useRef(0); const timerRef = useRef(null); // Timer tick while recording useEffect(() => { if (phase !== "recording") return; timerRef.current = setInterval(() => { setElapsed((Date.now() - startTimeRef.current) / 1000); }, 250); return () => clearInterval(timerRef.current); }, [phase]); const teardownMic = useCallback(() => { if (streamRef.current) streamRef.current.getTracks().forEach(t => t.stop()); streamRef.current = null; if (audioCtxRef.current) { try { audioCtxRef.current.close(); } catch {} } audioCtxRef.current = null; }, []); const startRecording = useCallback(async () => { setError(""); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); streamRef.current = stream; const candidates = [ "audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4", ]; const mime = candidates.find(t => window.MediaRecorder && MediaRecorder.isTypeSupported(t)) || ""; const mr = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined); chunksRef.current = []; mr.ondataavailable = (e) => { if (e.data && e.data.size > 0) chunksRef.current.push(e.data); }; mr.start(1000); mediaRecorderRef.current = mr; startTimeRef.current = Date.now(); setElapsed(0); setPhase("recording"); } catch (err) { teardownMic(); setError(err.name === "NotAllowedError" ? "Mikrofonzugriff nicht erlaubt. Bitte Berechtigung erteilen." : "Mikrofon konnte nicht gestartet werden: " + (err.message || err)); setPhase("error"); } }, [teardownMic]); const stopAndTranscribe = useCallback(async () => { const mr = mediaRecorderRef.current; if (!mr) return; durationRef.current = (Date.now() - startTimeRef.current) / 1000; setPhase("transcribing"); const blob = await new Promise((resolve) => { if (mr.state === "inactive") { const type = (chunksRef.current[0] && chunksRef.current[0].type) || "audio/webm"; resolve(new Blob(chunksRef.current, { type })); return; } mr.onstop = () => { const type = mr.mimeType || "audio/webm"; resolve(new Blob(chunksRef.current, { type })); }; try { mr.stop(); } catch { resolve(new Blob(chunksRef.current, { type: "audio/webm" })); } }); teardownMic(); if (!blob || blob.size === 0) { setError("Keine Audiodaten aufgenommen."); setPhase("error"); return; } try { const ext = (blob.type.includes("ogg") && "ogg") || (blob.type.includes("mp4") && "m4a") || "webm"; const transcript = await window.claude.transcribe(blob, `dictation.${ext}`); if (!transcript.trim()) { setError("Leere Transkription. Bitte erneut versuchen."); setPhase("error"); return; } onDone({ transcript: transcript.trim(), duration: durationRef.current }); setPhase("idle"); setElapsed(0); } catch (err) { setError("Transkription fehlgeschlagen: " + (err.message || err)); setPhase("error"); } }, [onDone, teardownMic]); const handleButton = () => { if (phase === "idle" || phase === "error") startRecording(); else if (phase === "recording") stopAndTranscribe(); }; // Cleanup on unmount useEffect(() => () => { clearInterval(timerRef.current); try { mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive" && mediaRecorderRef.current.stop(); } catch {} teardownMic(); }, [teardownMic]); const recording = phase === "recording"; const transcribing = phase === "transcribing"; const errored = phase === "error"; let hint = "Drücken zum Starten"; if (recording) hint = fmtDuration(elapsed); else if (transcribing) hint = "Transkribiere…"; else if (errored) hint = "Erneut versuchen"; const btnClass = "record-btn" + (recording ? " recording" : "") + (transcribing ? " transcribing" : ""); const ariaLabel = recording ? "Aufnahme stoppen" : "Aufnahme starten"; return (
{recording.needsFormat ? "Das Format konnte nicht eindeutig aus dem Gespräch erkannt werden. Bitte wählen Sie." : "Wählen Sie ein anderes Format. Der Brief wird neu generiert."}