(options: VoiceInputProps)
| 228 | }; |
| 229 | |
| 230 | export const useVoiceInput = (options: VoiceInputProps): VoiceInputResult => { |
| 231 | const {lang, onError, onListeningChange} = options; |
| 232 | |
| 233 | // Resolve support once — it cannot change for the lifetime of the page. |
| 234 | const ctor = useMemo(() => (isSecureVoiceContext() ? getRecognitionCtor() : null), []); |
| 235 | const isSupported = ctor !== null; |
| 236 | |
| 237 | const [isListening, setIsListening] = useState(false); |
| 238 | const [errorCode, setErrorCode] = useState<VoiceInputErrorCode | null>(null); |
| 239 | const [interimTranscript, setInterimTranscript] = useState(''); |
| 240 | const [finalTranscript, setFinalTranscript] = useState(''); |
| 241 | const transcript = finalTranscript + interimTranscript; |
| 242 | |
| 243 | const recognitionRef = useRef<SpeechRecognition | null>(null); |
| 244 | const startingRef = useRef(false); |
| 245 | const handleError = useEffectEvent((code: VoiceInputErrorCode) => { |
| 246 | onError?.(code); |
| 247 | }); |
| 248 | const handleListeningChange = useEffectEvent((v: boolean) => { |
| 249 | onListeningChange?.(v); |
| 250 | }); |
| 251 | |
| 252 | const stop = useCallback(() => { |
| 253 | recognitionRef.current?.stop(); |
| 254 | }, []); |
| 255 | |
| 256 | const start = useCallback(() => { |
| 257 | // Guard against double-start (a started recognizer throws InvalidStateError) |
| 258 | // and against re-entry while the permission prompt is open. |
| 259 | if (!ctor || recognitionRef.current || startingRef.current) { |
| 260 | return; |
| 261 | } |
| 262 | startingRef.current = true; |
| 263 | setErrorCode(null); |
| 264 | setInterimTranscript(''); |
| 265 | setFinalTranscript(''); |
| 266 | |
| 267 | // Request mic access first so the browser permission prompt reliably opens |
| 268 | // and a denial is reported before recognition starts. |
| 269 | void requestMicrophone().then(micError => { |
| 270 | startingRef.current = false; |
| 271 | if (micError) { |
| 272 | setErrorCode(micError); |
| 273 | handleError(micError); |
| 274 | return; |
| 275 | } |
| 276 | // Bail if already started while the permission prompt was open. |
| 277 | if (recognitionRef.current) { |
| 278 | return; |
| 279 | } |
| 280 | |
| 281 | const recognition = createRecognizer(ctor, lang ?? navigator.language ?? 'en-US', { |
| 282 | recognitionRef, |
| 283 | setListening: setIsListening, |
| 284 | onListeningChange: handleListeningChange, |
| 285 | onError: handleError, |
| 286 | setError: setErrorCode, |
| 287 | setInterim: setInterimTranscript, |
no test coverage detected