UNPKG

agents

Version:

A home for your AI agents

234 lines (233 loc) 9.42 kB
import { VoiceClient, WebSocketVoiceTransport } from "./client.js"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; //#region src/voice/react.tsx /** * React hook for voice-to-text input. Captures microphone audio, streams it * to a server-side VoiceAgent for STT, and returns the transcript as a string. * * Unlike `useVoiceAgent`, this hook is optimised for dictation — it accumulates * user transcripts into a single string and ignores assistant responses / TTS. * * @example * ```tsx * const { transcript, interimTranscript, isListening, start, stop } = useVoiceInput({ * agent: "voice-input-agent" * }); * * <textarea value={transcript + (interimTranscript ? " " + interimTranscript : "")} /> * <button onClick={isListening ? stop : start}> * {isListening ? "Stop" : "Dictate"} * </button> * ``` */ function useVoiceInput(options) { const connectionKey = useMemo(() => `${options.agent}:${options.name ?? "default"}:${options.host ?? ""}:${options.silenceThreshold ?? ""}:${options.silenceDurationMs ?? ""}`, [ options.agent, options.name, options.host, options.silenceThreshold, options.silenceDurationMs ]); const clientRef = useRef(null); const [transcript, setTranscript] = useState(""); const [interimTranscript, setInterimTranscript] = useState(null); const [isListening, setIsListening] = useState(false); const [turnMetrics, setTurnMetrics] = useState(null); const [audioLevel, setAudioLevel] = useState(0); const [isMuted, setIsMuted] = useState(false); const [error, setError] = useState(null); useEffect(() => { setIsListening(false); setInterimTranscript(null); setTurnMetrics(null); setAudioLevel(0); setIsMuted(false); setError(null); const client = new VoiceClient({ agent: options.agent, name: options.name, host: options.host, silenceThreshold: options.silenceThreshold, silenceDurationMs: options.silenceDurationMs }); clientRef.current = client; client.connect(); const onTranscript = () => { setTranscript(client.transcript.filter((m) => m.role === "user").map((m) => m.text).join(" ")); }; const onInterim = () => setInterimTranscript(client.interimTranscript); const onTurnMetrics = (value) => setTurnMetrics(value); const onAudioLevel = () => setAudioLevel(client.audioLevel); const onMute = () => setIsMuted(client.isMuted); const onError = () => setError(client.error); const onStatus = () => { const s = client.status; setIsListening(s === "listening" || s === "thinking"); }; client.addEventListener("transcriptchange", onTranscript); client.addEventListener("interimtranscript", onInterim); client.addEventListener("turnmetrics", onTurnMetrics); client.addEventListener("audiolevelchange", onAudioLevel); client.addEventListener("mutechange", onMute); client.addEventListener("error", onError); client.addEventListener("statuschange", onStatus); return () => { client.removeEventListener("transcriptchange", onTranscript); client.removeEventListener("interimtranscript", onInterim); client.removeEventListener("turnmetrics", onTurnMetrics); client.removeEventListener("audiolevelchange", onAudioLevel); client.removeEventListener("mutechange", onMute); client.removeEventListener("error", onError); client.removeEventListener("statuschange", onStatus); client.disconnect(); }; }, [connectionKey]); return { transcript, interimTranscript, turnMetrics, isListening, audioLevel, isMuted, error, start: useCallback(() => clientRef.current.startCall(), []), stop: useCallback(() => clientRef.current.endCall(), []), toggleMute: useCallback(() => clientRef.current.toggleMute(), []), clear: useCallback(() => setTranscript(""), []) }; } /** * React hook that wraps VoiceClient, syncing its state into React state. * All audio infrastructure (mic capture, playback, silence/interrupt detection, * voice protocol) is handled by VoiceClient — this hook just bridges to React. * * When the connection identity changes (agent, name, or host), the hook * automatically disconnects the old client, creates a new one, and reconnects. * The `onReconnect` callback fires when this happens. */ function useVoiceAgent(options) { const queryString = useMemo(() => options.query ? JSON.stringify(options.query) : "", [options.query]); const enabled = options.enabled ?? true; const outputDeviceId = options.outputDeviceId; const connectionKey = useMemo(() => `${options.agent}:${options.name ?? "default"}:${options.host ?? ""}:${options.silenceThreshold ?? ""}:${options.silenceDurationMs ?? ""}:${options.interruptThreshold ?? ""}:${options.interruptChunks ?? ""}:${queryString}`, [ options.agent, options.name, options.host, options.silenceThreshold, options.silenceDurationMs, options.interruptThreshold, options.interruptChunks, queryString ]); const effectKey = useMemo(() => `${enabled ? "enabled" : "disabled"}:${connectionKey}`, [enabled, connectionKey]); const clientRef = useRef(null); const activeKeyRef = useRef(null); const onReconnectRef = useRef(options.onReconnect); onReconnectRef.current = options.onReconnect; const [status, setStatus] = useState("idle"); const [transcript, setTranscript] = useState([]); const [metrics, setMetrics] = useState(null); const [turnMetrics, setTurnMetrics] = useState(null); const [audioLevel, setAudioLevel] = useState(0); const [isMuted, setIsMuted] = useState(false); const [connected, setConnected] = useState(false); const [error, setError] = useState(null); const [outputDeviceError, setOutputDeviceError] = useState(null); const [interimTranscript, setInterimTranscript] = useState(null); const [lastCustomMessage, setLastCustomMessage] = useState(null); useEffect(() => { setStatus("idle"); setTranscript([]); setMetrics(null); setTurnMetrics(null); setAudioLevel(0); setIsMuted(false); setConnected(false); setError(null); setOutputDeviceError(null); setInterimTranscript(null); setLastCustomMessage(null); if (!enabled) { clientRef.current = null; activeKeyRef.current = null; return; } const isReconnect = activeKeyRef.current !== null && activeKeyRef.current !== connectionKey; activeKeyRef.current = connectionKey; if (isReconnect) onReconnectRef.current?.(); const { enabled: _enabled, onReconnect: _onReconnect, ...clientOptions } = options; const client = new VoiceClient(clientOptions); clientRef.current = client; client.connect(); const onStatus = (s) => setStatus(s); const onTranscript = (t) => setTranscript(t); const onMetrics = (m) => setMetrics(m); const onTurnMetrics = (value) => setTurnMetrics(value); const onAudioLevel = (level) => setAudioLevel(level); const onMute = (muted) => setIsMuted(muted); const onConnection = (c) => setConnected(c); const onError = (e) => setError(e); const onOutputDeviceError = (e) => setOutputDeviceError(e); const onInterim = (text) => setInterimTranscript(text); client.addEventListener("statuschange", onStatus); client.addEventListener("transcriptchange", onTranscript); client.addEventListener("interimtranscript", onInterim); client.addEventListener("metricschange", onMetrics); client.addEventListener("turnmetrics", onTurnMetrics); client.addEventListener("audiolevelchange", onAudioLevel); client.addEventListener("mutechange", onMute); client.addEventListener("connectionchange", onConnection); client.addEventListener("error", onError); client.addEventListener("outputdeviceerror", onOutputDeviceError); return () => { client.removeEventListener("statuschange", onStatus); client.removeEventListener("transcriptchange", onTranscript); client.removeEventListener("interimtranscript", onInterim); client.removeEventListener("metricschange", onMetrics); client.removeEventListener("turnmetrics", onTurnMetrics); client.removeEventListener("audiolevelchange", onAudioLevel); client.removeEventListener("mutechange", onMute); client.removeEventListener("connectionchange", onConnection); client.removeEventListener("error", onError); client.removeEventListener("outputdeviceerror", onOutputDeviceError); if (clientRef.current === client) clientRef.current = null; client.disconnect(); }; }, [effectKey]); const startCall = useCallback(() => clientRef.current?.startCall() ?? Promise.resolve(), []); const endCall = useCallback(() => clientRef.current?.endCall(), []); const toggleMute = useCallback(() => clientRef.current?.toggleMute(), []); const sendText = useCallback((text) => clientRef.current?.sendText(text), []); const sendJSON = useCallback((data) => clientRef.current?.sendJSON(data), []); useEffect(() => { const client = clientRef.current; if (!client) return; const onCustom = (msg) => setLastCustomMessage(msg); client.addEventListener("custommessage", onCustom); return () => client.removeEventListener("custommessage", onCustom); }, [effectKey]); useEffect(() => { clientRef.current?.setOutputDevice(outputDeviceId); }, [effectKey, outputDeviceId]); return { status, transcript, interimTranscript, metrics, turnMetrics, audioLevel, isMuted, connected, error, outputDeviceError, startCall, endCall, toggleMute, sendText, sendJSON, lastCustomMessage }; } //#endregion export { WebSocketVoiceTransport, useVoiceAgent, useVoiceInput }; //# sourceMappingURL=react.js.map