import React, { useState, useRef, useEffect } from 'react'; const VoiceControls = ({ onCommand = () => {}, apiUrl = '/api/voice' }) => { const [state, setState] = useState('idle'); // idle, listening, processing const [transcript, setTranscript] = useState(''); const [hotkey, setHotkey] = useState('`'); // backtick const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); const hotkeyPressedRef = useRef(false); useEffect(() => { const handleKeyDown = (e) => { if (e.key === hotkey && !hotkeyPressedRef.current && state === 'idle') { hotkeyPressedRef.current = true; startRecording(); } }; const handleKeyUp = (e) => { if (e.key === hotkey && hotkeyPressedRef.current) { hotkeyPressedRef.current = false; stopRecording(); } }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); }; }, [state, hotkey]); const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorderRef.current = new MediaRecorder(stream); audioChunksRef.current = []; mediaRecorderRef.current.ondataavailable = (event) => { audioChunksRef.current.push(event.data); }; mediaRecorderRef.current.onstop = async () => { const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' }); await sendToTranscribe(audioBlob); stream.getTracks().forEach((track) => track.stop()); }; mediaRecorderRef.current.start(); setState('listening'); setTranscript('Listening...'); } catch (error) { console.error('Microphone access denied:', error); setTranscript('Microphone access denied'); } }; const stopRecording = () => { if (mediaRecorderRef.current && state === 'listening') { mediaRecorderRef.current.stop(); setState('processing'); setTranscript('Processing...'); } }; const sendToTranscribe = async (audioBlob) => { try { const formData = new FormData(); formData.append('audio', audioBlob, 'recording.wav'); const response = await fetch(`${apiUrl}/transcribe`, { method: 'POST', body: formData, }); const result = await response.json(); setTranscript(result.text || 'No speech detected'); setState('idle'); if (result.text) { onCommand(result.text); } } catch (error) { console.error('Transcription failed:', error); setTranscript('Transcription failed'); setState('idle'); } }; return (