@vinaverse/react-native 0.1.0-alpha.3 → 0.1.0-alpha.5

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.cjs CHANGED
@@ -1,14 +1,16 @@
1
1
  'use strict';
2
2
 
3
+ var agent = require('@vinaverse/react/agent');
4
+ var flashList = require('@shopify/flash-list');
5
+ var react$1 = require('@vinaverse/react');
3
6
  var reactNative = require('react-native');
7
+ var agent$1 = require('@vinaverse/client/agent');
4
8
  var react = require('react');
5
9
  var jsxRuntime = require('react/jsx-runtime');
6
10
  var tokens = require('@vinaverse/tokens');
7
- var react$1 = require('@vinaverse/react');
8
11
  var conversation = require('@vinaverse/client/conversation');
9
- var flashList = require('@shopify/flash-list');
10
12
 
11
- // src/cards/CardFrame.tsx
13
+ // src/index.ts
12
14
  var ChatContext = react.createContext(null);
13
15
  function ChatContextProvider(props) {
14
16
  return /* @__PURE__ */ jsxRuntime.jsx(ChatContext.Provider, { value: props.value, children: props.children });
@@ -57,6 +59,61 @@ function ThemeProvider(props) {
57
59
  function useTheme() {
58
60
  return react.useContext(ThemeContext);
59
61
  }
62
+ var Button = react.memo(function Button2(props) {
63
+ const { colors: colors2, radius: radius2, space: space2, size: size3, font: font2 } = useTheme();
64
+ const variant = props.variant ?? "secondary";
65
+ const inactive = props.disabled === true || props.busy === true;
66
+ const filled = variant === "primary" || props.selected === true;
67
+ const background = filled ? colors2.primary : variant === "secondary" ? colors2.surface : "transparent";
68
+ const foreground = filled ? colors2.onPrimary : colors2.primary;
69
+ const checkable = props.role === "radio" || props.role === "checkbox";
70
+ return /* @__PURE__ */ jsxRuntime.jsxs(
71
+ reactNative.Pressable,
72
+ {
73
+ accessibilityRole: props.role ?? "button",
74
+ accessibilityLabel: props.label,
75
+ ...props.hint ? { accessibilityHint: props.hint } : {},
76
+ accessibilityState: {
77
+ disabled: inactive,
78
+ busy: props.busy === true,
79
+ ...checkable ? props.role === "radio" ? { selected: props.selected === true } : { checked: props.selected === true } : {}
80
+ },
81
+ disabled: inactive,
82
+ onPress: props.onPress,
83
+ ...props.testID ? { testID: props.testID } : {},
84
+ style: ({ pressed }) => ({
85
+ minHeight: size3.touchTarget,
86
+ minWidth: size3.touchTarget,
87
+ paddingHorizontal: space2.md,
88
+ paddingVertical: space2.sm,
89
+ borderRadius: radius2.md,
90
+ borderWidth: variant === "text" ? 0 : 1,
91
+ borderColor: filled ? colors2.primary : colors2.border,
92
+ backgroundColor: background,
93
+ opacity: inactive ? 0.55 : pressed ? 0.8 : 1,
94
+ alignItems: "center",
95
+ justifyContent: "center",
96
+ flexDirection: "row",
97
+ gap: space2.xs
98
+ }),
99
+ children: [
100
+ props.busy ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: foreground }) : null,
101
+ props.children ?? /* @__PURE__ */ jsxRuntime.jsx(
102
+ reactNative.Text,
103
+ {
104
+ style: {
105
+ color: foreground,
106
+ fontSize: font2.size.md,
107
+ fontWeight: "500",
108
+ ...font2.family ? { fontFamily: font2.family } : {}
109
+ },
110
+ children: props.label
111
+ }
112
+ )
113
+ ]
114
+ }
115
+ );
116
+ });
60
117
  function cardOf(held) {
61
118
  return held.card;
62
119
  }
@@ -125,64 +182,26 @@ function useLink(link, held) {
125
182
  if (!link || !store.checkLink(link)) return null;
126
183
  return () => handlers.current?.onOpenLink?.(link, held);
127
184
  }
185
+ function ratingText(valuePpm, locale) {
186
+ const value = valuePpm / 1e6;
187
+ try {
188
+ return new Intl.NumberFormat(locale, {
189
+ minimumFractionDigits: 1,
190
+ maximumFractionDigits: 1
191
+ }).format(value);
192
+ } catch {
193
+ return value.toFixed(1);
194
+ }
195
+ }
196
+ function allowedImage(url, hosts) {
197
+ const safe = safeImage(url);
198
+ if (safe === null || !hosts) return safe;
199
+ const host = /^https:\/\/([^/:?#]+)/i.exec(safe)?.[1]?.toLowerCase();
200
+ return host && hosts.some((h) => h.toLowerCase() === host) ? safe : null;
201
+ }
128
202
  function safeImage(url) {
129
203
  return url && /^https:\/\//i.test(url) ? url : null;
130
204
  }
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
205
  function InfoCard(props) {
187
206
  const card = cardOf(props.card);
188
207
  const { locale, t } = react$1.useCopy();
@@ -191,7 +210,7 @@ function InfoCard(props) {
191
210
  const image = safeImage(card.image_url);
192
211
  const Img = slots.Image ?? reactNative.Image;
193
212
  const money = card.price ?? card.price_from;
194
- const rating = card.rating ? (card.rating.value_ppm / 1e6).toFixed(1) : null;
213
+ const rating = card.rating ? ratingText(card.rating.value_ppm, locale) : null;
195
214
  const amount = money ? react$1.formatMoney(money.amount, money.currency, locale) : null;
196
215
  const price = amount && !card.price ? t("priceFrom", { price: amount }) : amount;
197
216
  const when = card.starts_at ? react$1.formatTimeRange(card.starts_at, void 0, locale, timeZone) : null;
@@ -234,27 +253,32 @@ function InfoCard(props) {
234
253
  ] })
235
254
  ] }),
236
255
  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 })
256
+ /* @__PURE__ */ jsxRuntime.jsx(Badges, { badges: card.badges }),
257
+ props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(CardActions, { ...props })
253
258
  ]
254
259
  }
255
260
  );
256
261
  }
257
- function Actions(props) {
262
+ function Badges(props) {
263
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
264
+ if (!props.badges?.length) return null;
265
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space2.xs }, children: props.badges.map((b) => /* @__PURE__ */ jsxRuntime.jsx(
266
+ reactNative.Text,
267
+ {
268
+ style: {
269
+ color: colors2.text,
270
+ backgroundColor: colors2.surface,
271
+ borderRadius: radius2.pill,
272
+ paddingHorizontal: space2.sm,
273
+ paddingVertical: space2.xxs,
274
+ fontSize: font2.size.xs
275
+ },
276
+ children: b
277
+ },
278
+ b
279
+ )) });
280
+ }
281
+ function CardActions(props) {
258
282
  const card = cardOf(props.card);
259
283
  const { space: space2 } = useTheme();
260
284
  if (!card.actions?.length) return null;
@@ -286,6 +310,139 @@ function Action(props) {
286
310
  }
287
311
  );
288
312
  }
313
+ function LinksCard(props) {
314
+ const card = cardOf(props.card);
315
+ const { store, handlers } = useChat();
316
+ const links = (card.links ?? []).filter(
317
+ (l) => /^https:\/\//i.test(l.url) || store.checkLink(l.url)
318
+ );
319
+ return /* @__PURE__ */ jsxRuntime.jsxs(
320
+ CardFrame,
321
+ {
322
+ label: [card.title, ...links.map((l) => l.label)].join(", "),
323
+ restored: props.restored,
324
+ testID: `vinaverse-card-${card.id}`,
325
+ children: [
326
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { children: card.title }),
327
+ links.map((l) => /* @__PURE__ */ jsxRuntime.jsx(
328
+ Button,
329
+ {
330
+ label: l.label,
331
+ role: "link",
332
+ variant: "secondary",
333
+ disabled: props.restored,
334
+ onPress: () => {
335
+ const open = handlers.current?.onOpenLink;
336
+ if (open) open(l.url, props.card);
337
+ else void reactNative.Linking.openURL(l.url);
338
+ }
339
+ },
340
+ l.url
341
+ ))
342
+ ]
343
+ }
344
+ );
345
+ }
346
+ function ListingCard(props) {
347
+ const card = cardOf(props.card);
348
+ const { locale, t } = react$1.useCopy();
349
+ const { slots, timeZone, handlers, imagePolicy } = useChat();
350
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
351
+ const [failed, setFailed] = react.useState(null);
352
+ const photoUrl = allowedImage(
353
+ card.images?.[0]?.url ?? card.image_url,
354
+ imagePolicy?.hosts
355
+ );
356
+ const hero = photoUrl === failed ? null : photoUrl;
357
+ const Img = slots.Image ?? reactNative.Image;
358
+ const rating = card.rating ? `\u2605 ${ratingText(card.rating.value_ppm, locale)} (${card.rating.count})` : null;
359
+ const price = card.price_from ? t("priceFrom", {
360
+ price: react$1.formatMoney(
361
+ card.price_from.amount,
362
+ card.price_from.currency,
363
+ locale
364
+ )
365
+ }) : null;
366
+ const next = card.next_available ? t("nextAvailable", {
367
+ time: react$1.formatTimeRange(card.next_available, void 0, locale, timeZone)
368
+ }) : null;
369
+ const ref = card.ref;
370
+ const onOpenTarget = handlers.current?.onOpenTarget;
371
+ const open = ref && onOpenTarget && !props.restored ? () => onOpenTarget({ type: ref.type, id: ref.id }, props.card) : null;
372
+ const photo = {
373
+ width: "100%",
374
+ aspectRatio: imagePolicy?.aspectRatio ?? 1.6,
375
+ borderRadius: radius2.md
376
+ };
377
+ const top = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
378
+ hero ? /* @__PURE__ */ jsxRuntime.jsx(
379
+ Img,
380
+ {
381
+ source: { uri: hero },
382
+ onError: () => setFailed(hero),
383
+ style: { ...photo, backgroundColor: colors2.surface },
384
+ accessible: false,
385
+ testID: `vinaverse-photo-${card.id}`
386
+ }
387
+ ) : imagePolicy?.Placeholder ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: photo, testID: `vinaverse-placeholder-${card.id}`, children: /* @__PURE__ */ jsxRuntime.jsx(imagePolicy.Placeholder, { title: card.title }) }) : /* @__PURE__ */ jsxRuntime.jsx(
388
+ reactNative.View,
389
+ {
390
+ accessible: false,
391
+ importantForAccessibility: "no-hide-descendants",
392
+ testID: `vinaverse-monogram-${card.id}`,
393
+ style: {
394
+ ...photo,
395
+ backgroundColor: colors2.surface,
396
+ alignItems: "center",
397
+ justifyContent: "center"
398
+ },
399
+ children: /* @__PURE__ */ jsxRuntime.jsx(
400
+ reactNative.Text,
401
+ {
402
+ style: {
403
+ color: colors2.textMuted,
404
+ fontSize: font2.size.xl,
405
+ fontWeight: "600"
406
+ },
407
+ children: monogram(card.title)
408
+ }
409
+ )
410
+ }
411
+ ),
412
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space2.xxs }, children: [
413
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { numberOfLines: 2, children: card.title }),
414
+ card.subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: card.subtitle }) : null,
415
+ rating || price ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: [rating, price].filter(Boolean).join(" \xB7 ") }) : null
416
+ ] })
417
+ ] });
418
+ return /* @__PURE__ */ jsxRuntime.jsxs(
419
+ CardFrame,
420
+ {
421
+ label: [card.title, card.subtitle, rating, price, next].filter(Boolean).join(", "),
422
+ restored: props.restored,
423
+ testID: `vinaverse-card-${card.id}`,
424
+ children: [
425
+ open ? /* @__PURE__ */ jsxRuntime.jsx(
426
+ reactNative.Pressable,
427
+ {
428
+ accessibilityRole: "link",
429
+ accessibilityLabel: card.title,
430
+ onPress: open,
431
+ style: { gap: space2.sm },
432
+ testID: `vinaverse-open-${card.id}`,
433
+ children: top
434
+ }
435
+ ) : top,
436
+ card.body ? /* @__PURE__ */ jsxRuntime.jsx(Body, { children: card.body }) : null,
437
+ /* @__PURE__ */ jsxRuntime.jsx(Badges, { badges: next ? [next, ...card.badges ?? []] : card.badges }),
438
+ props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(CardActions, { ...props })
439
+ ]
440
+ }
441
+ );
442
+ }
443
+ function monogram(title) {
444
+ return title.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => [...w][0]?.toUpperCase() ?? "").join("");
445
+ }
289
446
  function NextStepCard(props) {
290
447
  const card = cardOf(props.card);
291
448
  const action = card.actions?.[0];
@@ -1154,7 +1311,10 @@ var DEFAULTS = {
1154
1311
  proposal: ProposalCard,
1155
1312
  question: QuestionCard,
1156
1313
  next_step: NextStepCard,
1157
- time_slots: TimeSlotsCard
1314
+ time_slots: TimeSlotsCard,
1315
+ professional: ListingCard,
1316
+ workspace: ListingCard,
1317
+ links: LinksCard
1158
1318
  };
1159
1319
  function defaultCard(kind) {
1160
1320
  return DEFAULTS[kind] ?? InfoCard;
@@ -1164,9 +1324,39 @@ var CardView = react.memo(function CardView2(props) {
1164
1324
  const { card, busy } = props.item;
1165
1325
  const Default = defaultCard(card.kind);
1166
1326
  const Custom = slots.cards?.[card.kind];
1167
- const own = { card, busy, restored: props.item.restored === true };
1327
+ const read = react.useMemo(() => agent$1.readCard(card.card), [card]);
1328
+ const own = { card, read, busy, restored: props.item.restored === true };
1168
1329
  return Custom ? /* @__PURE__ */ jsxRuntime.jsx(Custom, { ...own, Default }) : /* @__PURE__ */ jsxRuntime.jsx(Default, { ...own });
1169
1330
  });
1331
+ var keyOf = (item) => item.key;
1332
+ var kindOf = (item) => item.card.kind;
1333
+ function CardCarousel(props) {
1334
+ const { width } = reactNative.useWindowDimensions();
1335
+ const { t } = react$1.useCopy();
1336
+ const size3 = Math.min(Math.round(width * 0.8), 360);
1337
+ return /* @__PURE__ */ jsxRuntime.jsx(
1338
+ reactNative.View,
1339
+ {
1340
+ accessibilityRole: "list",
1341
+ accessibilityLabel: t("cardsInRow", {
1342
+ count: String(props.items.length)
1343
+ }),
1344
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1345
+ flashList.FlashList,
1346
+ {
1347
+ horizontal: true,
1348
+ data: props.items,
1349
+ keyExtractor: keyOf,
1350
+ getItemType: kindOf,
1351
+ renderItem: ({ item }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { width: size3 }, children: /* @__PURE__ */ jsxRuntime.jsx(CardView, { item }) }),
1352
+ snapToInterval: size3,
1353
+ decelerationRate: "fast",
1354
+ showsHorizontalScrollIndicator: false
1355
+ }
1356
+ )
1357
+ }
1358
+ );
1359
+ }
1170
1360
  var ActivityLine = react.memo(function ActivityLine2() {
1171
1361
  const { store } = useChat();
1172
1362
  const { t } = react$1.useCopy();
@@ -1189,6 +1379,38 @@ var ActivityLine = react.memo(function ActivityLine2() {
1189
1379
  }
1190
1380
  );
1191
1381
  });
1382
+ function useNativeLifecycle(store) {
1383
+ react.useEffect(() => {
1384
+ const apply = (state) => store.setForeground(state === "active");
1385
+ apply(reactNative.AppState.currentState);
1386
+ const subscription = reactNative.AppState.addEventListener("change", apply);
1387
+ return () => subscription.remove();
1388
+ }, [store]);
1389
+ }
1390
+ function useReducedMotion() {
1391
+ const [reduced, setReduced] = react.useState(false);
1392
+ react.useEffect(() => {
1393
+ let live = true;
1394
+ reactNative.AccessibilityInfo.isReduceMotionEnabled().then(
1395
+ (value) => {
1396
+ if (live) setReduced(value);
1397
+ },
1398
+ () => void 0
1399
+ );
1400
+ const subscription = reactNative.AccessibilityInfo.addEventListener(
1401
+ "reduceMotionChanged",
1402
+ setReduced
1403
+ );
1404
+ return () => {
1405
+ live = false;
1406
+ subscription.remove();
1407
+ };
1408
+ }, []);
1409
+ return reduced;
1410
+ }
1411
+ function announce(text) {
1412
+ if (text) reactNative.AccessibilityInfo.announceForAccessibility(text);
1413
+ }
1192
1414
  var Avatar = react.memo(function Avatar2(props) {
1193
1415
  const { colors: colors2, size: size3, font: font2 } = useTheme();
1194
1416
  const { slots } = useChat();
@@ -1301,37 +1523,6 @@ var ChatHeader = react.memo(function ChatHeader2() {
1301
1523
  };
1302
1524
  return Header ? /* @__PURE__ */ jsxRuntime.jsx(Header, { ...props, Default: DefaultHeader }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultHeader, { ...props });
1303
1525
  });
1304
- var ChatSkeleton = react.memo(function ChatSkeleton2() {
1305
- const { colors: colors2, radius: radius2, space: space2 } = useTheme();
1306
- const row = (width, end) => /* @__PURE__ */ jsxRuntime.jsx(
1307
- reactNative.View,
1308
- {
1309
- style: {
1310
- alignSelf: end ? "flex-end" : "flex-start",
1311
- width,
1312
- height: 40,
1313
- borderRadius: radius2.lg,
1314
- backgroundColor: colors2.surface,
1315
- marginHorizontal: space2.lg,
1316
- marginTop: space2.md
1317
- }
1318
- }
1319
- );
1320
- return /* @__PURE__ */ jsxRuntime.jsxs(
1321
- reactNative.View,
1322
- {
1323
- accessible: true,
1324
- accessibilityRole: "progressbar",
1325
- accessibilityState: { busy: true },
1326
- testID: "vinaverse-skeleton",
1327
- children: [
1328
- row("62%", false),
1329
- row("45%", true),
1330
- row("70%", false)
1331
- ]
1332
- }
1333
- );
1334
- });
1335
1526
  function DefaultComposer(props) {
1336
1527
  const { t, direction } = react$1.useCopy();
1337
1528
  const { colors: colors2, radius: radius2, space: space2, font: font2, size: size3 } = useTheme();
@@ -1473,23 +1664,37 @@ var DisclosureBar = react.memo(function DisclosureBar2() {
1473
1664
  }
1474
1665
  );
1475
1666
  });
1476
- function FailureActionButton(props) {
1477
- const { handlers, failureAction } = useChat();
1478
- const { t } = react$1.useCopy();
1479
- if (!failureAction) return null;
1480
- return /* @__PURE__ */ jsxRuntime.jsx(
1481
- Button,
1667
+ var ChatSkeleton = react.memo(function ChatSkeleton2() {
1668
+ const { colors: colors2, radius: radius2, space: space2 } = useTheme();
1669
+ const row = (width, end) => /* @__PURE__ */ jsxRuntime.jsx(
1670
+ reactNative.View,
1482
1671
  {
1483
- label: failureAction.label ?? t("talkToPerson"),
1484
- variant: "text",
1485
- onPress: () => handlers.current.failureAction?.onPress(props.state)
1672
+ style: {
1673
+ alignSelf: end ? "flex-end" : "flex-start",
1674
+ width,
1675
+ height: 40,
1676
+ borderRadius: radius2.lg,
1677
+ backgroundColor: colors2.surface,
1678
+ marginHorizontal: space2.lg,
1679
+ marginTop: space2.md
1680
+ }
1486
1681
  }
1487
1682
  );
1488
- }
1489
- function FailureSlot(props) {
1490
- const { slot: Slot, state, Default } = props;
1491
- return Slot ? /* @__PURE__ */ jsxRuntime.jsx(Slot, { state, Default }) : /* @__PURE__ */ jsxRuntime.jsx(Default, { state });
1492
- }
1683
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1684
+ reactNative.View,
1685
+ {
1686
+ accessible: true,
1687
+ accessibilityRole: "progressbar",
1688
+ accessibilityState: { busy: true },
1689
+ testID: "vinaverse-skeleton",
1690
+ children: [
1691
+ row("62%", false),
1692
+ row("45%", true),
1693
+ row("70%", false)
1694
+ ]
1695
+ }
1696
+ );
1697
+ });
1493
1698
  function DefaultBubble(props) {
1494
1699
  const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
1495
1700
  const role = props.item.role;
@@ -1611,37 +1816,22 @@ function Frame(props) {
1611
1816
  }
1612
1817
  );
1613
1818
  }
1614
- function useNativeLifecycle(store) {
1615
- react.useEffect(() => {
1616
- const apply = (state) => store.setForeground(state === "active");
1617
- apply(reactNative.AppState.currentState);
1618
- const subscription = reactNative.AppState.addEventListener("change", apply);
1619
- return () => subscription.remove();
1620
- }, [store]);
1621
- }
1622
- function useReducedMotion() {
1623
- const [reduced, setReduced] = react.useState(false);
1624
- react.useEffect(() => {
1625
- let live = true;
1626
- reactNative.AccessibilityInfo.isReduceMotionEnabled().then(
1627
- (value) => {
1628
- if (live) setReduced(value);
1629
- },
1630
- () => void 0
1631
- );
1632
- const subscription = reactNative.AccessibilityInfo.addEventListener(
1633
- "reduceMotionChanged",
1634
- setReduced
1635
- );
1636
- return () => {
1637
- live = false;
1638
- subscription.remove();
1639
- };
1640
- }, []);
1641
- return reduced;
1819
+ function FailureActionButton(props) {
1820
+ const { handlers, failureAction } = useChat();
1821
+ const { t } = react$1.useCopy();
1822
+ if (!failureAction) return null;
1823
+ return /* @__PURE__ */ jsxRuntime.jsx(
1824
+ Button,
1825
+ {
1826
+ label: failureAction.label ?? t("talkToPerson"),
1827
+ variant: "text",
1828
+ onPress: () => handlers.current.failureAction?.onPress(props.state)
1829
+ }
1830
+ );
1642
1831
  }
1643
- function announce(text) {
1644
- if (text) reactNative.AccessibilityInfo.announceForAccessibility(text);
1832
+ function FailureSlot(props) {
1833
+ const { slot: Slot, state, Default } = props;
1834
+ return Slot ? /* @__PURE__ */ jsxRuntime.jsx(Slot, { state, Default }) : /* @__PURE__ */ jsxRuntime.jsx(Default, { state });
1645
1835
  }
1646
1836
  var SystemRow = react.memo(function SystemRow2(props) {
1647
1837
  const { t } = react$1.useCopy();
@@ -1724,7 +1914,44 @@ function Line(props) {
1724
1914
  }
1725
1915
  );
1726
1916
  }
1727
- var keyOf = (item) => item.key;
1917
+ var Welcome = react.memo(function Welcome2(props) {
1918
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
1919
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1920
+ reactNative.View,
1921
+ {
1922
+ style: { padding: space2.lg, gap: space2.md },
1923
+ testID: "vinaverse-welcome",
1924
+ children: [
1925
+ /* @__PURE__ */ jsxRuntime.jsx(
1926
+ reactNative.Text,
1927
+ {
1928
+ style: {
1929
+ alignSelf: "flex-start",
1930
+ maxWidth: "85%",
1931
+ color: colors2.agentText,
1932
+ backgroundColor: colors2.agentBubble,
1933
+ borderRadius: radius2.lg,
1934
+ paddingHorizontal: space2.md,
1935
+ paddingVertical: space2.sm,
1936
+ fontSize: font2.size.md,
1937
+ lineHeight: font2.size.md * font2.lineHeight
1938
+ },
1939
+ children: props.welcome.text
1940
+ }
1941
+ ),
1942
+ 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(
1943
+ Button,
1944
+ {
1945
+ label: chip.label,
1946
+ onPress: () => props.onChip(chip.message)
1947
+ },
1948
+ chip.label
1949
+ )) }) : null
1950
+ ]
1951
+ }
1952
+ );
1953
+ });
1954
+ var keyOf2 = (item) => item.key;
1728
1955
  function itemType(item) {
1729
1956
  if (item.type === "card") return `card:${item.card.kind}`;
1730
1957
  if (item.type === "message")
@@ -1734,43 +1961,111 @@ function itemType(item) {
1734
1961
  function authorOf(item) {
1735
1962
  return item?.type === "message" ? item.role : null;
1736
1963
  }
1964
+ function carousels(items, previous) {
1965
+ const runs = /* @__PURE__ */ new Map();
1966
+ const inside = /* @__PURE__ */ new Set();
1967
+ const groupOf = (item) => {
1968
+ if (item?.type !== "card") return null;
1969
+ const group = item.card.card.group;
1970
+ return group?.layout === "carousel" && typeof group.id === "string" ? group.id : null;
1971
+ };
1972
+ for (let i = 0; i < items.length; i++) {
1973
+ const id = groupOf(items[i]);
1974
+ if (id === null) continue;
1975
+ let end = i + 1;
1976
+ while (end < items.length && groupOf(items[end]) === id) end++;
1977
+ if (end - i > 1) {
1978
+ const run = items.slice(i, end);
1979
+ const key = run[0]?.key;
1980
+ const before = previous.get(key);
1981
+ runs.set(
1982
+ key,
1983
+ before && before.length === run.length && before.every((c, n) => c === run[n]) ? before : run
1984
+ );
1985
+ for (const card of run.slice(1)) inside.add(card.key);
1986
+ }
1987
+ i = end - 1;
1988
+ }
1989
+ return { runs, inside };
1990
+ }
1737
1991
  var Row = react.memo(function Row2(props) {
1738
1992
  const { item } = props;
1993
+ const { slots } = useChat();
1994
+ const Group = slots.CardGroup;
1995
+ if (props.inside) return null;
1996
+ if (props.run && Group) return /* @__PURE__ */ jsxRuntime.jsx(Group, { items: props.run });
1739
1997
  if (item.type === "card") return /* @__PURE__ */ jsxRuntime.jsx(CardView, { item });
1740
1998
  if (item.type === "system") return /* @__PURE__ */ jsxRuntime.jsx(SystemRow, { item });
1741
1999
  return item.state === "streaming" ? /* @__PURE__ */ jsxRuntime.jsx(StreamingBubble, { item, showAuthor: props.showAuthor }) : /* @__PURE__ */ jsxRuntime.jsx(MessageBubble, { item, showAuthor: props.showAuthor });
1742
2000
  });
1743
2001
  var MessageList = react.memo(function MessageList2() {
1744
- const { store, slots } = useChat();
2002
+ const { store, slots, starters, handlers, imagePolicy } = useChat();
1745
2003
  const { t } = react$1.useCopy();
1746
2004
  const { space: space2 } = useTheme();
1747
2005
  const reducedMotion = useReducedMotion();
1748
2006
  const items = react$1.useConversationSelector(store, (v) => v.items);
2007
+ usePrefetchPhotos(items, handlers.current?.prefetchImage, imagePolicy?.hosts);
1749
2008
  const opening = react$1.useConversationSelector(
1750
2009
  store,
1751
2010
  (v) => v.status === "starting" || v.status === "connecting"
1752
2011
  );
1753
2012
  const agentName = react$1.useConversationSelector(store, (v) => v.agent.name);
2013
+ const served = react$1.useConversationSelector(store, (v) => v.welcome);
2014
+ const welcome = react.useMemo(
2015
+ () => served ?? (starters?.length ? {
2016
+ text: t("welcomeFallback", {
2017
+ agent: agentName ?? t("agentFallbackName")
2018
+ }),
2019
+ chips: [...starters]
2020
+ } : null),
2021
+ [served, starters, t, agentName]
2022
+ );
1754
2023
  useAnnounceReplies(items);
2024
+ const Group = slots.CardGroup;
2025
+ const runsRef = react.useRef(/* @__PURE__ */ new Map());
2026
+ const grouping = react.useMemo(
2027
+ () => Group ? carousels(items, runsRef.current) : null,
2028
+ [items, Group]
2029
+ );
2030
+ runsRef.current = grouping?.runs ?? /* @__PURE__ */ new Map();
1755
2031
  const itemsRef = react.useRef(items);
1756
2032
  itemsRef.current = items;
2033
+ const groupingRef = react.useRef(grouping);
2034
+ groupingRef.current = grouping;
1757
2035
  const renderItem = react.useCallback(
1758
2036
  ({ item, index }) => /* @__PURE__ */ jsxRuntime.jsx(
1759
2037
  Row,
1760
2038
  {
1761
2039
  item,
1762
- showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1])
2040
+ showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1]),
2041
+ run: groupingRef.current?.runs.get(item.key),
2042
+ inside: groupingRef.current?.inside.has(item.key) === true
1763
2043
  }
1764
2044
  ),
1765
2045
  []
1766
2046
  );
2047
+ const onChip = react.useCallback(
2048
+ (message) => {
2049
+ store.send(message);
2050
+ const chip = welcome?.chips.find((c) => c.message === message);
2051
+ if (chip) handlers.current?.onChip?.(chip);
2052
+ },
2053
+ [store, welcome, handlers]
2054
+ );
1767
2055
  const Empty = slots.EmptyState;
1768
- const empty = opening ? /* @__PURE__ */ jsxRuntime.jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { agentName: agentName ?? t("agentFallbackName") }) : null;
2056
+ const empty = opening ? /* @__PURE__ */ jsxRuntime.jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsxRuntime.jsx(
2057
+ Empty,
2058
+ {
2059
+ agentName: agentName ?? t("agentFallbackName"),
2060
+ welcome,
2061
+ onChip
2062
+ }
2063
+ ) : welcome ? /* @__PURE__ */ jsxRuntime.jsx(Welcome, { welcome, onChip }) : null;
1769
2064
  const props = react.useMemo(
1770
2065
  () => ({
1771
2066
  data: items,
1772
2067
  renderItem,
1773
- keyExtractor: keyOf,
2068
+ keyExtractor: keyOf2,
1774
2069
  getItemType: itemType,
1775
2070
  maintainVisibleContentPosition: {
1776
2071
  startRenderingFromBottom: true,
@@ -1787,10 +2082,25 @@ var MessageList = react.memo(function MessageList2() {
1787
2082
  const List = slots.ListComponent;
1788
2083
  return List ? /* @__PURE__ */ jsxRuntime.jsx(List, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(flashList.FlashList, { ...props });
1789
2084
  });
2085
+ function usePrefetchPhotos(items, prefetch, hosts) {
2086
+ const done = react.useRef(/* @__PURE__ */ new Set());
2087
+ react.useEffect(() => {
2088
+ if (!prefetch) return;
2089
+ for (const item of items) {
2090
+ if (item.type !== "card") continue;
2091
+ const card = item.card.card;
2092
+ const url = allowedImage(card.images?.[0]?.url ?? card.image_url, hosts);
2093
+ if (url && !done.current.has(url)) {
2094
+ done.current.add(url);
2095
+ prefetch(url);
2096
+ }
2097
+ }
2098
+ }, [items, prefetch, hosts]);
2099
+ }
1790
2100
  function useAnnounceReplies(items) {
1791
2101
  const seen = react.useRef(void 0);
1792
2102
  const { t } = react$1.useCopy();
1793
- const { store } = useChat();
2103
+ const { store, handlers } = useChat();
1794
2104
  react.useEffect(() => {
1795
2105
  const last = items.at(-1);
1796
2106
  if (seen.current === void 0) {
@@ -1803,8 +2113,8 @@ function useAnnounceReplies(items) {
1803
2113
  const author = last.role === "staff" ? t("staffLabel", {
1804
2114
  by: store.getSnapshot().takeover?.by || t("staffFallbackRole")
1805
2115
  }) : store.getSnapshot().agent.name ?? t("agentFallbackName");
1806
- announce(`${author}: ${last.text}`);
1807
- }, [items, t, store]);
2116
+ (handlers.current?.announce ?? announce)(`${author}: ${last.text}`);
2117
+ }, [items, t, store, handlers]);
1808
2118
  }
1809
2119
  var CONNECTING_DELAY = tokens.motion.bannerDelay;
1810
2120
  var StatusBanner = react.memo(function StatusBanner2() {
@@ -1905,7 +2215,14 @@ function Banner(props) {
1905
2215
  );
1906
2216
  }
1907
2217
  function VinaverseChatProvider(props) {
1908
- const { conversation, components, agentAvatar, timeZone } = props;
2218
+ const {
2219
+ conversation,
2220
+ components,
2221
+ agentAvatar,
2222
+ timeZone,
2223
+ starters,
2224
+ imagePolicy
2225
+ } = props;
1909
2226
  const handlers = react.useRef(props);
1910
2227
  handlers.current = props;
1911
2228
  const slots = useStableSlots(components);
@@ -1922,9 +2239,19 @@ function VinaverseChatProvider(props) {
1922
2239
  slots,
1923
2240
  agentAvatar,
1924
2241
  timeZone,
1925
- failureAction
2242
+ failureAction,
2243
+ starters,
2244
+ imagePolicy
1926
2245
  }),
1927
- [conversation, slots, agentAvatar, timeZone, failureAction]
2246
+ [
2247
+ conversation,
2248
+ slots,
2249
+ agentAvatar,
2250
+ timeZone,
2251
+ failureAction,
2252
+ starters,
2253
+ imagePolicy
2254
+ ]
1928
2255
  );
1929
2256
  useNativeLifecycle(conversation);
1930
2257
  react.useEffect(() => {
@@ -1950,7 +2277,9 @@ function VinaverseChat(props) {
1950
2277
  keyboardVerticalOffset: props.keyboardVerticalOffset ?? 0,
1951
2278
  keyboardBehavior: props.keyboardBehavior ?? (reactNative.Platform.OS === "ios" ? "padding" : "none"),
1952
2279
  style: props.style,
1953
- onError: props.onWarning
2280
+ onError: props.onWarning,
2281
+ suggestion: props.suggestion ?? null,
2282
+ footer: props.footer
1954
2283
  }
1955
2284
  ) });
1956
2285
  }
@@ -1976,11 +2305,27 @@ function Layout(props) {
1976
2305
  /* @__PURE__ */ jsxRuntime.jsx(DisclosureBar, {}),
1977
2306
  /* @__PURE__ */ jsxRuntime.jsx(StatusBanner, {}),
1978
2307
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(MessageList, {}) }),
1979
- /* @__PURE__ */ jsxRuntime.jsx(Composer, { ...reportError ? { onError: reportError } : {} })
2308
+ props.suggestion ? /* @__PURE__ */ jsxRuntime.jsx(Suggestion, { chip: props.suggestion }) : null,
2309
+ /* @__PURE__ */ jsxRuntime.jsx(Composer, { ...reportError ? { onError: reportError } : {} }),
2310
+ props.footer
1980
2311
  ]
1981
2312
  }
1982
2313
  );
1983
2314
  }
2315
+ function Suggestion(props) {
2316
+ const { store, handlers } = useChat();
2317
+ const { space: space2 } = useTheme();
2318
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { paddingHorizontal: space2.lg, alignItems: "flex-start" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2319
+ Button,
2320
+ {
2321
+ label: props.chip.label,
2322
+ onPress: () => {
2323
+ store.send(props.chip.message);
2324
+ handlers.current?.onChip?.(props.chip);
2325
+ }
2326
+ }
2327
+ ) });
2328
+ }
1984
2329
  function useStableSlots(slots) {
1985
2330
  const held = react.useRef({});
1986
2331
  const next = slots ?? {};
@@ -1999,10 +2344,635 @@ function sameSlots(a, b) {
1999
2344
  }
2000
2345
  return true;
2001
2346
  }
2347
+ function AgentChat(props) {
2348
+ const {
2349
+ agentKey,
2350
+ place,
2351
+ entryPoint,
2352
+ askAbout,
2353
+ onContact,
2354
+ onOpenPrivacy,
2355
+ components,
2356
+ agentName,
2357
+ opener,
2358
+ ...chat
2359
+ } = props;
2360
+ const runtime = agent.useAgentRuntime();
2361
+ const { conversation, state, startNew } = agent.useAgentConversation(agentKey, {
2362
+ place: place ?? null,
2363
+ entryPoint: entryPoint ?? null
2364
+ });
2365
+ const store = conversation.store;
2366
+ const spoke = react$1.useConversationSelector(
2367
+ store,
2368
+ (v) => v.items.some((i) => i.type === "message" && i.role === "person")
2369
+ );
2370
+ const { Ended, Unreachable, UnavailableScreen, Footer, ...slots } = components ?? {};
2371
+ const copy = react.useMemo(
2372
+ () => agentName ? { ...props.copy, agentFallbackName: agentName } : props.copy,
2373
+ [agentName, props.copy]
2374
+ );
2375
+ const events = {
2376
+ onOpenTarget: chat.onOpenTarget ? (target, card) => {
2377
+ runtime.emit({
2378
+ type: "target_opened",
2379
+ key: conversation.key,
2380
+ cardId: card.id,
2381
+ kind: target.type
2382
+ });
2383
+ chat.onOpenTarget?.(target, card);
2384
+ } : void 0,
2385
+ onOpenLink: chat.onOpenLink ? (link, card) => {
2386
+ runtime.emit({
2387
+ type: "link_opened",
2388
+ key: conversation.key,
2389
+ cardId: card.id,
2390
+ kind: card.kind
2391
+ });
2392
+ chat.onOpenLink?.(link, card);
2393
+ } : void 0,
2394
+ onChip: (chip) => {
2395
+ runtime.emit({ type: "chip_tapped", key: conversation.key });
2396
+ chat.onChip?.(chip);
2397
+ }
2398
+ };
2399
+ const shared = {
2400
+ ...chat,
2401
+ ...events.onOpenTarget ? { onOpenTarget: events.onOpenTarget } : {},
2402
+ ...events.onOpenLink ? { onOpenLink: events.onOpenLink } : {},
2403
+ onChip: events.onChip,
2404
+ ...copy ? { copy } : {},
2405
+ ...onContact && !chat.failureAction ? { failureAction: { onPress: () => onContact() } } : {},
2406
+ conversation: store,
2407
+ components: slots
2408
+ };
2409
+ if (state.failure === "ended")
2410
+ return /* @__PURE__ */ jsxRuntime.jsx(VinaverseChatProvider, { ...shared, children: /* @__PURE__ */ jsxRuntime.jsx(Centered, { children: Ended ? /* @__PURE__ */ jsxRuntime.jsx(
2411
+ Ended,
2412
+ {
2413
+ onStartNew: startNew,
2414
+ code: state.code,
2415
+ Default: DefaultEnded
2416
+ }
2417
+ ) : /* @__PURE__ */ jsxRuntime.jsx(DefaultEnded, { onStartNew: startNew, code: state.code }) }) });
2418
+ if (state.failure === "unreachable")
2419
+ return /* @__PURE__ */ jsxRuntime.jsx(VinaverseChatProvider, { ...shared, children: /* @__PURE__ */ jsxRuntime.jsx(Centered, { children: Unreachable ? /* @__PURE__ */ jsxRuntime.jsx(
2420
+ Unreachable,
2421
+ {
2422
+ onRetry: conversation.retry,
2423
+ reason: state.reason,
2424
+ Default: DefaultUnreachable
2425
+ }
2426
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2427
+ DefaultUnreachable,
2428
+ {
2429
+ onRetry: conversation.retry,
2430
+ reason: state.reason
2431
+ }
2432
+ ) }) });
2433
+ if (state.failure === "unavailable") {
2434
+ const own = {
2435
+ reason: state.reason,
2436
+ code: state.code,
2437
+ onContact,
2438
+ onRetry: conversation.retry
2439
+ };
2440
+ return /* @__PURE__ */ jsxRuntime.jsx(VinaverseChatProvider, { ...shared, children: /* @__PURE__ */ jsxRuntime.jsx(Centered, { children: UnavailableScreen ? /* @__PURE__ */ jsxRuntime.jsx(UnavailableScreen, { ...own, Default: DefaultUnavailableScreen }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultUnavailableScreen, { ...own }) }) });
2441
+ }
2442
+ return /* @__PURE__ */ jsxRuntime.jsx(
2443
+ VinaverseChat,
2444
+ {
2445
+ ...shared,
2446
+ suggestion: opener && !spoke ? opener : state.moved && askAbout ? { label: askAbout, message: askAbout } : null,
2447
+ footer: Footer === null ? null : /* @__PURE__ */ jsxRuntime.jsx(
2448
+ FooterFor,
2449
+ {
2450
+ store,
2451
+ state,
2452
+ rate: conversation.rate,
2453
+ Footer,
2454
+ onOpenPrivacy
2455
+ }
2456
+ )
2457
+ }
2458
+ );
2459
+ }
2460
+ function FooterFor(props) {
2461
+ const turnId = react$1.useConversationSelector(props.store, newestWholeReply);
2462
+ const own = {
2463
+ turnId,
2464
+ rating: turnId ? props.state.ratings.get(turnId) : void 0,
2465
+ onRate: (rating) => {
2466
+ if (turnId) props.rate(turnId, rating);
2467
+ },
2468
+ onOpenPrivacy: props.onOpenPrivacy
2469
+ };
2470
+ const { Footer } = props;
2471
+ return Footer ? /* @__PURE__ */ jsxRuntime.jsx(Footer, { ...own, Default: AgentFooter }) : /* @__PURE__ */ jsxRuntime.jsx(AgentFooter, { ...own });
2472
+ }
2473
+ function newestWholeReply(view) {
2474
+ for (let i = view.items.length - 1; i >= 0; i--) {
2475
+ const item = view.items[i];
2476
+ if (item?.type !== "message" || item.role !== "agent") continue;
2477
+ const whole = item.state === "sent" && !item.partial && !item.refusal && item.key.startsWith("a:");
2478
+ return whole ? item.key.slice(2) : null;
2479
+ }
2480
+ return null;
2481
+ }
2482
+ function Centered(props) {
2483
+ const { colors: colors2, space: space2 } = useTheme();
2484
+ return /* @__PURE__ */ jsxRuntime.jsx(
2485
+ reactNative.View,
2486
+ {
2487
+ style: {
2488
+ flex: 1,
2489
+ alignItems: "center",
2490
+ justifyContent: "center",
2491
+ padding: space2.xl,
2492
+ backgroundColor: colors2.background
2493
+ },
2494
+ children: props.children
2495
+ }
2496
+ );
2497
+ }
2498
+ function Notice(props) {
2499
+ const { colors: colors2, space: space2, font: font2 } = useTheme();
2500
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2501
+ reactNative.View,
2502
+ {
2503
+ accessibilityRole: "alert",
2504
+ style: { alignItems: "center", gap: space2.md },
2505
+ children: [
2506
+ /* @__PURE__ */ jsxRuntime.jsx(
2507
+ reactNative.Text,
2508
+ {
2509
+ style: {
2510
+ color: colors2.text,
2511
+ fontSize: font2.size.lg,
2512
+ fontWeight: "600",
2513
+ textAlign: "center"
2514
+ },
2515
+ children: props.title
2516
+ }
2517
+ ),
2518
+ /* @__PURE__ */ jsxRuntime.jsx(
2519
+ reactNative.Text,
2520
+ {
2521
+ style: {
2522
+ color: colors2.textMuted,
2523
+ fontSize: font2.size.md,
2524
+ textAlign: "center"
2525
+ },
2526
+ children: props.body
2527
+ }
2528
+ ),
2529
+ props.action && props.onPress ? /* @__PURE__ */ jsxRuntime.jsx(
2530
+ Button,
2531
+ {
2532
+ label: props.action,
2533
+ variant: "primary",
2534
+ onPress: props.onPress
2535
+ }
2536
+ ) : null
2537
+ ]
2538
+ }
2539
+ );
2540
+ }
2541
+ function DefaultEnded(props) {
2542
+ const { t } = react$1.useCopy();
2543
+ return /* @__PURE__ */ jsxRuntime.jsx(
2544
+ Notice,
2545
+ {
2546
+ title: t("agentEnded"),
2547
+ body: t("agentEndedBody"),
2548
+ action: t("agentStartOver"),
2549
+ onPress: props.onStartNew
2550
+ }
2551
+ );
2552
+ }
2553
+ function DefaultUnavailableScreen(props) {
2554
+ const { t } = react$1.useCopy();
2555
+ const name = agent.useAgentName(t("agentFallbackName"));
2556
+ const title = t("agentOff", { agent: name });
2557
+ return /* @__PURE__ */ jsxRuntime.jsx(
2558
+ Notice,
2559
+ {
2560
+ title: title.charAt(0).toUpperCase() + title.slice(1),
2561
+ body: t("agentOffBody"),
2562
+ ...props.onContact ? { action: t("talkToPerson"), onPress: props.onContact } : {}
2563
+ }
2564
+ );
2565
+ }
2566
+ function DefaultUnreachable(props) {
2567
+ const { t } = react$1.useCopy();
2568
+ const name = agent.useAgentName(t("agentFallbackName"));
2569
+ return /* @__PURE__ */ jsxRuntime.jsx(
2570
+ Notice,
2571
+ {
2572
+ title: t("agentUnreachable", { agent: name }),
2573
+ body: t("agentUnreachableBody"),
2574
+ action: t("tryAgain"),
2575
+ onPress: props.onRetry
2576
+ }
2577
+ );
2578
+ }
2579
+ function AgentFooter(props) {
2580
+ const { t } = react$1.useCopy();
2581
+ const { colors: colors2, space: space2, font: font2 } = useTheme();
2582
+ const muted = { color: colors2.textMuted, fontSize: font2.size.sm };
2583
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2584
+ reactNative.View,
2585
+ {
2586
+ style: {
2587
+ flexDirection: "row",
2588
+ flexWrap: "wrap",
2589
+ alignItems: "center",
2590
+ gap: space2.xs,
2591
+ paddingHorizontal: space2.lg
2592
+ },
2593
+ children: [
2594
+ props.turnId === null ? null : props.rating ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: muted, children: t("helpfulThanks") }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2595
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: muted, children: t("helpful") }),
2596
+ /* @__PURE__ */ jsxRuntime.jsx(
2597
+ Button,
2598
+ {
2599
+ label: t("helpfulYes"),
2600
+ variant: "text",
2601
+ onPress: () => props.onRate("up")
2602
+ }
2603
+ ),
2604
+ /* @__PURE__ */ jsxRuntime.jsx(
2605
+ Button,
2606
+ {
2607
+ label: t("helpfulNo"),
2608
+ variant: "text",
2609
+ onPress: () => props.onRate("down")
2610
+ }
2611
+ )
2612
+ ] }),
2613
+ props.onOpenPrivacy ? /* @__PURE__ */ jsxRuntime.jsx(
2614
+ Button,
2615
+ {
2616
+ label: t("privacy"),
2617
+ variant: "text",
2618
+ role: "link",
2619
+ onPress: props.onOpenPrivacy
2620
+ }
2621
+ ) : null
2622
+ ]
2623
+ }
2624
+ );
2625
+ }
2626
+ var LAUNCHER_SIZE = 56;
2627
+ function AgentLauncher(props) {
2628
+ return /* @__PURE__ */ jsxRuntime.jsx(react$1.CopyProvider, { locale: props.locale, copy: props.copy, children: /* @__PURE__ */ jsxRuntime.jsx(ThemeProvider, { theme: props.theme, colorScheme: props.colorScheme, children: /* @__PURE__ */ jsxRuntime.jsx(Launcher, { ...props }) }) });
2629
+ }
2630
+ function useKeyboardShown() {
2631
+ const [shown, setShown] = react.useState(false);
2632
+ react.useEffect(() => {
2633
+ const show = reactNative.Keyboard.addListener("keyboardDidShow", () => setShown(true));
2634
+ const hide = reactNative.Keyboard.addListener("keyboardDidHide", () => setShown(false));
2635
+ return () => {
2636
+ show.remove();
2637
+ hide.remove();
2638
+ };
2639
+ }, []);
2640
+ return shown;
2641
+ }
2642
+ function Launcher(props) {
2643
+ const { t } = react$1.useCopy();
2644
+ const name = agent.useAgentName(t("agentFallbackName"));
2645
+ const keyboard = useKeyboardShown();
2646
+ const reducedMotion = useReducedMotion();
2647
+ const { route = null, routes, mode } = props;
2648
+ const visible = props.hidden !== true && !(keyboard && props.hideWithKeyboard !== false) && (!routes || route !== null && routes.includes(route));
2649
+ const launcher = agent.useAgentLauncher({
2650
+ subject: props.subject,
2651
+ mode,
2652
+ visible,
2653
+ onOpen: () => props.onOpen({ route, mode }),
2654
+ ...props.storage ? { storage: props.storage } : {},
2655
+ ...props.storageKey ? { storageKey: props.storageKey } : {},
2656
+ ...props.delayMs === void 0 ? {} : { delayMs: props.delayMs }
2657
+ });
2658
+ const text = typeof props.greeting === "function" ? props.greeting(mode) : props.greeting ?? t("launcherGreeting", { agent: name });
2659
+ const say = props.announce ?? announce;
2660
+ const said = react.useRef(false);
2661
+ react.useEffect(() => {
2662
+ if (!launcher.greets) {
2663
+ said.current = false;
2664
+ return;
2665
+ }
2666
+ if (said.current) return;
2667
+ said.current = true;
2668
+ say(text);
2669
+ }, [launcher.greets, text, say]);
2670
+ if (!launcher.shows) return null;
2671
+ const edge = props.placement?.edge ?? "right";
2672
+ const look = props.appearance ?? {};
2673
+ const greeting = {
2674
+ text,
2675
+ closeLabel: t("close"),
2676
+ onOpen: launcher.open,
2677
+ onClose: launcher.dismiss,
2678
+ tail: look.tail !== false,
2679
+ edge
2680
+ };
2681
+ const button = {
2682
+ label: t("launcherOpen", { agent: name }),
2683
+ name,
2684
+ onPress: launcher.open,
2685
+ Mark: props.Mark,
2686
+ ring: look.ring ?? null,
2687
+ shadow: look.shadow !== false
2688
+ };
2689
+ const { Greeting, Button: Button3, Entrance } = props.components ?? {};
2690
+ const enter = (part, children) => Entrance ? /* @__PURE__ */ jsxRuntime.jsx(
2691
+ Entrance,
2692
+ {
2693
+ part,
2694
+ reducedMotion,
2695
+ Default: LauncherEntrance,
2696
+ children
2697
+ }
2698
+ ) : /* @__PURE__ */ jsxRuntime.jsx(LauncherEntrance, { part, reducedMotion, children });
2699
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2700
+ reactNative.View,
2701
+ {
2702
+ pointerEvents: "box-none",
2703
+ style: [
2704
+ {
2705
+ position: "absolute",
2706
+ bottom: props.placement?.bottom ?? 16,
2707
+ [edge]: props.placement?.inset ?? 16,
2708
+ alignItems: edge === "right" ? "flex-end" : "flex-start",
2709
+ gap: 8
2710
+ },
2711
+ props.style
2712
+ ],
2713
+ testID: "vinaverse-launcher",
2714
+ children: [
2715
+ launcher.greets ? enter(
2716
+ "greeting",
2717
+ Greeting ? /* @__PURE__ */ jsxRuntime.jsx(Greeting, { ...greeting, Default: LauncherGreeting }) : /* @__PURE__ */ jsxRuntime.jsx(LauncherGreeting, { ...greeting })
2718
+ ) : null,
2719
+ enter(
2720
+ "button",
2721
+ Button3 ? /* @__PURE__ */ jsxRuntime.jsx(Button3, { ...button, Default: LauncherButton }) : /* @__PURE__ */ jsxRuntime.jsx(LauncherButton, { ...button })
2722
+ )
2723
+ ]
2724
+ }
2725
+ );
2726
+ }
2727
+ function LauncherEntrance(props) {
2728
+ const { motion: motion3 } = useTheme();
2729
+ const shown = react.useRef(new reactNative.Animated.Value(props.reducedMotion ? 1 : 0)).current;
2730
+ react.useEffect(() => {
2731
+ if (props.reducedMotion) {
2732
+ shown.setValue(1);
2733
+ return;
2734
+ }
2735
+ const run = reactNative.Animated.timing(shown, {
2736
+ toValue: 1,
2737
+ duration: motion3.normal,
2738
+ useNativeDriver: true
2739
+ });
2740
+ run.start();
2741
+ return () => run.stop();
2742
+ }, [shown, props.reducedMotion, motion3.normal]);
2743
+ return /* @__PURE__ */ jsxRuntime.jsx(
2744
+ reactNative.Animated.View,
2745
+ {
2746
+ pointerEvents: "box-none",
2747
+ style: {
2748
+ opacity: shown,
2749
+ transform: [
2750
+ {
2751
+ scale: shown.interpolate({
2752
+ inputRange: [0, 1],
2753
+ outputRange: [0.9, 1]
2754
+ })
2755
+ }
2756
+ ]
2757
+ },
2758
+ testID: `vinaverse-launcher-entrance-${props.part}`,
2759
+ children: props.children
2760
+ }
2761
+ );
2762
+ }
2763
+ function LauncherGreeting(props) {
2764
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
2765
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2766
+ reactNative.View,
2767
+ {
2768
+ style: {
2769
+ maxWidth: 296,
2770
+ flexDirection: "row",
2771
+ alignItems: "flex-start",
2772
+ borderRadius: radius2.lg,
2773
+ backgroundColor: colors2.text
2774
+ },
2775
+ testID: "vinaverse-launcher-greeting",
2776
+ children: [
2777
+ props.tail ? /* @__PURE__ */ jsxRuntime.jsx(
2778
+ reactNative.View,
2779
+ {
2780
+ accessible: false,
2781
+ style: {
2782
+ position: "absolute",
2783
+ bottom: -5,
2784
+ [props.edge]: LAUNCHER_SIZE / 2 - 6,
2785
+ width: 12,
2786
+ height: 12,
2787
+ backgroundColor: colors2.text,
2788
+ transform: [{ rotate: "45deg" }]
2789
+ },
2790
+ testID: "vinaverse-launcher-tail"
2791
+ }
2792
+ ) : null,
2793
+ /* @__PURE__ */ jsxRuntime.jsx(
2794
+ reactNative.Pressable,
2795
+ {
2796
+ accessibilityRole: "button",
2797
+ accessibilityLabel: props.text,
2798
+ onPress: props.onOpen,
2799
+ style: {
2800
+ flexShrink: 1,
2801
+ paddingVertical: space2.sm,
2802
+ paddingLeft: space2.md
2803
+ },
2804
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors2.background, fontSize: font2.size.md }, children: props.text })
2805
+ }
2806
+ ),
2807
+ /* @__PURE__ */ jsxRuntime.jsx(
2808
+ reactNative.Pressable,
2809
+ {
2810
+ accessibilityRole: "button",
2811
+ accessibilityLabel: props.closeLabel,
2812
+ onPress: props.onClose,
2813
+ style: {
2814
+ minWidth: 44,
2815
+ minHeight: 44,
2816
+ alignItems: "center",
2817
+ justifyContent: "center"
2818
+ },
2819
+ testID: "vinaverse-launcher-close",
2820
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors2.background, fontSize: font2.size.md }, children: "\xD7" })
2821
+ }
2822
+ )
2823
+ ]
2824
+ }
2825
+ );
2826
+ }
2827
+ function LauncherButton(props) {
2828
+ const { colors: colors2, font: font2 } = useTheme();
2829
+ const { Mark, ring } = props;
2830
+ return /* @__PURE__ */ jsxRuntime.jsx(
2831
+ reactNative.Pressable,
2832
+ {
2833
+ accessibilityRole: "button",
2834
+ accessibilityLabel: props.label,
2835
+ onPress: props.onPress,
2836
+ style: ({ pressed }) => ({
2837
+ width: LAUNCHER_SIZE,
2838
+ height: LAUNCHER_SIZE,
2839
+ borderRadius: LAUNCHER_SIZE / 2,
2840
+ alignItems: "center",
2841
+ justifyContent: "center",
2842
+ backgroundColor: colors2.primary,
2843
+ opacity: pressed ? 0.85 : 1,
2844
+ ...ring ? { borderWidth: ring.width, borderColor: ring.color } : {},
2845
+ ...props.shadow ? {
2846
+ elevation: 6,
2847
+ shadowColor: "#000",
2848
+ shadowOpacity: 0.2,
2849
+ shadowRadius: 8,
2850
+ shadowOffset: { width: 0, height: 3 }
2851
+ } : {}
2852
+ }),
2853
+ testID: "vinaverse-launcher-button",
2854
+ children: Mark ? /* @__PURE__ */ jsxRuntime.jsx(Mark, { size: 34, color: colors2.onPrimary }) : /* @__PURE__ */ jsxRuntime.jsx(
2855
+ reactNative.Text,
2856
+ {
2857
+ style: {
2858
+ color: colors2.onPrimary,
2859
+ fontSize: font2.size.lg,
2860
+ fontWeight: "600"
2861
+ },
2862
+ children: (props.name.trim()[0] ?? "?").toUpperCase()
2863
+ }
2864
+ )
2865
+ }
2866
+ );
2867
+ }
2868
+ function AskAboutRow(props) {
2869
+ return /* @__PURE__ */ jsxRuntime.jsx(react$1.CopyProvider, { locale: props.locale, copy: props.copy, children: /* @__PURE__ */ jsxRuntime.jsx(ThemeProvider, { theme: props.theme, colorScheme: props.colorScheme, children: /* @__PURE__ */ jsxRuntime.jsx(Row3, { ...props }) }) });
2870
+ }
2871
+ function Row3(props) {
2872
+ const { t } = react$1.useCopy();
2873
+ const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
2874
+ const name = agent.useAgentName(t("agentFallbackName"));
2875
+ const { available } = agent.useAgentFeatures();
2876
+ const [shows, setShows] = react.useState(null);
2877
+ if (shows === null && available) setShows(true);
2878
+ if (!shows) return null;
2879
+ const { Mark } = props;
2880
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2881
+ reactNative.Pressable,
2882
+ {
2883
+ accessibilityRole: "button",
2884
+ accessibilityLabel: props.body ? `${props.title}. ${props.body}` : props.title,
2885
+ onPress: props.onPress,
2886
+ style: ({ pressed }) => ({
2887
+ flexDirection: "row",
2888
+ alignItems: "center",
2889
+ gap: space2.md,
2890
+ padding: space2.md,
2891
+ borderRadius: radius2.lg,
2892
+ backgroundColor: colors2.surface,
2893
+ opacity: pressed ? 0.85 : 1
2894
+ }),
2895
+ testID: "vinaverse-ask-about",
2896
+ children: [
2897
+ /* @__PURE__ */ jsxRuntime.jsx(
2898
+ reactNative.View,
2899
+ {
2900
+ style: {
2901
+ width: 36,
2902
+ height: 36,
2903
+ borderRadius: 18,
2904
+ alignItems: "center",
2905
+ justifyContent: "center",
2906
+ backgroundColor: colors2.primary
2907
+ },
2908
+ children: Mark ? /* @__PURE__ */ jsxRuntime.jsx(Mark, { size: 22, color: colors2.onPrimary }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors2.onPrimary, fontWeight: "600" }, children: (name.trim()[0] ?? "?").toUpperCase() })
2909
+ }
2910
+ ),
2911
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1, gap: space2.xxs }, children: [
2912
+ /* @__PURE__ */ jsxRuntime.jsx(
2913
+ reactNative.Text,
2914
+ {
2915
+ style: {
2916
+ color: colors2.text,
2917
+ fontSize: font2.size.md,
2918
+ fontWeight: "600"
2919
+ },
2920
+ children: props.title
2921
+ }
2922
+ ),
2923
+ props.body ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors2.textMuted, fontSize: font2.size.sm }, children: props.body }) : null
2924
+ ] }),
2925
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors2.textMuted, fontSize: font2.size.lg }, children: "\u203A" })
2926
+ ]
2927
+ }
2928
+ );
2929
+ }
2002
2930
 
2931
+ Object.defineProperty(exports, "AgentRuntimeProvider", {
2932
+ enumerable: true,
2933
+ get: function () { return agent.AgentRuntimeProvider; }
2934
+ });
2935
+ Object.defineProperty(exports, "greetedKey", {
2936
+ enumerable: true,
2937
+ get: function () { return agent.greetedKey; }
2938
+ });
2939
+ Object.defineProperty(exports, "useAgentConversation", {
2940
+ enumerable: true,
2941
+ get: function () { return agent.useAgentConversation; }
2942
+ });
2943
+ Object.defineProperty(exports, "useAgentFeatures", {
2944
+ enumerable: true,
2945
+ get: function () { return agent.useAgentFeatures; }
2946
+ });
2947
+ Object.defineProperty(exports, "useAgentLauncher", {
2948
+ enumerable: true,
2949
+ get: function () { return agent.useAgentLauncher; }
2950
+ });
2951
+ Object.defineProperty(exports, "useAgentName", {
2952
+ enumerable: true,
2953
+ get: function () { return agent.useAgentName; }
2954
+ });
2955
+ Object.defineProperty(exports, "useAgentProfile", {
2956
+ enumerable: true,
2957
+ get: function () { return agent.useAgentProfile; }
2958
+ });
2959
+ Object.defineProperty(exports, "useAgentRuntime", {
2960
+ enumerable: true,
2961
+ get: function () { return agent.useAgentRuntime; }
2962
+ });
2963
+ Object.defineProperty(exports, "useAgentStatus", {
2964
+ enumerable: true,
2965
+ get: function () { return agent.useAgentStatus; }
2966
+ });
2003
2967
  exports.ActivityLine = ActivityLine;
2968
+ exports.AgentChat = AgentChat;
2969
+ exports.AgentFooter = AgentFooter;
2970
+ exports.AgentLauncher = AgentLauncher;
2971
+ exports.AskAboutRow = AskAboutRow;
2004
2972
  exports.Avatar = Avatar;
2005
2973
  exports.Button = Button;
2974
+ exports.CardActions = CardActions;
2975
+ exports.CardCarousel = CardCarousel;
2006
2976
  exports.CardFrame = CardFrame;
2007
2977
  exports.CardView = CardView;
2008
2978
  exports.ChatHeader = ChatHeader;
@@ -2010,12 +2980,21 @@ exports.ChatSkeleton = ChatSkeleton;
2010
2980
  exports.Composer = Composer;
2011
2981
  exports.DefaultBubble = DefaultBubble;
2012
2982
  exports.DefaultComposer = DefaultComposer;
2983
+ exports.DefaultEnded = DefaultEnded;
2013
2984
  exports.DefaultHeader = DefaultHeader;
2014
2985
  exports.DefaultTurnFailed = DefaultTurnFailed;
2015
2986
  exports.DefaultUnavailable = DefaultUnavailable;
2987
+ exports.DefaultUnavailableScreen = DefaultUnavailableScreen;
2988
+ exports.DefaultUnreachable = DefaultUnreachable;
2016
2989
  exports.DisclosureBar = DisclosureBar;
2017
2990
  exports.FailureActionButton = FailureActionButton;
2018
2991
  exports.InfoCard = InfoCard;
2992
+ exports.LAUNCHER_SIZE = LAUNCHER_SIZE;
2993
+ exports.LauncherButton = LauncherButton;
2994
+ exports.LauncherEntrance = LauncherEntrance;
2995
+ exports.LauncherGreeting = LauncherGreeting;
2996
+ exports.LinksCard = LinksCard;
2997
+ exports.ListingCard = ListingCard;
2019
2998
  exports.MessageBubble = MessageBubble;
2020
2999
  exports.MessageList = MessageList;
2021
3000
  exports.NextStepCard = NextStepCard;
@@ -2029,14 +3008,21 @@ exports.ThemeProvider = ThemeProvider;
2029
3008
  exports.TimeSlotsCard = TimeSlotsCard;
2030
3009
  exports.VinaverseChat = VinaverseChat;
2031
3010
  exports.VinaverseChatProvider = VinaverseChatProvider;
3011
+ exports.Welcome = Welcome;
3012
+ exports.allowedImage = allowedImage;
2032
3013
  exports.announce = announce;
2033
3014
  exports.answerText = answerText;
3015
+ exports.carousels = carousels;
2034
3016
  exports.createTheme = createTheme;
2035
3017
  exports.defaultCard = defaultCard;
2036
3018
  exports.fieldText = fieldText;
2037
3019
  exports.itemType = itemType;
3020
+ exports.monogram = monogram;
3021
+ exports.newestWholeReply = newestWholeReply;
3022
+ exports.ratingText = ratingText;
2038
3023
  exports.themeProblems = themeProblems;
2039
3024
  exports.useChat = useChat;
3025
+ exports.useKeyboardShown = useKeyboardShown;
2040
3026
  exports.useNativeLifecycle = useNativeLifecycle;
2041
3027
  exports.useReducedMotion = useReducedMotion;
2042
3028
  exports.useTheme = useTheme;