{"mappings":"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","sources":["packages/@react-spectrum/ai/src/useVoiceInput.ts"],"sourcesContent":["/*\n * Copyright 2026 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n  Dispatch,\n  RefObject,\n  SetStateAction,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState\n} from 'react';\nimport {useEffectEvent} from 'react-aria/private/utils/useEffectEvent';\n\n/**\n * Chromium brands whose Web Speech backend is known to work. Other Chromium\n * forks (Arc, Brave, Edge) expose the API but route to a backend that does not\n * return results, so they are excluded. Safari has no `userAgentData` and is\n * trusted directly. Add a brand here to enable another Chromium fork.\n */\nconst VOICE_INPUT_CHROMIUM_BROWSERS = new Set<string>(['Google Chrome']);\n\n/** Normalised voice-recognition error. `null` is never surfaced (e.g. aborts). */\nexport type VoiceInputErrorCode =\n  | 'not-allowed'\n  | 'network'\n  | 'no-speech'\n  | 'audio-capture'\n  | 'language-not-supported'\n  | 'phrases-not-supported'\n  | 'unknown';\n\n// doesn't have onStart/onInterim/etc, didn't seem like they were needed since coworker just used\n// it to contruct the transcript while still allowing the field be editable by the user and it was quite buggy\nexport interface VoiceInputProps {\n  lang?: string;\n  onError?: (code: VoiceInputErrorCode) => void;\n  onListeningChange?: (isListening: boolean) => void;\n}\n\n// TODO: make a accompanying aria hook later, this is more stately like\nexport interface VoiceInputResult {\n  isSupported: boolean;\n  isListening: boolean;\n  errorCode: VoiceInputErrorCode | null;\n  // TODO: this returns the full voice transcript rather than offering a interim and final one like cooworker's does\n  // can alway offer that later\n  transcript: string;\n  start: () => void;\n  stop: () => void;\n  toggle: () => void;\n}\n\n/** Resolves the recognizer constructor, applying the Chromium brand allowlist. */\nconst getRecognitionCtor = (): SpeechRecognitionConstructor | null => {\n  if (typeof window === 'undefined') {\n    return null;\n  }\n  const ctor = window.SpeechRecognition ?? window.webkitSpeechRecognition ?? null;\n  if (!ctor) {\n    return null;\n  }\n  const brands = navigator.userAgentData?.brands;\n  if (brands && !brands.some(b => VOICE_INPUT_CHROMIUM_BROWSERS.has(b.brand))) {\n    return null;\n  }\n  return ctor;\n};\n\n/** SpeechRecognition requires a secure context (HTTPS / localhost). */\nconst isSecureVoiceContext = (): boolean =>\n  typeof window !== 'undefined' && window.isSecureContext === true;\n\n/** Maps a raw recognition error to a normalised code, or `null` to ignore it. */\nconst mapErrorCode = (error: string): VoiceInputErrorCode | null => {\n  switch (error) {\n    case 'aborted':\n      return null;\n    case 'not-allowed':\n    case 'service-not-allowed':\n      return 'not-allowed';\n    case 'network':\n      return 'network';\n    case 'no-speech':\n      return 'no-speech';\n    case 'audio-capture':\n      return 'audio-capture';\n    case 'language-not-supported':\n      return 'language-not-supported';\n    case 'phrases-not-supported':\n      return 'phrases-not-supported';\n    default:\n      return 'unknown';\n  }\n};\n\n/** Maps a getUserMedia rejection to a normalised error code. */\nconst mapMediaError = (err: unknown): VoiceInputErrorCode => {\n  const name = err instanceof DOMException ? err.name : '';\n  if (name === 'NotAllowedError' || name === 'SecurityError') {\n    return 'not-allowed';\n  }\n  if (name === 'NotFoundError' || name === 'NotReadableError') {\n    return 'audio-capture';\n  }\n  return 'unknown';\n};\n\n/**\n * Requests microphone access so the browser permission prompt reliably opens and\n * a denial is detected before recognition starts. Returns `null` on success (or\n * when getUserMedia is unavailable, deferring to the recognizer's own prompt),\n * or an error code on failure. The granted stream is released immediately —\n * SpeechRecognition opens its own.\n */\nconst requestMicrophone = async (): Promise<VoiceInputErrorCode | null> => {\n  const media = typeof navigator !== 'undefined' ? navigator.mediaDevices : undefined;\n  if (!media?.getUserMedia) {\n    return null;\n  }\n  try {\n    const stream = await media.getUserMedia({audio: true});\n    stream.getTracks().forEach(track => track.stop());\n    return null;\n  } catch (err) {\n    return mapMediaError(err);\n  }\n};\n\n/** Accumulates interim vs final transcripts from a recognition result event. */\nconst readTranscript = (event: SpeechRecognitionEvent): {interim: string; final: string} => {\n  let interim = '';\n  let final = '';\n  for (let i = event.resultIndex; i < event.results.length; i++) {\n    const result = event.results[i];\n    const alternative = result?.[0];\n    if (result && alternative) {\n      if (result.isFinal) {\n        final += alternative.transcript;\n      } else {\n        interim += alternative.transcript;\n      }\n    }\n  }\n  return {interim, final};\n};\n\ninterface RecognizerSetup {\n  recognitionRef: RefObject<SpeechRecognition | null>;\n  setListening: Dispatch<SetStateAction<boolean>>;\n  onListeningChange: (isListening: boolean) => void;\n  onError: (code: VoiceInputErrorCode) => void;\n  setError: Dispatch<SetStateAction<VoiceInputErrorCode | null>>;\n  setInterim: Dispatch<SetStateAction<string>>;\n  appendFinal: (s: string) => void;\n}\n\n/**\n * Creates and wires a SpeechRecognition instance. On error/end the ref is cleared,\n * but only when this instance is still the active one, so a late event from a\n * previous session can't wipe out a newly-started recognizer.\n */\nconst createRecognizer = (\n  ctor: SpeechRecognitionConstructor,\n  lang: string,\n  setup: RecognizerSetup\n): SpeechRecognition => {\n  const recognition = new ctor();\n  recognition.continuous = true;\n  recognition.interimResults = true;\n  recognition.lang = lang;\n\n  // Ignore events from a session that is no longer the active recognizer (e.g. a\n  // late onend/onerror after a new session started), so they can't flip the\n  // listening state, surface a st ale error, or clear a newer recognizer.\n  const isActive = () => setup.recognitionRef.current === recognition;\n\n  recognition.onstart = () => {\n    setup.setListening(true);\n    setup.onListeningChange(true);\n  };\n  recognition.onresult = event => {\n    if (!isActive()) {\n      return;\n    }\n    const {interim, final} = readTranscript(event);\n    if (final) {\n      setup.appendFinal(final);\n    }\n    setup.setInterim(interim);\n  };\n  recognition.onerror = event => {\n    if (!isActive()) {\n      return;\n    }\n    const code = mapErrorCode(event.error);\n    if (code) {\n      setup.setError(code);\n      setup.onError(code);\n    }\n    setup.setListening(false);\n    setup.onListeningChange(false);\n    setup.setInterim('');\n    // Clear so the next press starts a fresh session instead of calling stop().\n    setup.recognitionRef.current = null;\n  };\n  recognition.onend = () => {\n    if (!isActive()) {\n      return;\n    }\n    setup.setListening(false);\n    setup.onListeningChange(false);\n    setup.setInterim('');\n    setup.recognitionRef.current = null;\n  };\n\n  return recognition;\n};\n\nexport const useVoiceInput = (options: VoiceInputProps): VoiceInputResult => {\n  const {lang, onError, onListeningChange} = options;\n\n  // Resolve support once — it cannot change for the lifetime of the page.\n  const ctor = useMemo(() => (isSecureVoiceContext() ? getRecognitionCtor() : null), []);\n  const isSupported = ctor !== null;\n\n  const [isListening, setIsListening] = useState(false);\n  const [errorCode, setErrorCode] = useState<VoiceInputErrorCode | null>(null);\n  const [interimTranscript, setInterimTranscript] = useState('');\n  const [finalTranscript, setFinalTranscript] = useState('');\n  const transcript = finalTranscript + interimTranscript;\n\n  const recognitionRef = useRef<SpeechRecognition | null>(null);\n  const startingRef = useRef(false);\n  const handleError = useEffectEvent((code: VoiceInputErrorCode) => {\n    onError?.(code);\n  });\n  const handleListeningChange = useEffectEvent((v: boolean) => {\n    onListeningChange?.(v);\n  });\n\n  const stop = useCallback(() => {\n    recognitionRef.current?.stop();\n  }, []);\n\n  const start = useCallback(() => {\n    // Guard against double-start (a started recognizer throws InvalidStateError)\n    // and against re-entry while the permission prompt is open.\n    if (!ctor || recognitionRef.current || startingRef.current) {\n      return;\n    }\n    startingRef.current = true;\n    setErrorCode(null);\n    setInterimTranscript('');\n    setFinalTranscript('');\n\n    // Request mic access first so the browser permission prompt reliably opens\n    // and a denial is reported before recognition starts.\n    void requestMicrophone().then(micError => {\n      startingRef.current = false;\n      if (micError) {\n        setErrorCode(micError);\n        handleError(micError);\n        return;\n      }\n      // Bail if already started while the permission prompt was open.\n      if (recognitionRef.current) {\n        return;\n      }\n\n      const recognition = createRecognizer(ctor, lang ?? navigator.language ?? 'en-US', {\n        recognitionRef,\n        setListening: setIsListening,\n        onListeningChange: handleListeningChange,\n        onError: handleError,\n        setError: setErrorCode,\n        setInterim: setInterimTranscript,\n        appendFinal: s => setFinalTranscript(prev => prev + s)\n      });\n      recognitionRef.current = recognition;\n      recognition.start();\n    });\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\n  }, [ctor, lang, handleError, handleListeningChange]);\n\n  const toggle = useCallback(() => {\n    if (recognitionRef.current) {\n      stop();\n    } else {\n      start();\n    }\n  }, [start, stop]);\n\n  // Abort on unmount so dangling callbacks never fire after teardown.\n  useEffect(() => {\n    return () => {\n      recognitionRef.current?.abort();\n    };\n  }, []);\n\n  return {isSupported, isListening, errorCode, transcript, start, stop, toggle};\n};\n"],"names":[],"version":3,"file":"useVoiceInput.mjs.map"}