"use client"; /** * Browser dictation for "teach it back" (and anything else that wants it): * the Web Speech API's recognition side. Chrome, Edge and some Safari builds * expose it as `SpeechRecognition` / `webkitSpeechRecognition`; other engines * do not, so the UI asks `speechInputAvailable()` first and simply hides the * mic when the answer is no. * * Nothing leaves the app ourselves: the audio goes to whatever recogniser the * browser uses, exactly like the system dictation key on a phone keyboard. * The API types are not in TypeScript's DOM lib, so the minimal shapes live * here rather than leaking `any`. */ type RecognitionResultLike = { isFinal: boolean; 0: { transcript: string } }; type RecognitionEventLike = { resultIndex: number; results: ArrayLike }; type RecognitionLike = { lang: string; continuous: boolean; interimResults: boolean; maxAlternatives: number; start(): void; stop(): void; abort(): void; onresult: ((event: RecognitionEventLike) => void) | null; onerror: (() => void) | null; onend: (() => void) | null; }; type RecognitionCtor = new () => RecognitionLike; function recognitionCtor(): RecognitionCtor | null { if (typeof window === "undefined") return null; const scope = window as unknown as { SpeechRecognition?: RecognitionCtor; webkitSpeechRecognition?: RecognitionCtor }; return scope.SpeechRecognition ?? scope.webkitSpeechRecognition ?? null; } export function speechInputAvailable(): boolean { return recognitionCtor() !== null; } export type SpeechInput = { stop: () => void }; /** * Starts listening. Final phrases arrive through `onText` (append them to the * draft); `onEnd` fires exactly once when listening stops, however it stops. * Returns null when the browser cannot listen or `start()` fails. */ export function startSpeechInput(options: { /** BCP-47 tag, usually `speechLocale(deck.language, getLocale())`. */ lang: string; onText: (text: string) => void; onEnd: () => void; }): SpeechInput | null { const Ctor = recognitionCtor(); if (!Ctor) return null; let recognition: RecognitionLike; try { recognition = new Ctor(); } catch { return null; } let finished = false; const finish = () => { if (finished) return; finished = true; options.onEnd(); }; recognition.lang = options.lang; recognition.continuous = true; recognition.interimResults = true; recognition.maxAlternatives = 1; recognition.onresult = (event) => { for (let index = event.resultIndex; index < event.results.length; index += 1) { const result = event.results[index]; if (!result?.isFinal) continue; const transcript = result[0]?.transcript?.trim(); if (transcript) options.onText(transcript); } }; recognition.onerror = finish; recognition.onend = finish; try { recognition.start(); } catch { finish(); return null; } return { stop() { try { recognition.stop(); } catch { /* already stopped */ } finish(); }, }; }