import { splitProps, Show, createSignal, createEffect, on, onCleanup } from 'solid-js'; import { cn } from '../utils/cn'; import { Button } from '../ui/button'; import { Tooltip } from '../ui/tooltip'; /** Imperative handle exposed via `controllerRef` — surfaces the playback controls * so the `` facade can forward them as instance methods. Both * the native (speechSynthesis) and model (synthesize → Audio) paths run through * these, so manual clicks and programmatic calls behave identically. */ export interface VoiceOutputController { /** Speak the current `text` (native by default, model if `synthesize` is set). */ speak(): void; /** Pause playback (resumable). */ pause(): void; /** Resume paused playback. */ resume(): void; /** Stop playback and reset. */ stop(): void; } export interface VoiceOutputProps { /** The utterance to read aloud. */ text: string; /** Speak automatically when `text` is set/changed. */ autoplay?: boolean; /** TTS model seam: given text, return an audio Blob to play. When set, the * native speechSynthesis path is bypassed. Mirrors VoiceInput's `onTranscribe`. */ onSynthesize?: (text: string) => Promise; /** Fires whenever playback starts or stops. */ onSpeakingChange?: (speaking: boolean) => void; /** Fires once the model path resolves audio (model path only). */ onSynthesized?: (blob: Blob) => void; disabled?: boolean; class?: string; /** Receive the imperative controller once mounted. The `` * facade forwards these as element methods (speak/pause/resume/stop). */ controllerRef?: (controller: VoiceOutputController) => void; } /** True when the browser exposes the Web Speech synthesis API. */ function hasSpeechSynthesis(): boolean { return typeof window !== 'undefined' && 'speechSynthesis' in window; } export function VoiceOutput(props: VoiceOutputProps) { const [local] = splitProps(props, ['text', 'disabled', 'class']); const [isSpeaking, setIsSpeaking] = createSignal(false); const [isLoading, setIsLoading] = createSignal(false); // The model path plays through one reused