@vinaverse/react-native 0.1.0-alpha.3 → 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/README.md CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  A ready, themeable conversation with your Vinaverse agent for React Native and Expo apps. Drop it into a screen,
4
4
  give it a conversation whose session comes from your backend, and it draws everything the realtime protocol defines:
5
- streaming replies, cards (people, services, bookings, questions, proposals, next steps), held and unsent messages, a
5
+ streaming replies, cards (photo cards for people and places, services, bookings, questions, proposals, next steps,
6
+ your own help pages), held and unsent messages, a
6
7
  person from your business taking over, and every way a session ends. No native module; New Architecture; FlashList 2.3.2 or later.
7
8
 
8
9
  ```tsx
9
10
  import { createConversation } from '@vinaverse/client/conversation';
10
- import { VinaverseChat } from '@vinaverse/react-native';
11
+ import { CardCarousel, VinaverseChat } from '@vinaverse/react-native';
11
12
 
12
13
  // Outside the screen: it connects early and survives navigation.
13
14
  const conversation = createConversation({
@@ -15,6 +16,8 @@ const conversation = createConversation({
15
16
  locale: 'es-US',
16
17
  deepLinkPrefixes: ['yourapp://bookings/'],
17
18
  });
19
+ // Opt-in: a turn's listing cards in one swipeable row (without it they stack). Module level: a stable object.
20
+ const components = { CardGroup: CardCarousel };
18
21
 
19
22
  export function AgentScreen() {
20
23
  return (
@@ -22,6 +25,7 @@ export function AgentScreen() {
22
25
  conversation={conversation}
23
26
  locale="es-US"
24
27
  theme={{ light: { primary: '#7A2E8E', personBubble: '#7A2E8E' } }}
28
+ components={components}
25
29
  onProposal={(card, decision) => api.decideProposal(card, decision)} // your backend, the person's token
26
30
  onOpenLink={(link) => router.push(link)}
27
31
  />
@@ -65,6 +69,12 @@ kit share one copy of `@vinaverse/client`.
65
69
  - **Background**: the kit tells the conversation when the app goes to the background and comes back (`AppState`).
66
70
  By default the connection stays, without pings; `createConversation({ closeInBackground: true, refreshSecret })`
67
71
  closes it in the background and opens it again, with a fresh secret, in the foreground. Nothing typed is lost.
72
+ - **A session's end**: pass `expires_at` on from the session your backend created. Past it (the tenant's tool token
73
+ with it), the person's next message gets a new session for the same conversation from `getSession`; the old one is
74
+ never reopened with a fresh secret. A reconnect keeps everything drawn: the session's transcript merges by id.
75
+ - **Modes**: a customer and a professional each get their own conversation, `createConversationsByMode`, and the
76
+ screen draws `conversations.select(mode)`. Each mode keeps its own history on the device, and `getSession` gets the
77
+ `mode` so your backend creates the session for that mode's agent.
68
78
  - **Language**: the agent answers in `subject.locale`, which your backend sets when it creates the session; the kit's
69
79
  `locale` prop is only for its own words, dates and money (and is passed to `getSession` as a hint you may ignore).
70
80
 
@@ -103,6 +113,26 @@ part as `Default`. With neither, the kit draws both as before.
103
113
  | `retry` | try again, when it can be: the person's text once more, for a turn the server allows to retry (once); a new session for `unavailable`. Undefined otherwise |
104
114
  | `conversationId` | the conversation, once a session named it: for your support records |
105
115
 
116
+ ## Listings, carousels, links and the welcome
117
+
118
+ - **Professionals and spaces** draw as photo cards: the hero photo (through your `Image` slot, so `expo-image`'s cache
119
+ applies), the name's monogram when there is none or it fails, rating with its count, "From $45" and the next open
120
+ time.
121
+ - **A carousel** for the cards a turn draws together: add the kit's `CardCarousel`. Without it they draw one under
122
+ another; it is opt-in so an app that does not use it does not ship it.
123
+
124
+ ```tsx
125
+ import { CardCarousel, VinaverseChat } from '@vinaverse/react-native';
126
+ const components = { Image: ExpoImage, CardGroup: CardCarousel }; // module level: a stable object
127
+ <VinaverseChat conversation={conversation} components={components} onOpenLink={openLink} />;
128
+ ```
129
+
130
+ - **Links**: when your scope keeps the agent from answering, it shows your own help, support, policy, contact or
131
+ feedback pages. Each opens through `onOpenLink` (an https page too: your in-app browser), a deep link only when it
132
+ passes your deep-link prefixes.
133
+ - **The welcome**: pass `welcome` on from the session your backend created; the kit greets an empty conversation with
134
+ it and its chips (no model call). A chip sends its message as the person's own. An `EmptyState` slot gets both.
135
+
106
136
  ## The keyboard
107
137
 
108
138
  `keyboardBehavior` keeps the composer above the keyboard: `padding` on iOS by default, nothing on Android (the window
package/dist/index.cjs CHANGED
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
+ var flashList = require('@shopify/flash-list');
4
+ var react$1 = require('@vinaverse/react');
3
5
  var reactNative = require('react-native');
4
6
  var react = require('react');
5
7
  var jsxRuntime = require('react/jsx-runtime');
6
8
  var tokens = require('@vinaverse/tokens');
7
- var react$1 = require('@vinaverse/react');
8
9
  var conversation = require('@vinaverse/client/conversation');
9
- var flashList = require('@shopify/flash-list');
10
10
 
11
- // src/cards/CardFrame.tsx
11
+ // src/cards/CardCarousel.tsx
12
12
  var ChatContext = react.createContext(null);
13
13
  function ChatContextProvider(props) {
14
14
  return /* @__PURE__ */ jsxRuntime.jsx(ChatContext.Provider, { value: props.value, children: props.children });
@@ -57,6 +57,61 @@ function ThemeProvider(props) {
57
57
  function useTheme() {
58
58
  return react.useContext(ThemeContext);
59
59
  }
60
+ var Button = react.memo(function Button2(props) {
61
+ const { colors: colors2, radius: radius2, space: space2, size: size3, font: font2 } = useTheme();
62
+ const variant = props.variant ?? "secondary";
63
+ const inactive = props.disabled === true || props.busy === true;
64
+ const filled = variant === "primary" || props.selected === true;
65
+ const background = filled ? colors2.primary : variant === "secondary" ? colors2.surface : "transparent";
66
+ const foreground = filled ? colors2.onPrimary : colors2.primary;
67
+ const checkable = props.role === "radio" || props.role === "checkbox";
68
+ return /* @__PURE__ */ jsxRuntime.jsxs(
69
+ reactNative.Pressable,
70
+ {
71
+ accessibilityRole: props.role ?? "button",
72
+ accessibilityLabel: props.label,
73
+ ...props.hint ? { accessibilityHint: props.hint } : {},
74
+ accessibilityState: {
75
+ disabled: inactive,
76
+ busy: props.busy === true,
77
+ ...checkable ? props.role === "radio" ? { selected: props.selected === true } : { checked: props.selected === true } : {}
78
+ },
79
+ disabled: inactive,
80
+ onPress: props.onPress,
81
+ ...props.testID ? { testID: props.testID } : {},
82
+ style: ({ pressed }) => ({
83
+ minHeight: size3.touchTarget,
84
+ minWidth: size3.touchTarget,
85
+ paddingHorizontal: space2.md,
86
+ paddingVertical: space2.sm,
87
+ borderRadius: radius2.md,
88
+ borderWidth: variant === "text" ? 0 : 1,
89
+ borderColor: filled ? colors2.primary : colors2.border,
90
+ backgroundColor: background,
91
+ opacity: inactive ? 0.55 : pressed ? 0.8 : 1,
92
+ alignItems: "center",
93
+ justifyContent: "center",
94
+ flexDirection: "row",
95
+ gap: space2.xs
96
+ }),
97
+ children: [
98
+ props.busy ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: foreground }) : null,
99
+ props.children ?? /* @__PURE__ */ jsxRuntime.jsx(
100
+ reactNative.Text,
101
+ {
102
+ style: {
103
+ color: foreground,
104
+ fontSize: font2.size.md,
105
+ fontWeight: "500",
106
+ ...font2.family ? { fontFamily: font2.family } : {}
107
+ },
108
+ children: props.label
109
+ }
110
+ )
111
+ ]
112
+ }
113
+ );
114
+ });
60
115
  function cardOf(held) {
61
116
  return held.card;
62
117
  }
@@ -125,64 +180,20 @@ function useLink(link, held) {
125
180
  if (!link || !store.checkLink(link)) return null;
126
181
  return () => handlers.current?.onOpenLink?.(link, held);
127
182
  }
183
+ function ratingText(valuePpm, locale) {
184
+ const value = valuePpm / 1e6;
185
+ try {
186
+ return new Intl.NumberFormat(locale, {
187
+ minimumFractionDigits: 1,
188
+ maximumFractionDigits: 1
189
+ }).format(value);
190
+ } catch {
191
+ return value.toFixed(1);
192
+ }
193
+ }
128
194
  function safeImage(url) {
129
195
  return url && /^https:\/\//i.test(url) ? url : null;
130
196
  }
131
- var Button = react.memo(function Button2(props) {
132
- const { colors: colors2, radius: radius2, space: space2, size: size3, font: font2 } = useTheme();
133
- const variant = props.variant ?? "secondary";
134
- const inactive = props.disabled === true || props.busy === true;
135
- const filled = variant === "primary" || props.selected === true;
136
- const background = filled ? colors2.primary : variant === "secondary" ? colors2.surface : "transparent";
137
- const foreground = filled ? colors2.onPrimary : colors2.primary;
138
- const checkable = props.role === "radio" || props.role === "checkbox";
139
- return /* @__PURE__ */ jsxRuntime.jsxs(
140
- reactNative.Pressable,
141
- {
142
- accessibilityRole: props.role ?? "button",
143
- accessibilityLabel: props.label,
144
- ...props.hint ? { accessibilityHint: props.hint } : {},
145
- accessibilityState: {
146
- disabled: inactive,
147
- busy: props.busy === true,
148
- ...checkable ? props.role === "radio" ? { selected: props.selected === true } : { checked: props.selected === true } : {}
149
- },
150
- disabled: inactive,
151
- onPress: props.onPress,
152
- ...props.testID ? { testID: props.testID } : {},
153
- style: ({ pressed }) => ({
154
- minHeight: size3.touchTarget,
155
- minWidth: size3.touchTarget,
156
- paddingHorizontal: space2.md,
157
- paddingVertical: space2.sm,
158
- borderRadius: radius2.md,
159
- borderWidth: variant === "text" ? 0 : 1,
160
- borderColor: filled ? colors2.primary : colors2.border,
161
- backgroundColor: background,
162
- opacity: inactive ? 0.55 : pressed ? 0.8 : 1,
163
- alignItems: "center",
164
- justifyContent: "center",
165
- flexDirection: "row",
166
- gap: space2.xs
167
- }),
168
- children: [
169
- props.busy ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: foreground }) : null,
170
- props.children ?? /* @__PURE__ */ jsxRuntime.jsx(
171
- reactNative.Text,
172
- {
173
- style: {
174
- color: foreground,
175
- fontSize: font2.size.md,
176
- fontWeight: "500",
177
- ...font2.family ? { fontFamily: font2.family } : {}
178
- },
179
- children: props.label
180
- }
181
- )
182
- ]
183
- }
184
- );
185
- });
186
197
  function InfoCard(props) {
187
198
  const card = cardOf(props.card);
188
199
  const { locale, t } = react$1.useCopy();
@@ -191,7 +202,7 @@ function InfoCard(props) {
191
202
  const image = safeImage(card.image_url);
192
203
  const Img = slots.Image ?? reactNative.Image;
193
204
  const money = card.price ?? card.price_from;
194
- const rating = card.rating ? (card.rating.value_ppm / 1e6).toFixed(1) : null;
205
+ const rating = card.rating ? ratingText(card.rating.value_ppm, locale) : null;
195
206
  const amount = money ? react$1.formatMoney(money.amount, money.currency, locale) : null;
196
207
  const price = amount && !card.price ? t("priceFrom", { price: amount }) : amount;
197
208
  const when = card.starts_at ? react$1.formatTimeRange(card.starts_at, void 0, locale, timeZone) : null;
@@ -234,27 +245,32 @@ function InfoCard(props) {
234
245
  ] })
235
246
  ] }),
236
247
  card.body ? /* @__PURE__ */ jsxRuntime.jsx(Body, { children: card.body }) : null,
237
- card.badges?.length ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space2.xs }, children: card.badges.map((b) => /* @__PURE__ */ jsxRuntime.jsx(
238
- reactNative.Text,
239
- {
240
- style: {
241
- color: colors2.text,
242
- backgroundColor: colors2.surface,
243
- borderRadius: radius2.pill,
244
- paddingHorizontal: space2.sm,
245
- paddingVertical: space2.xxs,
246
- fontSize: font2.size.xs
247
- },
248
- children: b
249
- },
250
- b
251
- )) }) : null,
252
- props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(Actions, { ...props })
248
+ /* @__PURE__ */ jsxRuntime.jsx(Badges, { badges: card.badges }),
249
+ props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(CardActions, { ...props })
253
250
  ]
254
251
  }
255
252
  );
256
253
  }
257
- function Actions(props) {
254
+ function Badges(props) {
255
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
256
+ if (!props.badges?.length) return null;
257
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space2.xs }, children: props.badges.map((b) => /* @__PURE__ */ jsxRuntime.jsx(
258
+ reactNative.Text,
259
+ {
260
+ style: {
261
+ color: colors2.text,
262
+ backgroundColor: colors2.surface,
263
+ borderRadius: radius2.pill,
264
+ paddingHorizontal: space2.sm,
265
+ paddingVertical: space2.xxs,
266
+ fontSize: font2.size.xs
267
+ },
268
+ children: b
269
+ },
270
+ b
271
+ )) });
272
+ }
273
+ function CardActions(props) {
258
274
  const card = cardOf(props.card);
259
275
  const { space: space2 } = useTheme();
260
276
  if (!card.actions?.length) return null;
@@ -286,6 +302,120 @@ function Action(props) {
286
302
  }
287
303
  );
288
304
  }
305
+ function LinksCard(props) {
306
+ const card = cardOf(props.card);
307
+ const { store, handlers } = useChat();
308
+ const links = (card.links ?? []).filter(
309
+ (l) => /^https:\/\//i.test(l.url) || store.checkLink(l.url)
310
+ );
311
+ return /* @__PURE__ */ jsxRuntime.jsxs(
312
+ CardFrame,
313
+ {
314
+ label: [card.title, ...links.map((l) => l.label)].join(", "),
315
+ restored: props.restored,
316
+ testID: `vinaverse-card-${card.id}`,
317
+ children: [
318
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { children: card.title }),
319
+ links.map((l) => /* @__PURE__ */ jsxRuntime.jsx(
320
+ Button,
321
+ {
322
+ label: l.label,
323
+ role: "link",
324
+ variant: "secondary",
325
+ disabled: props.restored,
326
+ onPress: () => {
327
+ const open = handlers.current?.onOpenLink;
328
+ if (open) open(l.url, props.card);
329
+ else void reactNative.Linking.openURL(l.url);
330
+ }
331
+ },
332
+ l.url
333
+ ))
334
+ ]
335
+ }
336
+ );
337
+ }
338
+ function ListingCard(props) {
339
+ const card = cardOf(props.card);
340
+ const { locale, t } = react$1.useCopy();
341
+ const { slots, timeZone } = useChat();
342
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
343
+ const [failed, setFailed] = react.useState(null);
344
+ const photoUrl = safeImage(card.images?.[0]?.url ?? card.image_url);
345
+ const hero = photoUrl === failed ? null : photoUrl;
346
+ const Img = slots.Image ?? reactNative.Image;
347
+ const rating = card.rating ? `\u2605 ${ratingText(card.rating.value_ppm, locale)} (${card.rating.count})` : null;
348
+ const price = card.price_from ? t("priceFrom", {
349
+ price: react$1.formatMoney(
350
+ card.price_from.amount,
351
+ card.price_from.currency,
352
+ locale
353
+ )
354
+ }) : null;
355
+ const next = card.next_available ? t("nextAvailable", {
356
+ time: react$1.formatTimeRange(card.next_available, void 0, locale, timeZone)
357
+ }) : null;
358
+ const photo = {
359
+ width: "100%",
360
+ aspectRatio: 1.6,
361
+ borderRadius: radius2.md
362
+ };
363
+ return /* @__PURE__ */ jsxRuntime.jsxs(
364
+ CardFrame,
365
+ {
366
+ label: [card.title, card.subtitle, rating, price, next].filter(Boolean).join(", "),
367
+ restored: props.restored,
368
+ testID: `vinaverse-card-${card.id}`,
369
+ children: [
370
+ hero ? /* @__PURE__ */ jsxRuntime.jsx(
371
+ Img,
372
+ {
373
+ source: { uri: hero },
374
+ onError: () => setFailed(hero),
375
+ style: { ...photo, backgroundColor: colors2.surface },
376
+ accessible: false,
377
+ testID: `vinaverse-photo-${card.id}`
378
+ }
379
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
380
+ reactNative.View,
381
+ {
382
+ accessible: false,
383
+ importantForAccessibility: "no-hide-descendants",
384
+ testID: `vinaverse-monogram-${card.id}`,
385
+ style: {
386
+ ...photo,
387
+ backgroundColor: colors2.surface,
388
+ alignItems: "center",
389
+ justifyContent: "center"
390
+ },
391
+ children: /* @__PURE__ */ jsxRuntime.jsx(
392
+ reactNative.Text,
393
+ {
394
+ style: {
395
+ color: colors2.textMuted,
396
+ fontSize: font2.size.xl,
397
+ fontWeight: "600"
398
+ },
399
+ children: monogram(card.title)
400
+ }
401
+ )
402
+ }
403
+ ),
404
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space2.xxs }, children: [
405
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { numberOfLines: 2, children: card.title }),
406
+ card.subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: card.subtitle }) : null,
407
+ rating || price ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: [rating, price].filter(Boolean).join(" \xB7 ") }) : null
408
+ ] }),
409
+ card.body ? /* @__PURE__ */ jsxRuntime.jsx(Body, { children: card.body }) : null,
410
+ /* @__PURE__ */ jsxRuntime.jsx(Badges, { badges: next ? [next, ...card.badges ?? []] : card.badges }),
411
+ props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(CardActions, { ...props })
412
+ ]
413
+ }
414
+ );
415
+ }
416
+ function monogram(title) {
417
+ return title.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => [...w][0]?.toUpperCase() ?? "").join("");
418
+ }
289
419
  function NextStepCard(props) {
290
420
  const card = cardOf(props.card);
291
421
  const action = card.actions?.[0];
@@ -1154,7 +1284,10 @@ var DEFAULTS = {
1154
1284
  proposal: ProposalCard,
1155
1285
  question: QuestionCard,
1156
1286
  next_step: NextStepCard,
1157
- time_slots: TimeSlotsCard
1287
+ time_slots: TimeSlotsCard,
1288
+ professional: ListingCard,
1289
+ workspace: ListingCard,
1290
+ links: LinksCard
1158
1291
  };
1159
1292
  function defaultCard(kind) {
1160
1293
  return DEFAULTS[kind] ?? InfoCard;
@@ -1167,6 +1300,35 @@ var CardView = react.memo(function CardView2(props) {
1167
1300
  const own = { card, busy, restored: props.item.restored === true };
1168
1301
  return Custom ? /* @__PURE__ */ jsxRuntime.jsx(Custom, { ...own, Default }) : /* @__PURE__ */ jsxRuntime.jsx(Default, { ...own });
1169
1302
  });
1303
+ var keyOf = (item) => item.key;
1304
+ var kindOf = (item) => item.card.kind;
1305
+ function CardCarousel(props) {
1306
+ const { width } = reactNative.useWindowDimensions();
1307
+ const { t } = react$1.useCopy();
1308
+ const size3 = Math.min(Math.round(width * 0.8), 360);
1309
+ return /* @__PURE__ */ jsxRuntime.jsx(
1310
+ reactNative.View,
1311
+ {
1312
+ accessibilityRole: "list",
1313
+ accessibilityLabel: t("cardsInRow", {
1314
+ count: String(props.items.length)
1315
+ }),
1316
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1317
+ flashList.FlashList,
1318
+ {
1319
+ horizontal: true,
1320
+ data: props.items,
1321
+ keyExtractor: keyOf,
1322
+ getItemType: kindOf,
1323
+ renderItem: ({ item }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { width: size3 }, children: /* @__PURE__ */ jsxRuntime.jsx(CardView, { item }) }),
1324
+ snapToInterval: size3,
1325
+ decelerationRate: "fast",
1326
+ showsHorizontalScrollIndicator: false
1327
+ }
1328
+ )
1329
+ }
1330
+ );
1331
+ }
1170
1332
  var ActivityLine = react.memo(function ActivityLine2() {
1171
1333
  const { store } = useChat();
1172
1334
  const { t } = react$1.useCopy();
@@ -1724,7 +1886,44 @@ function Line(props) {
1724
1886
  }
1725
1887
  );
1726
1888
  }
1727
- var keyOf = (item) => item.key;
1889
+ var Welcome = react.memo(function Welcome2(props) {
1890
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
1891
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1892
+ reactNative.View,
1893
+ {
1894
+ style: { padding: space2.lg, gap: space2.md },
1895
+ testID: "vinaverse-welcome",
1896
+ children: [
1897
+ /* @__PURE__ */ jsxRuntime.jsx(
1898
+ reactNative.Text,
1899
+ {
1900
+ style: {
1901
+ alignSelf: "flex-start",
1902
+ maxWidth: "85%",
1903
+ color: colors2.agentText,
1904
+ backgroundColor: colors2.agentBubble,
1905
+ borderRadius: radius2.lg,
1906
+ paddingHorizontal: space2.md,
1907
+ paddingVertical: space2.sm,
1908
+ fontSize: font2.size.md,
1909
+ lineHeight: font2.size.md * font2.lineHeight
1910
+ },
1911
+ children: props.welcome.text
1912
+ }
1913
+ ),
1914
+ props.welcome.chips.length ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space2.sm }, children: props.welcome.chips.map((chip) => /* @__PURE__ */ jsxRuntime.jsx(
1915
+ Button,
1916
+ {
1917
+ label: chip.label,
1918
+ onPress: () => props.onChip(chip.message)
1919
+ },
1920
+ chip.label
1921
+ )) }) : null
1922
+ ]
1923
+ }
1924
+ );
1925
+ });
1926
+ var keyOf2 = (item) => item.key;
1728
1927
  function itemType(item) {
1729
1928
  if (item.type === "card") return `card:${item.card.kind}`;
1730
1929
  if (item.type === "message")
@@ -1734,8 +1933,39 @@ function itemType(item) {
1734
1933
  function authorOf(item) {
1735
1934
  return item?.type === "message" ? item.role : null;
1736
1935
  }
1936
+ function carousels(items, previous) {
1937
+ const runs = /* @__PURE__ */ new Map();
1938
+ const inside = /* @__PURE__ */ new Set();
1939
+ const groupOf = (item) => {
1940
+ if (item?.type !== "card") return null;
1941
+ const group = item.card.card.group;
1942
+ return group?.layout === "carousel" && typeof group.id === "string" ? group.id : null;
1943
+ };
1944
+ for (let i = 0; i < items.length; i++) {
1945
+ const id = groupOf(items[i]);
1946
+ if (id === null) continue;
1947
+ let end = i + 1;
1948
+ while (end < items.length && groupOf(items[end]) === id) end++;
1949
+ if (end - i > 1) {
1950
+ const run = items.slice(i, end);
1951
+ const key = run[0]?.key;
1952
+ const before = previous.get(key);
1953
+ runs.set(
1954
+ key,
1955
+ before && before.length === run.length && before.every((c, n) => c === run[n]) ? before : run
1956
+ );
1957
+ for (const card of run.slice(1)) inside.add(card.key);
1958
+ }
1959
+ i = end - 1;
1960
+ }
1961
+ return { runs, inside };
1962
+ }
1737
1963
  var Row = react.memo(function Row2(props) {
1738
1964
  const { item } = props;
1965
+ const { slots } = useChat();
1966
+ const Group = slots.CardGroup;
1967
+ if (props.inside) return null;
1968
+ if (props.run && Group) return /* @__PURE__ */ jsxRuntime.jsx(Group, { items: props.run });
1739
1969
  if (item.type === "card") return /* @__PURE__ */ jsxRuntime.jsx(CardView, { item });
1740
1970
  if (item.type === "system") return /* @__PURE__ */ jsxRuntime.jsx(SystemRow, { item });
1741
1971
  return item.state === "streaming" ? /* @__PURE__ */ jsxRuntime.jsx(StreamingBubble, { item, showAuthor: props.showAuthor }) : /* @__PURE__ */ jsxRuntime.jsx(MessageBubble, { item, showAuthor: props.showAuthor });
@@ -1751,26 +1981,49 @@ var MessageList = react.memo(function MessageList2() {
1751
1981
  (v) => v.status === "starting" || v.status === "connecting"
1752
1982
  );
1753
1983
  const agentName = react$1.useConversationSelector(store, (v) => v.agent.name);
1984
+ const welcome = react$1.useConversationSelector(store, (v) => v.welcome);
1754
1985
  useAnnounceReplies(items);
1986
+ const Group = slots.CardGroup;
1987
+ const runsRef = react.useRef(/* @__PURE__ */ new Map());
1988
+ const grouping = react.useMemo(
1989
+ () => Group ? carousels(items, runsRef.current) : null,
1990
+ [items, Group]
1991
+ );
1992
+ runsRef.current = grouping?.runs ?? /* @__PURE__ */ new Map();
1755
1993
  const itemsRef = react.useRef(items);
1756
1994
  itemsRef.current = items;
1995
+ const groupingRef = react.useRef(grouping);
1996
+ groupingRef.current = grouping;
1757
1997
  const renderItem = react.useCallback(
1758
1998
  ({ item, index }) => /* @__PURE__ */ jsxRuntime.jsx(
1759
1999
  Row,
1760
2000
  {
1761
2001
  item,
1762
- showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1])
2002
+ showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1]),
2003
+ run: groupingRef.current?.runs.get(item.key),
2004
+ inside: groupingRef.current?.inside.has(item.key) === true
1763
2005
  }
1764
2006
  ),
1765
2007
  []
1766
2008
  );
2009
+ const onChip = react.useCallback(
2010
+ (message) => void store.send(message),
2011
+ [store]
2012
+ );
1767
2013
  const Empty = slots.EmptyState;
1768
- const empty = opening ? /* @__PURE__ */ jsxRuntime.jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { agentName: agentName ?? t("agentFallbackName") }) : null;
2014
+ const empty = opening ? /* @__PURE__ */ jsxRuntime.jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsxRuntime.jsx(
2015
+ Empty,
2016
+ {
2017
+ agentName: agentName ?? t("agentFallbackName"),
2018
+ welcome,
2019
+ onChip
2020
+ }
2021
+ ) : welcome ? /* @__PURE__ */ jsxRuntime.jsx(Welcome, { welcome, onChip }) : null;
1769
2022
  const props = react.useMemo(
1770
2023
  () => ({
1771
2024
  data: items,
1772
2025
  renderItem,
1773
- keyExtractor: keyOf,
2026
+ keyExtractor: keyOf2,
1774
2027
  getItemType: itemType,
1775
2028
  maintainVisibleContentPosition: {
1776
2029
  startRenderingFromBottom: true,
@@ -2003,6 +2256,8 @@ function sameSlots(a, b) {
2003
2256
  exports.ActivityLine = ActivityLine;
2004
2257
  exports.Avatar = Avatar;
2005
2258
  exports.Button = Button;
2259
+ exports.CardActions = CardActions;
2260
+ exports.CardCarousel = CardCarousel;
2006
2261
  exports.CardFrame = CardFrame;
2007
2262
  exports.CardView = CardView;
2008
2263
  exports.ChatHeader = ChatHeader;
@@ -2016,6 +2271,8 @@ exports.DefaultUnavailable = DefaultUnavailable;
2016
2271
  exports.DisclosureBar = DisclosureBar;
2017
2272
  exports.FailureActionButton = FailureActionButton;
2018
2273
  exports.InfoCard = InfoCard;
2274
+ exports.LinksCard = LinksCard;
2275
+ exports.ListingCard = ListingCard;
2019
2276
  exports.MessageBubble = MessageBubble;
2020
2277
  exports.MessageList = MessageList;
2021
2278
  exports.NextStepCard = NextStepCard;
@@ -2029,12 +2286,16 @@ exports.ThemeProvider = ThemeProvider;
2029
2286
  exports.TimeSlotsCard = TimeSlotsCard;
2030
2287
  exports.VinaverseChat = VinaverseChat;
2031
2288
  exports.VinaverseChatProvider = VinaverseChatProvider;
2289
+ exports.Welcome = Welcome;
2032
2290
  exports.announce = announce;
2033
2291
  exports.answerText = answerText;
2292
+ exports.carousels = carousels;
2034
2293
  exports.createTheme = createTheme;
2035
2294
  exports.defaultCard = defaultCard;
2036
2295
  exports.fieldText = fieldText;
2037
2296
  exports.itemType = itemType;
2297
+ exports.monogram = monogram;
2298
+ exports.ratingText = ratingText;
2038
2299
  exports.themeProblems = themeProblems;
2039
2300
  exports.useChat = useChat;
2040
2301
  exports.useNativeLifecycle = useNativeLifecycle;