@vinaverse/react-native 0.1.0-alpha.2 → 0.1.0-alpha.4

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
@@ -1,12 +1,12 @@
1
- import { Pressable, ActivityIndicator, Text, View, Image, TextInput, AccessibilityInfo, useColorScheme, AppState, Platform, KeyboardAvoidingView } from 'react-native';
1
+ import { FlashList } from '@shopify/flash-list';
2
+ import { useCopy, useConversationSelector, useStreamingText, formatMoney, formatTimeRange, formatDate, bannedWordsIn, CopyProvider } from '@vinaverse/react';
3
+ import { Pressable, ActivityIndicator, Text, View, Image, TextInput, AccessibilityInfo, Linking, useColorScheme, useWindowDimensions, AppState, Platform, KeyboardAvoidingView } from 'react-native';
2
4
  import { createContext, memo, useCallback, useState, useRef, useMemo, useEffect, useContext } from 'react';
3
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
6
  import { contrastRatio, colors, size, motion, radius, space, font, contrastProblems } from '@vinaverse/tokens';
5
- import { useCopy, useConversationSelector, useStreamingText, formatTimeRange, formatMoney, formatDate, bannedWordsIn, CopyProvider } from '@vinaverse/react';
6
7
  import { codePoints, MAX_TEXT, answerProblem } from '@vinaverse/client/conversation';
7
- import { FlashList } from '@shopify/flash-list';
8
8
 
9
- // src/cards/CardFrame.tsx
9
+ // src/cards/CardCarousel.tsx
10
10
  var ChatContext = createContext(null);
11
11
  function ChatContextProvider(props) {
12
12
  return /* @__PURE__ */ jsx(ChatContext.Provider, { value: props.value, children: props.children });
@@ -55,6 +55,61 @@ function ThemeProvider(props) {
55
55
  function useTheme() {
56
56
  return useContext(ThemeContext);
57
57
  }
58
+ var Button = memo(function Button2(props) {
59
+ const { colors: colors2, radius: radius2, space: space2, size: size3, font: font2 } = useTheme();
60
+ const variant = props.variant ?? "secondary";
61
+ const inactive = props.disabled === true || props.busy === true;
62
+ const filled = variant === "primary" || props.selected === true;
63
+ const background = filled ? colors2.primary : variant === "secondary" ? colors2.surface : "transparent";
64
+ const foreground = filled ? colors2.onPrimary : colors2.primary;
65
+ const checkable = props.role === "radio" || props.role === "checkbox";
66
+ return /* @__PURE__ */ jsxs(
67
+ Pressable,
68
+ {
69
+ accessibilityRole: props.role ?? "button",
70
+ accessibilityLabel: props.label,
71
+ ...props.hint ? { accessibilityHint: props.hint } : {},
72
+ accessibilityState: {
73
+ disabled: inactive,
74
+ busy: props.busy === true,
75
+ ...checkable ? props.role === "radio" ? { selected: props.selected === true } : { checked: props.selected === true } : {}
76
+ },
77
+ disabled: inactive,
78
+ onPress: props.onPress,
79
+ ...props.testID ? { testID: props.testID } : {},
80
+ style: ({ pressed }) => ({
81
+ minHeight: size3.touchTarget,
82
+ minWidth: size3.touchTarget,
83
+ paddingHorizontal: space2.md,
84
+ paddingVertical: space2.sm,
85
+ borderRadius: radius2.md,
86
+ borderWidth: variant === "text" ? 0 : 1,
87
+ borderColor: filled ? colors2.primary : colors2.border,
88
+ backgroundColor: background,
89
+ opacity: inactive ? 0.55 : pressed ? 0.8 : 1,
90
+ alignItems: "center",
91
+ justifyContent: "center",
92
+ flexDirection: "row",
93
+ gap: space2.xs
94
+ }),
95
+ children: [
96
+ props.busy ? /* @__PURE__ */ jsx(ActivityIndicator, { color: foreground }) : null,
97
+ props.children ?? /* @__PURE__ */ jsx(
98
+ Text,
99
+ {
100
+ style: {
101
+ color: foreground,
102
+ fontSize: font2.size.md,
103
+ fontWeight: "500",
104
+ ...font2.family ? { fontFamily: font2.family } : {}
105
+ },
106
+ children: props.label
107
+ }
108
+ )
109
+ ]
110
+ }
111
+ );
112
+ });
58
113
  function cardOf(held) {
59
114
  return held.card;
60
115
  }
@@ -123,64 +178,20 @@ function useLink(link, held) {
123
178
  if (!link || !store.checkLink(link)) return null;
124
179
  return () => handlers.current?.onOpenLink?.(link, held);
125
180
  }
181
+ function ratingText(valuePpm, locale) {
182
+ const value = valuePpm / 1e6;
183
+ try {
184
+ return new Intl.NumberFormat(locale, {
185
+ minimumFractionDigits: 1,
186
+ maximumFractionDigits: 1
187
+ }).format(value);
188
+ } catch {
189
+ return value.toFixed(1);
190
+ }
191
+ }
126
192
  function safeImage(url) {
127
193
  return url && /^https:\/\//i.test(url) ? url : null;
128
194
  }
129
- var Button = memo(function Button2(props) {
130
- const { colors: colors2, radius: radius2, space: space2, size: size3, font: font2 } = useTheme();
131
- const variant = props.variant ?? "secondary";
132
- const inactive = props.disabled === true || props.busy === true;
133
- const filled = variant === "primary" || props.selected === true;
134
- const background = filled ? colors2.primary : variant === "secondary" ? colors2.surface : "transparent";
135
- const foreground = filled ? colors2.onPrimary : colors2.primary;
136
- const checkable = props.role === "radio" || props.role === "checkbox";
137
- return /* @__PURE__ */ jsxs(
138
- Pressable,
139
- {
140
- accessibilityRole: props.role ?? "button",
141
- accessibilityLabel: props.label,
142
- ...props.hint ? { accessibilityHint: props.hint } : {},
143
- accessibilityState: {
144
- disabled: inactive,
145
- busy: props.busy === true,
146
- ...checkable ? props.role === "radio" ? { selected: props.selected === true } : { checked: props.selected === true } : {}
147
- },
148
- disabled: inactive,
149
- onPress: props.onPress,
150
- ...props.testID ? { testID: props.testID } : {},
151
- style: ({ pressed }) => ({
152
- minHeight: size3.touchTarget,
153
- minWidth: size3.touchTarget,
154
- paddingHorizontal: space2.md,
155
- paddingVertical: space2.sm,
156
- borderRadius: radius2.md,
157
- borderWidth: variant === "text" ? 0 : 1,
158
- borderColor: filled ? colors2.primary : colors2.border,
159
- backgroundColor: background,
160
- opacity: inactive ? 0.55 : pressed ? 0.8 : 1,
161
- alignItems: "center",
162
- justifyContent: "center",
163
- flexDirection: "row",
164
- gap: space2.xs
165
- }),
166
- children: [
167
- props.busy ? /* @__PURE__ */ jsx(ActivityIndicator, { color: foreground }) : null,
168
- props.children ?? /* @__PURE__ */ jsx(
169
- Text,
170
- {
171
- style: {
172
- color: foreground,
173
- fontSize: font2.size.md,
174
- fontWeight: "500",
175
- ...font2.family ? { fontFamily: font2.family } : {}
176
- },
177
- children: props.label
178
- }
179
- )
180
- ]
181
- }
182
- );
183
- });
184
195
  function InfoCard(props) {
185
196
  const card = cardOf(props.card);
186
197
  const { locale, t } = useCopy();
@@ -189,7 +200,7 @@ function InfoCard(props) {
189
200
  const image = safeImage(card.image_url);
190
201
  const Img = slots.Image ?? Image;
191
202
  const money = card.price ?? card.price_from;
192
- const rating = card.rating ? (card.rating.value_ppm / 1e6).toFixed(1) : null;
203
+ const rating = card.rating ? ratingText(card.rating.value_ppm, locale) : null;
193
204
  const amount = money ? formatMoney(money.amount, money.currency, locale) : null;
194
205
  const price = amount && !card.price ? t("priceFrom", { price: amount }) : amount;
195
206
  const when = card.starts_at ? formatTimeRange(card.starts_at, void 0, locale, timeZone) : null;
@@ -232,27 +243,32 @@ function InfoCard(props) {
232
243
  ] })
233
244
  ] }),
234
245
  card.body ? /* @__PURE__ */ jsx(Body, { children: card.body }) : null,
235
- card.badges?.length ? /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space2.xs }, children: card.badges.map((b) => /* @__PURE__ */ jsx(
236
- Text,
237
- {
238
- style: {
239
- color: colors2.text,
240
- backgroundColor: colors2.surface,
241
- borderRadius: radius2.pill,
242
- paddingHorizontal: space2.sm,
243
- paddingVertical: space2.xxs,
244
- fontSize: font2.size.xs
245
- },
246
- children: b
247
- },
248
- b
249
- )) }) : null,
250
- props.restored ? null : /* @__PURE__ */ jsx(Actions, { ...props })
246
+ /* @__PURE__ */ jsx(Badges, { badges: card.badges }),
247
+ props.restored ? null : /* @__PURE__ */ jsx(CardActions, { ...props })
251
248
  ]
252
249
  }
253
250
  );
254
251
  }
255
- function Actions(props) {
252
+ function Badges(props) {
253
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
254
+ if (!props.badges?.length) return null;
255
+ return /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space2.xs }, children: props.badges.map((b) => /* @__PURE__ */ jsx(
256
+ Text,
257
+ {
258
+ style: {
259
+ color: colors2.text,
260
+ backgroundColor: colors2.surface,
261
+ borderRadius: radius2.pill,
262
+ paddingHorizontal: space2.sm,
263
+ paddingVertical: space2.xxs,
264
+ fontSize: font2.size.xs
265
+ },
266
+ children: b
267
+ },
268
+ b
269
+ )) });
270
+ }
271
+ function CardActions(props) {
256
272
  const card = cardOf(props.card);
257
273
  const { space: space2 } = useTheme();
258
274
  if (!card.actions?.length) return null;
@@ -284,6 +300,120 @@ function Action(props) {
284
300
  }
285
301
  );
286
302
  }
303
+ function LinksCard(props) {
304
+ const card = cardOf(props.card);
305
+ const { store, handlers } = useChat();
306
+ const links = (card.links ?? []).filter(
307
+ (l) => /^https:\/\//i.test(l.url) || store.checkLink(l.url)
308
+ );
309
+ return /* @__PURE__ */ jsxs(
310
+ CardFrame,
311
+ {
312
+ label: [card.title, ...links.map((l) => l.label)].join(", "),
313
+ restored: props.restored,
314
+ testID: `vinaverse-card-${card.id}`,
315
+ children: [
316
+ /* @__PURE__ */ jsx(Title, { children: card.title }),
317
+ links.map((l) => /* @__PURE__ */ jsx(
318
+ Button,
319
+ {
320
+ label: l.label,
321
+ role: "link",
322
+ variant: "secondary",
323
+ disabled: props.restored,
324
+ onPress: () => {
325
+ const open = handlers.current?.onOpenLink;
326
+ if (open) open(l.url, props.card);
327
+ else void Linking.openURL(l.url);
328
+ }
329
+ },
330
+ l.url
331
+ ))
332
+ ]
333
+ }
334
+ );
335
+ }
336
+ function ListingCard(props) {
337
+ const card = cardOf(props.card);
338
+ const { locale, t } = useCopy();
339
+ const { slots, timeZone } = useChat();
340
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
341
+ const [failed, setFailed] = useState(null);
342
+ const photoUrl = safeImage(card.images?.[0]?.url ?? card.image_url);
343
+ const hero = photoUrl === failed ? null : photoUrl;
344
+ const Img = slots.Image ?? Image;
345
+ const rating = card.rating ? `\u2605 ${ratingText(card.rating.value_ppm, locale)} (${card.rating.count})` : null;
346
+ const price = card.price_from ? t("priceFrom", {
347
+ price: formatMoney(
348
+ card.price_from.amount,
349
+ card.price_from.currency,
350
+ locale
351
+ )
352
+ }) : null;
353
+ const next = card.next_available ? t("nextAvailable", {
354
+ time: formatTimeRange(card.next_available, void 0, locale, timeZone)
355
+ }) : null;
356
+ const photo = {
357
+ width: "100%",
358
+ aspectRatio: 1.6,
359
+ borderRadius: radius2.md
360
+ };
361
+ return /* @__PURE__ */ jsxs(
362
+ CardFrame,
363
+ {
364
+ label: [card.title, card.subtitle, rating, price, next].filter(Boolean).join(", "),
365
+ restored: props.restored,
366
+ testID: `vinaverse-card-${card.id}`,
367
+ children: [
368
+ hero ? /* @__PURE__ */ jsx(
369
+ Img,
370
+ {
371
+ source: { uri: hero },
372
+ onError: () => setFailed(hero),
373
+ style: { ...photo, backgroundColor: colors2.surface },
374
+ accessible: false,
375
+ testID: `vinaverse-photo-${card.id}`
376
+ }
377
+ ) : /* @__PURE__ */ jsx(
378
+ View,
379
+ {
380
+ accessible: false,
381
+ importantForAccessibility: "no-hide-descendants",
382
+ testID: `vinaverse-monogram-${card.id}`,
383
+ style: {
384
+ ...photo,
385
+ backgroundColor: colors2.surface,
386
+ alignItems: "center",
387
+ justifyContent: "center"
388
+ },
389
+ children: /* @__PURE__ */ jsx(
390
+ Text,
391
+ {
392
+ style: {
393
+ color: colors2.textMuted,
394
+ fontSize: font2.size.xl,
395
+ fontWeight: "600"
396
+ },
397
+ children: monogram(card.title)
398
+ }
399
+ )
400
+ }
401
+ ),
402
+ /* @__PURE__ */ jsxs(View, { style: { gap: space2.xxs }, children: [
403
+ /* @__PURE__ */ jsx(Title, { numberOfLines: 2, children: card.title }),
404
+ card.subtitle ? /* @__PURE__ */ jsx(Body, { muted: true, children: card.subtitle }) : null,
405
+ rating || price ? /* @__PURE__ */ jsx(Body, { muted: true, children: [rating, price].filter(Boolean).join(" \xB7 ") }) : null
406
+ ] }),
407
+ card.body ? /* @__PURE__ */ jsx(Body, { children: card.body }) : null,
408
+ /* @__PURE__ */ jsx(Badges, { badges: next ? [next, ...card.badges ?? []] : card.badges }),
409
+ props.restored ? null : /* @__PURE__ */ jsx(CardActions, { ...props })
410
+ ]
411
+ }
412
+ );
413
+ }
414
+ function monogram(title) {
415
+ return title.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => [...w][0]?.toUpperCase() ?? "").join("");
416
+ }
287
417
  function NextStepCard(props) {
288
418
  const card = cardOf(props.card);
289
419
  const action = card.actions?.[0];
@@ -1152,7 +1282,10 @@ var DEFAULTS = {
1152
1282
  proposal: ProposalCard,
1153
1283
  question: QuestionCard,
1154
1284
  next_step: NextStepCard,
1155
- time_slots: TimeSlotsCard
1285
+ time_slots: TimeSlotsCard,
1286
+ professional: ListingCard,
1287
+ workspace: ListingCard,
1288
+ links: LinksCard
1156
1289
  };
1157
1290
  function defaultCard(kind) {
1158
1291
  return DEFAULTS[kind] ?? InfoCard;
@@ -1165,6 +1298,35 @@ var CardView = memo(function CardView2(props) {
1165
1298
  const own = { card, busy, restored: props.item.restored === true };
1166
1299
  return Custom ? /* @__PURE__ */ jsx(Custom, { ...own, Default }) : /* @__PURE__ */ jsx(Default, { ...own });
1167
1300
  });
1301
+ var keyOf = (item) => item.key;
1302
+ var kindOf = (item) => item.card.kind;
1303
+ function CardCarousel(props) {
1304
+ const { width } = useWindowDimensions();
1305
+ const { t } = useCopy();
1306
+ const size3 = Math.min(Math.round(width * 0.8), 360);
1307
+ return /* @__PURE__ */ jsx(
1308
+ View,
1309
+ {
1310
+ accessibilityRole: "list",
1311
+ accessibilityLabel: t("cardsInRow", {
1312
+ count: String(props.items.length)
1313
+ }),
1314
+ children: /* @__PURE__ */ jsx(
1315
+ FlashList,
1316
+ {
1317
+ horizontal: true,
1318
+ data: props.items,
1319
+ keyExtractor: keyOf,
1320
+ getItemType: kindOf,
1321
+ renderItem: ({ item }) => /* @__PURE__ */ jsx(View, { style: { width: size3 }, children: /* @__PURE__ */ jsx(CardView, { item }) }),
1322
+ snapToInterval: size3,
1323
+ decelerationRate: "fast",
1324
+ showsHorizontalScrollIndicator: false
1325
+ }
1326
+ )
1327
+ }
1328
+ );
1329
+ }
1168
1330
  var ActivityLine = memo(function ActivityLine2() {
1169
1331
  const { store } = useChat();
1170
1332
  const { t } = useCopy();
@@ -1471,6 +1633,23 @@ var DisclosureBar = memo(function DisclosureBar2() {
1471
1633
  }
1472
1634
  );
1473
1635
  });
1636
+ function FailureActionButton(props) {
1637
+ const { handlers, failureAction } = useChat();
1638
+ const { t } = useCopy();
1639
+ if (!failureAction) return null;
1640
+ return /* @__PURE__ */ jsx(
1641
+ Button,
1642
+ {
1643
+ label: failureAction.label ?? t("talkToPerson"),
1644
+ variant: "text",
1645
+ onPress: () => handlers.current.failureAction?.onPress(props.state)
1646
+ }
1647
+ );
1648
+ }
1649
+ function FailureSlot(props) {
1650
+ const { slot: Slot, state, Default } = props;
1651
+ return Slot ? /* @__PURE__ */ jsx(Slot, { state, Default }) : /* @__PURE__ */ jsx(Default, { state });
1652
+ }
1474
1653
  function DefaultBubble(props) {
1475
1654
  const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
1476
1655
  const role = props.item.role;
@@ -1626,9 +1805,29 @@ function announce(text) {
1626
1805
  }
1627
1806
  var SystemRow = memo(function SystemRow2(props) {
1628
1807
  const { t } = useCopy();
1629
- const { store } = useChat();
1630
- const { colors: colors2, space: space2, font: font2 } = useTheme();
1808
+ const { store, slots } = useChat();
1809
+ const conversationId = useConversationSelector(
1810
+ store,
1811
+ (v) => v.conversationId
1812
+ );
1631
1813
  const { event, data } = props.item;
1814
+ if (event === "turn_failed") {
1815
+ const key = props.item.key;
1816
+ const state = {
1817
+ kind: "turn_failed",
1818
+ reason: typeof data.code === "string" ? data.code : null,
1819
+ retry: data.retry === true ? () => void store.retryTurn(key) : void 0,
1820
+ conversationId
1821
+ };
1822
+ return /* @__PURE__ */ jsx(
1823
+ FailureSlot,
1824
+ {
1825
+ slot: slots.Error,
1826
+ state,
1827
+ Default: DefaultTurnFailed
1828
+ }
1829
+ );
1830
+ }
1632
1831
  const agent = store.getSnapshot().agent.name ?? t("agentFallbackName");
1633
1832
  const text = (() => {
1634
1833
  switch (event) {
@@ -1642,18 +1841,30 @@ var SystemRow = memo(function SystemRow2(props) {
1642
1841
  return data.expected_response ? t("handoffCreated", { expected: data.expected_response }) : t("handoffCreatedNoTime");
1643
1842
  case "flow_completed":
1644
1843
  return data.result === "ok" ? t("flowSent") : data.result === "failed" ? t("flowNotSent") : null;
1645
- case "turn_failed":
1646
- return t("turnFailed");
1647
1844
  default:
1648
1845
  return null;
1649
1846
  }
1650
1847
  })();
1651
1848
  if (!text) return null;
1849
+ return /* @__PURE__ */ jsx(Line, { text });
1850
+ });
1851
+ function DefaultTurnFailed(props) {
1852
+ const { t } = useCopy();
1853
+ const { failureAction } = useChat();
1854
+ const text = t("turnFailed");
1855
+ if (!failureAction) return /* @__PURE__ */ jsx(Line, { text, danger: true });
1856
+ return /* @__PURE__ */ jsxs(View, { style: { alignItems: "center" }, children: [
1857
+ /* @__PURE__ */ jsx(Line, { text, danger: true }),
1858
+ /* @__PURE__ */ jsx(FailureActionButton, { state: props.state })
1859
+ ] });
1860
+ }
1861
+ function Line(props) {
1862
+ const { colors: colors2, space: space2, font: font2 } = useTheme();
1652
1863
  return /* @__PURE__ */ jsx(
1653
1864
  View,
1654
1865
  {
1655
1866
  accessible: true,
1656
- accessibilityLabel: text,
1867
+ accessibilityLabel: props.text,
1657
1868
  style: {
1658
1869
  alignItems: "center",
1659
1870
  paddingHorizontal: space2.xl,
@@ -1663,17 +1874,54 @@ var SystemRow = memo(function SystemRow2(props) {
1663
1874
  Text,
1664
1875
  {
1665
1876
  style: {
1666
- color: event === "turn_failed" ? colors2.danger : colors2.textMuted,
1877
+ color: props.danger ? colors2.danger : colors2.textMuted,
1667
1878
  fontSize: font2.size.sm,
1668
1879
  textAlign: "center"
1669
1880
  },
1670
- children: text
1881
+ children: props.text
1671
1882
  }
1672
1883
  )
1673
1884
  }
1674
1885
  );
1886
+ }
1887
+ var Welcome = memo(function Welcome2(props) {
1888
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
1889
+ return /* @__PURE__ */ jsxs(
1890
+ View,
1891
+ {
1892
+ style: { padding: space2.lg, gap: space2.md },
1893
+ testID: "vinaverse-welcome",
1894
+ children: [
1895
+ /* @__PURE__ */ jsx(
1896
+ Text,
1897
+ {
1898
+ style: {
1899
+ alignSelf: "flex-start",
1900
+ maxWidth: "85%",
1901
+ color: colors2.agentText,
1902
+ backgroundColor: colors2.agentBubble,
1903
+ borderRadius: radius2.lg,
1904
+ paddingHorizontal: space2.md,
1905
+ paddingVertical: space2.sm,
1906
+ fontSize: font2.size.md,
1907
+ lineHeight: font2.size.md * font2.lineHeight
1908
+ },
1909
+ children: props.welcome.text
1910
+ }
1911
+ ),
1912
+ props.welcome.chips.length ? /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space2.sm }, children: props.welcome.chips.map((chip) => /* @__PURE__ */ jsx(
1913
+ Button,
1914
+ {
1915
+ label: chip.label,
1916
+ onPress: () => props.onChip(chip.message)
1917
+ },
1918
+ chip.label
1919
+ )) }) : null
1920
+ ]
1921
+ }
1922
+ );
1675
1923
  });
1676
- var keyOf = (item) => item.key;
1924
+ var keyOf2 = (item) => item.key;
1677
1925
  function itemType(item) {
1678
1926
  if (item.type === "card") return `card:${item.card.kind}`;
1679
1927
  if (item.type === "message")
@@ -1683,8 +1931,39 @@ function itemType(item) {
1683
1931
  function authorOf(item) {
1684
1932
  return item?.type === "message" ? item.role : null;
1685
1933
  }
1934
+ function carousels(items, previous) {
1935
+ const runs = /* @__PURE__ */ new Map();
1936
+ const inside = /* @__PURE__ */ new Set();
1937
+ const groupOf = (item) => {
1938
+ if (item?.type !== "card") return null;
1939
+ const group = item.card.card.group;
1940
+ return group?.layout === "carousel" && typeof group.id === "string" ? group.id : null;
1941
+ };
1942
+ for (let i = 0; i < items.length; i++) {
1943
+ const id = groupOf(items[i]);
1944
+ if (id === null) continue;
1945
+ let end = i + 1;
1946
+ while (end < items.length && groupOf(items[end]) === id) end++;
1947
+ if (end - i > 1) {
1948
+ const run = items.slice(i, end);
1949
+ const key = run[0]?.key;
1950
+ const before = previous.get(key);
1951
+ runs.set(
1952
+ key,
1953
+ before && before.length === run.length && before.every((c, n) => c === run[n]) ? before : run
1954
+ );
1955
+ for (const card of run.slice(1)) inside.add(card.key);
1956
+ }
1957
+ i = end - 1;
1958
+ }
1959
+ return { runs, inside };
1960
+ }
1686
1961
  var Row = memo(function Row2(props) {
1687
1962
  const { item } = props;
1963
+ const { slots } = useChat();
1964
+ const Group = slots.CardGroup;
1965
+ if (props.inside) return null;
1966
+ if (props.run && Group) return /* @__PURE__ */ jsx(Group, { items: props.run });
1688
1967
  if (item.type === "card") return /* @__PURE__ */ jsx(CardView, { item });
1689
1968
  if (item.type === "system") return /* @__PURE__ */ jsx(SystemRow, { item });
1690
1969
  return item.state === "streaming" ? /* @__PURE__ */ jsx(StreamingBubble, { item, showAuthor: props.showAuthor }) : /* @__PURE__ */ jsx(MessageBubble, { item, showAuthor: props.showAuthor });
@@ -1700,26 +1979,49 @@ var MessageList = memo(function MessageList2() {
1700
1979
  (v) => v.status === "starting" || v.status === "connecting"
1701
1980
  );
1702
1981
  const agentName = useConversationSelector(store, (v) => v.agent.name);
1982
+ const welcome = useConversationSelector(store, (v) => v.welcome);
1703
1983
  useAnnounceReplies(items);
1984
+ const Group = slots.CardGroup;
1985
+ const runsRef = useRef(/* @__PURE__ */ new Map());
1986
+ const grouping = useMemo(
1987
+ () => Group ? carousels(items, runsRef.current) : null,
1988
+ [items, Group]
1989
+ );
1990
+ runsRef.current = grouping?.runs ?? /* @__PURE__ */ new Map();
1704
1991
  const itemsRef = useRef(items);
1705
1992
  itemsRef.current = items;
1993
+ const groupingRef = useRef(grouping);
1994
+ groupingRef.current = grouping;
1706
1995
  const renderItem = useCallback(
1707
1996
  ({ item, index }) => /* @__PURE__ */ jsx(
1708
1997
  Row,
1709
1998
  {
1710
1999
  item,
1711
- showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1])
2000
+ showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1]),
2001
+ run: groupingRef.current?.runs.get(item.key),
2002
+ inside: groupingRef.current?.inside.has(item.key) === true
1712
2003
  }
1713
2004
  ),
1714
2005
  []
1715
2006
  );
2007
+ const onChip = useCallback(
2008
+ (message) => void store.send(message),
2009
+ [store]
2010
+ );
1716
2011
  const Empty = slots.EmptyState;
1717
- const empty = opening ? /* @__PURE__ */ jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsx(Empty, { agentName: agentName ?? t("agentFallbackName") }) : null;
2012
+ const empty = opening ? /* @__PURE__ */ jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsx(
2013
+ Empty,
2014
+ {
2015
+ agentName: agentName ?? t("agentFallbackName"),
2016
+ welcome,
2017
+ onChip
2018
+ }
2019
+ ) : welcome ? /* @__PURE__ */ jsx(Welcome, { welcome, onChip }) : null;
1718
2020
  const props = useMemo(
1719
2021
  () => ({
1720
2022
  data: items,
1721
2023
  renderItem,
1722
- keyExtractor: keyOf,
2024
+ keyExtractor: keyOf2,
1723
2025
  getItemType: itemType,
1724
2026
  maintainVisibleContentPosition: {
1725
2027
  startRenderingFromBottom: true,
@@ -1757,10 +2059,23 @@ function useAnnounceReplies(items) {
1757
2059
  }
1758
2060
  var CONNECTING_DELAY = motion.bannerDelay;
1759
2061
  var StatusBanner = memo(function StatusBanner2() {
1760
- const { store } = useChat();
2062
+ const { store, slots } = useChat();
1761
2063
  const { t } = useCopy();
1762
- const { colors: colors2, space: space2, font: font2 } = useTheme();
1763
2064
  const banner = useConversationSelector(store, (v) => v.banner);
2065
+ const status = useConversationSelector(store, (v) => v.status);
2066
+ const conversationId = useConversationSelector(
2067
+ store,
2068
+ (v) => v.conversationId
2069
+ );
2070
+ const unavailable = useMemo(
2071
+ () => ({
2072
+ kind: "unavailable",
2073
+ reason: status,
2074
+ retry: () => store.start(),
2075
+ conversationId
2076
+ }),
2077
+ [store, status, conversationId]
2078
+ );
1764
2079
  const [late, setLate] = useState(false);
1765
2080
  useEffect(() => {
1766
2081
  setLate(false);
@@ -1769,12 +2084,20 @@ var StatusBanner = memo(function StatusBanner2() {
1769
2084
  return () => clearTimeout(timer);
1770
2085
  }, [banner]);
1771
2086
  if (banner === null || banner === "connecting" && !late) return null;
2087
+ if (banner === "unavailable")
2088
+ return /* @__PURE__ */ jsx(
2089
+ FailureSlot,
2090
+ {
2091
+ slot: slots.Unavailable,
2092
+ state: unavailable,
2093
+ Default: DefaultUnavailable
2094
+ }
2095
+ );
1772
2096
  const text = {
1773
2097
  connecting: t("bannerConnecting"),
1774
2098
  reconnecting: t("bannerReconnecting"),
1775
2099
  offline: t("bannerOffline"),
1776
- continued_elsewhere: t("bannerContinuedElsewhere"),
1777
- unavailable: t("bannerUnavailable")
2100
+ continued_elsewhere: t("bannerContinuedElsewhere")
1778
2101
  }[banner];
1779
2102
  const action = banner === "continued_elsewhere" ? /* @__PURE__ */ jsx(
1780
2103
  Button,
@@ -1783,7 +2106,7 @@ var StatusBanner = memo(function StatusBanner2() {
1783
2106
  variant: "text",
1784
2107
  onPress: () => void store.continueHere()
1785
2108
  }
1786
- ) : banner === "offline" || banner === "unavailable" ? /* @__PURE__ */ jsx(
2109
+ ) : banner === "offline" ? /* @__PURE__ */ jsx(
1787
2110
  Button,
1788
2111
  {
1789
2112
  label: t("tryAgain"),
@@ -1791,6 +2114,18 @@ var StatusBanner = memo(function StatusBanner2() {
1791
2114
  onPress: () => store.start()
1792
2115
  }
1793
2116
  ) : null;
2117
+ return /* @__PURE__ */ jsx(Banner, { text, children: action });
2118
+ });
2119
+ function DefaultUnavailable(props) {
2120
+ const { t } = useCopy();
2121
+ const { retry } = props.state;
2122
+ return /* @__PURE__ */ jsxs(Banner, { text: t("bannerUnavailable"), children: [
2123
+ retry ? /* @__PURE__ */ jsx(Button, { label: t("tryAgain"), variant: "text", onPress: retry }) : null,
2124
+ /* @__PURE__ */ jsx(FailureActionButton, { state: props.state })
2125
+ ] });
2126
+ }
2127
+ function Banner(props) {
2128
+ const { colors: colors2, space: space2, font: font2 } = useTheme();
1794
2129
  return /* @__PURE__ */ jsxs(
1795
2130
  View,
1796
2131
  {
@@ -1812,22 +2147,35 @@ var StatusBanner = memo(function StatusBanner2() {
1812
2147
  Text,
1813
2148
  {
1814
2149
  style: { color: colors2.text, fontSize: font2.size.sm, flexShrink: 1 },
1815
- children: text
2150
+ children: props.text
1816
2151
  }
1817
2152
  ),
1818
- action
2153
+ props.children
1819
2154
  ]
1820
2155
  }
1821
2156
  );
1822
- });
2157
+ }
1823
2158
  function VinaverseChatProvider(props) {
1824
2159
  const { conversation, components, agentAvatar, timeZone } = props;
1825
2160
  const handlers = useRef(props);
1826
2161
  handlers.current = props;
1827
2162
  const slots = useStableSlots(components);
2163
+ const hasAction = props.failureAction !== void 0;
2164
+ const actionLabel = props.failureAction?.label;
2165
+ const failureAction = useMemo(
2166
+ () => hasAction ? { label: actionLabel } : null,
2167
+ [hasAction, actionLabel]
2168
+ );
1828
2169
  const value = useMemo(
1829
- () => ({ store: conversation, handlers, slots, agentAvatar, timeZone }),
1830
- [conversation, slots, agentAvatar, timeZone]
2170
+ () => ({
2171
+ store: conversation,
2172
+ handlers,
2173
+ slots,
2174
+ agentAvatar,
2175
+ timeZone,
2176
+ failureAction
2177
+ }),
2178
+ [conversation, slots, agentAvatar, timeZone, failureAction]
1831
2179
  );
1832
2180
  useNativeLifecycle(conversation);
1833
2181
  useEffect(() => {
@@ -1903,6 +2251,6 @@ function sameSlots(a, b) {
1903
2251
  return true;
1904
2252
  }
1905
2253
 
1906
- export { ActivityLine, Avatar, Button, CardFrame, CardView, ChatHeader, ChatSkeleton, Composer, DefaultBubble, DefaultComposer, DefaultHeader, DisclosureBar, InfoCard, MessageBubble, MessageList, NextStepCard, ProposalCard, QuestionCard, Row, StatusBanner, StreamingBubble, SystemRow, ThemeProvider, TimeSlotsCard, VinaverseChat, VinaverseChatProvider, announce, answerText, createTheme, defaultCard, fieldText, itemType, themeProblems, useChat, useNativeLifecycle, useReducedMotion, useTheme };
2254
+ export { ActivityLine, Avatar, Button, CardActions, CardCarousel, CardFrame, CardView, ChatHeader, ChatSkeleton, Composer, DefaultBubble, DefaultComposer, DefaultHeader, DefaultTurnFailed, DefaultUnavailable, DisclosureBar, FailureActionButton, InfoCard, LinksCard, ListingCard, MessageBubble, MessageList, NextStepCard, ProposalCard, QuestionCard, Row, StatusBanner, StreamingBubble, SystemRow, ThemeProvider, TimeSlotsCard, VinaverseChat, VinaverseChatProvider, Welcome, announce, answerText, carousels, createTheme, defaultCard, fieldText, itemType, monogram, ratingText, themeProblems, useChat, useNativeLifecycle, useReducedMotion, useTheme };
1907
2255
  //# sourceMappingURL=index.js.map
1908
2256
  //# sourceMappingURL=index.js.map