import { useEffect, useRef, useState } from "preact/hooks"; interface VoiceRecorderProps { text: string; audio_file: string; role: string; recordingCallback: (data: Blob | null) => void; } export default function VoiceRecorder( { text, audio_file, role, recordingCallback }: VoiceRecorderProps, ) { const [recording, setRecording] = useState(false); const [audioUrl, setAudioUrl] = useState(null); const [initialAudioUrl, setInitialAudioUrl] = useState(null); const [audioBlob, setAudioBlob] = useState(null); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); const selectedMimeTypeRef = useRef(undefined); const initialAudioRef = useRef(null); const pickBestAudioMimeType = (): string | undefined => { if (typeof MediaRecorder === "undefined") return undefined; if (typeof MediaRecorder.isTypeSupported !== "function") return undefined; const candidates = [ "audio/mp4", "audio/mp4;codecs=mp4a.40.2", "audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/ogg", ]; return candidates.find((t) => MediaRecorder.isTypeSupported(t)); }; // Fetch and play the initial audio_file when the component mounts useEffect(() => { if (!audio_file) return; const fetchAudio = async () => { try { const isDirectUrl = audio_file.startsWith("blob:") || audio_file.startsWith("http") || audio_file.startsWith("/"); if (isDirectUrl) { setInitialAudioUrl(audio_file); return; } const response = await fetch( `/api/audio-blob/${encodeURIComponent(audio_file)}`, { credentials: "include" }, ); if (!response.ok) { console.error("Failed to fetch audio file:", response.statusText); return; } const blob = await response.blob(); const url = URL.createObjectURL(blob); setInitialAudioUrl(url); } catch (err) { console.error("Error fetching audio file:", err); } }; fetchAudio(); // Cleanup blob URL on unmount return () => { if (initialAudioUrl) { URL.revokeObjectURL(initialAudioUrl); } }; }, [audio_file]); // Auto-play when initial audio is loaded useEffect(() => { if (initialAudioUrl && initialAudioRef.current) { initialAudioRef.current.play().catch(() => { // Autoplay might be blocked, user must interact }); } }, [initialAudioUrl]); const startRecording = async () => { if (audioUrl) { URL.revokeObjectURL(audioUrl); } setAudioUrl(null); setAudioBlob(null); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); if (typeof MediaRecorder === "undefined") { alert("Audio recording is not supported in this browser."); stream.getTracks().forEach((t) => t.stop()); return; } const bestMimeType = pickBestAudioMimeType(); const mimeTypeCandidates = [ bestMimeType, "audio/mp4", "audio/mp4;codecs=mp4a.40.2", "audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/ogg", undefined, ].filter((t, i, arr) => arr.indexOf(t) === i); let mediaRecorder: MediaRecorder | null = null; let lastError: unknown = undefined; for (const candidate of mimeTypeCandidates) { try { mediaRecorder = candidate ? new MediaRecorder(stream, { mimeType: candidate }) : new MediaRecorder(stream); selectedMimeTypeRef.current = candidate; break; } catch (err) { lastError = err; mediaRecorder = null; continue; } } if (!mediaRecorder) { console.error("Failed to create MediaRecorder", lastError); alert("Audio recording is not supported in this browser."); stream.getTracks().forEach((t) => t.stop()); return; } mediaRecorderRef.current = mediaRecorder; mediaRecorder.onerror = (e) => { const recErr = (e as ErrorEvent | Event) as unknown as { error?: DOMException; }; const err = recErr?.error; console.error("MediaRecorder error", err || e); stream.getTracks().forEach((t) => t.stop()); setRecording(false); alert( "Recording failed. This Safari version may not support MediaRecorder with audio-only streams.", ); }; mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) { chunksRef.current.push(e.data); } }; mediaRecorder.onstop = () => { const fallbackType = chunksRef.current[0]?.type || mediaRecorder.mimeType || selectedMimeTypeRef.current || ""; const blob = new Blob( chunksRef.current, fallbackType ? { type: fallbackType } : undefined, ); chunksRef.current = []; setAudioBlob(blob); setAudioUrl(URL.createObjectURL(blob)); stream.getTracks().forEach((t) => t.stop()); }; mediaRecorder.start(); setRecording(true); } catch (err) { alert("Microphone access denied or not supported."); } }; const stopRecording = () => { mediaRecorderRef.current?.stop(); setRecording(false); }; const handleSave = () => { if (audioBlob) { recordingCallback(audioBlob); } }; const handleClose = () => { recordingCallback(null); }; return (
e.stopPropagation()} >
{text}
{/* Initial audio playback */}
{initialAudioUrl && (
{role === "admin" && (
)}
{audioUrl && }
); }