@ringg/react-native 0.3.0 → 0.4.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.
package/dist/index.js CHANGED
@@ -30,26 +30,73 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ CHAT_WIDGET_AFTER_TEXT_MS: () => CHAT_WIDGET_AFTER_TEXT_MS,
34
+ CHAT_WIDGET_GRACE_MS: () => CHAT_WIDGET_GRACE_MS,
35
+ DEFAULT_CONFIG: () => WIDGET_DEFAULTS,
36
+ DEFAULT_ENVIRONMENT_URLS: () => DEFAULT_ENVIRONMENT_URLS,
37
+ DEFAULT_WIDGET_THEME: () => DEFAULT_WIDGET_THEME,
33
38
  GradientFill: () => GradientFill,
39
+ MIN_TYPING_INDICATOR_MS: () => MIN_TYPING_INDICATOR_MS,
34
40
  Markdown: () => Markdown,
35
41
  RinggWidget: () => RinggWidget,
42
+ WIDGET_PRESET_THEMES: () => WIDGET_PRESET_THEMES,
36
43
  WidgetThemeProvider: () => WidgetThemeProvider,
37
44
  appOrigin: () => appOrigin,
45
+ buildPayload: () => buildPayload,
46
+ colorToBackground: () => colorToBackground,
47
+ colorToBorder: () => colorToBorder,
48
+ colorToSolid: () => colorToSolid,
38
49
  createAudioSession: () => createAudioSession,
39
50
  createCallbackEventBus: () => createCallbackEventBus,
51
+ createComponentStore: () => createComponentStore,
52
+ createDomEventBus: () => createDomEventBus,
40
53
  createHostActionDispatcher: () => createHostActionDispatcher,
41
54
  createLiveKitTransport: () => createLiveKitTransport,
55
+ createMessageStore: () => createMessageStore,
42
56
  createNativeMicPermission: () => createNativeMicPermission,
43
57
  createNotificationPlayer: () => createNotificationPlayer,
58
+ createRinggApiClient: () => createRinggApiClient,
44
59
  createRinggWidgetController: () => createRinggWidgetController,
60
+ createSessionStore: () => createSessionStore,
61
+ createShellStore: () => createShellStore,
45
62
  createSilentNotificationPlayer: () => createSilentNotificationPlayer,
63
+ createSlashCommandStore: () => createSlashCommandStore,
64
+ createStaticUrlResolver: () => createStaticUrlResolver,
65
+ createStore: () => createStore,
66
+ createSystemClock: () => createSystemClock,
67
+ createTypingStore: () => createTypingStore,
68
+ defaultUrlResolver: () => defaultUrlResolver,
69
+ extractColorsFromGradient: () => extractColorsFromGradient,
70
+ formatBlocksAction: () => formatBlocksAction,
71
+ formatComponentResponse: () => formatComponentResponse,
72
+ formatDynamicData: () => formatDynamicData,
73
+ formatSlashCommand: () => formatSlashCommand,
74
+ formatSlotDate: () => formatSlotDate,
75
+ formatSlotTime: () => formatSlotTime,
76
+ getButtonRadius: () => getButtonRadius,
77
+ getContrastingTextColor: () => getContrastingTextColor,
78
+ getDominantColor: () => getDominantColor,
79
+ grantedMicPermission: () => grantedMicPermission,
80
+ groupSlotsByDate: () => groupSlotsByDate,
81
+ isBlocks: () => isBlocks,
82
+ isButtons: () => isButtons,
83
+ isCalendarBooking: () => isCalendarBooking,
84
+ isConfirmation: () => isConfirmation,
85
+ isForm: () => isForm,
86
+ isGradient: () => isGradient,
87
+ isInteractiveFlow: () => isInteractiveFlow,
88
+ isLightColor: () => isLightColor,
89
+ mergeComponentTheme: () => mergeComponentTheme,
90
+ mergeWidgetTheme: () => mergeWidgetTheme,
46
91
  resolveButtonRadius: () => resolveButtonRadius,
47
92
  resolveFontFamily: () => resolveFontFamily,
48
93
  resolveRadius: () => resolveRadius,
49
94
  ringgId: () => ringgId,
95
+ silentNotificationPlayer: () => silentNotificationPlayer,
50
96
  solidColor: () => solidColor,
51
97
  toSize: () => toSize,
52
98
  toViewStyle: () => toViewStyle,
99
+ transformRpcToComponent: () => transformRpcToComponent,
53
100
  useRinggComponents: () => useRinggComponents,
54
101
  useRinggMessages: () => useRinggMessages,
55
102
  useRinggSession: () => useRinggSession,
@@ -77,6 +124,117 @@ var DEFAULT_WIDGET_THEME = {
77
124
  "borderRadius": "16px",
78
125
  "fontFamily": "inherit"
79
126
  };
127
+ var WIDGET_PRESET_THEMES = {
128
+ "light": {
129
+ "primaryColor": "#18181b",
130
+ "primaryTextColor": "#ffffff",
131
+ "backgroundColor": "#ffffff",
132
+ "surfaceColor": "#f4f4f5",
133
+ "textColor": "#18181b",
134
+ "mutedTextColor": "#71717a",
135
+ "borderColor": "#e4e4e7"
136
+ },
137
+ "warm": {
138
+ "primaryColor": "#57534e",
139
+ "primaryTextColor": "#ffffff",
140
+ "backgroundColor": "#fefefe",
141
+ "surfaceColor": "#fafaf9",
142
+ "textColor": "#1c1917",
143
+ "mutedTextColor": "#78716c",
144
+ "borderColor": "#e7e5e4"
145
+ },
146
+ "blue": {
147
+ "primaryColor": "#2563eb",
148
+ "primaryTextColor": "#ffffff",
149
+ "backgroundColor": "#fafcff",
150
+ "surfaceColor": "#f0f7ff",
151
+ "textColor": "#1e3a8a",
152
+ "mutedTextColor": "#64748b",
153
+ "borderColor": "#dbeafe"
154
+ },
155
+ "indigo": {
156
+ "primaryColor": "#6366f1",
157
+ "primaryTextColor": "#ffffff",
158
+ "backgroundColor": "#fbfbff",
159
+ "surfaceColor": "#f5f5ff",
160
+ "textColor": "#312e81",
161
+ "mutedTextColor": "#6b7280",
162
+ "borderColor": "#e0e7ff"
163
+ },
164
+ "green": {
165
+ "primaryColor": "#16a34a",
166
+ "primaryTextColor": "#ffffff",
167
+ "backgroundColor": "#fbfefb",
168
+ "surfaceColor": "#f0fdf4",
169
+ "textColor": "#14532d",
170
+ "mutedTextColor": "#6b7280",
171
+ "borderColor": "#dcfce7"
172
+ },
173
+ "orange": {
174
+ "primaryColor": "#ea580c",
175
+ "primaryTextColor": "#ffffff",
176
+ "backgroundColor": "#fffdfb",
177
+ "surfaceColor": "#fff7ed",
178
+ "textColor": "#7c2d12",
179
+ "mutedTextColor": "#78716c",
180
+ "borderColor": "#ffedd5"
181
+ },
182
+ "dark": {
183
+ "primaryColor": "#fafafa",
184
+ "primaryTextColor": "#18181b",
185
+ "backgroundColor": "#18181b",
186
+ "surfaceColor": "#27272a",
187
+ "textColor": "#fafafa",
188
+ "mutedTextColor": "#a1a1aa",
189
+ "borderColor": "#3f3f46"
190
+ },
191
+ "sunset": {
192
+ "primaryColor": "linear-gradient(135deg, #f97316, #ef4444)",
193
+ "primaryTextColor": "#ffffff",
194
+ "backgroundColor": "#fffbf7",
195
+ "surfaceColor": "#fff3e8",
196
+ "textColor": "#7c2d12",
197
+ "mutedTextColor": "#9a3412",
198
+ "borderColor": "#fed7aa"
199
+ },
200
+ "ocean": {
201
+ "primaryColor": "linear-gradient(135deg, #667eea, #764ba2)",
202
+ "primaryTextColor": "#ffffff",
203
+ "backgroundColor": "#faf9ff",
204
+ "surfaceColor": "#f0edff",
205
+ "textColor": "#312e81",
206
+ "mutedTextColor": "#6366f1",
207
+ "borderColor": "#ddd6fe"
208
+ },
209
+ "emerald": {
210
+ "primaryColor": "linear-gradient(135deg, #10b981, #0ea5e9)",
211
+ "primaryTextColor": "#ffffff",
212
+ "backgroundColor": "#f7fdfb",
213
+ "surfaceColor": "#ecfdf5",
214
+ "textColor": "#064e3b",
215
+ "mutedTextColor": "#047857",
216
+ "borderColor": "#a7f3d0"
217
+ },
218
+ "midnight": {
219
+ "primaryColor": "linear-gradient(135deg, #6366f1, #8b5cf6)",
220
+ "primaryTextColor": "#ffffff",
221
+ "backgroundColor": "#18181b",
222
+ "surfaceColor": "#27272a",
223
+ "textColor": "#fafafa",
224
+ "mutedTextColor": "#a1a1aa",
225
+ "borderColor": "#3f3f46"
226
+ },
227
+ "policybazaar": {
228
+ "primaryColor": "linear-gradient(88deg, #9403fd, #00adfe)",
229
+ "primaryTextColor": "#ffffff",
230
+ "backgroundColor": "#ffffff",
231
+ "surfaceColor": "#f4f2ff",
232
+ "agentBubbleColor": "linear-gradient(107deg, #f0fff9, #f7eeff)",
233
+ "textColor": "#18181b",
234
+ "mutedTextColor": "#64748b",
235
+ "borderColor": "#e6e2ff"
236
+ }
237
+ };
80
238
  var TIMING = {
81
239
  "chatWidgetGraceMs": 800,
82
240
  "chatWidgetAfterTextMs": 120,
@@ -97,6 +255,47 @@ var WIDGET_DEFAULTS = {
97
255
  };
98
256
 
99
257
  // ../core/dist/ports/event-bus.js
258
+ function createDomEventBus() {
259
+ const listeners = /* @__PURE__ */ new Map();
260
+ return {
261
+ emit(event, payload) {
262
+ if (typeof window !== "undefined") {
263
+ window.dispatchEvent(new CustomEvent(event, { detail: payload }));
264
+ }
265
+ },
266
+ on(event, handler) {
267
+ if (typeof window === "undefined")
268
+ return () => {
269
+ };
270
+ const wrapped = ((e) => handler(e.detail));
271
+ let set = listeners.get(event);
272
+ if (!set) {
273
+ set = /* @__PURE__ */ new Set();
274
+ listeners.set(event, set);
275
+ }
276
+ set.add(wrapped);
277
+ window.addEventListener(event, wrapped);
278
+ return () => {
279
+ window.removeEventListener(event, wrapped);
280
+ listeners.get(event)?.delete(wrapped);
281
+ };
282
+ },
283
+ off(event) {
284
+ const remove = (name, handlers) => {
285
+ handlers.forEach((h) => window.removeEventListener(name, h));
286
+ };
287
+ if (event) {
288
+ const handlers = listeners.get(event);
289
+ if (handlers)
290
+ remove(event, handlers);
291
+ listeners.delete(event);
292
+ } else {
293
+ listeners.forEach((handlers, name) => remove(name, handlers));
294
+ listeners.clear();
295
+ }
296
+ }
297
+ };
298
+ }
100
299
  function createCallbackEventBus() {
101
300
  const listeners = /* @__PURE__ */ new Map();
102
301
  return {
@@ -151,6 +350,13 @@ var silentNotificationPlayer = {
151
350
  };
152
351
 
153
352
  // ../core/dist/api/client.js
353
+ function createStaticUrlResolver(urls) {
354
+ return {
355
+ resolve(mode) {
356
+ return urls[mode];
357
+ }
358
+ };
359
+ }
154
360
  function createRinggApiClient(config) {
155
361
  const { backendUrl, livekitUrl } = config.urlResolver.resolve(config.mode);
156
362
  const authHeaders = config.authorization ? { Authorization: config.authorization } : config.xApiKey ? { "X-API-KEY": config.xApiKey } : {};
@@ -275,11 +481,47 @@ function getDominantColor(color) {
275
481
  return color;
276
482
  return extractColorsFromGradient(color)[0] ?? color;
277
483
  }
484
+ function colorToBackground(color) {
485
+ if (isGradient(color)) {
486
+ return { background: color };
487
+ }
488
+ return { backgroundColor: color };
489
+ }
490
+ function colorToSolid(color) {
491
+ return getDominantColor(color);
492
+ }
493
+ function colorToBorder(color) {
494
+ if (isGradient(color)) {
495
+ return {
496
+ borderImage: `${color} 1`,
497
+ borderStyle: "solid"
498
+ };
499
+ }
500
+ return { borderColor: color };
501
+ }
278
502
 
279
503
  // ../core/dist/theme/theme-engine.js
280
504
  function mergeWidgetTheme(theme) {
281
505
  return { ...DEFAULT_WIDGET_THEME, ...theme };
282
506
  }
507
+ function mergeComponentTheme(componentTheme, widgetTheme) {
508
+ const base = widgetTheme ?? DEFAULT_WIDGET_THEME;
509
+ return {
510
+ primaryColor: base.primaryColor,
511
+ primaryTextColor: base.primaryTextColor,
512
+ backgroundColor: base.backgroundColor,
513
+ surfaceColor: base.surfaceColor,
514
+ textColor: base.textColor,
515
+ mutedTextColor: base.mutedTextColor,
516
+ borderColor: base.borderColor,
517
+ errorColor: base.errorColor,
518
+ successColor: base.successColor,
519
+ fontFamily: base.fontFamily,
520
+ borderRadius: base.borderRadius,
521
+ buttonStyle: base.buttonStyle,
522
+ ...componentTheme
523
+ };
524
+ }
283
525
  function getButtonRadius(style) {
284
526
  switch (style) {
285
527
  case "pill":
@@ -291,6 +533,25 @@ function getButtonRadius(style) {
291
533
  return "12px";
292
534
  }
293
535
  }
536
+ function getLuminance(hexColor) {
537
+ const solid = getDominantColor(hexColor);
538
+ const hex = solid.replace("#", "");
539
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
540
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
541
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
542
+ const toLinear = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
543
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
544
+ }
545
+ function isLightColor(hexColor) {
546
+ try {
547
+ return getLuminance(hexColor) > 0.5;
548
+ } catch {
549
+ return true;
550
+ }
551
+ }
552
+ function getContrastingTextColor(bgColor) {
553
+ return isLightColor(bgColor) ? "#212121" : "#ffffff";
554
+ }
294
555
 
295
556
  // ../core/dist/rpc/formatter.js
296
557
  function transformRpcToComponent(componentName, rpcPayload) {
@@ -474,6 +735,68 @@ function formatDynamicData(payload) {
474
735
  };
475
736
  }
476
737
 
738
+ // ../core/dist/components/helpers.js
739
+ function groupSlotsByDate(slots) {
740
+ const grouped = /* @__PURE__ */ new Map();
741
+ slots.forEach((slot) => {
742
+ const dateKey = new Date(slot.datetime).toISOString().split("T")[0];
743
+ if (!grouped.has(dateKey)) {
744
+ grouped.set(dateKey, []);
745
+ }
746
+ grouped.get(dateKey).push(slot);
747
+ });
748
+ return Array.from(grouped.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([dateKey, dateSlots]) => ({
749
+ date: dateKey,
750
+ dateLabel: new Date(dateKey).toLocaleDateString("en-US", {
751
+ weekday: "short",
752
+ month: "short",
753
+ day: "numeric"
754
+ }),
755
+ slots: dateSlots.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime())
756
+ }));
757
+ }
758
+ function formatSlotTime(datetime, timezone) {
759
+ return new Date(datetime).toLocaleTimeString("en-US", {
760
+ hour: "numeric",
761
+ minute: "2-digit",
762
+ hour12: true,
763
+ timeZone: timezone
764
+ });
765
+ }
766
+ function formatSlotDate(datetime, timezone) {
767
+ return new Date(datetime).toLocaleDateString("en-US", {
768
+ weekday: "long",
769
+ month: "long",
770
+ day: "numeric",
771
+ timeZone: timezone
772
+ });
773
+ }
774
+ function buildPayload(template, values) {
775
+ const result = {};
776
+ for (const [key, raw] of Object.entries(template)) {
777
+ result[key] = raw.replace(/\{\{(\w+)\}\}/g, (_, name) => values[name] ?? "");
778
+ }
779
+ return result;
780
+ }
781
+ function isCalendarBooking(payload) {
782
+ return payload.component_type === "calendar_booking";
783
+ }
784
+ function isForm(payload) {
785
+ return payload.component_type === "form";
786
+ }
787
+ function isButtons(payload) {
788
+ return payload.component_type === "buttons";
789
+ }
790
+ function isConfirmation(payload) {
791
+ return payload.component_type === "confirmation";
792
+ }
793
+ function isInteractiveFlow(payload) {
794
+ return payload.component_type === "interactive_flow";
795
+ }
796
+ function isBlocks(payload) {
797
+ return payload.component_type === "blocks";
798
+ }
799
+
477
800
  // ../core/dist/stores/store.js
478
801
  function createStore(build) {
479
802
  const listeners = /* @__PURE__ */ new Set();
@@ -1263,7 +1586,7 @@ var useRinggSlashCommands = (controller) => useStoreSnapshot(controller.slashCom
1263
1586
 
1264
1587
  // src/RinggWidget.tsx
1265
1588
  var import_react24 = require("react");
1266
- var import_react_native28 = require("react-native");
1589
+ var import_react_native31 = require("react-native");
1267
1590
  var import_lucide_react_native10 = require("lucide-react-native");
1268
1591
 
1269
1592
  // src/components/action-button.tsx
@@ -1871,7 +2194,7 @@ var import_react_native13 = require("react-native");
1871
2194
  var import_lucide_react_native8 = require("lucide-react-native");
1872
2195
 
1873
2196
  // src/lib/component-types.ts
1874
- function groupSlotsByDate(slots) {
2197
+ function groupSlotsByDate2(slots) {
1875
2198
  const grouped = /* @__PURE__ */ new Map();
1876
2199
  slots.forEach((slot) => {
1877
2200
  const date = new Date(slot.datetime);
@@ -1890,7 +2213,7 @@ function groupSlotsByDate(slots) {
1890
2213
  };
1891
2214
  });
1892
2215
  }
1893
- function formatSlotTime(datetime, timezone) {
2216
+ function formatSlotTime2(datetime, timezone) {
1894
2217
  const date = new Date(datetime);
1895
2218
  return date.toLocaleTimeString("en-US", {
1896
2219
  hour: "numeric",
@@ -1899,7 +2222,7 @@ function formatSlotTime(datetime, timezone) {
1899
2222
  timeZone: timezone
1900
2223
  });
1901
2224
  }
1902
- function formatSlotDate(datetime, timezone) {
2225
+ function formatSlotDate2(datetime, timezone) {
1903
2226
  const date = new Date(datetime);
1904
2227
  return date.toLocaleDateString("en-US", {
1905
2228
  weekday: "short",
@@ -1908,7 +2231,7 @@ function formatSlotDate(datetime, timezone) {
1908
2231
  timeZone: timezone
1909
2232
  });
1910
2233
  }
1911
- function buildPayload(template, values) {
2234
+ function buildPayload2(template, values) {
1912
2235
  const result = {};
1913
2236
  for (const [key, value] of Object.entries(template)) {
1914
2237
  if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
@@ -1920,13 +2243,13 @@ function buildPayload(template, values) {
1920
2243
  }
1921
2244
  return result;
1922
2245
  }
1923
- function isButtons(payload) {
2246
+ function isButtons2(payload) {
1924
2247
  return payload.component_type === "buttons";
1925
2248
  }
1926
- function isInteractiveFlow(payload) {
2249
+ function isInteractiveFlow2(payload) {
1927
2250
  return payload.component_type === "interactive_flow";
1928
2251
  }
1929
- function isBlocks(payload) {
2252
+ function isBlocks2(payload) {
1930
2253
  return payload.component_type === "blocks";
1931
2254
  }
1932
2255
 
@@ -2138,7 +2461,7 @@ var CalendarStep = ({ payload, isProcessing, onComplete }) => {
2138
2461
  const buttonRadius = getButtonRadius2(theme.buttonStyle);
2139
2462
  const fontFamily = resolveFontFamily(theme.fontFamily);
2140
2463
  const { data } = payload;
2141
- const groupedSlots = (0, import_react8.useMemo)(() => groupSlotsByDate(data.available_slots), [data.available_slots]);
2464
+ const groupedSlots = (0, import_react8.useMemo)(() => groupSlotsByDate2(data.available_slots), [data.available_slots]);
2142
2465
  const [selectedDate, setSelectedDate] = (0, import_react8.useState)(groupedSlots[0]?.date || "");
2143
2466
  const [selectedSlot, setSelectedSlot] = (0, import_react8.useState)(null);
2144
2467
  const selectedDateGroup = groupedSlots.find((g) => g.date === selectedDate);
@@ -2202,7 +2525,7 @@ var CalendarStep = ({ payload, isProcessing, onComplete }) => {
2202
2525
  {
2203
2526
  color: isSelected ? theme.primaryColor : theme.backgroundColor,
2204
2527
  style: [styles9.slot, { borderRadius: buttonRadius, borderColor: isSelected ? solidColor(theme.primaryColor) : theme.borderColor }, isSelected ? styles9.selectedShadow : null],
2205
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Text, { style: [styles9.slotLabel, { color: isSelected ? theme.primaryTextColor : theme.textColor, fontFamily }], numberOfLines: 1, children: formatSlotTime(slot.datetime, data.timezone) })
2528
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Text, { style: [styles9.slotLabel, { color: isSelected ? theme.primaryTextColor : theme.textColor, fontFamily }], numberOfLines: 1, children: formatSlotTime2(slot.datetime, data.timezone) })
2206
2529
  }
2207
2530
  ) }) }, slot.id);
2208
2531
  }) })
@@ -2744,8 +3067,8 @@ var EnterFade = ({ translateX = 0, translateY = 0, durationMs = ENTER_DURATION_M
2744
3067
  );
2745
3068
  };
2746
3069
  var InteractiveFlow = ({ initialComponent, onApiCall, onComplete, onSendResponse, onSelectionDisplayed, quickReplyAlign = "end" }) => {
2747
- const isMultiStepFlow = isInteractiveFlow(initialComponent);
2748
- const isFreeButtonGroup = !isMultiStepFlow && isButtons(initialComponent) && initialComponent.data.presentation === "free";
3070
+ const isMultiStepFlow = isInteractiveFlow2(initialComponent);
3071
+ const isFreeButtonGroup = !isMultiStepFlow && isButtons2(initialComponent) && initialComponent.data.presentation === "free";
2749
3072
  const widgetTheme = useWidgetTheme();
2750
3073
  const theme = (0, import_react12.useMemo)(
2751
3074
  () => mergeTheme({
@@ -2791,7 +3114,7 @@ var InteractiveFlow = ({ initialComponent, onApiCall, onComplete, onSendResponse
2791
3114
  try {
2792
3115
  const isLastStep = currentStepIndex === totalSteps - 1;
2793
3116
  const selectedButtonId = typeof stepData.selected_button === "string" ? stepData.selected_button : null;
2794
- const buttonsComponent = currentComponent !== void 0 && isButtons(currentComponent) ? currentComponent : null;
3117
+ const buttonsComponent = currentComponent !== void 0 && isButtons2(currentComponent) ? currentComponent : null;
2795
3118
  const submittedButton = buttonsComponent && selectedButtonId ? buttonsComponent.data.buttons.find((button) => button.id === selectedButtonId) ?? { id: selectedButtonId, label: selectedButtonId, action: { type: "trigger_component" } } : null;
2796
3119
  const shouldShowSubmittedSelection = buttonsComponent?.data.completionDisplay === "selected_item" && !!submittedButton;
2797
3120
  if (onSendResponse) {
@@ -2830,7 +3153,7 @@ var InteractiveFlow = ({ initialComponent, onApiCall, onComplete, onSendResponse
2830
3153
  for (const [key, value] of Object.entries(newAllData)) {
2831
3154
  stringifiedData[key] = Array.isArray(value) ? JSON.stringify(value) : String(value);
2832
3155
  }
2833
- const apiPayload = buildPayload(on_complete.payload, {
3156
+ const apiPayload = buildPayload2(on_complete.payload, {
2834
3157
  ...stringifiedData,
2835
3158
  all_data: JSON.stringify(newAllData)
2836
3159
  });
@@ -2909,8 +3232,8 @@ var InteractiveFlow = ({ initialComponent, onApiCall, onComplete, onSendResponse
2909
3232
  handleStepComplete({
2910
3233
  slot_id: slotId,
2911
3234
  slot_datetime: slotDatetime,
2912
- slot_time: formatSlotTime(slotDatetime, timezone),
2913
- slot_date: formatSlotDate(slotDatetime, timezone)
3235
+ slot_time: formatSlotTime2(slotDatetime, timezone),
3236
+ slot_date: formatSlotDate2(slotDatetime, timezone)
2914
3237
  });
2915
3238
  }
2916
3239
  }
@@ -4245,7 +4568,7 @@ var FeedbackScreen = ({ callMode, isSubmitting, feedbackScreenConfig, messages,
4245
4568
  transcript.map((message, index) => {
4246
4569
  if (message.componentType && message.componentData) {
4247
4570
  const componentData = message.componentData;
4248
- if (isBlocks(componentData)) {
4571
+ if (isBlocks2(componentData)) {
4249
4572
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.View, { testID: ringgId("blocks-widget"), style: styles16.replayed, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(blocks_step_default, { config: componentData.data, disabled: true, onAction: async () => {
4250
4573
  } }) }, `c_${message.timestamp}__${index}`);
4251
4574
  }
@@ -4941,370 +5264,83 @@ var renderTextWithLinks = (text, links, linkStyle) => {
4941
5264
  return parts;
4942
5265
  };
4943
5266
 
4944
- // src/RinggWidget.tsx
4945
- var import_jsx_runtime28 = require("react/jsx-runtime");
4946
- var PANEL_MAX_WIDTH = 400;
4947
- var PANEL_HEIGHT = 456;
4948
- var PANEL_HEIGHT_RATIO = 0.9;
4949
- var EDGE_INSET = 16;
4950
- var BOTTOM_SAFE_INSET = 24;
4951
- var TRIGGER_SIZE = 52;
4952
- var TRIGGER_RADIUS = 20;
4953
- var DEFAULT_TRIGGER_ICON_SIZE = 24;
4954
- var PANEL_SPRING = { stiffness: 380, damping: 30 };
4955
- var TRIGGER_SPRING = { stiffness: 400, damping: 25 };
4956
- var LEGAL_LINKS = { "Privacy Policy": "https://www.ringg.ai/privacy", "T&C": "https://www.ringg.ai/terms" };
4957
- var LEGAL_TEXT = "By continuing, you agree to our\nPrivacy Policy and T&C.";
4958
- var isFreeButtonsMessage = (message) => {
4959
- const componentData = message?.componentData;
4960
- return !!componentData && isButtons(componentData) && componentData.data.presentation === "free";
5267
+ // src/config/default-urls.ts
5268
+ var DEFAULT_ENVIRONMENT_URLS = {
5269
+ dev: {
5270
+ backendUrl: "https://calling-dev.ringg.ai/ca/api/v0",
5271
+ livekitUrl: "wss://ringg-ai-dev-92tubwpz.livekit.cloud"
5272
+ },
5273
+ stage: {
5274
+ backendUrl: "https://stage-api.ringg.ai/ca/api/v0",
5275
+ livekitUrl: "wss://mercury.webrtc-stage.ringg.ai"
5276
+ },
5277
+ prod: {
5278
+ backendUrl: "https://prod-api.ringg.ai/ca/api/v0",
5279
+ livekitUrl: "wss://mercury.webrtc.ringg.ai"
5280
+ }
4961
5281
  };
4962
- var isAttachableAgentMessage = (message) => !!message && !message.componentType && !message.isSelf && Boolean(message.message.trim());
4963
- var WidgetPanel = ({ controller, room, width, height }) => {
4964
- const theme = useWidgetTheme();
4965
- const { config } = controller;
4966
- const {
4967
- title = WIDGET_DEFAULTS.title,
4968
- description = WIDGET_DEFAULTS.description,
4969
- buttons,
4970
- defaultExpanded = WIDGET_DEFAULTS.defaultExpanded,
4971
- hideTabSelector = WIDGET_DEFAULTS.hideTabSelector,
4972
- bypassStartScreen = WIDGET_DEFAULTS.bypassStartScreen,
4973
- logoUrl,
4974
- logoStyles,
4975
- typingWords,
4976
- legalDisclaimer,
4977
- feedbackScreen,
4978
- voiceCall
4979
- } = config;
4980
- const shell = useRinggShell(controller);
4981
- const session = useRinggSession(controller);
4982
- const { messages } = useRinggMessages(controller);
4983
- const { isTyping: isAgentTyping } = useRinggTyping(controller);
4984
- const { completedFlowIds } = useRinggComponents(controller);
4985
- const { commands } = useRinggSlashCommands(controller);
4986
- const [alertEndCall, setAlertEndCall] = (0, import_react24.useState)(false);
4987
- const microphone = useMicrophone(room);
4988
- const callMode = shell.callMode;
4989
- const showFeedback = shell.viewState === "feedback";
4990
- const isLoading = session.isLoading;
4991
- const error = session.error;
4992
- const connectionState = session.connectionState;
4993
- const isCalling = connectionState === "connected";
4994
- const isSessionLive = session.isSessionLive;
4995
- const isAudioMode = callMode === "audio";
4996
- const showVoiceAnimation = isAudioMode && isCalling && (voiceCall?.showAnimation ?? WIDGET_DEFAULTS.voiceCallShowAnimation);
4997
- const showVoiceTranscript = !isAudioMode || (voiceCall?.showTranscript ?? WIDGET_DEFAULTS.voiceCallShowTranscript);
4998
- const mergedSlashCommands = (0, import_react24.useMemo)(() => [...commands], [commands]);
4999
- const sortedMessages = (0, import_react24.useMemo)(() => [...messages].sort((a, b) => a.timestamp - b.timestamp), [messages]);
5000
- const transcriptRef = (0, import_react24.useRef)(null);
5001
- const previousMessagesCountRef = (0, import_react24.useRef)(0);
5002
- (0, import_react24.useEffect)(() => {
5003
- const newMessageAdded = messages.length > previousMessagesCountRef.current;
5004
- previousMessagesCountRef.current = messages.length;
5005
- transcriptRef.current?.scrollToEnd({ animated: newMessageAdded });
5006
- }, [messages, isAgentTyping]);
5007
- const handleCallStart = (mediaType) => void controller.startCall(mediaType);
5008
- const handleCallEnd = () => {
5009
- setAlertEndCall(false);
5010
- void controller.endCall();
5011
- };
5012
- const handleFeedbackSubmit = (rating, comment) => void controller.submitFeedback(rating, comment);
5013
- const handleFeedbackSkip = () => controller.skipFeedback();
5014
- const handleUserMessageSent = () => controller.typing.showOnUserSend();
5015
- const handleFlowComplete = (componentId) => controller.markFlowComplete(componentId);
5016
- const handleSelectionDisplayed = (componentId, label) => controller.displaySelection(componentId, label);
5017
- const sendComponentResponse = (componentId, responseData) => controller.sendComponentResponse(componentId, responseData);
5018
- const sendSlashCommand = (command) => void controller.sendSlashCommand(command);
5019
- const callComponentApi = controller.callComponentApi;
5020
- const panelRadius = resolveRadius(theme.borderRadius, 16);
5021
- const fontFamily = resolveFontFamily(theme.fontFamily);
5022
- const chrome = /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
5023
- !defaultExpanded && (isSessionLive || showFeedback) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Touchable, { testID: ringgId("minimize-button"), accessibilityLabel: "Minimize widget", onPress: () => controller.minimizeWidget(), style: [styles24.chromeButton, styles24.minimizeButton], children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_lucide_react_native10.Minus, { size: 16, color: solidColor(theme.mutedTextColor) }) }) : null,
5024
- !defaultExpanded ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5025
- Touchable,
5026
- {
5027
- testID: ringgId("close-button"),
5028
- accessibilityLabel: isSessionLive ? "End call" : "Close widget",
5029
- onPress: () => isSessionLive ? setAlertEndCall(true) : showFeedback ? handleFeedbackSkip() : controller.minimizeWidget(),
5030
- style: [styles24.chromeButton, styles24.closeButton],
5031
- children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_lucide_react_native10.X, { size: 16, color: solidColor(theme.mutedTextColor) })
5032
- }
5033
- ) : null
5034
- ] });
5035
- const transcript = /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
5036
- import_react_native28.ScrollView,
5037
- {
5038
- testID: ringgId("transcript"),
5039
- ref: transcriptRef,
5040
- style: styles24.transcript,
5041
- contentContainerStyle: [styles24.transcriptList, !showVoiceAnimation && styles24.transcriptTopClearance, !isAudioMode && styles24.transcriptBottomClearance],
5042
- keyboardShouldPersistTaps: "handled",
5043
- showsVerticalScrollIndicator: false,
5044
- children: [
5045
- sortedMessages.map((segment, index) => {
5046
- if (segment.kind === "system") return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(system_log_default, { label: segment.message, meta: segment.systemMeta, level: segment.systemLevel }, `sys_${segment.timestamp}_${index}`);
5047
- const previousSegment = sortedMessages[index - 1];
5048
- const nextSegment = sortedMessages[index + 1];
5049
- const attachFreeButtonsToPrevious = isFreeButtonsMessage(segment) && isAttachableAgentMessage(previousSegment);
5050
- const skipStandaloneRender = isAttachableAgentMessage(segment) && isFreeButtonsMessage(nextSegment);
5051
- if (skipStandaloneRender) return null;
5052
- if (attachFreeButtonsToPrevious && previousSegment && segment.componentData) {
5053
- const componentData = segment.componentData;
5054
- const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
5055
- if (isCompletedSelectedItem) return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(message_item_default, { itemIndex: index, message: previousSegment }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
5056
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react24.Fragment, { children: [
5057
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(message_item_default, { itemIndex: index, message: previousSegment }),
5058
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.View, { testID: ringgId("attached-buttons"), style: styles24.fullWidth, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5059
- interactive_flow_default,
5060
- {
5061
- initialComponent: segment.componentData,
5062
- onApiCall: callComponentApi,
5063
- onComplete: () => handleFlowComplete(componentData.component_id),
5064
- onSendResponse: sendComponentResponse,
5065
- onSelectionDisplayed: handleSelectionDisplayed,
5066
- quickReplyAlign: isAudioMode ? "center" : "end"
5067
- }
5068
- ) })
5069
- ] }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
5070
- }
5071
- if (segment.componentType === "blocks" && segment.componentData) {
5072
- const blocksPayload = segment.componentData;
5073
- if (isBlocks(blocksPayload)) {
5074
- const blocksConfig = blocksPayload.data;
5075
- const blockId = blocksPayload.component_id;
5076
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.View, { testID: ringgId("blocks-widget"), style: styles24.fullWidth, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5077
- blocks_step_default,
5078
- {
5079
- config: blocksConfig,
5080
- disabled: completedFlowIds.has(blockId),
5081
- onAction: (action) => {
5082
- handleUserMessageSent();
5083
- handleFlowComplete(blockId);
5084
- const selection = typeof action.value === "string" || typeof action.value === "number" ? String(action.value) : action.label;
5085
- if (selection) handleSelectionDisplayed(blockId, selection);
5086
- return controller.sendBlocksAction(blocksConfig.tool_id, blockId, { action_id: action.action_id, value: action.value, values: action.values });
5087
- }
5088
- }
5089
- ) }, `blocks_${blockId}`);
5090
- }
5091
- }
5092
- if (segment.componentType && segment.componentData) {
5093
- const componentData = segment.componentData;
5094
- const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
5095
- if (isCompletedSelectedItem) return null;
5096
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5097
- interactive_flow_default,
5098
- {
5099
- initialComponent: segment.componentData,
5100
- onApiCall: callComponentApi,
5101
- onComplete: () => handleFlowComplete(componentData.component_id),
5102
- onSendResponse: sendComponentResponse,
5103
- onSelectionDisplayed: handleSelectionDisplayed,
5104
- quickReplyAlign: isAudioMode ? "center" : "end"
5105
- },
5106
- `flow_${segment.componentData.component_id}_${index}`
5107
- );
5108
- }
5109
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(message_item_default, { itemIndex: index, message: segment }, `${segment.timestamp}__${index}`);
5110
- }),
5111
- isAgentTyping ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(typing_indicator_default, { words: typingWords }) : null
5112
- ]
5113
- }
5114
- );
5115
- const controls = /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5116
- call_controls_default,
5117
- {
5118
- buttonConfig: buttons,
5119
- callMode,
5120
- isCalling: isSessionLive,
5121
- enabledSlashCommands: mergedSlashCommands,
5122
- micEnabled: microphone?.enabled ?? true,
5123
- onToggleMic: microphone?.toggle,
5124
- onSlashCommand: sendSlashCommand,
5125
- onUserMessage: handleUserMessageSent,
5126
- onCallEnd: handleCallEnd,
5127
- onSendMessage: (message) => controller.sendMessage(message)
5128
- }
5129
- );
5130
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
5131
- import_react_native28.View,
5132
- {
5133
- testID: ringgId("widget-root"),
5134
- style: [styles24.panel, { width, height, borderRadius: panelRadius, backgroundColor: solidColor(theme.backgroundColor), borderColor: solidColor(theme.borderColor) }],
5135
- children: [
5136
- chrome,
5137
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5138
- ConfirmDialog,
5139
- {
5140
- open: alertEndCall,
5141
- title: `Are you sure you want to end ${callMode === "text" ? "chat" : "call"}?`,
5142
- description: "Your current conversation will be closed and cannot be accessed later.",
5143
- confirmLabel: `End ${callMode === "text" ? "Chat" : "Call"}`,
5144
- cancelLabel: "Cancel",
5145
- onConfirm: handleCallEnd,
5146
- onCancel: () => setAlertEndCall(false)
5147
- }
5148
- ),
5149
- showFeedback ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5150
- feedback_screen_default,
5151
- {
5152
- callMode,
5153
- isSubmitting: isLoading,
5154
- feedbackScreenConfig: feedbackScreen,
5155
- messages,
5156
- title,
5157
- logoUrl,
5158
- logoStyles,
5159
- onSubmit: handleFeedbackSubmit,
5160
- onSkip: handleFeedbackSkip
5161
- }
5162
- ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { testID: ringgId("widget-body"), style: [styles24.body, isSessionLive ? styles24.bodyLive : styles24.bodyIdle], children: [
5163
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(header_default, { isCalling: isSessionLive, isLoading, title, description, logoUrl, logoStyles }),
5164
- showVoiceAnimation ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.View, { testID: ringgId("voice-animation-wrapper"), style: [styles24.voiceWrapper, showVoiceTranscript && messages.length > 0 ? styles24.voiceWrapperCompact : styles24.voiceWrapperFull], children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(VoiceAnimationLeaf, { room }) }) : null,
5165
- isSessionLive && !showVoiceAnimation && (messages.length === 0 && !isAgentTyping || !showVoiceTranscript) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.View, { testID: ringgId("body-spacer"), style: styles24.bodySpacer }) : null,
5166
- (messages.length > 0 || isAgentTyping) && isSessionLive && showVoiceTranscript ? transcript : null,
5167
- isSessionLive && callMode === "text" ? (
5168
- // Transparent bottom bar: it only positions the composer over the
5169
- // transcript, so the gutters stay clear while messages scroll under.
5170
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { testID: ringgId("controls-bar"), style: styles24.controlsBar, children: [
5171
- error.hasError ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Text, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null,
5172
- controls
5173
- ] })
5174
- ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
5175
- controls,
5176
- error.hasError ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Text, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null
5177
- ] }),
5178
- !isSessionLive && isLoading ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { testID: ringgId("connecting-status"), children: [
5179
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Text, { testID: ringgId("connecting-label"), style: [styles24.connecting, { color: theme.mutedTextColor, fontFamily }], children: "Connecting..." }),
5180
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(loading_bar_default, { ariaLabel: callMode === "text" ? "Starting chat" : "Connecting" })
5181
- ] }) : null,
5182
- !isLoading && connectionState === "disconnected" && (!bypassStartScreen || error.hasError) ? hideTabSelector ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(action_button_default, { callMode, buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { testID: ringgId("start-actions"), style: styles24.startActions, children: [
5183
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(action_button_default, { dual: true, callMode: "text", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }),
5184
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(action_button_default, { dual: true, callMode: "audio", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart })
5185
- ] }) : null,
5186
- !isSessionLive ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Text, { testID: ringgId("legal-disclaimer"), style: [styles24.legal, { color: theme.mutedTextColor, fontFamily }], children: legalDisclaimer ? renderTextWithLinks(legalDisclaimer.text, legalDisclaimer.links) : renderTextWithLinks(LEGAL_TEXT, LEGAL_LINKS, { color: solidColor(theme.primaryColor), textDecorationLine: "underline" }) }) : null
5187
- ] })
5188
- ]
5189
- }
5190
- );
5191
- };
5192
- var RinggWidget = ({ controller, room }) => {
5193
- const shell = useRinggShell(controller);
5194
- const { width: screenWidth, height: screenHeight } = (0, import_react_native28.useWindowDimensions)();
5195
- const { buttons, defaultExpanded = WIDGET_DEFAULTS.defaultExpanded, widgetPosition } = controller.config;
5196
- const isOpen = shell.viewState === "open" || shell.viewState === "feedback";
5197
- const hideTriggerOnExpand = widgetPosition?.hideTriggerOnExpand ?? true;
5198
- const showTrigger = !defaultExpanded && (!hideTriggerOnExpand || !isOpen);
5199
- const panel = usePresence(isOpen, PANEL_SPRING);
5200
- const trigger = usePresence(showTrigger, TRIGGER_SPRING);
5201
- const panelWidth = Math.min(screenWidth - EDGE_INSET * 2, PANEL_MAX_WIDTH);
5202
- const panelHeight = Math.min(screenHeight * PANEL_HEIGHT_RATIO, PANEL_HEIGHT);
5203
- const triggerIconSize = toSize(buttons?.modalTrigger?.icon?.size, DEFAULT_TRIGGER_ICON_SIZE);
5204
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(WidgetThemeProvider, { theme: controller.theme, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { pointerEvents: "box-none", style: import_react_native28.StyleSheet.absoluteFill, children: [
5205
- panel.mounted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.KeyboardAvoidingView, { pointerEvents: "box-none", behavior: import_react_native28.Platform.OS === "ios" ? "padding" : void 0, style: styles24.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5206
- import_react_native28.Animated.View,
5207
- {
5208
- renderToHardwareTextureAndroid: true,
5209
- shouldRasterizeIOS: true,
5210
- style: [
5211
- styles24.panelAnchor,
5212
- {
5213
- opacity: panel.progress,
5214
- transform: [{ translateY: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }, { scale: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) }]
5215
- }
5216
- ],
5217
- children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(WidgetPanel, { controller, room, width: panelWidth, height: panelHeight })
5218
- }
5219
- ) }) : null,
5220
- trigger.mounted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(WidgetTrigger, { controller, iconSize: triggerIconSize, progress: trigger.progress }) : null
5221
- ] }) });
5222
- };
5223
- var WidgetTrigger = ({ controller, iconSize, progress }) => {
5224
- const theme = useWidgetTheme();
5225
- const { buttons } = controller.config;
5226
- return (
5227
- // Web pops the trigger with a spring and fades it out again when the panel
5228
- // takes over; both ends of that are visible on every open/close cycle.
5229
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Animated.View, { style: [styles24.trigger, { opacity: progress, transform: [{ scale: progress.interpolate({ inputRange: [0, 1], outputRange: [0.8, 1] }) }] }], children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Touchable, { testID: ringgId("trigger-button"), accessibilityLabel: "Open call widget", onPress: () => controller.handleTriggerClick(), children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(GradientFill, { color: theme.primaryColor, style: [styles24.triggerSurface, toViewStyle(buttons?.modalTrigger?.styles)], children: buttons?.modalTrigger?.icon?.url ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5230
- import_react_native28.Image,
5231
- {
5232
- testID: ringgId("trigger-icon"),
5233
- accessibilityLabel: "Widget trigger",
5234
- source: { uri: buttons.modalTrigger.icon.url },
5235
- resizeMode: "contain",
5236
- style: { width: iconSize, height: iconSize }
5237
- }
5238
- ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(PhoneCallRingIcon, { testID: ringgId("trigger-icon"), size: iconSize, fill: solidColor(theme.primaryTextColor) }) }) }) })
5239
- );
5240
- };
5241
- var styles24 = import_react_native28.StyleSheet.create({
5242
- overlay: { ...import_react_native28.StyleSheet.absoluteFill, justifyContent: "flex-end", alignItems: "flex-end" },
5243
- panelAnchor: { marginHorizontal: EDGE_INSET, marginTop: EDGE_INSET, marginBottom: BOTTOM_SAFE_INSET },
5244
- panel: {
5245
- borderWidth: import_react_native28.StyleSheet.hairlineWidth,
5246
- // `overflow: hidden` is what keeps the transcript, the header bar and the
5247
- // rounded corners agreeing with each other.
5248
- overflow: "hidden",
5249
- // Web stacks three shadows at 1-2% alpha — the panel sits just off the page
5250
- // rather than floating above it. 12% read as a hard slab, and on Android
5251
- // `elevation` is the only one of these that applies, so it was the entire
5252
- // effect instead of a supporting layer.
5253
- shadowColor: "#000000",
5254
- shadowOpacity: 0.05,
5255
- shadowRadius: 20,
5256
- shadowOffset: { width: 0, height: 8 },
5257
- elevation: 4
5258
- },
5259
- trigger: { position: "absolute", right: EDGE_INSET, bottom: BOTTOM_SAFE_INSET },
5260
- triggerSurface: { width: TRIGGER_SIZE, height: TRIGGER_SIZE, borderRadius: TRIGGER_RADIUS, alignItems: "center", justifyContent: "center" },
5261
- chromeButton: { position: "absolute", top: 10, zIndex: 20, width: 28, height: 28, borderRadius: 14, alignItems: "center", justifyContent: "center" },
5262
- minimizeButton: { right: 42 },
5263
- closeButton: { right: 10 },
5264
- body: { flex: 1 },
5265
- bodyLive: { paddingHorizontal: 20, paddingVertical: 8, gap: 10 },
5266
- bodyIdle: { padding: 24, gap: 12 },
5267
- bodySpacer: { flex: 1, marginTop: 56 },
5268
- voiceWrapper: { width: "100%", alignItems: "stretch", justifyContent: "center", marginTop: 56 },
5269
- voiceWrapperCompact: { height: 56, flexShrink: 0 },
5270
- voiceWrapperFull: { flex: 1, minHeight: 0 },
5271
- transcript: { flex: 1 },
5272
- // `justifyContent: flex-end` is web's `mt-auto`: a short conversation sits at
5273
- // the bottom of the panel rather than floating at the top.
5274
- transcriptList: { flexGrow: 1, justifyContent: "flex-end", gap: 12 },
5275
- transcriptTopClearance: { paddingTop: 64 },
5276
- transcriptBottomClearance: { paddingBottom: 80 },
5277
- fullWidth: { width: "100%" },
5278
- controlsBar: { position: "absolute", zIndex: 10, bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingVertical: 12 },
5279
- error: { textAlign: "center", fontSize: 14, marginBottom: 8 },
5280
- connecting: { fontSize: 14, textAlign: "center" },
5281
- startActions: { flexDirection: "row", alignItems: "center", gap: 8, width: "100%" },
5282
- legal: { fontSize: 12, lineHeight: 20, textAlign: "center" }
5283
- });
5284
-
5285
- // src/transport/livekit-transport.ts
5286
- var import_react_native30 = require("@livekit/react-native");
5287
- var import_livekit_client5 = require("livekit-client");
5288
-
5289
- // src/platform/audio-session.ts
5290
- var import_react_native29 = require("@livekit/react-native");
5291
- var PREFERRED_OUTPUTS = ["bluetooth", "headset", "speaker", "earpiece"];
5292
- var createAudioSession = () => {
5293
- let active = false;
5294
- const start = async () => {
5295
- if (active) return;
5296
- active = true;
5297
- try {
5298
- await import_react_native29.AudioSession.configureAudio({
5299
- android: {
5300
- audioTypeOptions: import_react_native29.AndroidAudioTypePresets.communication,
5301
- preferredOutputList: [...PREFERRED_OUTPUTS]
5302
- },
5303
- ios: { defaultOutput: "speaker" }
5304
- });
5305
- await import_react_native29.AudioSession.startAudioSession();
5306
- } catch {
5307
- }
5282
+ var defaultUrlResolver = createStaticUrlResolver(DEFAULT_ENVIRONMENT_URLS);
5283
+
5284
+ // src/platform/mic-permission.ts
5285
+ var import_react_native28 = require("react-native");
5286
+ var import_react_native_webrtc = require("@livekit/react-native-webrtc");
5287
+ var stopTracks = (stream) => {
5288
+ stream.getTracks().forEach((track) => track.stop());
5289
+ };
5290
+ var probeMicrophone = async () => {
5291
+ try {
5292
+ const stream = await import_react_native_webrtc.mediaDevices.getUserMedia({ audio: true });
5293
+ stopTracks(stream);
5294
+ return true;
5295
+ } catch {
5296
+ return false;
5297
+ }
5298
+ };
5299
+ var ANDROID_RATIONALE = {
5300
+ title: "Microphone access",
5301
+ message: "Voice calls need the microphone to hear you.",
5302
+ buttonPositive: "Allow",
5303
+ buttonNegative: "Not now"
5304
+ };
5305
+ var createNativeMicPermission = () => ({
5306
+ async isGranted() {
5307
+ if (import_react_native28.Platform.OS === "android") {
5308
+ return import_react_native28.PermissionsAndroid.check(import_react_native28.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO);
5309
+ }
5310
+ return probeMicrophone();
5311
+ },
5312
+ async request() {
5313
+ if (import_react_native28.Platform.OS === "android") {
5314
+ const result = await import_react_native28.PermissionsAndroid.request(import_react_native28.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, ANDROID_RATIONALE);
5315
+ return result === import_react_native28.PermissionsAndroid.RESULTS.GRANTED;
5316
+ }
5317
+ return probeMicrophone();
5318
+ }
5319
+ });
5320
+
5321
+ // src/transport/livekit-transport.ts
5322
+ var import_react_native30 = require("@livekit/react-native");
5323
+ var import_livekit_client5 = require("livekit-client");
5324
+
5325
+ // src/platform/audio-session.ts
5326
+ var import_react_native29 = require("@livekit/react-native");
5327
+ var PREFERRED_OUTPUTS = ["bluetooth", "headset", "speaker", "earpiece"];
5328
+ var createAudioSession = () => {
5329
+ let active = false;
5330
+ const start = async () => {
5331
+ if (active) return;
5332
+ active = true;
5333
+ try {
5334
+ await import_react_native29.AudioSession.configureAudio({
5335
+ android: {
5336
+ audioTypeOptions: import_react_native29.AndroidAudioTypePresets.communication,
5337
+ preferredOutputList: [...PREFERRED_OUTPUTS]
5338
+ },
5339
+ ios: { defaultOutput: "speaker" }
5340
+ });
5341
+ await import_react_native29.AudioSession.startAudioSession();
5342
+ } catch {
5343
+ }
5308
5344
  };
5309
5345
  const stop = async () => {
5310
5346
  if (!active) return;
@@ -5630,41 +5666,371 @@ var createLiveKitTransport = (options = {}) => {
5630
5666
  return { transport, room, dispose };
5631
5667
  };
5632
5668
 
5633
- // src/platform/mic-permission.ts
5634
- var import_react_native31 = require("react-native");
5635
- var import_react_native_webrtc = require("@livekit/react-native-webrtc");
5636
- var stopTracks = (stream) => {
5637
- stream.getTracks().forEach((track) => track.stop());
5638
- };
5639
- var probeMicrophone = async () => {
5640
- try {
5641
- const stream = await import_react_native_webrtc.mediaDevices.getUserMedia({ audio: true });
5642
- stopTracks(stream);
5643
- return true;
5644
- } catch {
5645
- return false;
5646
- }
5647
- };
5648
- var ANDROID_RATIONALE = {
5649
- title: "Microphone access",
5650
- message: "Voice calls need the microphone to hear you.",
5651
- buttonPositive: "Allow",
5652
- buttonNegative: "Not now"
5669
+ // src/RinggWidget.tsx
5670
+ var import_jsx_runtime28 = require("react/jsx-runtime");
5671
+ var PANEL_MAX_WIDTH = 400;
5672
+ var PANEL_HEIGHT = 456;
5673
+ var PANEL_HEIGHT_RATIO = 0.9;
5674
+ var EDGE_INSET = 16;
5675
+ var BOTTOM_SAFE_INSET = 24;
5676
+ var TRIGGER_SIZE = 52;
5677
+ var TRIGGER_RADIUS = 20;
5678
+ var DEFAULT_TRIGGER_ICON_SIZE = 24;
5679
+ var PANEL_SPRING = { stiffness: 380, damping: 30 };
5680
+ var TRIGGER_SPRING = { stiffness: 400, damping: 25 };
5681
+ var LEGAL_LINKS = { "Privacy Policy": "https://www.ringg.ai/privacy", "T&C": "https://www.ringg.ai/terms" };
5682
+ var LEGAL_TEXT = "By continuing, you agree to our\nPrivacy Policy and T&C.";
5683
+ var isFreeButtonsMessage = (message) => {
5684
+ const componentData = message?.componentData;
5685
+ return !!componentData && isButtons2(componentData) && componentData.data.presentation === "free";
5653
5686
  };
5654
- var createNativeMicPermission = () => ({
5655
- async isGranted() {
5656
- if (import_react_native31.Platform.OS === "android") {
5657
- return import_react_native31.PermissionsAndroid.check(import_react_native31.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO);
5687
+ var isAttachableAgentMessage = (message) => !!message && !message.componentType && !message.isSelf && Boolean(message.message.trim());
5688
+ var WidgetPanel = ({ controller, room, width, height }) => {
5689
+ const theme = useWidgetTheme();
5690
+ const { config } = controller;
5691
+ const {
5692
+ title = WIDGET_DEFAULTS.title,
5693
+ description = WIDGET_DEFAULTS.description,
5694
+ buttons,
5695
+ defaultExpanded = WIDGET_DEFAULTS.defaultExpanded,
5696
+ hideTabSelector = WIDGET_DEFAULTS.hideTabSelector,
5697
+ bypassStartScreen = WIDGET_DEFAULTS.bypassStartScreen,
5698
+ logoUrl,
5699
+ logoStyles,
5700
+ typingWords,
5701
+ legalDisclaimer,
5702
+ feedbackScreen,
5703
+ voiceCall
5704
+ } = config;
5705
+ const shell = useRinggShell(controller);
5706
+ const session = useRinggSession(controller);
5707
+ const { messages } = useRinggMessages(controller);
5708
+ const { isTyping: isAgentTyping } = useRinggTyping(controller);
5709
+ const { completedFlowIds } = useRinggComponents(controller);
5710
+ const { commands } = useRinggSlashCommands(controller);
5711
+ const [alertEndCall, setAlertEndCall] = (0, import_react24.useState)(false);
5712
+ const microphone = useMicrophone(room);
5713
+ const callMode = shell.callMode;
5714
+ const showFeedback = shell.viewState === "feedback";
5715
+ const isLoading = session.isLoading;
5716
+ const error = session.error;
5717
+ const connectionState = session.connectionState;
5718
+ const isCalling = connectionState === "connected";
5719
+ const isSessionLive = session.isSessionLive;
5720
+ const isAudioMode = callMode === "audio";
5721
+ const showVoiceAnimation = isAudioMode && isCalling && (voiceCall?.showAnimation ?? WIDGET_DEFAULTS.voiceCallShowAnimation);
5722
+ const showVoiceTranscript = !isAudioMode || (voiceCall?.showTranscript ?? WIDGET_DEFAULTS.voiceCallShowTranscript);
5723
+ const mergedSlashCommands = (0, import_react24.useMemo)(() => [...commands], [commands]);
5724
+ const sortedMessages = (0, import_react24.useMemo)(() => [...messages].sort((a, b) => a.timestamp - b.timestamp), [messages]);
5725
+ const transcriptRef = (0, import_react24.useRef)(null);
5726
+ const previousMessagesCountRef = (0, import_react24.useRef)(0);
5727
+ (0, import_react24.useEffect)(() => {
5728
+ const newMessageAdded = messages.length > previousMessagesCountRef.current;
5729
+ previousMessagesCountRef.current = messages.length;
5730
+ transcriptRef.current?.scrollToEnd({ animated: newMessageAdded });
5731
+ }, [messages, isAgentTyping]);
5732
+ const handleCallStart = (mediaType) => void controller.startCall(mediaType);
5733
+ const handleCallEnd = () => {
5734
+ setAlertEndCall(false);
5735
+ void controller.endCall();
5736
+ };
5737
+ const handleFeedbackSubmit = (rating, comment) => void controller.submitFeedback(rating, comment);
5738
+ const handleFeedbackSkip = () => controller.skipFeedback();
5739
+ const handleUserMessageSent = () => controller.typing.showOnUserSend();
5740
+ const handleFlowComplete = (componentId) => controller.markFlowComplete(componentId);
5741
+ const handleSelectionDisplayed = (componentId, label) => controller.displaySelection(componentId, label);
5742
+ const sendComponentResponse = (componentId, responseData) => controller.sendComponentResponse(componentId, responseData);
5743
+ const sendSlashCommand = (command) => void controller.sendSlashCommand(command);
5744
+ const callComponentApi = controller.callComponentApi;
5745
+ const panelRadius = resolveRadius(theme.borderRadius, 16);
5746
+ const fontFamily = resolveFontFamily(theme.fontFamily);
5747
+ const chrome = /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
5748
+ !defaultExpanded && (isSessionLive || showFeedback) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Touchable, { testID: ringgId("minimize-button"), accessibilityLabel: "Minimize widget", onPress: () => controller.minimizeWidget(), style: [styles24.chromeButton, styles24.minimizeButton], children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_lucide_react_native10.Minus, { size: 16, color: solidColor(theme.mutedTextColor) }) }) : null,
5749
+ !defaultExpanded ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5750
+ Touchable,
5751
+ {
5752
+ testID: ringgId("close-button"),
5753
+ accessibilityLabel: isSessionLive ? "End call" : "Close widget",
5754
+ onPress: () => isSessionLive ? setAlertEndCall(true) : showFeedback ? handleFeedbackSkip() : controller.minimizeWidget(),
5755
+ style: [styles24.chromeButton, styles24.closeButton],
5756
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_lucide_react_native10.X, { size: 16, color: solidColor(theme.mutedTextColor) })
5757
+ }
5758
+ ) : null
5759
+ ] });
5760
+ const transcript = /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
5761
+ import_react_native31.ScrollView,
5762
+ {
5763
+ testID: ringgId("transcript"),
5764
+ ref: transcriptRef,
5765
+ style: styles24.transcript,
5766
+ contentContainerStyle: [styles24.transcriptList, !showVoiceAnimation && styles24.transcriptTopClearance, !isAudioMode && styles24.transcriptBottomClearance],
5767
+ keyboardShouldPersistTaps: "handled",
5768
+ showsVerticalScrollIndicator: false,
5769
+ children: [
5770
+ sortedMessages.map((segment, index) => {
5771
+ if (segment.kind === "system") return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(system_log_default, { label: segment.message, meta: segment.systemMeta, level: segment.systemLevel }, `sys_${segment.timestamp}_${index}`);
5772
+ const previousSegment = sortedMessages[index - 1];
5773
+ const nextSegment = sortedMessages[index + 1];
5774
+ const attachFreeButtonsToPrevious = isFreeButtonsMessage(segment) && isAttachableAgentMessage(previousSegment);
5775
+ const skipStandaloneRender = isAttachableAgentMessage(segment) && isFreeButtonsMessage(nextSegment);
5776
+ if (skipStandaloneRender) return null;
5777
+ if (attachFreeButtonsToPrevious && previousSegment && segment.componentData) {
5778
+ const componentData = segment.componentData;
5779
+ const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
5780
+ if (isCompletedSelectedItem) return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(message_item_default, { itemIndex: index, message: previousSegment }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
5781
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react24.Fragment, { children: [
5782
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(message_item_default, { itemIndex: index, message: previousSegment }),
5783
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.View, { testID: ringgId("attached-buttons"), style: styles24.fullWidth, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5784
+ interactive_flow_default,
5785
+ {
5786
+ initialComponent: segment.componentData,
5787
+ onApiCall: callComponentApi,
5788
+ onComplete: () => handleFlowComplete(componentData.component_id),
5789
+ onSendResponse: sendComponentResponse,
5790
+ onSelectionDisplayed: handleSelectionDisplayed,
5791
+ quickReplyAlign: isAudioMode ? "center" : "end"
5792
+ }
5793
+ ) })
5794
+ ] }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
5795
+ }
5796
+ if (segment.componentType === "blocks" && segment.componentData) {
5797
+ const blocksPayload = segment.componentData;
5798
+ if (isBlocks2(blocksPayload)) {
5799
+ const blocksConfig = blocksPayload.data;
5800
+ const blockId = blocksPayload.component_id;
5801
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.View, { testID: ringgId("blocks-widget"), style: styles24.fullWidth, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5802
+ blocks_step_default,
5803
+ {
5804
+ config: blocksConfig,
5805
+ disabled: completedFlowIds.has(blockId),
5806
+ onAction: (action) => {
5807
+ handleUserMessageSent();
5808
+ handleFlowComplete(blockId);
5809
+ const selection = typeof action.value === "string" || typeof action.value === "number" ? String(action.value) : action.label;
5810
+ if (selection) handleSelectionDisplayed(blockId, selection);
5811
+ return controller.sendBlocksAction(blocksConfig.tool_id, blockId, { action_id: action.action_id, value: action.value, values: action.values });
5812
+ }
5813
+ }
5814
+ ) }, `blocks_${blockId}`);
5815
+ }
5816
+ }
5817
+ if (segment.componentType && segment.componentData) {
5818
+ const componentData = segment.componentData;
5819
+ const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
5820
+ if (isCompletedSelectedItem) return null;
5821
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5822
+ interactive_flow_default,
5823
+ {
5824
+ initialComponent: segment.componentData,
5825
+ onApiCall: callComponentApi,
5826
+ onComplete: () => handleFlowComplete(componentData.component_id),
5827
+ onSendResponse: sendComponentResponse,
5828
+ onSelectionDisplayed: handleSelectionDisplayed,
5829
+ quickReplyAlign: isAudioMode ? "center" : "end"
5830
+ },
5831
+ `flow_${segment.componentData.component_id}_${index}`
5832
+ );
5833
+ }
5834
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(message_item_default, { itemIndex: index, message: segment }, `${segment.timestamp}__${index}`);
5835
+ }),
5836
+ isAgentTyping ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(typing_indicator_default, { words: typingWords }) : null
5837
+ ]
5658
5838
  }
5659
- return probeMicrophone();
5660
- },
5661
- async request() {
5662
- if (import_react_native31.Platform.OS === "android") {
5663
- const result = await import_react_native31.PermissionsAndroid.request(import_react_native31.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, ANDROID_RATIONALE);
5664
- return result === import_react_native31.PermissionsAndroid.RESULTS.GRANTED;
5839
+ );
5840
+ const controls = /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5841
+ call_controls_default,
5842
+ {
5843
+ buttonConfig: buttons,
5844
+ callMode,
5845
+ isCalling: isSessionLive,
5846
+ enabledSlashCommands: mergedSlashCommands,
5847
+ micEnabled: microphone?.enabled ?? true,
5848
+ onToggleMic: microphone?.toggle,
5849
+ onSlashCommand: sendSlashCommand,
5850
+ onUserMessage: handleUserMessageSent,
5851
+ onCallEnd: handleCallEnd,
5852
+ onSendMessage: (message) => controller.sendMessage(message)
5665
5853
  }
5666
- return probeMicrophone();
5667
- }
5854
+ );
5855
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
5856
+ import_react_native31.View,
5857
+ {
5858
+ testID: ringgId("widget-root"),
5859
+ style: [styles24.panel, { width, height, borderRadius: panelRadius, backgroundColor: solidColor(theme.backgroundColor), borderColor: solidColor(theme.borderColor) }],
5860
+ children: [
5861
+ chrome,
5862
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5863
+ ConfirmDialog,
5864
+ {
5865
+ open: alertEndCall,
5866
+ title: `Are you sure you want to end ${callMode === "text" ? "chat" : "call"}?`,
5867
+ description: "Your current conversation will be closed and cannot be accessed later.",
5868
+ confirmLabel: `End ${callMode === "text" ? "Chat" : "Call"}`,
5869
+ cancelLabel: "Cancel",
5870
+ onConfirm: handleCallEnd,
5871
+ onCancel: () => setAlertEndCall(false)
5872
+ }
5873
+ ),
5874
+ showFeedback ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5875
+ feedback_screen_default,
5876
+ {
5877
+ callMode,
5878
+ isSubmitting: isLoading,
5879
+ feedbackScreenConfig: feedbackScreen,
5880
+ messages,
5881
+ title,
5882
+ logoUrl,
5883
+ logoStyles,
5884
+ onSubmit: handleFeedbackSubmit,
5885
+ onSkip: handleFeedbackSkip
5886
+ }
5887
+ ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native31.View, { testID: ringgId("widget-body"), style: [styles24.body, isSessionLive ? styles24.bodyLive : styles24.bodyIdle], children: [
5888
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(header_default, { isCalling: isSessionLive, isLoading, title, description, logoUrl, logoStyles }),
5889
+ showVoiceAnimation ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.View, { testID: ringgId("voice-animation-wrapper"), style: [styles24.voiceWrapper, showVoiceTranscript && messages.length > 0 ? styles24.voiceWrapperCompact : styles24.voiceWrapperFull], children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(VoiceAnimationLeaf, { room }) }) : null,
5890
+ isSessionLive && !showVoiceAnimation && (messages.length === 0 && !isAgentTyping || !showVoiceTranscript) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.View, { testID: ringgId("body-spacer"), style: styles24.bodySpacer }) : null,
5891
+ (messages.length > 0 || isAgentTyping) && isSessionLive && showVoiceTranscript ? transcript : null,
5892
+ isSessionLive && callMode === "text" ? (
5893
+ // Transparent bottom bar: it only positions the composer over the
5894
+ // transcript, so the gutters stay clear while messages scroll under.
5895
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native31.View, { testID: ringgId("controls-bar"), style: styles24.controlsBar, children: [
5896
+ error.hasError ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.Text, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null,
5897
+ controls
5898
+ ] })
5899
+ ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
5900
+ controls,
5901
+ error.hasError ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.Text, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null
5902
+ ] }),
5903
+ !isSessionLive && isLoading ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native31.View, { testID: ringgId("connecting-status"), children: [
5904
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.Text, { testID: ringgId("connecting-label"), style: [styles24.connecting, { color: theme.mutedTextColor, fontFamily }], children: "Connecting..." }),
5905
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(loading_bar_default, { ariaLabel: callMode === "text" ? "Starting chat" : "Connecting" })
5906
+ ] }) : null,
5907
+ !isLoading && connectionState === "disconnected" && (!bypassStartScreen || error.hasError) ? hideTabSelector ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(action_button_default, { callMode, buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native31.View, { testID: ringgId("start-actions"), style: styles24.startActions, children: [
5908
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(action_button_default, { dual: true, callMode: "text", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }),
5909
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(action_button_default, { dual: true, callMode: "audio", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart })
5910
+ ] }) : null,
5911
+ !isSessionLive ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.Text, { testID: ringgId("legal-disclaimer"), style: [styles24.legal, { color: theme.mutedTextColor, fontFamily }], children: legalDisclaimer ? renderTextWithLinks(legalDisclaimer.text, legalDisclaimer.links) : renderTextWithLinks(LEGAL_TEXT, LEGAL_LINKS, { color: solidColor(theme.primaryColor), textDecorationLine: "underline" }) }) : null
5912
+ ] })
5913
+ ]
5914
+ }
5915
+ );
5916
+ };
5917
+ var ControlledRinggWidget = ({ controller, room }) => {
5918
+ const shell = useRinggShell(controller);
5919
+ const { width: screenWidth, height: screenHeight } = (0, import_react_native31.useWindowDimensions)();
5920
+ const { buttons, defaultExpanded = WIDGET_DEFAULTS.defaultExpanded, widgetPosition } = controller.config;
5921
+ const isOpen = shell.viewState === "open" || shell.viewState === "feedback";
5922
+ const hideTriggerOnExpand = widgetPosition?.hideTriggerOnExpand ?? true;
5923
+ const showTrigger = !defaultExpanded && (!hideTriggerOnExpand || !isOpen);
5924
+ const panel = usePresence(isOpen, PANEL_SPRING);
5925
+ const trigger = usePresence(showTrigger, TRIGGER_SPRING);
5926
+ const panelWidth = Math.min(screenWidth - EDGE_INSET * 2, PANEL_MAX_WIDTH);
5927
+ const panelHeight = Math.min(screenHeight * PANEL_HEIGHT_RATIO, PANEL_HEIGHT);
5928
+ const triggerIconSize = toSize(buttons?.modalTrigger?.icon?.size, DEFAULT_TRIGGER_ICON_SIZE);
5929
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(WidgetThemeProvider, { theme: controller.theme, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native31.View, { pointerEvents: "box-none", style: import_react_native31.StyleSheet.absoluteFill, children: [
5930
+ panel.mounted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.KeyboardAvoidingView, { pointerEvents: "box-none", behavior: import_react_native31.Platform.OS === "ios" ? "padding" : void 0, style: styles24.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5931
+ import_react_native31.Animated.View,
5932
+ {
5933
+ renderToHardwareTextureAndroid: true,
5934
+ shouldRasterizeIOS: true,
5935
+ style: [
5936
+ styles24.panelAnchor,
5937
+ {
5938
+ opacity: panel.progress,
5939
+ transform: [{ translateY: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }, { scale: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) }]
5940
+ }
5941
+ ],
5942
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(WidgetPanel, { controller, room, width: panelWidth, height: panelHeight })
5943
+ }
5944
+ ) }) : null,
5945
+ trigger.mounted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(WidgetTrigger, { controller, iconSize: triggerIconSize, progress: trigger.progress }) : null
5946
+ ] }) });
5947
+ };
5948
+ var WidgetTrigger = ({ controller, iconSize, progress }) => {
5949
+ const theme = useWidgetTheme();
5950
+ const { buttons } = controller.config;
5951
+ return (
5952
+ // Web pops the trigger with a spring and fades it out again when the panel
5953
+ // takes over; both ends of that are visible on every open/close cycle.
5954
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native31.Animated.View, { style: [styles24.trigger, { opacity: progress, transform: [{ scale: progress.interpolate({ inputRange: [0, 1], outputRange: [0.8, 1] }) }] }], children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Touchable, { testID: ringgId("trigger-button"), accessibilityLabel: "Open call widget", onPress: () => controller.handleTriggerClick(), children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(GradientFill, { color: theme.primaryColor, style: [styles24.triggerSurface, toViewStyle(buttons?.modalTrigger?.styles)], children: buttons?.modalTrigger?.icon?.url ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5955
+ import_react_native31.Image,
5956
+ {
5957
+ testID: ringgId("trigger-icon"),
5958
+ accessibilityLabel: "Widget trigger",
5959
+ source: { uri: buttons.modalTrigger.icon.url },
5960
+ resizeMode: "contain",
5961
+ style: { width: iconSize, height: iconSize }
5962
+ }
5963
+ ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(PhoneCallRingIcon, { testID: ringgId("trigger-icon"), size: iconSize, fill: solidColor(theme.primaryTextColor) }) }) }) })
5964
+ );
5965
+ };
5966
+ var ManagedRinggWidget = ({ config, ports, onReady }) => {
5967
+ const stack = (0, import_react24.useMemo)(() => {
5968
+ const livekit = ports?.transport ? null : createLiveKitTransport();
5969
+ const controller = createRinggWidgetController(config, {
5970
+ urlResolver: defaultUrlResolver,
5971
+ micPermission: createNativeMicPermission(),
5972
+ ...ports,
5973
+ transport: ports?.transport ?? livekit.transport
5974
+ });
5975
+ return { controller, livekit };
5976
+ }, [config]);
5977
+ (0, import_react24.useEffect)(
5978
+ () => () => {
5979
+ stack.controller.destroy();
5980
+ stack.livekit?.dispose();
5981
+ },
5982
+ [stack]
5983
+ );
5984
+ const onReadyRef = (0, import_react24.useRef)(onReady);
5985
+ onReadyRef.current = onReady;
5986
+ (0, import_react24.useEffect)(() => {
5987
+ onReadyRef.current?.(stack.controller);
5988
+ }, [stack]);
5989
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ControlledRinggWidget, { controller: stack.controller, room: stack.livekit?.room });
5990
+ };
5991
+ var RinggWidget = (props) => props.config ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ManagedRinggWidget, { config: props.config, ports: props.ports, onReady: props.onReady }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ControlledRinggWidget, { controller: props.controller, room: props.room });
5992
+ var styles24 = import_react_native31.StyleSheet.create({
5993
+ overlay: { ...import_react_native31.StyleSheet.absoluteFill, justifyContent: "flex-end", alignItems: "flex-end" },
5994
+ panelAnchor: { marginHorizontal: EDGE_INSET, marginTop: EDGE_INSET, marginBottom: BOTTOM_SAFE_INSET },
5995
+ panel: {
5996
+ borderWidth: import_react_native31.StyleSheet.hairlineWidth,
5997
+ // `overflow: hidden` is what keeps the transcript, the header bar and the
5998
+ // rounded corners agreeing with each other.
5999
+ overflow: "hidden",
6000
+ // Web stacks three shadows at 1-2% alpha — the panel sits just off the page
6001
+ // rather than floating above it. 12% read as a hard slab, and on Android
6002
+ // `elevation` is the only one of these that applies, so it was the entire
6003
+ // effect instead of a supporting layer.
6004
+ shadowColor: "#000000",
6005
+ shadowOpacity: 0.05,
6006
+ shadowRadius: 20,
6007
+ shadowOffset: { width: 0, height: 8 },
6008
+ elevation: 4
6009
+ },
6010
+ trigger: { position: "absolute", right: EDGE_INSET, bottom: BOTTOM_SAFE_INSET },
6011
+ triggerSurface: { width: TRIGGER_SIZE, height: TRIGGER_SIZE, borderRadius: TRIGGER_RADIUS, alignItems: "center", justifyContent: "center" },
6012
+ chromeButton: { position: "absolute", top: 10, zIndex: 20, width: 28, height: 28, borderRadius: 14, alignItems: "center", justifyContent: "center" },
6013
+ minimizeButton: { right: 42 },
6014
+ closeButton: { right: 10 },
6015
+ body: { flex: 1 },
6016
+ bodyLive: { paddingHorizontal: 20, paddingVertical: 8, gap: 10 },
6017
+ bodyIdle: { padding: 24, gap: 12 },
6018
+ bodySpacer: { flex: 1, marginTop: 56 },
6019
+ voiceWrapper: { width: "100%", alignItems: "stretch", justifyContent: "center", marginTop: 56 },
6020
+ voiceWrapperCompact: { height: 56, flexShrink: 0 },
6021
+ voiceWrapperFull: { flex: 1, minHeight: 0 },
6022
+ transcript: { flex: 1 },
6023
+ // `justifyContent: flex-end` is web's `mt-auto`: a short conversation sits at
6024
+ // the bottom of the panel rather than floating at the top.
6025
+ transcriptList: { flexGrow: 1, justifyContent: "flex-end", gap: 12 },
6026
+ transcriptTopClearance: { paddingTop: 64 },
6027
+ transcriptBottomClearance: { paddingBottom: 80 },
6028
+ fullWidth: { width: "100%" },
6029
+ controlsBar: { position: "absolute", zIndex: 10, bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingVertical: 12 },
6030
+ error: { textAlign: "center", fontSize: 14, marginBottom: 8 },
6031
+ connecting: { fontSize: 14, textAlign: "center" },
6032
+ startActions: { flexDirection: "row", alignItems: "center", gap: 8, width: "100%" },
6033
+ legal: { fontSize: 12, lineHeight: 20, textAlign: "center" }
5668
6034
  });
5669
6035
 
5670
6036
  // src/platform/app-origin.ts
@@ -5733,26 +6099,73 @@ var createHostActionDispatcher = (onAction) => (action, onLog) => {
5733
6099
  };
5734
6100
  // Annotate the CommonJS export names for ESM import in node:
5735
6101
  0 && (module.exports = {
6102
+ CHAT_WIDGET_AFTER_TEXT_MS,
6103
+ CHAT_WIDGET_GRACE_MS,
6104
+ DEFAULT_CONFIG,
6105
+ DEFAULT_ENVIRONMENT_URLS,
6106
+ DEFAULT_WIDGET_THEME,
5736
6107
  GradientFill,
6108
+ MIN_TYPING_INDICATOR_MS,
5737
6109
  Markdown,
5738
6110
  RinggWidget,
6111
+ WIDGET_PRESET_THEMES,
5739
6112
  WidgetThemeProvider,
5740
6113
  appOrigin,
6114
+ buildPayload,
6115
+ colorToBackground,
6116
+ colorToBorder,
6117
+ colorToSolid,
5741
6118
  createAudioSession,
5742
6119
  createCallbackEventBus,
6120
+ createComponentStore,
6121
+ createDomEventBus,
5743
6122
  createHostActionDispatcher,
5744
6123
  createLiveKitTransport,
6124
+ createMessageStore,
5745
6125
  createNativeMicPermission,
5746
6126
  createNotificationPlayer,
6127
+ createRinggApiClient,
5747
6128
  createRinggWidgetController,
6129
+ createSessionStore,
6130
+ createShellStore,
5748
6131
  createSilentNotificationPlayer,
6132
+ createSlashCommandStore,
6133
+ createStaticUrlResolver,
6134
+ createStore,
6135
+ createSystemClock,
6136
+ createTypingStore,
6137
+ defaultUrlResolver,
6138
+ extractColorsFromGradient,
6139
+ formatBlocksAction,
6140
+ formatComponentResponse,
6141
+ formatDynamicData,
6142
+ formatSlashCommand,
6143
+ formatSlotDate,
6144
+ formatSlotTime,
6145
+ getButtonRadius,
6146
+ getContrastingTextColor,
6147
+ getDominantColor,
6148
+ grantedMicPermission,
6149
+ groupSlotsByDate,
6150
+ isBlocks,
6151
+ isButtons,
6152
+ isCalendarBooking,
6153
+ isConfirmation,
6154
+ isForm,
6155
+ isGradient,
6156
+ isInteractiveFlow,
6157
+ isLightColor,
6158
+ mergeComponentTheme,
6159
+ mergeWidgetTheme,
5749
6160
  resolveButtonRadius,
5750
6161
  resolveFontFamily,
5751
6162
  resolveRadius,
5752
6163
  ringgId,
6164
+ silentNotificationPlayer,
5753
6165
  solidColor,
5754
6166
  toSize,
5755
6167
  toViewStyle,
6168
+ transformRpcToComponent,
5756
6169
  useRinggComponents,
5757
6170
  useRinggMessages,
5758
6171
  useRinggSession,