@ringg/react-native 0.0.1-alpha.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +237 -197
  2. package/dist/index.d.mts +1355 -0
  3. package/dist/index.d.ts +1355 -0
  4. package/dist/index.js +5765 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +5705 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +35 -65
  9. package/LICENSE +0 -21
  10. package/app.plugin.js +0 -51
  11. package/lib/module/adapters/index.js +0 -4
  12. package/lib/module/adapters/index.js.map +0 -1
  13. package/lib/module/adapters/livekit-rn-adapter.js +0 -216
  14. package/lib/module/adapters/livekit-rn-adapter.js.map +0 -1
  15. package/lib/module/components/RinggWidget.js +0 -554
  16. package/lib/module/components/RinggWidget.js.map +0 -1
  17. package/lib/module/components/steps/ButtonsStep.js +0 -146
  18. package/lib/module/components/steps/ButtonsStep.js.map +0 -1
  19. package/lib/module/components/steps/CalendarStep.js +0 -190
  20. package/lib/module/components/steps/CalendarStep.js.map +0 -1
  21. package/lib/module/components/steps/ConfirmationStep.js +0 -175
  22. package/lib/module/components/steps/ConfirmationStep.js.map +0 -1
  23. package/lib/module/components/steps/FormStep.js +0 -346
  24. package/lib/module/components/steps/FormStep.js.map +0 -1
  25. package/lib/module/components/steps/InteractiveFlow.js +0 -249
  26. package/lib/module/components/steps/InteractiveFlow.js.map +0 -1
  27. package/lib/module/components/ui/ActionButton.js +0 -92
  28. package/lib/module/components/ui/ActionButton.js.map +0 -1
  29. package/lib/module/components/ui/CallControls.js +0 -168
  30. package/lib/module/components/ui/CallControls.js.map +0 -1
  31. package/lib/module/components/ui/FeedbackScreen.js +0 -180
  32. package/lib/module/components/ui/FeedbackScreen.js.map +0 -1
  33. package/lib/module/components/ui/Icons.js +0 -294
  34. package/lib/module/components/ui/Icons.js.map +0 -1
  35. package/lib/module/components/ui/MessageBubble.js +0 -80
  36. package/lib/module/components/ui/MessageBubble.js.map +0 -1
  37. package/lib/module/components/ui/TabSelector.js +0 -80
  38. package/lib/module/components/ui/TabSelector.js.map +0 -1
  39. package/lib/module/components/ui/WidgetHeader.js +0 -168
  40. package/lib/module/components/ui/WidgetHeader.js.map +0 -1
  41. package/lib/module/context/RinggProvider.js +0 -134
  42. package/lib/module/context/RinggProvider.js.map +0 -1
  43. package/lib/module/context/index.js +0 -4
  44. package/lib/module/context/index.js.map +0 -1
  45. package/lib/module/globals.d.js +0 -2
  46. package/lib/module/globals.d.js.map +0 -1
  47. package/lib/module/hooks/index.js +0 -10
  48. package/lib/module/hooks/index.js.map +0 -1
  49. package/lib/module/hooks/useRinggCall.js +0 -259
  50. package/lib/module/hooks/useRinggCall.js.map +0 -1
  51. package/lib/module/hooks/useRinggChat.js +0 -141
  52. package/lib/module/hooks/useRinggChat.js.map +0 -1
  53. package/lib/module/hooks/useRinggComponents.js +0 -236
  54. package/lib/module/hooks/useRinggComponents.js.map +0 -1
  55. package/lib/module/hooks/useRinggFeedback.js +0 -66
  56. package/lib/module/hooks/useRinggFeedback.js.map +0 -1
  57. package/lib/module/hooks/useRinggTheme.js +0 -39
  58. package/lib/module/hooks/useRinggTheme.js.map +0 -1
  59. package/lib/module/hooks/useRinggTranscripts.js +0 -70
  60. package/lib/module/hooks/useRinggTranscripts.js.map +0 -1
  61. package/lib/module/hooks/useRinggWidgetState.js +0 -51
  62. package/lib/module/hooks/useRinggWidgetState.js.map +0 -1
  63. package/lib/module/index.js +0 -56
  64. package/lib/module/index.js.map +0 -1
  65. package/lib/module/package.json +0 -1
  66. package/lib/typescript/adapters/index.d.ts +0 -2
  67. package/lib/typescript/adapters/index.d.ts.map +0 -1
  68. package/lib/typescript/adapters/livekit-rn-adapter.d.ts +0 -38
  69. package/lib/typescript/adapters/livekit-rn-adapter.d.ts.map +0 -1
  70. package/lib/typescript/components/RinggWidget.d.ts +0 -44
  71. package/lib/typescript/components/RinggWidget.d.ts.map +0 -1
  72. package/lib/typescript/components/steps/ButtonsStep.d.ts +0 -17
  73. package/lib/typescript/components/steps/ButtonsStep.d.ts.map +0 -1
  74. package/lib/typescript/components/steps/CalendarStep.d.ts +0 -16
  75. package/lib/typescript/components/steps/CalendarStep.d.ts.map +0 -1
  76. package/lib/typescript/components/steps/ConfirmationStep.d.ts +0 -16
  77. package/lib/typescript/components/steps/ConfirmationStep.d.ts.map +0 -1
  78. package/lib/typescript/components/steps/FormStep.d.ts +0 -17
  79. package/lib/typescript/components/steps/FormStep.d.ts.map +0 -1
  80. package/lib/typescript/components/steps/InteractiveFlow.d.ts +0 -23
  81. package/lib/typescript/components/steps/InteractiveFlow.d.ts.map +0 -1
  82. package/lib/typescript/components/ui/ActionButton.d.ts +0 -22
  83. package/lib/typescript/components/ui/ActionButton.d.ts.map +0 -1
  84. package/lib/typescript/components/ui/CallControls.d.ts +0 -22
  85. package/lib/typescript/components/ui/CallControls.d.ts.map +0 -1
  86. package/lib/typescript/components/ui/FeedbackScreen.d.ts +0 -24
  87. package/lib/typescript/components/ui/FeedbackScreen.d.ts.map +0 -1
  88. package/lib/typescript/components/ui/Icons.d.ts +0 -28
  89. package/lib/typescript/components/ui/Icons.d.ts.map +0 -1
  90. package/lib/typescript/components/ui/MessageBubble.d.ts +0 -17
  91. package/lib/typescript/components/ui/MessageBubble.d.ts.map +0 -1
  92. package/lib/typescript/components/ui/TabSelector.d.ts +0 -21
  93. package/lib/typescript/components/ui/TabSelector.d.ts.map +0 -1
  94. package/lib/typescript/components/ui/WidgetHeader.d.ts +0 -20
  95. package/lib/typescript/components/ui/WidgetHeader.d.ts.map +0 -1
  96. package/lib/typescript/context/RinggProvider.d.ts +0 -53
  97. package/lib/typescript/context/RinggProvider.d.ts.map +0 -1
  98. package/lib/typescript/context/index.d.ts +0 -3
  99. package/lib/typescript/context/index.d.ts.map +0 -1
  100. package/lib/typescript/hooks/index.d.ts +0 -15
  101. package/lib/typescript/hooks/index.d.ts.map +0 -1
  102. package/lib/typescript/hooks/useRinggCall.d.ts +0 -41
  103. package/lib/typescript/hooks/useRinggCall.d.ts.map +0 -1
  104. package/lib/typescript/hooks/useRinggChat.d.ts +0 -29
  105. package/lib/typescript/hooks/useRinggChat.d.ts.map +0 -1
  106. package/lib/typescript/hooks/useRinggComponents.d.ts +0 -52
  107. package/lib/typescript/hooks/useRinggComponents.d.ts.map +0 -1
  108. package/lib/typescript/hooks/useRinggFeedback.d.ts +0 -23
  109. package/lib/typescript/hooks/useRinggFeedback.d.ts.map +0 -1
  110. package/lib/typescript/hooks/useRinggTheme.d.ts +0 -40
  111. package/lib/typescript/hooks/useRinggTheme.d.ts.map +0 -1
  112. package/lib/typescript/hooks/useRinggTranscripts.d.ts +0 -30
  113. package/lib/typescript/hooks/useRinggTranscripts.d.ts.map +0 -1
  114. package/lib/typescript/hooks/useRinggWidgetState.d.ts +0 -38
  115. package/lib/typescript/hooks/useRinggWidgetState.d.ts.map +0 -1
  116. package/lib/typescript/index.d.ts +0 -44
  117. package/lib/typescript/index.d.ts.map +0 -1
  118. package/lib/typescript/package.json +0 -1
@@ -0,0 +1,1355 @@
1
+ import { FC, ReactNode } from 'react';
2
+ import { Room, RoomOptions } from 'livekit-client';
3
+ import { StyleProp, ViewStyle } from 'react-native';
4
+
5
+ /**
6
+ * Theme types for the widget.
7
+ * Covers both widget-level theming and per-component theming.
8
+ */
9
+ type ButtonStyle = "rounded" | "pill" | "square";
10
+ type ButtonSize = "sm" | "md" | "lg";
11
+ /**
12
+ * Widget-level theme — applies to the entire widget chrome
13
+ * (header, messages, buttons, trigger FAB, etc.)
14
+ */
15
+ interface WidgetTheme {
16
+ /** Main accent color (buttons, selected states). Supports CSS gradients. */
17
+ primaryColor?: string;
18
+ /** Text color on primary-colored elements */
19
+ primaryTextColor?: string;
20
+ /** Widget background */
21
+ backgroundColor?: string;
22
+ /** Cards, inputs background */
23
+ surfaceColor?: string;
24
+ /** Agent message bubble background — empty string renders agent messages bubble-less. Supports CSS gradients. */
25
+ agentBubbleColor?: string;
26
+ /** Main text color */
27
+ textColor?: string;
28
+ /** Secondary/muted text color */
29
+ mutedTextColor?: string;
30
+ /** Border color */
31
+ borderColor?: string;
32
+ /** Error state color */
33
+ errorColor?: string;
34
+ /** Success state color */
35
+ successColor?: string;
36
+ /** Button corner style */
37
+ buttonStyle?: ButtonStyle;
38
+ /** Global border radius (e.g., "12px", "20px") */
39
+ borderRadius?: string;
40
+ /** Font family */
41
+ fontFamily?: string;
42
+ }
43
+ /**
44
+ * Component-level theme — applies to interactive components
45
+ * (calendar, form, buttons, confirmation). Extends widget theme with
46
+ * component-specific properties; component values win over widget values.
47
+ */
48
+ interface ComponentTheme {
49
+ primaryColor?: string;
50
+ primaryTextColor?: string;
51
+ backgroundColor?: string;
52
+ surfaceColor?: string;
53
+ textColor?: string;
54
+ mutedTextColor?: string;
55
+ borderColor?: string;
56
+ errorColor?: string;
57
+ successColor?: string;
58
+ fontFamily?: string;
59
+ fontSize?: string;
60
+ borderRadius?: string;
61
+ padding?: string;
62
+ buttonStyle?: ButtonStyle;
63
+ buttonSize?: ButtonSize;
64
+ }
65
+ /** Fully resolved widget theme — all fields guaranteed present */
66
+ type ResolvedWidgetTheme = Required<WidgetTheme>;
67
+
68
+ /**
69
+ * Slash command types — commands available during text chat.
70
+ *
71
+ * `tool_type` is an open union: the known values get literal types, but the
72
+ * backend can introduce new tool types without a core release breaking on
73
+ * them. Domain-specific tools (e.g. client-pack widgets) register handlers
74
+ * via the ComponentStore registry rather than adding literals here.
75
+ */
76
+ type SlashCommandToolType = "CALLBACK_SCHEDULE_TOOL" | "WIDGET_TOOL" | (string & {});
77
+ interface SlashCommand {
78
+ tool_type: SlashCommandToolType;
79
+ command: string;
80
+ display_name: string;
81
+ description: string;
82
+ }
83
+
84
+ /**
85
+ * Widget configuration types.
86
+ * These define the public API surface for consumers initializing the widget.
87
+ */
88
+
89
+ type MediaType = "audio" | "text";
90
+ type EnvironmentMode = "dev" | "stage" | "prod";
91
+ type TriggerPlacement = "fixed" | "absolute";
92
+ type TriggerAlignment = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
93
+ type WidgetAlignment = "bottom-right" | "bottom-center" | "bottom-left";
94
+ interface WidgetPositionConfig {
95
+ triggerPlacement?: TriggerPlacement;
96
+ triggerAlignment?: TriggerAlignment;
97
+ hideTriggerOnExpand?: boolean;
98
+ widgetAlignment?: WidgetAlignment;
99
+ }
100
+ interface LegalDisclaimerConfig {
101
+ text: string;
102
+ links?: Record<string, string>;
103
+ }
104
+ /** Styles that are safe to pass cross-platform (subset of CSS properties that map to RN) */
105
+ interface PortableStyles {
106
+ height?: string | number;
107
+ width?: string | number;
108
+ backgroundColor?: string;
109
+ padding?: string | number;
110
+ borderRadius?: string | number;
111
+ }
112
+ interface ButtonIconConfig {
113
+ size?: string | number;
114
+ url?: string;
115
+ }
116
+ interface ButtonsConfig {
117
+ mic?: {
118
+ styles?: PortableStyles;
119
+ };
120
+ modalTrigger?: {
121
+ styles?: PortableStyles;
122
+ icon?: ButtonIconConfig;
123
+ };
124
+ text?: {
125
+ textBeforeCall?: string;
126
+ textDuringCall?: string;
127
+ styles?: PortableStyles;
128
+ };
129
+ call?: {
130
+ textBeforeCall?: string;
131
+ textDuringCall?: string;
132
+ styles?: PortableStyles;
133
+ };
134
+ }
135
+ interface FeedbackScreenConfig {
136
+ title?: string;
137
+ description?: string;
138
+ starsCount?: number;
139
+ starsStyles?: {
140
+ filledColor?: string;
141
+ emptyColor?: string;
142
+ };
143
+ placeholder?: string;
144
+ submitBtnCTA?: string;
145
+ submitBtnStyles?: PortableStyles;
146
+ }
147
+ interface InnerWindowConfig {
148
+ width?: string | number;
149
+ height?: string | number;
150
+ borderRadius?: string | number;
151
+ }
152
+ /** Voice-call view options (audio mode only). */
153
+ interface VoiceCallConfig {
154
+ showAnimation?: boolean;
155
+ showTranscript?: boolean;
156
+ }
157
+ /**
158
+ * Inline chat pills for DOM action / system events.
159
+ * Off by default; set `enabled: true` to show pills in the chat thread.
160
+ * `showIds: true` appends the action id alongside the meta.
161
+ */
162
+ interface EventLogsConfig {
163
+ enabled?: boolean;
164
+ showIds?: boolean;
165
+ }
166
+ /**
167
+ * Custom variables passed to the agent. Values are forwarded verbatim as
168
+ * `custom_args_values`, so the full JSON value space the backend accepts is
169
+ * allowed (source contract: agents-cdn global.d.ts).
170
+ */
171
+ type WidgetVariables = Record<string, string | number | boolean | object | Array<unknown>>;
172
+ /**
173
+ * The main configuration object consumers pass to initialize the widget.
174
+ * Used by both web (<ringg-widget> / loadAgent) and React Native
175
+ * (<RinggWidget />) entry points.
176
+ */
177
+ interface RinggWidgetConfig {
178
+ /** Agent identifier — required */
179
+ agentId: string;
180
+ /** API key for authentication (X-API-KEY header) */
181
+ xApiKey?: string;
182
+ /** JWT for authentication (Authorization header) — wins over xApiKey when both are set */
183
+ authorization?: string;
184
+ /**
185
+ * `Origin` to present to the backend, which allow-lists an agent's callers
186
+ * by that value. Browsers send it themselves, so web leaves this unset and
187
+ * MUST NOT set it — `Origin` is a forbidden header there. Native callers
188
+ * send nothing on their own and have to supply their own identity, by
189
+ * convention `<platform>://<bundleId>` (see @ringg/react-native's
190
+ * `appOrigin`). Whatever string is given is sent verbatim; only the backend
191
+ * decides whether it is allowed.
192
+ */
193
+ clientOrigin?: string;
194
+ /** Custom variables passed to the agent (e.g., user name, role) */
195
+ variables?: WidgetVariables;
196
+ /** Environment mode — selects which UrlResolver entry to use */
197
+ mode?: EnvironmentMode;
198
+ /** Widget title displayed in the header */
199
+ title?: string;
200
+ /** Widget description displayed in the header */
201
+ description?: string;
202
+ /** Default communication tab */
203
+ defaultTab?: MediaType;
204
+ /** Whether to hide the audio/text tab selector */
205
+ hideTabSelector?: boolean;
206
+ /** Start expanded instead of as a trigger button */
207
+ defaultExpanded?: boolean;
208
+ /** Skip the start screen — trigger click starts the call directly */
209
+ bypassStartScreen?: boolean;
210
+ /** Skip the post-call feedback screen */
211
+ bypassFeedbackScreen?: boolean;
212
+ /** Custom logo URL for the widget header */
213
+ logoUrl?: string;
214
+ /** Styles for the logo element */
215
+ logoStyles?: PortableStyles;
216
+ /** Slash commands available from config (merged with runtime ones) */
217
+ enabledSlashCommands?: SlashCommand[];
218
+ /** App-wide theming */
219
+ theme?: WidgetTheme;
220
+ /** Widget positioning (web-only, ignored on RN) */
221
+ widgetPosition?: WidgetPositionConfig;
222
+ /** Legal disclaimer shown before call start */
223
+ legalDisclaimer?: LegalDisclaimerConfig;
224
+ /** Button customizations */
225
+ buttons?: ButtonsConfig;
226
+ /** Post-call feedback screen customizations */
227
+ feedbackScreen?: FeedbackScreenConfig;
228
+ /** Widget window dimensions (web-only) */
229
+ innerWindowProps?: InnerWindowConfig;
230
+ /** Voice-call view options */
231
+ voiceCall?: VoiceCallConfig;
232
+ /** URL for the notification sound */
233
+ notificationTuneUrl?: string;
234
+ /** Words rotated inside the typing indicator */
235
+ typingWords?: string[];
236
+ /** Inline chat pills for DOM action / system events (off by default) */
237
+ eventLogs?: EventLogsConfig;
238
+ }
239
+
240
+ /**
241
+ * Interactive component types pushed from the backend during a call.
242
+ * These define the data contracts for calendar, form, buttons, confirmation,
243
+ * flows, and Block Kit. Domain-specific components (e.g. policy finder) are
244
+ * NOT defined here — client packs register them via the component registry.
245
+ */
246
+
247
+ interface BaseComponent {
248
+ component_type: string;
249
+ component_id: string;
250
+ data: unknown;
251
+ theme?: ComponentTheme;
252
+ }
253
+ interface CalendarSlot {
254
+ id: string;
255
+ datetime: string;
256
+ }
257
+ interface CalendarBookingData {
258
+ available_slots: CalendarSlot[];
259
+ timezone: string;
260
+ title: string;
261
+ }
262
+ interface CalendarBookingPayload extends BaseComponent {
263
+ component_type: "calendar_booking";
264
+ data: CalendarBookingData;
265
+ }
266
+ type FormFieldType = "text" | "email" | "tel" | "number" | "select" | "multiselect" | "textarea" | "date" | "boolean";
267
+ interface FormFieldOption {
268
+ value: string;
269
+ label: string;
270
+ }
271
+ interface FormField {
272
+ name: string;
273
+ type: FormFieldType;
274
+ label: string;
275
+ placeholder?: string;
276
+ required?: boolean;
277
+ options?: string[] | FormFieldOption[];
278
+ validation?: {
279
+ pattern?: string;
280
+ minLength?: number;
281
+ maxLength?: number;
282
+ message?: string;
283
+ };
284
+ }
285
+ interface FormData {
286
+ title: string;
287
+ description?: string;
288
+ fields: FormField[];
289
+ submit_label?: string;
290
+ }
291
+ interface FormPayload extends BaseComponent {
292
+ component_type: "form";
293
+ data: FormData;
294
+ }
295
+ interface ButtonAction {
296
+ type: "navigate" | "trigger_component" | "api_call";
297
+ url?: string;
298
+ component_type?: string;
299
+ method?: "POST" | "GET";
300
+ endpoint?: string;
301
+ payload?: Record<string, string>;
302
+ }
303
+ interface ButtonItem {
304
+ id: string;
305
+ label: string;
306
+ style?: "primary" | "secondary" | "outline" | "destructive";
307
+ action: ButtonAction;
308
+ }
309
+ interface ButtonsData {
310
+ title?: string;
311
+ /** "free" renders bare chips attached to the previous agent message (quick replies); default is a boxed card. */
312
+ presentation?: "boxed" | "free";
313
+ /** What a completed pick leaves behind: a confirmation card (default) or the selection echoed as a user bubble. */
314
+ completionDisplay?: "confirmation" | "selected_item";
315
+ buttons: ButtonItem[];
316
+ }
317
+ interface ButtonsPayload extends BaseComponent {
318
+ component_type: "buttons";
319
+ data: ButtonsData;
320
+ }
321
+ interface ConfirmationData {
322
+ title: string;
323
+ message: string;
324
+ icon?: "success" | "info" | "warning" | "error";
325
+ details?: Array<{
326
+ label: string;
327
+ value: string;
328
+ }>;
329
+ }
330
+ interface ConfirmationPayload extends BaseComponent {
331
+ component_type: "confirmation";
332
+ data: ConfirmationData;
333
+ }
334
+ interface FlowStep {
335
+ id: string;
336
+ title: string;
337
+ component: SimpleComponentPayload;
338
+ }
339
+ interface InteractiveFlowData {
340
+ title?: string;
341
+ steps: FlowStep[];
342
+ on_complete: {
343
+ method: "POST" | "GET";
344
+ endpoint: string;
345
+ payload: Record<string, string>;
346
+ };
347
+ }
348
+ interface InteractiveFlowPayload extends BaseComponent {
349
+ component_type: "interactive_flow";
350
+ data: InteractiveFlowData;
351
+ }
352
+ /**
353
+ * Block Kit arrives over the `ringg.blocks` text stream (chunked, so no RPC
354
+ * size cap). The block tree itself is rendered by the platform UI; core
355
+ * treats the blocks as opaque and only carries the envelope.
356
+ */
357
+ interface BlocksData {
358
+ blocks: unknown[];
359
+ tool_id?: string;
360
+ [key: string]: unknown;
361
+ }
362
+ interface BlocksPayload extends BaseComponent {
363
+ component_type: "blocks";
364
+ data: BlocksData;
365
+ }
366
+ type SimpleComponentPayload = CalendarBookingPayload | FormPayload | ButtonsPayload | ConfirmationPayload;
367
+ type ComponentPayload = SimpleComponentPayload | InteractiveFlowPayload | BlocksPayload;
368
+ /**
369
+ * Client-pack components core doesn't model (e.g. `disease_data`). Packs
370
+ * commit them to the timeline via `controller.addLocalComponent`; the platform
371
+ * UI owns their rendering.
372
+ */
373
+ interface CustomComponentPayload {
374
+ component_type: string;
375
+ component_id: string;
376
+ [key: string]: unknown;
377
+ }
378
+ interface ComponentActionResponse {
379
+ success: boolean;
380
+ message: string;
381
+ next_step?: SimpleComponentPayload;
382
+ confirmation?: ConfirmationData;
383
+ }
384
+
385
+ /**
386
+ * Chat message types — the normalized message format used across platforms.
387
+ */
388
+
389
+ interface ChatMessage {
390
+ name: string;
391
+ message: string;
392
+ isSelf: boolean;
393
+ /** Epoch ms; may carry a sub-ms fraction used only for stable ordering */
394
+ timestamp: number;
395
+ /** Reference URL attached to an agent reply (RAG source link) */
396
+ sourceUrl?: string;
397
+ /** When present, this message renders a component instead of text */
398
+ componentType?: string;
399
+ componentData?: ComponentPayload | CustomComponentPayload;
400
+ /** "system" renders an inline event pill (DOM action fired, etc.) instead of a bubble */
401
+ kind?: "system";
402
+ /** Secondary text shown next to a system pill's label (e.g. the event name) */
403
+ systemMeta?: string;
404
+ systemLevel?: "info" | "error";
405
+ }
406
+ interface ErrorState {
407
+ hasError: boolean;
408
+ message: string;
409
+ }
410
+
411
+ /**
412
+ * Widget event types — dispatched to the host application.
413
+ * These provide lifecycle hooks for the embedding app to react to widget
414
+ * state changes. Wire names are the `ringg:` prefixed kebab/snake names
415
+ * that existing integrations already listen for.
416
+ */
417
+
418
+ type WidgetEventName = "ringg:widget_status" | "ringg:conversation_status" | "ringg:feedback_status" | "ringg:calendar_booking" | "ringg:component_acknowledgement";
419
+ interface WidgetStatusPayload {
420
+ status: "maximised" | "minimised";
421
+ mode: MediaType;
422
+ }
423
+ interface ConversationStatusPayload {
424
+ status: "started" | "ended";
425
+ mode: MediaType;
426
+ callId: string;
427
+ }
428
+ interface FeedbackStatusPayload {
429
+ status: "submitted" | "skipped";
430
+ callId: string;
431
+ rating?: number;
432
+ }
433
+ interface CalendarBookingEventPayload {
434
+ status: "shown" | "confirmed" | "failed";
435
+ componentId: string;
436
+ slotId?: string;
437
+ message?: string;
438
+ }
439
+ interface ComponentAcknowledgementPayload {
440
+ componentName: string;
441
+ componentId: string;
442
+ status: string;
443
+ }
444
+ interface WidgetEventMap {
445
+ "ringg:widget_status": WidgetStatusPayload;
446
+ "ringg:conversation_status": ConversationStatusPayload;
447
+ "ringg:feedback_status": FeedbackStatusPayload;
448
+ "ringg:calendar_booking": CalendarBookingEventPayload;
449
+ "ringg:component_acknowledgement": ComponentAcknowledgementPayload;
450
+ }
451
+
452
+ /**
453
+ * Host-page DOM action contract (`execute_dom_action` RPC payloads).
454
+ *
455
+ * Core normalizes the backend wire shape ({ action_id, event_name, detail } or
456
+ * the internal { id, kind, event_name, default_payload }) into `DomAction` and
457
+ * hands it to the platform's `onDomAction` port together with a logger that
458
+ * feeds the eventLogs chat pills. Only web can actually execute these — other
459
+ * platforms leave the port unset and the action is acked and dropped.
460
+ */
461
+ type DomActionKind = "trigger_event";
462
+ interface DomAction {
463
+ id: string;
464
+ kind: DomActionKind;
465
+ description?: string;
466
+ event_name?: string;
467
+ default_payload?: Record<string, unknown>;
468
+ }
469
+ interface DomActionLogEntry {
470
+ id?: string;
471
+ label: string;
472
+ meta?: string;
473
+ level: "info" | "error";
474
+ }
475
+ type DomActionLogger = (entry: DomActionLogEntry) => void;
476
+
477
+ /**
478
+ * Transport port — the surface core needs from a realtime SDK.
479
+ *
480
+ * Web injects an adapter over `livekit-client`; React Native injects one over
481
+ * `@livekit/react-native`. Core NEVER imports a LiveKit SDK directly — this
482
+ * interface is the only coupling point, mirroring the room APIs the widget
483
+ * actually uses (connect/prewarm, mic, RPC methods, text streams,
484
+ * transcription, connection state).
485
+ */
486
+ type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting";
487
+ interface TranscriptionSegment {
488
+ /** Stable segment id — updates stream in for the same id as STT refines */
489
+ id: string;
490
+ text: string;
491
+ final: boolean;
492
+ /** True when the segment belongs to the local participant */
493
+ isLocal: boolean;
494
+ /** Display name of the speaking participant, when known */
495
+ participantName?: string;
496
+ /** Epoch ms the segment was first received */
497
+ receivedAt: number;
498
+ }
499
+ interface IncomingChatMessage {
500
+ message: string;
501
+ /** True when sent by the local participant */
502
+ isSelf: boolean;
503
+ /** True when sent by the agent participant */
504
+ isAgent: boolean;
505
+ senderName?: string;
506
+ timestamp: number;
507
+ }
508
+ interface RpcInvocation {
509
+ method: string;
510
+ payload: string;
511
+ callerIdentity: string;
512
+ }
513
+ /** Incoming text stream (e.g. `lk.transcription` replies, `ringg.blocks`) */
514
+ interface IncomingTextStream {
515
+ /** Resolves with the full text once the stream completes */
516
+ readAll(): Promise<string>;
517
+ participantIdentity: string;
518
+ /** True when the stream was opened by the agent participant */
519
+ isAgent: boolean;
520
+ }
521
+ interface TransportAdapter {
522
+ connect(url: string, token: string): Promise<void>;
523
+ disconnect(): Promise<void>;
524
+ /** Best-effort prewarm (DNS/TLS/region resolution); failures must not throw */
525
+ prepareConnection(url: string): Promise<void>;
526
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
527
+ /** Send a chat message to the room. Core commits the local echo itself, so
528
+ * adapters must NOT surface locally-sent messages via onChatMessage. */
529
+ sendChatMessage(text: string): Promise<void>;
530
+ /** Invoke an RPC on the agent participant. Rejects if no agent is present. */
531
+ performRpcToAgent(method: string, payload: string): Promise<string>;
532
+ /** Handler return value is sent back as the RPC response payload. */
533
+ registerRpcMethod(method: string, handler: (invocation: RpcInvocation) => Promise<string>): void;
534
+ unregisterRpcMethod(method: string): void;
535
+ registerTextStreamHandler(topic: string, handler: (stream: IncomingTextStream) => void): void;
536
+ unregisterTextStreamHandler(topic: string): void;
537
+ onConnectionStateChange(handler: (state: ConnectionState) => void): () => void;
538
+ /** Fires when the room disconnects or the agent participant leaves */
539
+ onSessionEnded(handler: () => void): () => void;
540
+ onTranscription(handler: (segment: TranscriptionSegment) => void): () => void;
541
+ onChatMessage(handler: (message: IncomingChatMessage) => void): () => void;
542
+ /**
543
+ * Fires when acquiring a media device fails after connect (LiveKit's
544
+ * `MediaDevicesError`) — e.g. the mic permission is revoked mid-call. Core
545
+ * surfaces the production error message on it.
546
+ */
547
+ onMediaDevicesError(handler: () => void): () => void;
548
+ }
549
+
550
+ /**
551
+ * Event bus port — abstraction over platform-specific host-event dispatch.
552
+ *
553
+ * Web: DOM CustomEvents on `window` (existing integrations listen for these)
554
+ * React Native: callback props / in-memory listeners
555
+ */
556
+
557
+ interface EventBus {
558
+ emit<K extends WidgetEventName>(event: K, payload: WidgetEventMap[K]): void;
559
+ /** Subscribe to a typed event. Returns an unsubscribe function. */
560
+ on<K extends WidgetEventName>(event: K, handler: (payload: WidgetEventMap[K]) => void): () => void;
561
+ /** Remove all listeners for one event, or all events if omitted. */
562
+ off(event?: WidgetEventName): void;
563
+ }
564
+ /** In-memory event bus — for React Native or testing. */
565
+ declare function createCallbackEventBus(): EventBus;
566
+
567
+ /**
568
+ * Clock port — injectable time and timers.
569
+ *
570
+ * The widget's UX-critical logic is timing logic (typing-indicator minimum
571
+ * duration, chat-widget grace buffering). Routing all time access through
572
+ * this port makes that logic deterministic under test.
573
+ */
574
+ type TimerHandle = ReturnType<typeof setTimeout>;
575
+ interface Clock {
576
+ /** Monotonic-enough milliseconds (wall clock is fine outside tests) */
577
+ now(): number;
578
+ setTimeout(fn: () => void, ms: number): TimerHandle;
579
+ clearTimeout(handle: TimerHandle): void;
580
+ }
581
+
582
+ /**
583
+ * Small platform ports — capabilities core needs but cannot implement
584
+ * portably. Web and React Native each provide implementations.
585
+ */
586
+ /** Microphone permission — navigator.permissions on web, RN permissions API on native */
587
+ interface MicPermissionPort {
588
+ isGranted(): Promise<boolean>;
589
+ /** Prompt the user; resolves with the final grant state */
590
+ request(): Promise<boolean>;
591
+ }
592
+ /** New-message notification sound — HTMLAudioElement on web, a sound lib on RN */
593
+ interface NotificationPlayer {
594
+ /** Best-effort playback; must never throw (autoplay policies etc.) */
595
+ play(): void;
596
+ }
597
+
598
+ /**
599
+ * API client for the Ringg backend.
600
+ * Pure fetch-based — works in any JS runtime (browser, Node, React Native).
601
+ * URLs are always injected by the platform entry point via a UrlResolver;
602
+ * core embeds no endpoints.
603
+ */
604
+
605
+ interface EnvironmentUrls {
606
+ backendUrl: string;
607
+ livekitUrl: string;
608
+ }
609
+ interface UrlResolver {
610
+ resolve(mode: EnvironmentMode): EnvironmentUrls;
611
+ }
612
+
613
+ /**
614
+ * RPC message formatting — transforms between backend RPC format and internal
615
+ * component format.
616
+ *
617
+ * The backend sends `send_dynamic_data` RPCs like:
618
+ * { component_type: "render_component_calendar", component_config: { ... } }
619
+ * The widget needs them in the internal format:
620
+ * { component_type: "calendar_booking", component_id: "calendar_1234", data: { ... } }
621
+ *
622
+ * This module handles both directions.
623
+ */
624
+
625
+ /** A Block Kit action as it goes on the wire (the UI-only `label` never does). */
626
+ interface BlocksActionWire {
627
+ action_id: string;
628
+ value?: unknown;
629
+ values: Record<string, unknown>;
630
+ }
631
+
632
+ /**
633
+ * Base store — the one state-sharing primitive in core.
634
+ *
635
+ * Platform bindings stay thin because they all consume the same shape:
636
+ * web (Lit): subscribe(() => this.host.requestUpdate())
637
+ * RN (React): useSyncExternalStore(store.subscribe, store.getSnapshot)
638
+ *
639
+ * Snapshots must be immutable values — stores rebuild them on change so
640
+ * reference equality is a valid dirty check. `getSnapshot`/`subscribe` are
641
+ * closures, so they stay bound when bindings pass them unbound (React:
642
+ * `useSyncExternalStore(store.subscribe, store.getSnapshot)`).
643
+ */
644
+ interface Store<T> {
645
+ getSnapshot(): T;
646
+ subscribe(listener: (snapshot: T) => void): () => void;
647
+ }
648
+
649
+ /**
650
+ * Shell store — widget open/close/feedback lifecycle and the active call id.
651
+ * Ported from the alpha WidgetStateController; emits `ringg:widget_status`
652
+ * host events on visibility transitions.
653
+ */
654
+
655
+ type WidgetViewState = "closed" | "open" | "feedback";
656
+ interface ShellSnapshot {
657
+ viewState: WidgetViewState;
658
+ currentCallId: string | null;
659
+ callMode: MediaType;
660
+ }
661
+ interface ShellStore extends Store<ShellSnapshot> {
662
+ readonly isOpen: boolean;
663
+ /**
664
+ * Feedback is pending (post-call screen not yet submitted/skipped). Mirrors
665
+ * the production hook's independent `showFeedback` flag: stays true across
666
+ * minimize so reopening restores the feedback view. The derived snapshot
667
+ * `viewState` only reads "feedback" while the panel is actually open.
668
+ */
669
+ readonly isShowingFeedback: boolean;
670
+ open(): void;
671
+ minimize(): void;
672
+ /** Forget the call, clear feedback and minimize — a conversation fully ended. */
673
+ close(): void;
674
+ toggle(): void;
675
+ showFeedback(): void;
676
+ setCallMode(mode: MediaType): void;
677
+ setCurrentCallId(callId: string | null): void;
678
+ dispose(): void;
679
+ }
680
+
681
+ /**
682
+ * Session store — the call lifecycle state machine.
683
+ *
684
+ * idle → starting → live-optimistic → connected → idle
685
+ *
686
+ * "live-optimistic" is load-bearing UX (lifted from production): the flag
687
+ * flips the moment /calling/webcall succeeds — before transport.connect()
688
+ * resolves. The call exists server-side at that point, so the calling layout
689
+ * mounts one HTTP round trip after Start instead of waiting out the LiveKit
690
+ * handshake. A connect failure rolls it back so the start screen (with the
691
+ * error) returns instead of a dead calling layout.
692
+ */
693
+
694
+ type SessionPhase = "idle" | "starting" | "live-optimistic" | "connected";
695
+ interface SessionSnapshot {
696
+ phase: SessionPhase;
697
+ /** True while a start/end request is in flight */
698
+ isLoading: boolean;
699
+ /** UI gate for the calling layout — optimistic OR actually connected */
700
+ isSessionLive: boolean;
701
+ connectionState: ConnectionState;
702
+ error: ErrorState;
703
+ }
704
+ interface SessionStartResult {
705
+ callId: string;
706
+ slashCommands: SlashCommand[];
707
+ }
708
+ interface SessionStore extends Store<SessionSnapshot> {
709
+ readonly isSessionLive: boolean;
710
+ /** Warm DNS/TLS/region on widget open — best-effort, never blocks connect. */
711
+ prewarm(): void;
712
+ /**
713
+ * Start a call. Resolves with the call id + runtime slash commands on
714
+ * success; resolves null when blocked (mic denied) or failed (error is in
715
+ * the snapshot). The caller (controller) resets conversation stores first.
716
+ */
717
+ start(params: {
718
+ agentId: string;
719
+ variables: WidgetVariables;
720
+ mediaType: MediaType;
721
+ }): Promise<SessionStartResult | null>;
722
+ /** Disconnect the transport and return to idle. Never throws. */
723
+ end(): Promise<void>;
724
+ clearError(): void;
725
+ dispose(): void;
726
+ }
727
+
728
+ /**
729
+ * Message store — the single ordered conversation timeline.
730
+ *
731
+ * Three sources feed it and must interleave correctly:
732
+ * - chat messages (text mode, incl. the local optimistic echo)
733
+ * - transcription segments (audio mode; segments UPDATE in place by id as STT
734
+ * refines, keeping their original position)
735
+ * - component messages (RPC widgets, Block Kit — commit-time stamped so a
736
+ * buffered widget sorts strictly after the agent message that released it)
737
+ *
738
+ * Ordering: entries sort by timestamp. The store hands out strictly
739
+ * monotonically increasing timestamps (sub-ms bumped when the clock hasn't
740
+ * advanced), replacing the production widget's `Date.now() + Math.random()`
741
+ * collision-avoidance hacks with a deterministic policy.
742
+ */
743
+
744
+ interface MessageSnapshot {
745
+ messages: readonly ChatMessage[];
746
+ }
747
+ interface MessageStore extends Store<MessageSnapshot> {
748
+ /** Next timestamp, strictly greater than any previously issued one. */
749
+ nextTimestamp(): number;
750
+ /** Commit a chat message. Agent messages absorb any pending source URL. */
751
+ addChatMessage(params: {
752
+ message: string;
753
+ isSelf: boolean;
754
+ name?: string;
755
+ timestamp?: number;
756
+ }): ChatMessage;
757
+ /** Commit a component message (commit-time stamped — see module docs). */
758
+ addComponent(component: ComponentPayload | CustomComponentPayload): ChatMessage;
759
+ /** Commit an inline system-event pill (DOM action fired, etc.). */
760
+ addSystemLog(params: {
761
+ label: string;
762
+ meta?: string;
763
+ level: "info" | "error";
764
+ }): void;
765
+ /**
766
+ * Render the user's quick-reply selection as a chat bubble anchored just
767
+ * after the prompting component — even if a follow-up agent message has
768
+ * already arrived.
769
+ */
770
+ addLocalSelection(componentId: string, label: string): void;
771
+ /**
772
+ * Upsert a transcription segment (audio mode). Position is stable by id.
773
+ * When a source URL is pending, it attaches to the newest transcription if
774
+ * that is an agent utterance without one; otherwise it is dropped
775
+ * (production clears the ref on any update that doesn't append).
776
+ */
777
+ upsertTranscription(segment: TranscriptionSegment): void;
778
+ /** Stash a RAG source URL; the next agent reply carries it. */
779
+ setPendingSourceUrl(url: string): void;
780
+ /** Drop a stashed source URL without attaching it. */
781
+ clearPendingSourceUrl(): void;
782
+ reset(): void;
783
+ dispose(): void;
784
+ }
785
+
786
+ /**
787
+ * Typing store — drives the "agent is typing" indicator (text mode).
788
+ *
789
+ * Behavior lifted from the production widget:
790
+ * - Dots appear the moment the user sends (we always expect a reply); the
791
+ * min-duration timer anchors only when they first appear, so back-to-back
792
+ * sends keep one continuous indicator instead of resetting it.
793
+ * - Any landed backend response — text or RPC — answers the pending turn and
794
+ * drops the dots, unconditionally.
795
+ * - Replies that land faster than MIN_TYPING_INDICATOR_MS wait out the
796
+ * remainder before committing, otherwise the indicator just flashes.
797
+ */
798
+
799
+ interface TypingSnapshot {
800
+ isTyping: boolean;
801
+ }
802
+ interface TypingStore extends Store<TypingSnapshot> {
803
+ /** User sent a message — show the dots (idempotent while already shown). */
804
+ showOnUserSend(): void;
805
+ /** A backend response landed (or the turn errored) — drop the dots now. */
806
+ clear(): void;
807
+ /**
808
+ * Resolve once the indicator has been visible for MIN_TYPING_INDICATOR_MS.
809
+ * Callers await this before committing a fast agent reply.
810
+ * Resolves immediately when the dots aren't showing.
811
+ */
812
+ waitMinDuration(): Promise<void>;
813
+ dispose(): void;
814
+ }
815
+
816
+ /**
817
+ * Component store — interactive components pushed by the backend mid-call.
818
+ *
819
+ * Chat-mode anchoring (lifted from production): widget RPCs land instantly
820
+ * while the agent's text streams a beat later. In text mode we buffer the
821
+ * widget until the accompanying message lands, then commit it right after so
822
+ * it sorts below. If no message arrives within CHAT_WIDGET_GRACE_MS, flush
823
+ * anyway. Audio mode commits immediately.
824
+ *
825
+ * Also owns the response side: which components still await a user response
826
+ * (and the original config needed to format it), completed flows, and the
827
+ * extension registry that lets client packs handle domain-specific
828
+ * `send_dynamic_data` component types without polluting core.
829
+ */
830
+
831
+ interface ComponentSnapshot {
832
+ completedFlowIds: ReadonlySet<string>;
833
+ }
834
+ interface PendingResponse {
835
+ name: string;
836
+ originalConfig: unknown;
837
+ }
838
+ /**
839
+ * Extension hook for domain-specific dynamic-data payloads (client packs).
840
+ * Return true when the payload was handled; false lets core continue with
841
+ * its built-in handling.
842
+ */
843
+ type DynamicDataExtension = (componentName: string, parsed: Record<string, unknown>) => boolean;
844
+ interface ComponentStore extends Store<ComponentSnapshot> {
845
+ /** Queue or commit a component depending on call mode (see module docs). */
846
+ add(component: ComponentPayload, mode: MediaType): void;
847
+ /** The agent's text landed — release buffered widgets right after it. */
848
+ onAgentTextLanded(): void;
849
+ flush(): void;
850
+ trackPendingResponse(componentId: string, pending: PendingResponse): void;
851
+ /** Consume the pending-response record for a component (one-shot). */
852
+ takePendingResponse(componentId: string): PendingResponse | undefined;
853
+ markFlowComplete(componentId: string): void;
854
+ /** Register a client-pack handler. Returns an unregister function. */
855
+ registerExtension(extension: DynamicDataExtension): () => void;
856
+ /** Offer a payload to extensions; true when one handled it. */
857
+ runExtensions(componentName: string, parsed: Record<string, unknown>): boolean;
858
+ /** Drop buffered widgets + timers so nothing fires after the call ended. */
859
+ reset(): void;
860
+ dispose(): void;
861
+ }
862
+
863
+ /**
864
+ * Slash command store — merges commands declared in config with commands the
865
+ * backend enables at webcall time. Config commands win on collisions.
866
+ */
867
+
868
+ interface SlashCommandSnapshot {
869
+ commands: readonly SlashCommand[];
870
+ }
871
+ interface SlashCommandStore extends Store<SlashCommandSnapshot> {
872
+ /** Commands from the webcall response */
873
+ setRuntimeCommands(commands: SlashCommand[]): void;
874
+ reset(): void;
875
+ dispose(): void;
876
+ }
877
+
878
+ /**
879
+ * RinggWidgetController — the composition root of the widget brain.
880
+ *
881
+ * Owns every store, wires transport events into them, and exposes the
882
+ * imperative API the platform UIs call. This is the headless replacement for
883
+ * the production widget's App.tsx orchestration: platform bindings render
884
+ * snapshots and forward user intents; ALL behavior lives here or in the
885
+ * stores.
886
+ */
887
+
888
+ interface ControllerPorts {
889
+ transport: TransportAdapter;
890
+ urlResolver: UrlResolver;
891
+ eventBus?: EventBus;
892
+ clock?: Clock;
893
+ micPermission?: MicPermissionPort;
894
+ notification?: NotificationPlayer;
895
+ /**
896
+ * Host-page action executor (web-only: `execute_dom_action` payloads).
897
+ * Core normalizes the wire payload into a `DomAction` and provides the
898
+ * eventLogs-gated system-log sink. Platforms that can't execute host
899
+ * actions leave this unset; payloads are acked and dropped.
900
+ */
901
+ onDomAction?: (action: DomAction, log: DomActionLogger) => void;
902
+ }
903
+ interface RinggWidgetController {
904
+ readonly config: RinggWidgetConfig;
905
+ readonly theme: ResolvedWidgetTheme;
906
+ readonly eventBus: EventBus;
907
+ readonly shell: ShellStore;
908
+ readonly session: SessionStore;
909
+ readonly messages: MessageStore;
910
+ readonly typing: TypingStore;
911
+ readonly components: ComponentStore;
912
+ readonly slashCommands: SlashCommandStore;
913
+ openWidget(): void;
914
+ minimizeWidget(): void;
915
+ /** Trigger click: toggles, or starts directly with bypassStartScreen. */
916
+ handleTriggerClick(): void;
917
+ startCall(mediaType: MediaType): Promise<void>;
918
+ endCall(): Promise<void>;
919
+ sendMessage(text: string): Promise<void>;
920
+ /** Send the user's component response (slot pick, form submit, button tap). */
921
+ sendComponentResponse(componentId: string, responseData: Record<string, unknown>): Promise<void>;
922
+ /**
923
+ * Block Kit action → agent (`receive_component_blocks` envelope over
924
+ * `receive_dynamic_data`). Resolves only after the RPC round-trips — the UI
925
+ * ties its in-flight spinner to this Promise. Transport failures are
926
+ * swallowed (production logs and moves on), so it never rejects.
927
+ */
928
+ sendBlocksAction(toolId: string | undefined, componentId: string, action: BlocksActionWire): Promise<void>;
929
+ /**
930
+ * Component API for interactive-flow `on_complete` / `api_call` button
931
+ * actions. Relative endpoints resolve against the backend URL; auth headers
932
+ * match the widget's other backend calls.
933
+ */
934
+ callComponentApi(method: "POST" | "GET", endpoint: string, payload: Record<string, string>): Promise<ComponentActionResponse>;
935
+ /**
936
+ * Raw dynamic-data RPC for client packs (e.g. disease search/submit) — the
937
+ * payload carries its own `component_type`. Rejects on transport failure so
938
+ * packs can implement their own fallbacks.
939
+ */
940
+ sendDynamicData(payload: Record<string, unknown>): Promise<void>;
941
+ /**
942
+ * Commit a pack-owned component to the timeline immediately (parity with
943
+ * production's local widget messages — no buffering, no shell side effects).
944
+ */
945
+ addLocalComponent(component: CustomComponentPayload): void;
946
+ /** Show the user's quick-reply selection as a chat bubble. */
947
+ displaySelection(componentId: string, label: string): void;
948
+ markFlowComplete(componentId: string): void;
949
+ /** Client-pack hook for domain-specific dynamic-data payloads. */
950
+ registerDynamicDataExtension(extension: DynamicDataExtension): () => void;
951
+ sendSlashCommand(command: SlashCommand): Promise<void>;
952
+ submitFeedback(rating: number, comment: string): Promise<void>;
953
+ skipFeedback(): void;
954
+ destroy(): void;
955
+ }
956
+ declare function createRinggWidgetController(config: RinggWidgetConfig, ports: ControllerPorts): RinggWidgetController;
957
+
958
+ /**
959
+ * React bindings — shared by @ringg/web (React DOM) and @ringg/react-native.
960
+ *
961
+ * Every hook is a useSyncExternalStore call over a core store; there is no
962
+ * behavior here and there must never be. A fix to typing timing, message
963
+ * ordering, or session lifecycle lands in the stores once and both platforms
964
+ * pick it up.
965
+ *
966
+ * Imported via the "@ringg/core/react" subpath so non-React consumers of
967
+ * core never touch the react dependency (it's an optional peer).
968
+ */
969
+
970
+ declare function useStoreSnapshot<T>(store: Store<T>): T;
971
+ declare const useRinggMessages: (controller: RinggWidgetController) => MessageSnapshot;
972
+ declare const useRinggTyping: (controller: RinggWidgetController) => TypingSnapshot;
973
+ declare const useRinggSession: (controller: RinggWidgetController) => SessionSnapshot;
974
+ declare const useRinggShell: (controller: RinggWidgetController) => ShellSnapshot;
975
+ declare const useRinggComponents: (controller: RinggWidgetController) => ComponentSnapshot;
976
+ declare const useRinggSlashCommands: (controller: RinggWidgetController) => SlashCommandSnapshot;
977
+
978
+ /**
979
+ * RinggWidget — the assembled React Native widget.
980
+ *
981
+ * The native counterpart of the web assembly: the same tree, the same order,
982
+ * the same branches. It holds NO conversation state — every snapshot comes
983
+ * from `@ringg/core` via the shared hooks and every intent is forwarded to the
984
+ * controller. If a behaviour looks like it belongs here, it belongs in core.
985
+ *
986
+ * Three things genuinely differ from web, all forced by the platform:
987
+ *
988
+ * 1. There is no `position: fixed`. The widget is an absolutely-filled
989
+ * overlay with `pointerEvents="box-none"`, so taps pass through everywhere
990
+ * except the trigger and the panel. Mount it as the LAST child of the app
991
+ * root; `widgetPosition` (a web-only config) is ignored and the panel
992
+ * sizes itself to the device.
993
+ * 2. The keyboard covers the bottom of the screen. The panel lives inside a
994
+ * `KeyboardAvoidingView` so the composer stays visible while typing —
995
+ * without it the input is simply unreachable in text mode.
996
+ * 3. The transcript cannot measure a child's offset the way web does to
997
+ * anchor the newest turn's first line. It pins to the bottom instead, and
998
+ * the "new message" / "typing toggled" distinction is preserved so a long
999
+ * reply arriving does not yank the view while the user is reading.
1000
+ */
1001
+
1002
+ interface RinggWidgetProps {
1003
+ controller: RinggWidgetController;
1004
+ /**
1005
+ * The LiveKit room from `createLiveKitTransport()`. Optional: it only powers
1006
+ * the in-call audio visualizer and the mute button. Without it the widget is
1007
+ * fully functional and the visualizer shows its ambient idle animation.
1008
+ */
1009
+ room?: Room;
1010
+ }
1011
+ declare const RinggWidget: FC<RinggWidgetProps>;
1012
+
1013
+ /**
1014
+ * LiveKit React Native transport — the real `TransportAdapter` over
1015
+ * `@livekit/react-native`, behavior-matched to the web adapter (which is
1016
+ * itself matched to production, @desivocal/agents-cdn).
1017
+ *
1018
+ * The wire behavior is deliberately identical to `web/src/transport/
1019
+ * livekit-transport.ts` — same Room options, same double-send chat, same
1020
+ * agent-classification and session-ended rules — because both talk to the same
1021
+ * backend and core drives them through the same port. Read that file's parity
1022
+ * notes; they apply here verbatim.
1023
+ *
1024
+ * Only what the platform forces differs:
1025
+ * - remote audio needs no elements. Mobile plays subscribed audio through the
1026
+ * OS, so `<RoomAudioRenderer />`'s job becomes owning an audio SESSION
1027
+ * (see platform/audio-session.ts) rather than attaching media elements;
1028
+ * - `registerGlobals()` must run before a Room is constructed — the adapter
1029
+ * does it so a missing app-entry call is not a mysterious runtime failure;
1030
+ * - autoplay policy has no mobile equivalent, so the web adapter's
1031
+ * `startAudio()` recovery has no counterpart.
1032
+ */
1033
+
1034
+ interface LiveKitTransportOptions {
1035
+ /**
1036
+ * Extra LiveKit `RoomOptions` merged over the parity defaults
1037
+ * (`dynacast: true, adaptiveStream: true`). Rarely needed.
1038
+ */
1039
+ roomOptions?: RoomOptions;
1040
+ /**
1041
+ * Own the native audio session for the duration of a call — the mobile
1042
+ * equivalent of production's `<RoomAudioRenderer />`. Default true; disable
1043
+ * when the host app already manages an audio session (an in-app player, a
1044
+ * CallKit/ConnectionService integration).
1045
+ */
1046
+ manageAudioSession?: boolean;
1047
+ /**
1048
+ * Call `registerGlobals()` before constructing the Room. Default true.
1049
+ * Set false when the app already calls it at its entry point — it is
1050
+ * idempotent, so this is a formality rather than a correctness switch.
1051
+ */
1052
+ registerGlobals?: boolean;
1053
+ }
1054
+ interface LiveKitTransport {
1055
+ transport: TransportAdapter;
1056
+ /**
1057
+ * The underlying Room, for presentational concerns the port cannot express
1058
+ * (visualizer track handles, `isMicrophoneEnabled` readback — spec §11).
1059
+ * State-changing calls must keep going through the transport/core.
1060
+ */
1061
+ room: Room;
1062
+ /** Tears down listeners, handlers and the audio session; disconnects the room. */
1063
+ dispose(): void;
1064
+ }
1065
+ declare const createLiveKitTransport: (options?: LiveKitTransportOptions) => LiveKitTransport;
1066
+
1067
+ /**
1068
+ * Native audio session — React Native's replacement for the web adapter's
1069
+ * hidden `<audio>` elements.
1070
+ *
1071
+ * On web, remote audio needs a DOM element to play into. On mobile it needs
1072
+ * the opposite: nothing to attach, but an OS-level audio session that has to
1073
+ * be configured and activated before the call and released after it, or the
1074
+ * mic indicator stays lit and the app keeps ducking other audio.
1075
+ *
1076
+ * The configuration encodes the mobile audio invariants (AGENTS.md rule 10):
1077
+ * communication mode so the call survives backgrounding and routes through the
1078
+ * earpiece/bluetooth stack rather than the media stream, and a preferred
1079
+ * output list that follows a headset or bluetooth device when one appears.
1080
+ *
1081
+ * Every call is best-effort. A device that refuses to hand over the audio
1082
+ * session must not take the call down with it — the user still gets a
1083
+ * (possibly routed-oddly) conversation, which beats a hard failure.
1084
+ */
1085
+ interface AudioSessionPort {
1086
+ /** Configure + activate. Safe to call repeatedly; only the first wins. */
1087
+ start(): Promise<void>;
1088
+ /** Release the session. Safe to call when never started. */
1089
+ stop(): Promise<void>;
1090
+ }
1091
+ declare const createAudioSession: () => AudioSessionPort;
1092
+
1093
+ /**
1094
+ * React Native `MicPermissionPort`.
1095
+ *
1096
+ * Core's session store drives this on audio start: `isGranted()` first, then
1097
+ * `request()`, aborting the call on denial — identical to web. What differs is
1098
+ * how the answer is obtained, and the two platforms genuinely differ:
1099
+ *
1100
+ * - Android exposes a real permission API, so the state can be READ without
1101
+ * prompting. That matters: `isGranted()` must never show a dialog, or the
1102
+ * user gets prompted before they have pressed anything.
1103
+ * - iOS has no readable pre-check from JS. The only probe is `getUserMedia`,
1104
+ * which prompts on first use — the same fallback the web port uses for
1105
+ * browsers without the Permissions API. Once answered, iOS resolves the
1106
+ * probe from its own record without prompting again.
1107
+ *
1108
+ * The probe must never keep the microphone: every track it opens is stopped
1109
+ * before returning, or the OS recording indicator stays lit before the call
1110
+ * has even started.
1111
+ */
1112
+
1113
+ declare const createNativeMicPermission: () => MicPermissionPort;
1114
+
1115
+ /**
1116
+ * App identity for the backend's caller allow-list.
1117
+ *
1118
+ * The Ringg backend allow-lists an agent's callers by `Origin`. A browser
1119
+ * attaches it automatically; a native app sends nothing, so it has to present
1120
+ * its own identity — by convention `<platform>://<bundleId>`, the scheme the
1121
+ * backend accepts for app clients. Without it the webcall request is rejected
1122
+ * before authentication even matters (`400 Origin header is required`).
1123
+ *
1124
+ * The bundle id is the INTEGRATOR's, not ours, so the value allow-listed in
1125
+ * the dashboard is their app. React Native cannot read it without a native
1126
+ * module, and this package refuses to grow one for a single string — so the id
1127
+ * is a parameter. Expo apps have it in `expo-application`'s `applicationId`;
1128
+ * bare apps already know their own.
1129
+ *
1130
+ * Mirrors `flutter/lib/src/platform/app_origin.dart`, which derives the same
1131
+ * string from `package_info_plus`.
1132
+ */
1133
+ /**
1134
+ * `<platform>://<bundleId>` for the running app — e.g. `android://com.acme.app`.
1135
+ *
1136
+ * Returns undefined on a platform the backend has no scheme for, which
1137
+ * includes react-native-web: there the browser sends a real `Origin` and
1138
+ * setting one from JavaScript is forbidden anyway.
1139
+ */
1140
+ declare const appOrigin: (bundleId: string) => string | undefined;
1141
+
1142
+ /**
1143
+ * React Native `NotificationPlayer`.
1144
+ *
1145
+ * Core decides WHEN to play (an agent reply or component landing while the
1146
+ * widget is closed); the port only owns the how. On web that is one line —
1147
+ * `new Audio(url)`. RN has no audio playback in the platform at all: every
1148
+ * option (expo-audio, react-native-sound, react-native-video) is a native
1149
+ * module, and forcing one on integrators would mean an extra pod/gradle
1150
+ * dependency and an Expo config plugin for a notification chime.
1151
+ *
1152
+ * So the decision is inverted: the widget ships silent by default and the
1153
+ * integrator opts in with three lines against whichever player their app
1154
+ * already has. `notificationTuneUrl` from the config is handed to that player,
1155
+ * so the URL stays a config concern on every platform.
1156
+ */
1157
+
1158
+ /** What an integrator's audio library has to be able to do. */
1159
+ type PlayTune = (tuneUrl: string) => void;
1160
+ /**
1161
+ * The default. Silent, and deliberately so — a widget that cannot find an
1162
+ * audio library should be quiet, not noisy about it on every reply.
1163
+ */
1164
+ declare const createSilentNotificationPlayer: () => NotificationPlayer;
1165
+ /**
1166
+ * Bridges the config's tune URL to a host-provided player.
1167
+ *
1168
+ * ```ts
1169
+ * import { createAudioPlayer } from "expo-audio";
1170
+ * const notifications = createNotificationPlayer(DEFAULT_CONFIG.notificationTuneUrl, (url) => createAudioPlayer(url).play());
1171
+ * ```
1172
+ *
1173
+ * Playback failures are swallowed: a chime is best-effort by contract, and a
1174
+ * rejected promise here must never surface mid-conversation.
1175
+ */
1176
+ declare const createNotificationPlayer: (tuneUrl: string, playTune: PlayTune) => NotificationPlayer;
1177
+
1178
+ /**
1179
+ * Host action dispatcher — the React Native counterpart of the web widget's
1180
+ * `executeDomAction`.
1181
+ *
1182
+ * Core normalises the `execute_dom_action` RPC into a `DomAction` and hands it
1183
+ * to the `onDomAction` port. On web that ends in `window.dispatchEvent`, which
1184
+ * works because the host page is already an event bus. RN has no such ambient
1185
+ * bus, so the integrator supplies the handler and gets the event name plus the
1186
+ * payload the agent sent — the same `{...default_payload, action_id}` detail
1187
+ * the web CustomEvent carries, so a shared backend config drives both.
1188
+ *
1189
+ * Validation, logging and the "unknown kind" path stay identical to web: those
1190
+ * are contract behaviour, not platform detail.
1191
+ */
1192
+
1193
+ /** What the host receives — the web CustomEvent, minus the DOM. */
1194
+ interface HostAction {
1195
+ /** The agent-configured event name (`CustomEvent.type` on web). */
1196
+ name: string;
1197
+ /** `default_payload` with the action id folded in (`CustomEvent.detail`). */
1198
+ payload: Record<string, unknown>;
1199
+ }
1200
+ type HostActionHandler = (action: HostAction) => void;
1201
+ /**
1202
+ * Builds the `onDomAction` port. `onAction` runs in the caller's context and
1203
+ * is allowed to throw — a broken host handler is reported, never fatal.
1204
+ */
1205
+ declare const createHostActionDispatcher: (onAction: HostActionHandler) => (action: DomAction | undefined | null, onLog?: DomActionLogger) => void;
1206
+
1207
+ /**
1208
+ * Widget theme for React Native.
1209
+ *
1210
+ * Deliberately thin: the theme VALUES (defaults, presets) and the merge/
1211
+ * contrast LOGIC both come from `@ringg/core` — `mergeWidgetTheme` here is
1212
+ * core's function, not a copy. The web package still carries its own copy of
1213
+ * that table for byte-fidelity with agents-cdn; RN has no such legacy to match
1214
+ * and takes the shared one, so a token change reaches this platform through
1215
+ * codegen (AGENTS.md rule 2).
1216
+ *
1217
+ * What IS local is unit translation. The theme surface is CSS-shaped
1218
+ * (`"16px"`, `"9999px"`, `fontFamily: "inherit"`) because that's the contract
1219
+ * integrators already write against on web; RN needs numbers and a real family
1220
+ * name, so the `resolve*` helpers below are the single conversion point.
1221
+ */
1222
+
1223
+ interface WidgetThemeProviderProps {
1224
+ theme?: WidgetTheme;
1225
+ children: ReactNode;
1226
+ }
1227
+ /** `"16px"` → `16`. Unparseable values fall back rather than laying out as NaN. */
1228
+ declare const resolveRadius: (value: string | undefined, fallback?: number) => number;
1229
+ /** Button corner radius as an RN number, from core's shared style → CSS mapping. */
1230
+ declare const resolveButtonRadius: (style: ButtonStyle | undefined) => number;
1231
+ /**
1232
+ * `"inherit"` has no meaning in RN — there is no cascade. Returning undefined
1233
+ * lets `<Text>` fall through to the platform system font, which is what
1234
+ * "inherit" resolves to on web for an unstyled host page.
1235
+ */
1236
+ declare const resolveFontFamily: (fontFamily: string | undefined) => string | undefined;
1237
+ declare const WidgetThemeProvider: FC<WidgetThemeProviderProps>;
1238
+ declare const useWidgetTheme: () => ResolvedWidgetTheme;
1239
+
1240
+ /**
1241
+ * Gradient fills for React Native.
1242
+ *
1243
+ * The theme surface is CSS-shaped — `primaryColor` may be a solid hex OR a
1244
+ * `linear-gradient(...)` string (see core `types/theme.ts`, and the gradient
1245
+ * presets in `widget-theme`). The DOM renders those for free; RN has no
1246
+ * gradient primitive at all, so every themed surface goes through here.
1247
+ *
1248
+ * Colour PARSING is not duplicated — `isGradient` / `extractColorsFromGradient`
1249
+ * / `getDominantColor` come from `@ringg/core`, the same functions the web
1250
+ * widget uses (AGENTS.md rule 2: shared values come from core, never a local
1251
+ * re-implementation). Only the two things core cannot know about — the angle in
1252
+ * SVG coordinates, and how to paint it — live in this file.
1253
+ *
1254
+ * Non-linear gradients (radial/conic) degrade to their dominant colour rather
1255
+ * than approximating badly; the theme presets are all linear.
1256
+ */
1257
+
1258
+ interface GradientFillProps {
1259
+ /** A theme colour: solid (`#0a0a0b`, `rgb(...)`) or a CSS gradient string. */
1260
+ color: string;
1261
+ /** Applied to the wrapper — size, padding, radius, alignment. */
1262
+ style?: StyleProp<ViewStyle>;
1263
+ children?: ReactNode;
1264
+ }
1265
+ /**
1266
+ * Paints `color` behind `children`. Solid colours take the cheap path
1267
+ * (`backgroundColor`); gradients get an absolutely-filled SVG layer that
1268
+ * inherits the wrapper's rounding through `overflow: hidden`.
1269
+ */
1270
+ declare const GradientFill: FC<GradientFillProps>;
1271
+ /**
1272
+ * A guaranteed-solid colour, for the properties RN can only paint flat —
1273
+ * borders, shadows, icon tints, status dots.
1274
+ */
1275
+ declare const solidColor: (color: string) => string;
1276
+
1277
+ /**
1278
+ * `PortableStyles` → React Native styles.
1279
+ *
1280
+ * `PortableStyles` (core `types/config.ts`) is the deliberately small subset of
1281
+ * CSS the config surface accepts precisely so it can cross platforms. Web hands
1282
+ * the values straight to the DOM; RN cannot — it has no CSS parser, so a
1283
+ * `"16px"` string silently breaks layout instead of throwing.
1284
+ *
1285
+ * This is the one place that converts. Rules, matching the Dart port
1286
+ * (`flutter/lib/src/ui/widgets/portable_styles.dart`):
1287
+ * - numbers pass through as density-independent pixels;
1288
+ * - `"16px"` / `"16"` → `16`;
1289
+ * - `"50%"` survives only where RN accepts percentages (width/height);
1290
+ * - anything else (`calc()`, `em`, `auto`, gradients in `backgroundColor`) is
1291
+ * dropped rather than guessed at — a dropped style degrades, a wrong one
1292
+ * corrupts the layout.
1293
+ */
1294
+
1295
+ /** Convert a config `PortableStyles` bag into an RN `ViewStyle`. */
1296
+ declare const toViewStyle: (styles: PortableStyles | undefined) => ViewStyle;
1297
+ /** Icon sizing from `ButtonIconConfig.size` (same parsing, single value). */
1298
+ declare const toSize: (value: string | number | undefined, fallback: number) => number;
1299
+
1300
+ /**
1301
+ * Markdown for chat bubbles.
1302
+ *
1303
+ * The web widget hands agent replies to `react-markdown` + `remark-gfm` and
1304
+ * lets Tailwind's prose plugin style the result. Neither of those crosses to
1305
+ * RN, and this package takes no dependency for it, so what lives here is a
1306
+ * renderer for the subset that actually appears in agent replies. Everything
1307
+ * else falls through as the literal text the model wrote — a reply must never
1308
+ * show half-parsed markup.
1309
+ *
1310
+ * Supported: paragraphs, `**bold**`, `*italic*`, inline code, `[links](url)`
1311
+ * (handed to `Linking`), bullet and ordered lists, `#` through `######`
1312
+ * headings, fenced code blocks, blockquotes (which may contain further
1313
+ * blocks), horizontal rules, and backslash escapes.
1314
+ *
1315
+ * Not supported, deliberately: TABLES — GFM's headline feature, but a
1316
+ * phone-width bubble has nowhere to put one, so a table degrades to a
1317
+ * paragraph of its pipe-separated source. Also absent: images (rendered as
1318
+ * their alt text), strikethrough, task lists, footnotes, reference links, raw
1319
+ * HTML, and nested list indentation (nested items flatten into their parent
1320
+ * list).
1321
+ *
1322
+ * Soft line breaks inside a paragraph collapse into spaces, which is what the
1323
+ * browser does for the web widget.
1324
+ */
1325
+
1326
+ interface MarkdownProps {
1327
+ content: string;
1328
+ /** Body text. */
1329
+ color: string;
1330
+ /** Chrome that is not text: the rule, the quote bar, the code outline. */
1331
+ mutedColor: string;
1332
+ linkColor: string;
1333
+ fontSize?: number;
1334
+ fontFamily?: string;
1335
+ }
1336
+ declare const Markdown: FC<MarkdownProps>;
1337
+
1338
+ /**
1339
+ * Node identifiers for integrators and end-to-end tests.
1340
+ *
1341
+ * The web widget tags every meaningful node with `data-ringg="..."` and treats
1342
+ * those names as a contract — integrators select on them, so renaming one is a
1343
+ * breaking change. RN has no attribute namespace, but it has `testID`, which
1344
+ * surfaces to the same audiences (Detox, Maestro, Appium, the native view
1345
+ * hierarchy).
1346
+ *
1347
+ * So the mapping is mechanical and total: `data-ringg="header-title"` becomes
1348
+ * `testID="ringg-header-title"`. The prefix keeps widget nodes from colliding
1349
+ * with the host app's own testIDs, and the suffix stays byte-identical to web
1350
+ * so one selector list documents both platforms.
1351
+ */
1352
+ /** `"header-title"` → `"ringg-header-title"`. */
1353
+ declare const ringgId: (name: string) => string;
1354
+
1355
+ export { type AudioSessionPort, type ControllerPorts, GradientFill, type HostAction, type HostActionHandler, type LiveKitTransport, type LiveKitTransportOptions, Markdown, type PlayTune, RinggWidget, type RinggWidgetConfig, type RinggWidgetController, type RinggWidgetProps, type TransportAdapter, WidgetThemeProvider, appOrigin, createAudioSession, createCallbackEventBus, createHostActionDispatcher, createLiveKitTransport, createNativeMicPermission, createNotificationPlayer, createRinggWidgetController, createSilentNotificationPlayer, resolveButtonRadius, resolveFontFamily, resolveRadius, ringgId, solidColor, toSize, toViewStyle, useRinggComponents, useRinggMessages, useRinggSession, useRinggShell, useRinggSlashCommands, useRinggTyping, useStoreSnapshot, useWidgetTheme };