import { FC, ReactNode } from 'react'; import { Room, RoomOptions } from 'livekit-client'; import { StyleProp, ViewStyle } from 'react-native'; /** * Theme types for the widget. * Covers both widget-level theming and per-component theming. */ type ButtonStyle = "rounded" | "pill" | "square"; type ButtonSize = "sm" | "md" | "lg"; /** * Widget-level theme — applies to the entire widget chrome * (header, messages, buttons, trigger FAB, etc.) */ interface WidgetTheme { /** Main accent color (buttons, selected states). Supports CSS gradients. */ primaryColor?: string; /** Text color on primary-colored elements */ primaryTextColor?: string; /** Widget background */ backgroundColor?: string; /** Cards, inputs background */ surfaceColor?: string; /** Agent message bubble background — empty string renders agent messages bubble-less. Supports CSS gradients. */ agentBubbleColor?: string; /** Main text color */ textColor?: string; /** Secondary/muted text color */ mutedTextColor?: string; /** Border color */ borderColor?: string; /** Error state color */ errorColor?: string; /** Success state color */ successColor?: string; /** Button corner style */ buttonStyle?: ButtonStyle; /** Global border radius (e.g., "12px", "20px") */ borderRadius?: string; /** Font family */ fontFamily?: string; } /** * Component-level theme — applies to interactive components * (calendar, form, buttons, confirmation). Extends widget theme with * component-specific properties; component values win over widget values. */ interface ComponentTheme { primaryColor?: string; primaryTextColor?: string; backgroundColor?: string; surfaceColor?: string; textColor?: string; mutedTextColor?: string; borderColor?: string; errorColor?: string; successColor?: string; fontFamily?: string; fontSize?: string; borderRadius?: string; padding?: string; buttonStyle?: ButtonStyle; buttonSize?: ButtonSize; } /** Fully resolved widget theme — all fields guaranteed present */ type ResolvedWidgetTheme = Required; /** * Slash command types — commands available during text chat. * * `tool_type` is an open union: the known values get literal types, but the * backend can introduce new tool types without a core release breaking on * them. Domain-specific tools (e.g. client-pack widgets) register handlers * via the ComponentStore registry rather than adding literals here. */ type SlashCommandToolType = "CALLBACK_SCHEDULE_TOOL" | "WIDGET_TOOL" | (string & {}); interface SlashCommand { tool_type: SlashCommandToolType; command: string; display_name: string; description: string; } /** * Widget configuration types. * These define the public API surface for consumers initializing the widget. */ type MediaType = "audio" | "text"; type EnvironmentMode = "dev" | "stage" | "prod"; type TriggerPlacement = "fixed" | "absolute"; type TriggerAlignment = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right"; type WidgetAlignment = "bottom-right" | "bottom-center" | "bottom-left"; interface WidgetPositionConfig { triggerPlacement?: TriggerPlacement; triggerAlignment?: TriggerAlignment; hideTriggerOnExpand?: boolean; widgetAlignment?: WidgetAlignment; } interface LegalDisclaimerConfig { text: string; links?: Record; } /** Styles that are safe to pass cross-platform (subset of CSS properties that map to RN) */ interface PortableStyles { height?: string | number; width?: string | number; backgroundColor?: string; padding?: string | number; borderRadius?: string | number; } interface ButtonIconConfig { size?: string | number; url?: string; } interface ButtonsConfig { mic?: { styles?: PortableStyles; }; modalTrigger?: { styles?: PortableStyles; icon?: ButtonIconConfig; }; text?: { textBeforeCall?: string; textDuringCall?: string; styles?: PortableStyles; }; call?: { textBeforeCall?: string; textDuringCall?: string; styles?: PortableStyles; }; } interface FeedbackScreenConfig { title?: string; description?: string; starsCount?: number; starsStyles?: { filledColor?: string; emptyColor?: string; }; placeholder?: string; submitBtnCTA?: string; submitBtnStyles?: PortableStyles; } interface InnerWindowConfig { width?: string | number; height?: string | number; borderRadius?: string | number; } /** Voice-call view options (audio mode only). */ interface VoiceCallConfig { showAnimation?: boolean; showTranscript?: boolean; } /** * Inline chat pills for DOM action / system events. * Off by default; set `enabled: true` to show pills in the chat thread. * `showIds: true` appends the action id alongside the meta. */ interface EventLogsConfig { enabled?: boolean; showIds?: boolean; } /** * Custom variables passed to the agent. Values are forwarded verbatim as * `custom_args_values`, so the full JSON value space the backend accepts is * allowed (source contract: agents-cdn global.d.ts). */ type WidgetVariables = Record>; /** * The main configuration object consumers pass to initialize the widget. * Used by both web ( / loadAgent) and React Native * () entry points. */ interface RinggWidgetConfig { /** Agent identifier — required */ agentId: string; /** API key for authentication (X-API-KEY header) */ xApiKey?: string; /** JWT for authentication (Authorization header) — wins over xApiKey when both are set */ authorization?: string; /** * `Origin` to present to the backend, which allow-lists an agent's callers * by that value. Browsers send it themselves, so web leaves this unset and * MUST NOT set it — `Origin` is a forbidden header there. Native callers * send nothing on their own and have to supply their own identity, by * convention `://` (see @ringg/react-native's * `appOrigin`). Whatever string is given is sent verbatim; only the backend * decides whether it is allowed. */ clientOrigin?: string; /** Custom variables passed to the agent (e.g., user name, role) */ variables?: WidgetVariables; /** Environment mode — selects which UrlResolver entry to use */ mode?: EnvironmentMode; /** Widget title displayed in the header */ title?: string; /** Widget description displayed in the header */ description?: string; /** Default communication tab */ defaultTab?: MediaType; /** Whether to hide the audio/text tab selector */ hideTabSelector?: boolean; /** Start expanded instead of as a trigger button */ defaultExpanded?: boolean; /** Skip the start screen — trigger click starts the call directly */ bypassStartScreen?: boolean; /** Skip the post-call feedback screen */ bypassFeedbackScreen?: boolean; /** Custom logo URL for the widget header */ logoUrl?: string; /** Styles for the logo element */ logoStyles?: PortableStyles; /** Slash commands available from config (merged with runtime ones) */ enabledSlashCommands?: SlashCommand[]; /** App-wide theming */ theme?: WidgetTheme; /** Widget positioning (web-only, ignored on RN) */ widgetPosition?: WidgetPositionConfig; /** Legal disclaimer shown before call start */ legalDisclaimer?: LegalDisclaimerConfig; /** Button customizations */ buttons?: ButtonsConfig; /** Post-call feedback screen customizations */ feedbackScreen?: FeedbackScreenConfig; /** Widget window dimensions (web-only) */ innerWindowProps?: InnerWindowConfig; /** Voice-call view options */ voiceCall?: VoiceCallConfig; /** URL for the notification sound */ notificationTuneUrl?: string; /** Words rotated inside the typing indicator */ typingWords?: string[]; /** Inline chat pills for DOM action / system events (off by default) */ eventLogs?: EventLogsConfig; } /** * Interactive component types pushed from the backend during a call. * These define the data contracts for calendar, form, buttons, confirmation, * flows, and Block Kit. Domain-specific components (e.g. policy finder) are * NOT defined here — client packs register them via the component registry. */ interface BaseComponent { component_type: string; component_id: string; data: unknown; theme?: ComponentTheme; } interface CalendarSlot { id: string; datetime: string; } interface CalendarBookingData { available_slots: CalendarSlot[]; timezone: string; title: string; } interface CalendarBookingPayload extends BaseComponent { component_type: "calendar_booking"; data: CalendarBookingData; } type FormFieldType = "text" | "email" | "tel" | "number" | "select" | "multiselect" | "textarea" | "date" | "boolean"; interface FormFieldOption { value: string; label: string; } interface FormField { name: string; type: FormFieldType; label: string; placeholder?: string; required?: boolean; options?: string[] | FormFieldOption[]; validation?: { pattern?: string; minLength?: number; maxLength?: number; message?: string; }; } interface FormData { title: string; description?: string; fields: FormField[]; submit_label?: string; } interface FormPayload extends BaseComponent { component_type: "form"; data: FormData; } interface ButtonAction { type: "navigate" | "trigger_component" | "api_call"; url?: string; component_type?: string; method?: "POST" | "GET"; endpoint?: string; payload?: Record; } interface ButtonItem { id: string; label: string; style?: "primary" | "secondary" | "outline" | "destructive"; action: ButtonAction; } interface ButtonsData { title?: string; /** "free" renders bare chips attached to the previous agent message (quick replies); default is a boxed card. */ presentation?: "boxed" | "free"; /** What a completed pick leaves behind: a confirmation card (default) or the selection echoed as a user bubble. */ completionDisplay?: "confirmation" | "selected_item"; buttons: ButtonItem[]; } interface ButtonsPayload extends BaseComponent { component_type: "buttons"; data: ButtonsData; } interface ConfirmationData { title: string; message: string; icon?: "success" | "info" | "warning" | "error"; details?: Array<{ label: string; value: string; }>; } interface ConfirmationPayload extends BaseComponent { component_type: "confirmation"; data: ConfirmationData; } interface FlowStep { id: string; title: string; component: SimpleComponentPayload; } interface InteractiveFlowData { title?: string; steps: FlowStep[]; on_complete: { method: "POST" | "GET"; endpoint: string; payload: Record; }; } interface InteractiveFlowPayload extends BaseComponent { component_type: "interactive_flow"; data: InteractiveFlowData; } /** * Block Kit arrives over the `ringg.blocks` text stream (chunked, so no RPC * size cap). The block tree itself is rendered by the platform UI; core * treats the blocks as opaque and only carries the envelope. */ interface BlocksData { blocks: unknown[]; tool_id?: string; [key: string]: unknown; } interface BlocksPayload extends BaseComponent { component_type: "blocks"; data: BlocksData; } type SimpleComponentPayload = CalendarBookingPayload | FormPayload | ButtonsPayload | ConfirmationPayload; type ComponentPayload = SimpleComponentPayload | InteractiveFlowPayload | BlocksPayload; /** * Client-pack components core doesn't model (e.g. `disease_data`). Packs * commit them to the timeline via `controller.addLocalComponent`; the platform * UI owns their rendering. */ interface CustomComponentPayload { component_type: string; component_id: string; [key: string]: unknown; } interface ComponentActionResponse { success: boolean; message: string; next_step?: SimpleComponentPayload; confirmation?: ConfirmationData; } /** * Chat message types — the normalized message format used across platforms. */ interface ChatMessage { name: string; message: string; isSelf: boolean; /** Epoch ms; may carry a sub-ms fraction used only for stable ordering */ timestamp: number; /** Reference URL attached to an agent reply (RAG source link) */ sourceUrl?: string; /** When present, this message renders a component instead of text */ componentType?: string; componentData?: ComponentPayload | CustomComponentPayload; /** "system" renders an inline event pill (DOM action fired, etc.) instead of a bubble */ kind?: "system"; /** Secondary text shown next to a system pill's label (e.g. the event name) */ systemMeta?: string; systemLevel?: "info" | "error"; } interface ErrorState { hasError: boolean; message: string; } /** * Widget event types — dispatched to the host application. * These provide lifecycle hooks for the embedding app to react to widget * state changes. Wire names are the `ringg:` prefixed kebab/snake names * that existing integrations already listen for. */ type WidgetEventName = "ringg:widget_status" | "ringg:conversation_status" | "ringg:feedback_status" | "ringg:calendar_booking" | "ringg:component_acknowledgement"; interface WidgetStatusPayload { status: "maximised" | "minimised"; mode: MediaType; } interface ConversationStatusPayload { status: "started" | "ended"; mode: MediaType; callId: string; } interface FeedbackStatusPayload { status: "submitted" | "skipped"; callId: string; rating?: number; } interface CalendarBookingEventPayload { status: "shown" | "confirmed" | "failed"; componentId: string; slotId?: string; message?: string; } interface ComponentAcknowledgementPayload { componentName: string; componentId: string; status: string; } interface WidgetEventMap { "ringg:widget_status": WidgetStatusPayload; "ringg:conversation_status": ConversationStatusPayload; "ringg:feedback_status": FeedbackStatusPayload; "ringg:calendar_booking": CalendarBookingEventPayload; "ringg:component_acknowledgement": ComponentAcknowledgementPayload; } /** * Host-page DOM action contract (`execute_dom_action` RPC payloads). * * Core normalizes the backend wire shape ({ action_id, event_name, detail } or * the internal { id, kind, event_name, default_payload }) into `DomAction` and * hands it to the platform's `onDomAction` port together with a logger that * feeds the eventLogs chat pills. Only web can actually execute these — other * platforms leave the port unset and the action is acked and dropped. */ type DomActionKind = "trigger_event"; interface DomAction { id: string; kind: DomActionKind; description?: string; event_name?: string; default_payload?: Record; } interface DomActionLogEntry { id?: string; label: string; meta?: string; level: "info" | "error"; } type DomActionLogger = (entry: DomActionLogEntry) => void; /** * Transport port — the surface core needs from a realtime SDK. * * Web injects an adapter over `livekit-client`; React Native injects one over * `@livekit/react-native`. Core NEVER imports a LiveKit SDK directly — this * interface is the only coupling point, mirroring the room APIs the widget * actually uses (connect/prewarm, mic, RPC methods, text streams, * transcription, connection state). */ type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting"; interface TranscriptionSegment { /** Stable segment id — updates stream in for the same id as STT refines */ id: string; text: string; final: boolean; /** True when the segment belongs to the local participant */ isLocal: boolean; /** Display name of the speaking participant, when known */ participantName?: string; /** Epoch ms the segment was first received */ receivedAt: number; } interface IncomingChatMessage { message: string; /** True when sent by the local participant */ isSelf: boolean; /** True when sent by the agent participant */ isAgent: boolean; senderName?: string; timestamp: number; } interface RpcInvocation { method: string; payload: string; callerIdentity: string; } /** Incoming text stream (e.g. `lk.transcription` replies, `ringg.blocks`) */ interface IncomingTextStream { /** Resolves with the full text once the stream completes */ readAll(): Promise; participantIdentity: string; /** True when the stream was opened by the agent participant */ isAgent: boolean; } interface TransportAdapter { connect(url: string, token: string): Promise; disconnect(): Promise; /** Best-effort prewarm (DNS/TLS/region resolution); failures must not throw */ prepareConnection(url: string): Promise; setMicrophoneEnabled(enabled: boolean): Promise; /** Send a chat message to the room. Core commits the local echo itself, so * adapters must NOT surface locally-sent messages via onChatMessage. */ sendChatMessage(text: string): Promise; /** Invoke an RPC on the agent participant. Rejects if no agent is present. */ performRpcToAgent(method: string, payload: string): Promise; /** Handler return value is sent back as the RPC response payload. */ registerRpcMethod(method: string, handler: (invocation: RpcInvocation) => Promise): void; unregisterRpcMethod(method: string): void; registerTextStreamHandler(topic: string, handler: (stream: IncomingTextStream) => void): void; unregisterTextStreamHandler(topic: string): void; onConnectionStateChange(handler: (state: ConnectionState) => void): () => void; /** Fires when the room disconnects or the agent participant leaves */ onSessionEnded(handler: () => void): () => void; onTranscription(handler: (segment: TranscriptionSegment) => void): () => void; onChatMessage(handler: (message: IncomingChatMessage) => void): () => void; /** * Fires when acquiring a media device fails after connect (LiveKit's * `MediaDevicesError`) — e.g. the mic permission is revoked mid-call. Core * surfaces the production error message on it. */ onMediaDevicesError(handler: () => void): () => void; } /** * Event bus port — abstraction over platform-specific host-event dispatch. * * Web: DOM CustomEvents on `window` (existing integrations listen for these) * React Native: callback props / in-memory listeners */ interface EventBus { emit(event: K, payload: WidgetEventMap[K]): void; /** Subscribe to a typed event. Returns an unsubscribe function. */ on(event: K, handler: (payload: WidgetEventMap[K]) => void): () => void; /** Remove all listeners for one event, or all events if omitted. */ off(event?: WidgetEventName): void; } /** In-memory event bus — for React Native or testing. */ declare function createCallbackEventBus(): EventBus; /** * Clock port — injectable time and timers. * * The widget's UX-critical logic is timing logic (typing-indicator minimum * duration, chat-widget grace buffering). Routing all time access through * this port makes that logic deterministic under test. */ type TimerHandle = ReturnType; interface Clock { /** Monotonic-enough milliseconds (wall clock is fine outside tests) */ now(): number; setTimeout(fn: () => void, ms: number): TimerHandle; clearTimeout(handle: TimerHandle): void; } /** * Small platform ports — capabilities core needs but cannot implement * portably. Web and React Native each provide implementations. */ /** Microphone permission — navigator.permissions on web, RN permissions API on native */ interface MicPermissionPort { isGranted(): Promise; /** Prompt the user; resolves with the final grant state */ request(): Promise; } /** New-message notification sound — HTMLAudioElement on web, a sound lib on RN */ interface NotificationPlayer { /** Best-effort playback; must never throw (autoplay policies etc.) */ play(): void; } /** * API client for the Ringg backend. * Pure fetch-based — works in any JS runtime (browser, Node, React Native). * URLs are always injected by the platform entry point via a UrlResolver; * core embeds no endpoints. */ interface EnvironmentUrls { backendUrl: string; livekitUrl: string; } interface UrlResolver { resolve(mode: EnvironmentMode): EnvironmentUrls; } /** * RPC message formatting — transforms between backend RPC format and internal * component format. * * The backend sends `send_dynamic_data` RPCs like: * { component_type: "render_component_calendar", component_config: { ... } } * The widget needs them in the internal format: * { component_type: "calendar_booking", component_id: "calendar_1234", data: { ... } } * * This module handles both directions. */ /** A Block Kit action as it goes on the wire (the UI-only `label` never does). */ interface BlocksActionWire { action_id: string; value?: unknown; values: Record; } /** * Base store — the one state-sharing primitive in core. * * Platform bindings stay thin because they all consume the same shape: * web (Lit): subscribe(() => this.host.requestUpdate()) * RN (React): useSyncExternalStore(store.subscribe, store.getSnapshot) * * Snapshots must be immutable values — stores rebuild them on change so * reference equality is a valid dirty check. `getSnapshot`/`subscribe` are * closures, so they stay bound when bindings pass them unbound (React: * `useSyncExternalStore(store.subscribe, store.getSnapshot)`). */ interface Store { getSnapshot(): T; subscribe(listener: (snapshot: T) => void): () => void; } /** * Shell store — widget open/close/feedback lifecycle and the active call id. * Ported from the alpha WidgetStateController; emits `ringg:widget_status` * host events on visibility transitions. */ type WidgetViewState = "closed" | "open" | "feedback"; interface ShellSnapshot { viewState: WidgetViewState; currentCallId: string | null; callMode: MediaType; } interface ShellStore extends Store { readonly isOpen: boolean; /** * Feedback is pending (post-call screen not yet submitted/skipped). Mirrors * the production hook's independent `showFeedback` flag: stays true across * minimize so reopening restores the feedback view. The derived snapshot * `viewState` only reads "feedback" while the panel is actually open. */ readonly isShowingFeedback: boolean; open(): void; minimize(): void; /** Forget the call, clear feedback and minimize — a conversation fully ended. */ close(): void; toggle(): void; showFeedback(): void; setCallMode(mode: MediaType): void; setCurrentCallId(callId: string | null): void; dispose(): void; } /** * Session store — the call lifecycle state machine. * * idle → starting → live-optimistic → connected → idle * * "live-optimistic" is load-bearing UX (lifted from production): the flag * flips the moment /calling/webcall succeeds — before transport.connect() * resolves. The call exists server-side at that point, so the calling layout * mounts one HTTP round trip after Start instead of waiting out the LiveKit * handshake. A connect failure rolls it back so the start screen (with the * error) returns instead of a dead calling layout. */ type SessionPhase = "idle" | "starting" | "live-optimistic" | "connected"; interface SessionSnapshot { phase: SessionPhase; /** True while a start/end request is in flight */ isLoading: boolean; /** UI gate for the calling layout — optimistic OR actually connected */ isSessionLive: boolean; connectionState: ConnectionState; error: ErrorState; } interface SessionStartResult { callId: string; slashCommands: SlashCommand[]; } interface SessionStore extends Store { readonly isSessionLive: boolean; /** Warm DNS/TLS/region on widget open — best-effort, never blocks connect. */ prewarm(): void; /** * Start a call. Resolves with the call id + runtime slash commands on * success; resolves null when blocked (mic denied) or failed (error is in * the snapshot). The caller (controller) resets conversation stores first. */ start(params: { agentId: string; variables: WidgetVariables; mediaType: MediaType; }): Promise; /** Disconnect the transport and return to idle. Never throws. */ end(): Promise; clearError(): void; dispose(): void; } /** * Message store — the single ordered conversation timeline. * * Three sources feed it and must interleave correctly: * - chat messages (text mode, incl. the local optimistic echo) * - transcription segments (audio mode; segments UPDATE in place by id as STT * refines, keeping their original position) * - component messages (RPC widgets, Block Kit — commit-time stamped so a * buffered widget sorts strictly after the agent message that released it) * * Ordering: entries sort by timestamp. The store hands out strictly * monotonically increasing timestamps (sub-ms bumped when the clock hasn't * advanced), replacing the production widget's `Date.now() + Math.random()` * collision-avoidance hacks with a deterministic policy. */ interface MessageSnapshot { messages: readonly ChatMessage[]; } interface MessageStore extends Store { /** Next timestamp, strictly greater than any previously issued one. */ nextTimestamp(): number; /** Commit a chat message. Agent messages absorb any pending source URL. */ addChatMessage(params: { message: string; isSelf: boolean; name?: string; timestamp?: number; }): ChatMessage; /** Commit a component message (commit-time stamped — see module docs). */ addComponent(component: ComponentPayload | CustomComponentPayload): ChatMessage; /** Commit an inline system-event pill (DOM action fired, etc.). */ addSystemLog(params: { label: string; meta?: string; level: "info" | "error"; }): void; /** * Render the user's quick-reply selection as a chat bubble anchored just * after the prompting component — even if a follow-up agent message has * already arrived. */ addLocalSelection(componentId: string, label: string): void; /** * Upsert a transcription segment (audio mode). Position is stable by id. * When a source URL is pending, it attaches to the newest transcription if * that is an agent utterance without one; otherwise it is dropped * (production clears the ref on any update that doesn't append). */ upsertTranscription(segment: TranscriptionSegment): void; /** Stash a RAG source URL; the next agent reply carries it. */ setPendingSourceUrl(url: string): void; /** Drop a stashed source URL without attaching it. */ clearPendingSourceUrl(): void; reset(): void; dispose(): void; } /** * Typing store — drives the "agent is typing" indicator (text mode). * * Behavior lifted from the production widget: * - Dots appear the moment the user sends (we always expect a reply); the * min-duration timer anchors only when they first appear, so back-to-back * sends keep one continuous indicator instead of resetting it. * - Any landed backend response — text or RPC — answers the pending turn and * drops the dots, unconditionally. * - Replies that land faster than MIN_TYPING_INDICATOR_MS wait out the * remainder before committing, otherwise the indicator just flashes. */ interface TypingSnapshot { isTyping: boolean; } interface TypingStore extends Store { /** User sent a message — show the dots (idempotent while already shown). */ showOnUserSend(): void; /** A backend response landed (or the turn errored) — drop the dots now. */ clear(): void; /** * Resolve once the indicator has been visible for MIN_TYPING_INDICATOR_MS. * Callers await this before committing a fast agent reply. * Resolves immediately when the dots aren't showing. */ waitMinDuration(): Promise; dispose(): void; } /** * Component store — interactive components pushed by the backend mid-call. * * Chat-mode anchoring (lifted from production): widget RPCs land instantly * while the agent's text streams a beat later. In text mode we buffer the * widget until the accompanying message lands, then commit it right after so * it sorts below. If no message arrives within CHAT_WIDGET_GRACE_MS, flush * anyway. Audio mode commits immediately. * * Also owns the response side: which components still await a user response * (and the original config needed to format it), completed flows, and the * extension registry that lets client packs handle domain-specific * `send_dynamic_data` component types without polluting core. */ interface ComponentSnapshot { completedFlowIds: ReadonlySet; } interface PendingResponse { name: string; originalConfig: unknown; } /** * Extension hook for domain-specific dynamic-data payloads (client packs). * Return true when the payload was handled; false lets core continue with * its built-in handling. */ type DynamicDataExtension = (componentName: string, parsed: Record) => boolean; interface ComponentStore extends Store { /** Queue or commit a component depending on call mode (see module docs). */ add(component: ComponentPayload, mode: MediaType): void; /** The agent's text landed — release buffered widgets right after it. */ onAgentTextLanded(): void; flush(): void; trackPendingResponse(componentId: string, pending: PendingResponse): void; /** Consume the pending-response record for a component (one-shot). */ takePendingResponse(componentId: string): PendingResponse | undefined; markFlowComplete(componentId: string): void; /** Register a client-pack handler. Returns an unregister function. */ registerExtension(extension: DynamicDataExtension): () => void; /** Offer a payload to extensions; true when one handled it. */ runExtensions(componentName: string, parsed: Record): boolean; /** Drop buffered widgets + timers so nothing fires after the call ended. */ reset(): void; dispose(): void; } /** * Slash command store — merges commands declared in config with commands the * backend enables at webcall time. Config commands win on collisions. */ interface SlashCommandSnapshot { commands: readonly SlashCommand[]; } interface SlashCommandStore extends Store { /** Commands from the webcall response */ setRuntimeCommands(commands: SlashCommand[]): void; reset(): void; dispose(): void; } /** * RinggWidgetController — the composition root of the widget brain. * * Owns every store, wires transport events into them, and exposes the * imperative API the platform UIs call. This is the headless replacement for * the production widget's App.tsx orchestration: platform bindings render * snapshots and forward user intents; ALL behavior lives here or in the * stores. */ interface ControllerPorts { transport: TransportAdapter; urlResolver: UrlResolver; eventBus?: EventBus; clock?: Clock; micPermission?: MicPermissionPort; notification?: NotificationPlayer; /** * Host-page action executor (web-only: `execute_dom_action` payloads). * Core normalizes the wire payload into a `DomAction` and provides the * eventLogs-gated system-log sink. Platforms that can't execute host * actions leave this unset; payloads are acked and dropped. */ onDomAction?: (action: DomAction, log: DomActionLogger) => void; } interface RinggWidgetController { readonly config: RinggWidgetConfig; readonly theme: ResolvedWidgetTheme; readonly eventBus: EventBus; readonly shell: ShellStore; readonly session: SessionStore; readonly messages: MessageStore; readonly typing: TypingStore; readonly components: ComponentStore; readonly slashCommands: SlashCommandStore; openWidget(): void; minimizeWidget(): void; /** Trigger click: toggles, or starts directly with bypassStartScreen. */ handleTriggerClick(): void; startCall(mediaType: MediaType): Promise; endCall(): Promise; sendMessage(text: string): Promise; /** Send the user's component response (slot pick, form submit, button tap). */ sendComponentResponse(componentId: string, responseData: Record): Promise; /** * Block Kit action → agent (`receive_component_blocks` envelope over * `receive_dynamic_data`). Resolves only after the RPC round-trips — the UI * ties its in-flight spinner to this Promise. Transport failures are * swallowed (production logs and moves on), so it never rejects. */ sendBlocksAction(toolId: string | undefined, componentId: string, action: BlocksActionWire): Promise; /** * Component API for interactive-flow `on_complete` / `api_call` button * actions. Relative endpoints resolve against the backend URL; auth headers * match the widget's other backend calls. */ callComponentApi(method: "POST" | "GET", endpoint: string, payload: Record): Promise; /** * Raw dynamic-data RPC for client packs (e.g. disease search/submit) — the * payload carries its own `component_type`. Rejects on transport failure so * packs can implement their own fallbacks. */ sendDynamicData(payload: Record): Promise; /** * Commit a pack-owned component to the timeline immediately (parity with * production's local widget messages — no buffering, no shell side effects). */ addLocalComponent(component: CustomComponentPayload): void; /** Show the user's quick-reply selection as a chat bubble. */ displaySelection(componentId: string, label: string): void; markFlowComplete(componentId: string): void; /** Client-pack hook for domain-specific dynamic-data payloads. */ registerDynamicDataExtension(extension: DynamicDataExtension): () => void; sendSlashCommand(command: SlashCommand): Promise; submitFeedback(rating: number, comment: string): Promise; skipFeedback(): void; destroy(): void; } declare function createRinggWidgetController(config: RinggWidgetConfig, ports: ControllerPorts): RinggWidgetController; /** * React bindings — shared by @ringg/web (React DOM) and @ringg/react-native. * * Every hook is a useSyncExternalStore call over a core store; there is no * behavior here and there must never be. A fix to typing timing, message * ordering, or session lifecycle lands in the stores once and both platforms * pick it up. * * Imported via the "@ringg/core/react" subpath so non-React consumers of * core never touch the react dependency (it's an optional peer). */ declare function useStoreSnapshot(store: Store): T; declare const useRinggMessages: (controller: RinggWidgetController) => MessageSnapshot; declare const useRinggTyping: (controller: RinggWidgetController) => TypingSnapshot; declare const useRinggSession: (controller: RinggWidgetController) => SessionSnapshot; declare const useRinggShell: (controller: RinggWidgetController) => ShellSnapshot; declare const useRinggComponents: (controller: RinggWidgetController) => ComponentSnapshot; declare const useRinggSlashCommands: (controller: RinggWidgetController) => SlashCommandSnapshot; /** * RinggWidget — the assembled React Native widget. * * The native counterpart of the web assembly: the same tree, the same order, * the same branches. It holds NO conversation state — every snapshot comes * from `@ringg/core` via the shared hooks and every intent is forwarded to the * controller. If a behaviour looks like it belongs here, it belongs in core. * * Three things genuinely differ from web, all forced by the platform: * * 1. There is no `position: fixed`. The widget is an absolutely-filled * overlay with `pointerEvents="box-none"`, so taps pass through everywhere * except the trigger and the panel. Mount it as the LAST child of the app * root; `widgetPosition` (a web-only config) is ignored and the panel * sizes itself to the device. * 2. The keyboard covers the bottom of the screen. The panel lives inside a * `KeyboardAvoidingView` so the composer stays visible while typing — * without it the input is simply unreachable in text mode. * 3. The transcript cannot measure a child's offset the way web does to * anchor the newest turn's first line. It pins to the bottom instead, and * the "new message" / "typing toggled" distinction is preserved so a long * reply arriving does not yank the view while the user is reading. */ interface RinggWidgetProps { controller: RinggWidgetController; /** * The LiveKit room from `createLiveKitTransport()`. Optional: it only powers * the in-call audio visualizer and the mute button. Without it the widget is * fully functional and the visualizer shows its ambient idle animation. */ room?: Room; } declare const RinggWidget: FC; /** * LiveKit React Native transport — the real `TransportAdapter` over * `@livekit/react-native`, behavior-matched to the web adapter (which is * itself matched to production, @desivocal/agents-cdn). * * The wire behavior is deliberately identical to `web/src/transport/ * livekit-transport.ts` — same Room options, same double-send chat, same * agent-classification and session-ended rules — because both talk to the same * backend and core drives them through the same port. Read that file's parity * notes; they apply here verbatim. * * Only what the platform forces differs: * - remote audio needs no elements. Mobile plays subscribed audio through the * OS, so ``'s job becomes owning an audio SESSION * (see platform/audio-session.ts) rather than attaching media elements; * - `registerGlobals()` must run before a Room is constructed — the adapter * does it so a missing app-entry call is not a mysterious runtime failure; * - autoplay policy has no mobile equivalent, so the web adapter's * `startAudio()` recovery has no counterpart. */ interface LiveKitTransportOptions { /** * Extra LiveKit `RoomOptions` merged over the parity defaults * (`dynacast: true, adaptiveStream: true`). Rarely needed. */ roomOptions?: RoomOptions; /** * Own the native audio session for the duration of a call — the mobile * equivalent of production's ``. Default true; disable * when the host app already manages an audio session (an in-app player, a * CallKit/ConnectionService integration). */ manageAudioSession?: boolean; /** * Call `registerGlobals()` before constructing the Room. Default true. * Set false when the app already calls it at its entry point — it is * idempotent, so this is a formality rather than a correctness switch. */ registerGlobals?: boolean; } interface LiveKitTransport { transport: TransportAdapter; /** * The underlying Room, for presentational concerns the port cannot express * (visualizer track handles, `isMicrophoneEnabled` readback — spec §11). * State-changing calls must keep going through the transport/core. */ room: Room; /** Tears down listeners, handlers and the audio session; disconnects the room. */ dispose(): void; } declare const createLiveKitTransport: (options?: LiveKitTransportOptions) => LiveKitTransport; /** * Native audio session — React Native's replacement for the web adapter's * hidden `