import { useRef, useCallback, useMemo } from 'react'; // Extend Window for vendor-prefixed SpeechRecognition interface SpeechRecognitionEvent extends Event { results: SpeechRecognitionResultList; resultIndex: number; } type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance; interface SpeechRecognitionInstance extends EventTarget { continuous: boolean; interimResults: boolean; lang: string; start(): void; stop(): void; abort(): void; onresult: ((event: SpeechRecognitionEvent) => void) | null; onend: (() => void) | null; onerror: ((event: Event & { error: string }) => void) | null; } const getSpeechRecognitionCtor = (): SpeechRecognitionConstructor | null => { if (typeof window === 'undefined') return null; return ( (window as unknown as Record).SpeechRecognition ?? (window as unknown as Record).webkitSpeechRecognition ?? null ); }; export const isWebSpeechSupported = getSpeechRecognitionCtor() !== null; export function useSpeechRecognition() { const instanceRef = useRef(null); const transcriptRef = useRef(''); const resolveRef = useRef<((text: string) => void) | null>(null); const isSupported = useMemo(() => isWebSpeechSupported, []); const start = useCallback((continuous = false) => { const Ctor = getSpeechRecognitionCtor(); if (!Ctor) { return; } // Clean up any lingering instance if (instanceRef.current) { try { instanceRef.current.abort(); } catch {} } const recognition = new Ctor(); recognition.continuous = continuous; // desktop: true (click-to-toggle), mobile: false (hold-to-talk) recognition.interimResults = true; // get progressive updates while speaking recognition.lang = navigator.language || 'en-US'; transcriptRef.current = ''; instanceRef.current = recognition; recognition.onresult = (event: SpeechRecognitionEvent) => { // Grab the latest result — it's always the most complete/accurate const last = event.results[event.results.length - 1]; const text = last[0].transcript; transcriptRef.current = text; }; recognition.onend = () => { instanceRef.current = null; // If stop() is waiting for a result, resolve it now if (resolveRef.current) { resolveRef.current(transcriptRef.current); resolveRef.current = null; } }; recognition.onerror = (event) => { console.error('[SpeechRecognition] error:', event.error); instanceRef.current = null; if (resolveRef.current) { resolveRef.current(transcriptRef.current); resolveRef.current = null; } }; try { recognition.start(); } catch (e) { console.error('[SpeechRecognition] start failed:', e); } }, []); const stop = useCallback((): Promise => { return new Promise((resolve) => { const instance = instanceRef.current; if (!instance) { // Already ended (e.g. short utterance auto-stopped) resolve(transcriptRef.current); return; } // Wait for onend to fire with the final transcript resolveRef.current = resolve; try { instance.stop(); // triggers onend after finalizing } catch { instanceRef.current = null; resolve(transcriptRef.current); resolveRef.current = null; } // Safety timeout in case onend never fires setTimeout(() => { if (resolveRef.current) { resolveRef.current(transcriptRef.current); resolveRef.current = null; instanceRef.current = null; } }, 2000); }); }, []); const abort = useCallback(() => { transcriptRef.current = ''; const instance = instanceRef.current; if (instance) { try { instance.abort(); } catch {} instanceRef.current = null; } if (resolveRef.current) { resolveRef.current(''); resolveRef.current = null; } }, []); return { start, stop, abort, isSupported }; }