import { defineWebComponent } from './define'; import { VoiceInput, type VoiceInputController } from '../components/voice-input'; interface Props extends Record { /** * Transcriber the host supplies — records audio, returns the text. This is a * **function-valued property** (`el.transcribe = async blob => '...'`) because * a value-returning callback can't be modelled as a fire-and-forget event. */ transcribe?: (audio: Blob) => Promise; /** Disable the mic button (non-interactive). */ disabled?: boolean; /** * BCP-47 language tag for the native `SpeechRecognition` path (e.g. `en-US`). * Attribute: `recognition-lang` (the plain `lang` attribute is reserved by * `HTMLElement` and can't be a custom-element property). No effect when * `transcribe` is set or the browser lacks SpeechRecognition. */ recognitionLang?: string; /** Emit live partial transcripts (`kai-transcript-interim`) during native * recognition. Attribute: `interim`. No-op on the transcribe/fallback paths. */ interim?: boolean; } /** Events fired by ``. */ interface Events { /** Raw audio captured (before transcription) — for hosts that prefer to * handle transcription themselves instead of via the `transcribe` property. * Also the unsupported-fallback signal: no `transcribe`, no SpeechRecognition, * so only the blob is produced (no text). */ 'kai-audio-captured': { blob: Blob }; /** Final transcript — the `transcribe` property resolved, OR native * `SpeechRecognition` produced final text (no `transcribe` set). */ 'kai-transcription': { text: string }; /** Live partial transcript during native recognition (only when `interim` is * set). Fires repeatedly before the final `kai-transcription`. */ 'kai-transcript-interim': { text: string }; /** Recording started or stopped — lets the host drive its own UI (waveform, * push-to-talk indicator) in sync with the mic. Fires on real transitions * only (manual click and programmatic start()/stop()), never on mount. */ 'kai-recording-change': { recording: boolean }; } /** * `` — a mic button that records and transcribes. Works * natively by default: with no `transcribe` callback it uses the browser's * `SpeechRecognition` (Chrome/Safari; no Firefox — and cloud-based in Chrome). * Set `el.transcribe` to route audio through your own async transcriber instead. * Where neither is available it records the blob and emits `kai-audio-captured` * with no text. Also emits `kai-transcription` (final text) and, with `interim`, * `kai-transcript-interim` (live partials). */ defineWebComponent('kai-voice-input', { transcribe: undefined, disabled: false, recognitionLang: undefined, interim: false, }, (props, { dispatch, flag, expose }) => { // Pattern C: the VoiceInput component owns useVoiceRecorder; it hands up a // start/stop controller and an onRecordingChange callback. The facade captures // the controller and exposes delegating methods (start/stop both run the same // record→transcribe path as the mic click, so manual + programmatic emit the // same kai-audio-captured/kai-transcription/kai-recording-change events). let controller: VoiceInputController | undefined; expose({ /** Begin recording programmatically (e.g. push-to-talk bound to a global * key). Runs the same getUserMedia path as clicking the mic; no-ops if * already recording. */ start: () => controller?.start(), /** Stop the in-progress recording, producing the blob (→ kai-audio-captured) * and running transcription. Pairs with start() for push-to-talk. */ stop: () => controller?.stop(), }); return ( { dispatch('kai-audio-captured', { blob }); return props.transcribe ? props.transcribe(blob) : ''; }} onTranscription={(text) => dispatch('kai-transcription', { text })} onInterim={(text) => dispatch('kai-transcript-interim', { text })} onRecordingChange={(recording) => dispatch('kai-recording-change', { recording })} controllerRef={(c) => (controller = c)} /> ); });