{"version":3,"sources":["../../../../src/sensors/useSpeechRecognition/core.ts"],"sourcesContent":["import { batch, observable, type Observable } from \"@legendapp/state\";\nimport {\n  createSupported,\n  onUnmount,\n  peek,\n  type DeepMaybeObservable,\n  type ReadonlyObservable,\n  type Supportable,\n} from \"@usels/core\";\nimport { resolveWindowSource, type ConfigurableWindow } from \"@shared/configurable\";\n\n// Web Speech API types not yet in TypeScript's standard DOM lib\ninterface SpeechRecognitionEvent {\n  results: SpeechRecognitionResultList;\n}\n\ninterface SpeechRecognitionErrorEvent {\n  error: string;\n  message: string;\n}\n\ninterface SpeechRecognitionInstance {\n  lang: string;\n  continuous: boolean;\n  interimResults: boolean;\n  maxAlternatives: number;\n  onstart: (() => void) | null;\n  onresult: ((event: SpeechRecognitionEvent) => void) | null;\n  onerror: ((event: SpeechRecognitionErrorEvent) => void) | null;\n  onend: (() => void) | null;\n  start(): void;\n  stop(): void;\n  abort(): void;\n}\n\ntype SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;\n\ntype WindowWithSpeechRecognition = Window & {\n  SpeechRecognition?: SpeechRecognitionConstructor;\n  webkitSpeechRecognition?: SpeechRecognitionConstructor;\n};\n\nexport interface UseSpeechRecognitionOptions extends ConfigurableWindow {\n  /** Language for recognition. Default: \"en-US\" */\n  lang?: string;\n  /** Continuous recognition. Default: true */\n  continuous?: boolean;\n  /** Return interim results. Default: true */\n  interimResults?: boolean;\n  /** Maximum number of alternatives per result. Default: 1 */\n  maxAlternatives?: number;\n}\n\nexport interface UseSpeechRecognitionReturn extends Supportable {\n  /** Whether currently listening */\n  isListening$: ReadonlyObservable<boolean>;\n  /** Whether current result is final */\n  isFinal$: ReadonlyObservable<boolean>;\n  /** Recognized text */\n  result$: ReadonlyObservable<string>;\n  /** Recognition error */\n  error$: ReadonlyObservable<SpeechRecognitionErrorEvent | undefined>;\n  /** Start recognition */\n  start: () => void;\n  /** Stop recognition */\n  stop: () => void;\n  /** Toggle recognition */\n  toggle: (value?: boolean) => void;\n}\n\n/**\n * Framework-agnostic reactive wrapper around the\n * [Web Speech Recognition API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition).\n *\n * Each `start()` call instantiates a fresh `SpeechRecognition` object using the\n * latest option values, so reactive option updates take effect on the next\n * `start()` without needing to recreate the hook. Cleanup is registered via\n * `onUnmount`.\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function createSpeechRecognition(\n  options?: DeepMaybeObservable<UseSpeechRecognitionOptions>\n): UseSpeechRecognitionReturn {\n  const opts$ = observable(options);\n  const win$ = resolveWindowSource(opts$.window as unknown as Observable<unknown>);\n\n  const resolveCtor = (): SpeechRecognitionConstructor | undefined => {\n    const win = win$.peek() as WindowWithSpeechRecognition | null;\n    if (!win) return undefined;\n    return win.SpeechRecognition ?? win.webkitSpeechRecognition;\n  };\n\n  const isSupported$ = createSupported(() => {\n    const win = win$.get() as WindowWithSpeechRecognition | null;\n    if (!win) return false;\n    return !!(win.SpeechRecognition ?? win.webkitSpeechRecognition);\n  });\n\n  const isListening$ = observable(false);\n  const isFinal$ = observable(false);\n  const result$ = observable(\"\");\n  const error$ = observable<SpeechRecognitionErrorEvent | undefined>(undefined);\n\n  let recognition: SpeechRecognitionInstance | null = null;\n\n  const start = (): void => {\n    if (isListening$.peek()) return;\n    const Ctor = resolveCtor();\n    if (!Ctor) return;\n\n    const raw = opts$.peek();\n    const inst = new Ctor();\n    inst.lang = (peek(raw?.lang) as string | undefined) ?? \"en-US\";\n    inst.continuous = (peek(raw?.continuous) as boolean | undefined) ?? true;\n    inst.interimResults = (peek(raw?.interimResults) as boolean | undefined) ?? true;\n    inst.maxAlternatives = (peek(raw?.maxAlternatives) as number | undefined) ?? 1;\n\n    inst.onstart = () => {\n      batch(() => {\n        isListening$.set(true);\n        isFinal$.set(false);\n      });\n    };\n\n    inst.onresult = (event: SpeechRecognitionEvent) => {\n      const results = event.results;\n      if (results.length === 0) return;\n      let transcript = \"\";\n      for (let i = 0; i < results.length; i++) {\n        transcript += results[i][0].transcript;\n      }\n      batch(() => {\n        result$.set(transcript);\n        isFinal$.set(results[results.length - 1].isFinal);\n      });\n    };\n\n    inst.onerror = (event: SpeechRecognitionErrorEvent) => {\n      error$.set(event);\n    };\n\n    inst.onend = () => {\n      // isListening$ is also set in stop() as a defensive double-set;\n      // Legend-State deduplicates same-value sets so this is harmless.\n      isListening$.set(false);\n    };\n\n    recognition = inst;\n    inst.start();\n  };\n\n  const stop = (): void => {\n    if (recognition) {\n      recognition.stop();\n      recognition = null;\n    }\n    // Defensive set in case onend doesn't fire (e.g. recognition was never started)\n    isListening$.set(false);\n  };\n\n  const toggle = (value?: boolean): void => {\n    const shouldListen = value ?? !isListening$.peek();\n    if (shouldListen) {\n      start();\n    } else {\n      stop();\n    }\n  };\n\n  onUnmount(() => {\n    stop();\n  });\n\n  return {\n    isSupported$,\n    isListening$: isListening$ as ReadonlyObservable<boolean>,\n    isFinal$: isFinal$ as ReadonlyObservable<boolean>,\n    result$: result$ as ReadonlyObservable<string>,\n    error$: error$ as ReadonlyObservable<SpeechRecognitionErrorEvent | undefined>,\n    start,\n    stop,\n    toggle,\n  };\n}\n"],"mappings":"AAAA,SAAS,OAAO,kBAAmC;AACnD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,2BAAoD;AAAA;AAuEtD,SAAS,wBACd,SAC4B;AAC5B,QAAM,QAAQ,WAAW,OAAO;AAChC,QAAM,OAAO,oBAAoB,MAAM,MAAwC;AAE/E,QAAM,cAAc,MAAgD;AAClE,UAAM,MAAM,KAAK,KAAK;AACtB,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,IAAI,qBAAqB,IAAI;AAAA,EACtC;AAEA,QAAM,eAAe,gBAAgB,MAAM;AACzC,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,CAAC,EAAE,IAAI,qBAAqB,IAAI;AAAA,EACzC,CAAC;AAED,QAAM,eAAe,WAAW,KAAK;AACrC,QAAM,WAAW,WAAW,KAAK;AACjC,QAAM,UAAU,WAAW,EAAE;AAC7B,QAAM,SAAS,WAAoD,MAAS;AAE5E,MAAI,cAAgD;AAEpD,QAAM,QAAQ,MAAY;AACxB,QAAI,aAAa,KAAK,EAAG;AACzB,UAAM,OAAO,YAAY;AACzB,QAAI,CAAC,KAAM;AAEX,UAAM,MAAM,MAAM,KAAK;AACvB,UAAM,OAAO,IAAI,KAAK;AACtB,SAAK,OAAQ,KAAK,KAAK,IAAI,KAA4B;AACvD,SAAK,aAAc,KAAK,KAAK,UAAU,KAA6B;AACpE,SAAK,iBAAkB,KAAK,KAAK,cAAc,KAA6B;AAC5E,SAAK,kBAAmB,KAAK,KAAK,eAAe,KAA4B;AAE7E,SAAK,UAAU,MAAM;AACnB,YAAM,MAAM;AACV,qBAAa,IAAI,IAAI;AACrB,iBAAS,IAAI,KAAK;AAAA,MACpB,CAAC;AAAA,IACH;AAEA,SAAK,WAAW,CAAC,UAAkC;AACjD,YAAM,UAAU,MAAM;AACtB,UAAI,QAAQ,WAAW,EAAG;AAC1B,UAAI,aAAa;AACjB,eAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,sBAAc,QAAQ,CAAC,EAAE,CAAC,EAAE;AAAA,MAC9B;AACA,YAAM,MAAM;AACV,gBAAQ,IAAI,UAAU;AACtB,iBAAS,IAAI,QAAQ,QAAQ,SAAS,CAAC,EAAE,OAAO;AAAA,MAClD,CAAC;AAAA,IACH;AAEA,SAAK,UAAU,CAAC,UAAuC;AACrD,aAAO,IAAI,KAAK;AAAA,IAClB;AAEA,SAAK,QAAQ,MAAM;AAGjB,mBAAa,IAAI,KAAK;AAAA,IACxB;AAEA,kBAAc;AACd,SAAK,MAAM;AAAA,EACb;AAEA,QAAM,OAAO,MAAY;AACvB,QAAI,aAAa;AACf,kBAAY,KAAK;AACjB,oBAAc;AAAA,IAChB;AAEA,iBAAa,IAAI,KAAK;AAAA,EACxB;AAEA,QAAM,SAAS,CAAC,UAA0B;AACxC,UAAM,eAAe,SAAS,CAAC,aAAa,KAAK;AACjD,QAAI,cAAc;AAChB,YAAM;AAAA,IACR,OAAO;AACL,WAAK;AAAA,IACP;AAAA,EACF;AAEA,YAAU,MAAM;AACd,SAAK;AAAA,EACP,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}