import { useCallback, useEffect, useRef, useState } from "react"; export interface UseVoiceSearchResult { isListening: boolean; transcript: string; isSupported: boolean; hasError: boolean; errorMessage: string | null; startListening: () => void; stopListening: () => void; } /** * Hook for voice search using the Web Speech API. * Returns a microphone control interface. * Gracefully degrades when SpeechRecognition is unavailable. */ export function useVoiceSearch(): UseVoiceSearchResult { const [isListening, setIsListening] = useState(false); const [transcript, setTranscript] = useState(""); const [isSupported, setIsSupported] = useState(false); const [hasError, setHasError] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const recognitionRef = useRef(null); const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; // Check for SpeechRecognition support (standard + webkit prefix) const SpeechRecognitionCtor = (window as unknown as Record).SpeechRecognition ?? (window as unknown as Record).webkitSpeechRecognition; if (typeof SpeechRecognitionCtor === "function") { setIsSupported(true); const recognition = new (SpeechRecognitionCtor as new () => SpeechRecognition)(); recognition.continuous = false; recognition.interimResults = false; recognition.lang = "en-US"; recognition.onresult = (event: SpeechRecognitionEvent) => { const resultText = event.results[0]?.[0]?.transcript ?? ""; if (mountedRef.current) { setTranscript(resultText); setHasError(false); setErrorMessage(null); } }; recognition.onerror = (event: SpeechRecognitionErrorEvent) => { if (mountedRef.current) { setHasError(true); setErrorMessage(event.error); setIsListening(false); } }; recognition.onend = () => { if (mountedRef.current) { setIsListening(false); } }; recognitionRef.current = recognition; } return () => { mountedRef.current = false; if (recognitionRef.current) { recognitionRef.current.abort(); } }; }, []); const startListening = useCallback(() => { if (!recognitionRef.current) return; setTranscript(""); setHasError(false); setErrorMessage(null); try { recognitionRef.current.start(); setIsListening(true); } catch { // May throw if already started setIsListening(false); } }, []); const stopListening = useCallback(() => { if (!recognitionRef.current) return; recognitionRef.current.stop(); setIsListening(false); }, []); return { isListening, transcript, isSupported, hasError, errorMessage, startListening, stopListening, }; }