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