import React, { useState, useRef, useEffect } from 'react'; import { Box, IconButton, Typography, Tooltip, CircularProgress } from '@mui/material'; import { Mic as MicIcon, Stop as StopIcon, Close as CloseIcon, Check as CheckIcon } from '@mui/icons-material'; import { transcribeAudio } from '../api/client'; /** * AudioRecorder Component * Provides live audio recording with real-time visualization and transcription */ function AudioRecorder({ onTranscript, onCancel }) { const [isRecording, setIsRecording] = useState(false); const [recordingTime, setRecordingTime] = useState(0); const [isProcessing, setIsProcessing] = useState(false); const [audioLevel, setAudioLevel] = useState(0); const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); const timerRef = useRef(null); const analyserRef = useRef(null); const animationRef = useRef(null); useEffect(() => { return () => { // Cleanup on unmount stopRecording(); if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, []); const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Setup audio analyzer for visualization const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const source = audioContext.createMediaStreamSource(stream); const analyser = audioContext.createAnalyser(); analyser.fftSize = 256; source.connect(analyser); analyserRef.current = analyser; // Start visualization visualizeAudio(); // Setup media recorder const mediaRecorder = new MediaRecorder(stream); mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunksRef.current.push(event.data); } }; mediaRecorder.onstop = async () => { const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); await handleTranscription(audioBlob); // Stop all tracks stream.getTracks().forEach(track => track.stop()); }; mediaRecorder.start(); setIsRecording(true); // Start timer timerRef.current = setInterval(() => { setRecordingTime(prev => prev + 1); }, 1000); } catch (error) { console.error('Failed to start recording:', error); alert('Microphone access denied or not available'); } }; const stopRecording = () => { if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } if (animationRef.current) { cancelAnimationFrame(animationRef.current); } } }; const visualizeAudio = () => { if (!analyserRef.current) return; const analyser = analyserRef.current; const dataArray = new Uint8Array(analyser.frequencyBinCount); const updateLevel = () => { analyser.getByteFrequencyData(dataArray); const average = dataArray.reduce((a, b) => a + b) / dataArray.length; setAudioLevel(Math.min(100, (average / 255) * 100)); if (isRecording) { animationRef.current = requestAnimationFrame(updateLevel); } }; updateLevel(); }; const handleTranscription = async (audioBlob) => { setIsProcessing(true); try { const result = await transcribeAudio(audioBlob); if (result.success && result.transcript) { onTranscript(result.transcript); } else { alert('Transcription failed. Please try again.'); } } catch (error) { console.error('Transcription error:', error); alert('Failed to transcribe audio: ' + error.message); } finally { setIsProcessing(false); setRecordingTime(0); setAudioLevel(0); } }; const handleCancel = () => { if (isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); if (timerRef.current) { clearInterval(timerRef.current); } } setRecordingTime(0); setAudioLevel(0); onCancel(); }; const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; }; if (isProcessing) { return ( Transcribing audio... ); } return ( {/* Record/Stop Button */} {isRecording ? : } {/* Recording Info */} {isRecording ? 'Recording...' : 'Ready to record'} {isRecording && ( {formatTime(recordingTime)} )} {/* Audio Level Indicator */} {isRecording && ( )} {/* Cancel Button */} ); } export default AudioRecorder;