import { Button } from "@agent-native/toolkit/ui/button"; import { Checkbox } from "@agent-native/toolkit/ui/checkbox"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, } from "@agent-native/toolkit/ui/select"; import { IconAlertTriangle, IconBrain, IconKey, IconLanguage, IconLoader2, IconMicrophone, IconPhoneOff, IconPlugConnected, IconSettings, IconVolume, } from "@tabler/icons-react"; import { type MouseEvent, useCallback, useEffect, useId, useRef, useState, useSyncExternalStore, } from "react"; import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger, } from "../ui/popover.js"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js"; import { cn } from "../utils.js"; import { createRealtimeVoiceAudioLevelStore, type RealtimeVoiceAudioLevelStore, } from "./realtime-voice-audio-level.js"; export type RealtimeVoiceModeState = | "connecting" | "listening" | "speaking" | "working" | "error" | "ending"; /** * User-visible copy stays outside the shared component so host catalogs remain * the source of truth. Callers should provide these values through `useT()`. */ export interface RealtimeVoiceModeCopy { entryButtonLabel: string; promptTitle: string; promptDescription: string; setupTitle: string; setupDescription: string; connectBuilder: string; useOpenAiKey: string; startWithOpenAiKey: string; startVoiceMode: string; keepDictating: string; rememberPreference: string; showChat: string; hideChat: string; endVoiceMode: string; voiceSettings: string; settings: { microphone: string; defaultMicrophone: string; microphoneSwitchFailed: string; language: string; autoLanguage: string; languages: Record< Exclude< import("./useRealtimeVoiceMode.js").RealtimeVoiceLanguage, "auto" >, string >; intelligence: string; intelligenceLevels: Record< import("./useRealtimeVoiceMode.js").RealtimeVoiceIntelligence, string >; voiceStyle: string; voiceChangePending: string; voiceDescriptions: Record< import("./useRealtimeVoiceMode.js").RealtimeVoice, string >; }; status: Record; errors: { unsupported: string; responseFailed: string; sessionFailed: string; channelDisconnected: string; connectionTimedOut?: string; connectionFailed: string; offerFailed: string; }; } export interface RealtimeVoiceModeEntryProps { copy: RealtimeVoiceModeCopy; disabled?: boolean; providerStatusPending?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; onStartVoiceMode: () => void; onKeepDictating: () => void; preferredMode?: RealtimeVoiceInputMode | null; onRememberPreference?: (mode: RealtimeVoiceInputMode) => void; setupRequired?: boolean; openAiConfigured?: boolean; connectingBuilder?: boolean; onConnectBuilder?: () => void; onUseOpenAiKey?: () => void; className?: string; } export type RealtimeVoiceInputMode = "realtime" | "dictation"; /** * Composer mic entry point for apps that support a full-duplex voice session. * The first click offers voice mode without silently changing the existing * editable-dictation behavior. */ export function RealtimeVoiceModeEntry({ copy, disabled, providerStatusPending = false, open: controlledOpen, onOpenChange, onStartVoiceMode, onKeepDictating, preferredMode = null, onRememberPreference, setupRequired = false, openAiConfigured = false, connectingBuilder = false, onConnectBuilder, onUseOpenAiKey, className, }: RealtimeVoiceModeEntryProps) { const [uncontrolledOpen, setUncontrolledOpen] = useState(false); const open = controlledOpen ?? uncontrolledOpen; const titleId = useId(); const descriptionId = useId(); const rememberPreferenceId = useId(); const [rememberPreference, setRememberPreference] = useState(false); const [collisionBoundary, setCollisionBoundary] = useState(null); const setTriggerNode = useCallback((node: HTMLButtonElement | null) => { const nextBoundary = node?.closest(".agent-panel-root") ?? null; setCollisionBoundary((currentBoundary) => currentBoundary === nextBoundary ? currentBoundary : nextBoundary, ); }, []); const setOpen = (nextOpen: boolean) => { if (controlledOpen === undefined) setUncontrolledOpen(nextOpen); onOpenChange?.(nextOpen); }; const choose = (mode: RealtimeVoiceInputMode, callback: () => void) => { setOpen(false); if (rememberPreference) onRememberPreference?.(mode); callback(); }; return ( {copy.entryButtonLabel}
{setupRequired ? (
) : null}

{setupRequired ? copy.setupTitle : copy.promptTitle}

{setupRequired ? (

{copy.setupDescription}

) : null}
{setupRequired ? ( <> ) : ( <> )}
{!setupRequired ? ( ) : null}
); } export interface RealtimeVoiceModeDockProps { state: RealtimeVoiceModeState; copy: RealtimeVoiceModeCopy; chatVisible: boolean; audioLevels?: RealtimeVoiceAudioLevelStore; onToggleChat: () => void; onEndVoiceMode: () => void; settings?: RealtimeVoiceModeInlineSettings; errorMessage?: string | null; className?: string; } export interface RealtimeVoiceModeSettingOption { value: string; label: string; description?: string; } export interface RealtimeVoiceModeSelectSetting { label: string; value: string; options: readonly RealtimeVoiceModeSettingOption[]; onValueChange: (value: string) => void; disabled?: boolean; } export interface RealtimeVoiceModeInlineSettings { dialogLabel: string; appliesNextConversationNote?: string; microphoneError?: string; microphone: RealtimeVoiceModeSelectSetting; language: RealtimeVoiceModeSelectSetting; intelligence: RealtimeVoiceModeSelectSetting; voiceStyle: RealtimeVoiceModeSelectSetting; } const SILENT_AUDIO_LEVELS = createRealtimeVoiceAudioLevelStore(); const WAVEFORM_WEIGHTS = [0.55, 0.82, 1, 0.82, 0.55]; const AUDIO_ACTIVITY_THRESHOLD = 0.1; const WORKING_GLOW_HOLD_MS = 800; function usePrefersReducedMotion(): boolean { const [reduced, setReduced] = useState( () => typeof window !== "undefined" && Boolean(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches), ); useEffect(() => { if (typeof window === "undefined" || !window.matchMedia) return; const query = window.matchMedia("(prefers-reduced-motion: reduce)"); const update = () => setReduced(query.matches); update(); query.addEventListener?.("change", update); return () => query.removeEventListener?.("change", update); }, []); return reduced; } function VoiceWaveform({ level, reducedMotion, activity, }: { level: number; reducedMotion: boolean; activity: "idle" | "user" | "assistant"; }) { const visibleLevel = reducedMotion ? 0.45 : level; return (