@vinaverse/react-native 0.0.0-stage → 0.1.0-alpha.11

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.
Files changed (57) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +326 -2
  3. package/dist/ask.cjs +62 -0
  4. package/dist/ask.cjs.map +1 -0
  5. package/dist/ask.d.cts +38 -0
  6. package/dist/ask.d.ts +38 -0
  7. package/dist/ask.js +5 -0
  8. package/dist/ask.js.map +1 -0
  9. package/dist/chat.cjs +274 -0
  10. package/dist/chat.cjs.map +1 -0
  11. package/dist/chat.d.cts +292 -0
  12. package/dist/chat.d.ts +292 -0
  13. package/dist/chat.js +5 -0
  14. package/dist/chat.js.map +1 -0
  15. package/dist/chunk-2J2LIJLW.js +624 -0
  16. package/dist/chunk-2J2LIJLW.js.map +1 -0
  17. package/dist/chunk-4PXQ7DEY.cjs +680 -0
  18. package/dist/chunk-4PXQ7DEY.cjs.map +1 -0
  19. package/dist/chunk-6QYDPGRS.cjs +97 -0
  20. package/dist/chunk-6QYDPGRS.cjs.map +1 -0
  21. package/dist/chunk-7KPCO443.js +74 -0
  22. package/dist/chunk-7KPCO443.js.map +1 -0
  23. package/dist/chunk-EFEXTVIT.js +88 -0
  24. package/dist/chunk-EFEXTVIT.js.map +1 -0
  25. package/dist/chunk-KSHDF3MG.js +1996 -0
  26. package/dist/chunk-KSHDF3MG.js.map +1 -0
  27. package/dist/chunk-MG32E4JI.cjs +268 -0
  28. package/dist/chunk-MG32E4JI.cjs.map +1 -0
  29. package/dist/chunk-NMKJ7LIJ.cjs +76 -0
  30. package/dist/chunk-NMKJ7LIJ.cjs.map +1 -0
  31. package/dist/chunk-POJP52GE.js +261 -0
  32. package/dist/chunk-POJP52GE.js.map +1 -0
  33. package/dist/chunk-XAG2P447.cjs +2035 -0
  34. package/dist/chunk-XAG2P447.cjs.map +1 -0
  35. package/dist/context-gHCLFsJh.d.cts +212 -0
  36. package/dist/context-gHCLFsJh.d.ts +212 -0
  37. package/dist/history.cjs +673 -0
  38. package/dist/history.cjs.map +1 -0
  39. package/dist/history.d.cts +177 -0
  40. package/dist/history.d.ts +177 -0
  41. package/dist/history.js +618 -0
  42. package/dist/history.js.map +1 -0
  43. package/dist/index.cjs +304 -0
  44. package/dist/index.cjs.map +1 -0
  45. package/dist/index.d.cts +12 -0
  46. package/dist/index.d.ts +12 -0
  47. package/dist/index.js +7 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/launcher.cjs +86 -0
  50. package/dist/launcher.cjs.map +1 -0
  51. package/dist/launcher.d.cts +122 -0
  52. package/dist/launcher.d.ts +122 -0
  53. package/dist/launcher.js +5 -0
  54. package/dist/launcher.js.map +1 -0
  55. package/dist/theme-BhiEGv64.d.cts +35 -0
  56. package/dist/theme-BhiEGv64.d.ts +35 -0
  57. package/package.json +311 -3
@@ -0,0 +1,1996 @@
1
+ import { useTheme, useReducedMotion, announce } from './chunk-EFEXTVIT.js';
2
+ import { useContext, memo, useState, useMemo, useRef, useCallback, useEffect, createContext } from 'react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { Pressable, ActivityIndicator, Text, View, Image, Linking, TextInput, useWindowDimensions } from 'react-native';
5
+ import { useCopy, formatMoney, formatTimeRange, formatDayTime, useConversationSelector, formatDate, useStreamingText } from '@vinaverse/react';
6
+ import { answerProblem, codePoints } from '@vinaverse/client/conversation';
7
+ import { readCard } from '@vinaverse/client/agent';
8
+ import { FlashList } from '@shopify/flash-list';
9
+ import { contrastRatio, colors, size } from '@vinaverse/tokens';
10
+
11
+ var ChatContext = /* @__PURE__ */ createContext(
12
+ null
13
+ );
14
+ function ChatContextProvider(props) {
15
+ return /* @__PURE__ */ jsx(ChatContext.Provider, { value: props.value, children: props.children });
16
+ }
17
+ function useChat() {
18
+ const value = useContext(ChatContext);
19
+ if (!value)
20
+ throw new Error(
21
+ "Vinaverse parts render inside <VinaverseChat> or <VinaverseChatProvider>."
22
+ );
23
+ return value;
24
+ }
25
+ var Button = /* @__PURE__ */ memo(function Button2(props) {
26
+ const { colors, radius, space, size: size2, font } = useTheme();
27
+ const variant = props.variant ?? "secondary";
28
+ const inactive = props.disabled === true || props.busy === true;
29
+ const filled = variant === "primary" || props.selected === true;
30
+ const background = filled ? colors.primary : variant === "secondary" ? colors.surface : "transparent";
31
+ const foreground = filled ? colors.onPrimary : colors.primary;
32
+ const checkable = props.role === "radio" || props.role === "checkbox";
33
+ return /* @__PURE__ */ jsxs(
34
+ Pressable,
35
+ {
36
+ accessibilityRole: props.role ?? "button",
37
+ accessibilityLabel: props.label,
38
+ ...props.hint ? { accessibilityHint: props.hint } : {},
39
+ accessibilityState: {
40
+ disabled: inactive,
41
+ busy: props.busy === true,
42
+ ...checkable ? props.role === "radio" ? { selected: props.selected === true } : { checked: props.selected === true } : {}
43
+ },
44
+ disabled: inactive,
45
+ onPress: props.onPress,
46
+ ...props.testID ? { testID: props.testID } : {},
47
+ style: ({ pressed }) => ({
48
+ minHeight: size2.touchTarget,
49
+ minWidth: size2.touchTarget,
50
+ paddingHorizontal: space.md,
51
+ paddingVertical: space.sm,
52
+ borderRadius: radius.md,
53
+ borderWidth: variant === "text" ? 0 : 1,
54
+ borderColor: filled ? colors.primary : colors.border,
55
+ backgroundColor: background,
56
+ opacity: inactive ? 0.55 : pressed ? 0.8 : 1,
57
+ alignItems: "center",
58
+ justifyContent: "center",
59
+ flexDirection: "row",
60
+ gap: space.xs
61
+ }),
62
+ children: [
63
+ props.busy ? /* @__PURE__ */ jsx(ActivityIndicator, { color: foreground }) : null,
64
+ props.children ?? /* @__PURE__ */ jsx(
65
+ Text,
66
+ {
67
+ style: {
68
+ color: foreground,
69
+ fontSize: font.size.md,
70
+ fontWeight: "500",
71
+ ...font.family ? { fontFamily: font.family } : {}
72
+ },
73
+ children: props.label
74
+ }
75
+ )
76
+ ]
77
+ }
78
+ );
79
+ });
80
+ function cardOf(held) {
81
+ return held.card;
82
+ }
83
+ function CardFrame(props) {
84
+ const { colors, radius, space } = useTheme();
85
+ return /* @__PURE__ */ jsx(
86
+ View,
87
+ {
88
+ accessibilityLabel: props.label,
89
+ ...props.restored ? {
90
+ pointerEvents: "none",
91
+ accessibilityState: { disabled: true }
92
+ } : {},
93
+ ...props.testID ? { testID: props.testID } : {},
94
+ style: {
95
+ marginHorizontal: 16,
96
+ marginTop: space.sm,
97
+ padding: space.md,
98
+ gap: space.sm,
99
+ borderRadius: radius.lg,
100
+ borderWidth: 1,
101
+ borderColor: colors.divider,
102
+ backgroundColor: colors.background,
103
+ opacity: props.restored ? 0.7 : 1,
104
+ maxWidth: 520,
105
+ alignSelf: "stretch"
106
+ },
107
+ children: props.children
108
+ }
109
+ );
110
+ }
111
+ function Title(props) {
112
+ const { colors, font } = useTheme();
113
+ return /* @__PURE__ */ jsx(
114
+ Text,
115
+ {
116
+ accessibilityRole: "header",
117
+ numberOfLines: props.numberOfLines,
118
+ style: {
119
+ color: colors.text,
120
+ fontSize: font.size.md,
121
+ fontWeight: "600",
122
+ ...font.family ? { fontFamily: font.family } : {}
123
+ },
124
+ children: props.children
125
+ }
126
+ );
127
+ }
128
+ function Body(props) {
129
+ const { colors, font } = useTheme();
130
+ return /* @__PURE__ */ jsx(
131
+ Text,
132
+ {
133
+ style: {
134
+ color: props.danger ? colors.danger : props.muted ? colors.textMuted : colors.text,
135
+ fontSize: props.muted ? font.size.sm : font.size.md,
136
+ lineHeight: (props.muted ? font.size.sm : font.size.md) * font.lineHeight,
137
+ ...font.family ? { fontFamily: font.family } : {}
138
+ },
139
+ children: props.children
140
+ }
141
+ );
142
+ }
143
+ function useLink(link, held) {
144
+ const { store, handlers } = useChat();
145
+ if (!link || !store.checkLink(link)) return null;
146
+ return () => handlers.current?.onOpenLink?.(link, held);
147
+ }
148
+ function ratingText(valuePpm, locale) {
149
+ const value = valuePpm / 1e6;
150
+ try {
151
+ return new Intl.NumberFormat(locale, {
152
+ minimumFractionDigits: 1,
153
+ maximumFractionDigits: 1
154
+ }).format(value);
155
+ } catch {
156
+ return value.toFixed(1);
157
+ }
158
+ }
159
+ function allowedImage(url, hosts) {
160
+ const safe = safeImage(url);
161
+ if (safe === null || !hosts) return safe;
162
+ const host = /^https:\/\/([^/:?#]+)/i.exec(safe)?.[1]?.toLowerCase();
163
+ return host && hosts.some((h) => h.toLowerCase() === host) ? safe : null;
164
+ }
165
+ function safeImage(url) {
166
+ return url && /^https:\/\//i.test(url) ? url : null;
167
+ }
168
+ function InfoCard(props) {
169
+ const card = cardOf(props.card);
170
+ const { locale, t } = useCopy();
171
+ const { slots, timeZone, handlers } = useChat();
172
+ const { colors, radius, space, font } = useTheme();
173
+ const image = safeImage(card.image_url);
174
+ const Img = slots.Image ?? Image;
175
+ const money = card.price ?? card.price_from;
176
+ const rating = card.rating ? ratingText(card.rating.value_ppm, locale) : null;
177
+ const amount = money ? formatMoney(money.amount, money.currency, locale) : null;
178
+ const price = amount && !card.price ? t("priceFrom", { price: amount }) : amount;
179
+ const when = card.starts_at ? formatTimeRange(card.starts_at, void 0, locale, timeZone) : null;
180
+ const status = typeof card.status === "string" && card.status ? card.status : null;
181
+ const ref = card.ref;
182
+ const onOpenTarget = handlers.current?.onOpenTarget;
183
+ const open = ref && onOpenTarget && !props.restored ? () => onOpenTarget({ type: ref.type, id: ref.id }, props.card) : null;
184
+ const label = [
185
+ card.title,
186
+ card.subtitle,
187
+ status,
188
+ rating && `\u2605 ${rating}`,
189
+ price,
190
+ when
191
+ ].filter(Boolean).join(", ");
192
+ const top = /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", gap: space.md }, children: [
193
+ image ? /* @__PURE__ */ jsx(
194
+ Img,
195
+ {
196
+ source: { uri: image },
197
+ style: {
198
+ width: 64,
199
+ height: 64,
200
+ borderRadius: radius.md,
201
+ backgroundColor: colors.surface
202
+ },
203
+ accessible: false
204
+ }
205
+ ) : null,
206
+ /* @__PURE__ */ jsxs(View, { style: { flex: 1, gap: space.xxs }, children: [
207
+ /* @__PURE__ */ jsx(Title, { numberOfLines: 2, children: card.title }),
208
+ card.subtitle ? /* @__PURE__ */ jsx(Body, { muted: true, children: card.subtitle }) : null,
209
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.textMuted, fontSize: font.size.sm }, children: [
210
+ rating && `\u2605 ${rating}${card.rating ? ` (${card.rating.count})` : ""}`,
211
+ price,
212
+ when
213
+ ].filter(Boolean).join(" \xB7 ") })
214
+ ] })
215
+ ] });
216
+ return /* @__PURE__ */ jsxs(
217
+ CardFrame,
218
+ {
219
+ label,
220
+ restored: props.restored,
221
+ testID: `vinaverse-card-${card.id}`,
222
+ children: [
223
+ open ? /* @__PURE__ */ jsx(
224
+ Pressable,
225
+ {
226
+ accessibilityRole: "link",
227
+ accessibilityLabel: card.title,
228
+ onPress: open,
229
+ testID: `vinaverse-open-${card.id}`,
230
+ children: top
231
+ }
232
+ ) : top,
233
+ card.body ? /* @__PURE__ */ jsx(Body, { children: card.body }) : null,
234
+ /* @__PURE__ */ jsx(
235
+ Badges,
236
+ {
237
+ badges: status ? [status, ...card.badges ?? []] : card.badges
238
+ }
239
+ ),
240
+ props.restored ? null : /* @__PURE__ */ jsx(CardActions, { ...props })
241
+ ]
242
+ }
243
+ );
244
+ }
245
+ function Badges(props) {
246
+ const { colors, radius, space, font } = useTheme();
247
+ if (!props.badges?.length) return null;
248
+ return /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.xs }, children: props.badges.map((b) => /* @__PURE__ */ jsx(
249
+ Text,
250
+ {
251
+ style: {
252
+ color: colors.text,
253
+ backgroundColor: colors.surface,
254
+ borderRadius: radius.pill,
255
+ paddingHorizontal: space.sm,
256
+ paddingVertical: space.xxs,
257
+ fontSize: font.size.xs
258
+ },
259
+ children: b
260
+ },
261
+ b
262
+ )) });
263
+ }
264
+ function CardActions(props) {
265
+ const card = cardOf(props.card);
266
+ const { space } = useTheme();
267
+ if (!card.actions?.length) return null;
268
+ return /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.sm }, children: card.actions.map((a) => /* @__PURE__ */ jsx(
269
+ Action,
270
+ {
271
+ held: props.card,
272
+ action: a.action,
273
+ label: a.label,
274
+ link: a.deep_link
275
+ },
276
+ `${a.action}:${a.label}`
277
+ )) });
278
+ }
279
+ function Action(props) {
280
+ const { store, handlers } = useChat();
281
+ const open = useLink(props.link, props.held);
282
+ if (props.link && !open) return null;
283
+ return /* @__PURE__ */ jsx(
284
+ Button,
285
+ {
286
+ label: props.label,
287
+ role: open ? "link" : "button",
288
+ onPress: () => {
289
+ if (open) return open();
290
+ store.cardAction(props.held.id, props.action);
291
+ handlers.current?.onCardAction?.(props.held, props.action);
292
+ }
293
+ }
294
+ );
295
+ }
296
+ function LinksCard(props) {
297
+ const card = cardOf(props.card);
298
+ const { store, handlers } = useChat();
299
+ const links = (card.links ?? []).filter(
300
+ (l) => /^https:\/\//i.test(l.url) || store.checkLink(l.url)
301
+ );
302
+ return /* @__PURE__ */ jsxs(
303
+ CardFrame,
304
+ {
305
+ label: [card.title, ...links.map((l) => l.label)].join(", "),
306
+ restored: props.restored,
307
+ testID: `vinaverse-card-${card.id}`,
308
+ children: [
309
+ /* @__PURE__ */ jsx(Title, { children: card.title }),
310
+ links.map((l) => /* @__PURE__ */ jsx(
311
+ Button,
312
+ {
313
+ label: l.label,
314
+ role: "link",
315
+ variant: "secondary",
316
+ disabled: props.restored,
317
+ onPress: () => {
318
+ const open = handlers.current?.onOpenLink;
319
+ if (open) open(l.url, props.card);
320
+ else void Linking.openURL(l.url);
321
+ }
322
+ },
323
+ l.url
324
+ ))
325
+ ]
326
+ }
327
+ );
328
+ }
329
+ function ListingCard(props) {
330
+ const card = cardOf(props.card);
331
+ const { locale, t } = useCopy();
332
+ const { slots, timeZone, handlers, imagePolicy } = useChat();
333
+ const { colors, radius, space, font } = useTheme();
334
+ const [failed, setFailed] = useState(null);
335
+ const photoUrl = allowedImage(
336
+ card.images?.[0]?.url ?? card.image_url,
337
+ imagePolicy?.hosts
338
+ );
339
+ const hero = photoUrl === failed ? null : photoUrl;
340
+ const Img = slots.Image ?? Image;
341
+ const rating = card.rating ? `\u2605 ${ratingText(card.rating.value_ppm, locale)} (${card.rating.count})` : t("ratingNone") || null;
342
+ const price = card.price_from ? t("priceFrom", {
343
+ price: formatMoney(
344
+ card.price_from.amount,
345
+ card.price_from.currency,
346
+ locale
347
+ )
348
+ }) : null;
349
+ const next = card.next_available ? t("nextAvailable", {
350
+ // "today, 10:00 AM", "tomorrow, 10:00 AM", else the date (the copy's `today` and `tomorrow`).
351
+ time: formatDayTime(
352
+ card.next_available,
353
+ { today: t("today"), tomorrow: t("tomorrow") },
354
+ locale,
355
+ timeZone
356
+ )
357
+ }) : null;
358
+ const ref = card.ref;
359
+ const onOpenTarget = handlers.current?.onOpenTarget;
360
+ const open = ref && onOpenTarget && !props.restored ? () => onOpenTarget({ type: ref.type, id: ref.id }, props.card) : null;
361
+ const asked = imagePolicy?.aspectRatio;
362
+ const size2 = card.images?.[0];
363
+ const range = imagePolicy?.naturalAspect ?? { min: 1, max: 2 };
364
+ const own = asked === "natural" && !props.grouped && size2?.width && size2.height ? Math.min(range.max, Math.max(range.min, size2.width / size2.height)) : null;
365
+ const photo = {
366
+ width: "100%",
367
+ aspectRatio: own ?? (typeof asked === "number" ? asked : 1.6),
368
+ borderRadius: radius.md
369
+ };
370
+ const top = /* @__PURE__ */ jsxs(Fragment, { children: [
371
+ hero ? /* @__PURE__ */ jsx(
372
+ Img,
373
+ {
374
+ source: { uri: hero },
375
+ onError: () => setFailed(hero),
376
+ style: { ...photo, backgroundColor: colors.surface },
377
+ accessible: false,
378
+ testID: `vinaverse-photo-${card.id}`
379
+ }
380
+ ) : imagePolicy?.Placeholder ? /* @__PURE__ */ jsx(View, { style: photo, testID: `vinaverse-placeholder-${card.id}`, children: /* @__PURE__ */ jsx(imagePolicy.Placeholder, { title: card.title, kind: card.kind }) }) : /* @__PURE__ */ jsx(
381
+ View,
382
+ {
383
+ accessible: false,
384
+ importantForAccessibility: "no-hide-descendants",
385
+ testID: `vinaverse-monogram-${card.id}`,
386
+ style: {
387
+ ...photo,
388
+ backgroundColor: colors.surface,
389
+ alignItems: "center",
390
+ justifyContent: "center"
391
+ },
392
+ children: /* @__PURE__ */ jsx(
393
+ Text,
394
+ {
395
+ style: {
396
+ color: colors.textMuted,
397
+ fontSize: font.size.xl,
398
+ fontWeight: "600"
399
+ },
400
+ children: monogram(card.title)
401
+ }
402
+ )
403
+ }
404
+ ),
405
+ /* @__PURE__ */ jsxs(View, { style: { gap: space.xxs }, children: [
406
+ /* @__PURE__ */ jsx(Title, { numberOfLines: 2, children: card.title }),
407
+ card.subtitle ? /* @__PURE__ */ jsx(Body, { muted: true, children: card.subtitle }) : null,
408
+ rating || price ? /* @__PURE__ */ jsx(Body, { muted: true, children: [rating, price].filter(Boolean).join(" \xB7 ") }) : null
409
+ ] })
410
+ ] });
411
+ return /* @__PURE__ */ jsxs(
412
+ CardFrame,
413
+ {
414
+ label: [card.title, card.subtitle, rating, price, next].filter(Boolean).join(", "),
415
+ restored: props.restored,
416
+ testID: `vinaverse-card-${card.id}`,
417
+ children: [
418
+ open ? /* @__PURE__ */ jsx(
419
+ Pressable,
420
+ {
421
+ accessibilityRole: "link",
422
+ accessibilityLabel: card.title,
423
+ onPress: open,
424
+ style: { gap: space.sm },
425
+ testID: `vinaverse-open-${card.id}`,
426
+ children: top
427
+ }
428
+ ) : top,
429
+ card.body ? /* @__PURE__ */ jsx(Body, { children: card.body }) : null,
430
+ /* @__PURE__ */ jsx(Badges, { badges: next ? [next, ...card.badges ?? []] : card.badges }),
431
+ props.restored ? null : /* @__PURE__ */ jsx(CardActions, { ...props })
432
+ ]
433
+ }
434
+ );
435
+ }
436
+ function monogram(title) {
437
+ return title.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => [...w][0]?.toUpperCase() ?? "").join("");
438
+ }
439
+ function NextStepCard(props) {
440
+ const card = cardOf(props.card);
441
+ const action = card.actions?.[0];
442
+ const open = useLink(action?.deep_link, props.card);
443
+ return /* @__PURE__ */ jsxs(
444
+ CardFrame,
445
+ {
446
+ label: [card.title, card.body].filter(Boolean).join(". "),
447
+ restored: props.restored,
448
+ testID: `vinaverse-card-${card.id}`,
449
+ children: [
450
+ /* @__PURE__ */ jsx(Title, { children: card.title }),
451
+ card.body ? /* @__PURE__ */ jsx(Body, { muted: true, children: card.body }) : null,
452
+ action && open && !props.restored ? /* @__PURE__ */ jsx(
453
+ Button,
454
+ {
455
+ label: action.label,
456
+ variant: "primary",
457
+ role: "link",
458
+ onPress: open
459
+ }
460
+ ) : null
461
+ ]
462
+ }
463
+ );
464
+ }
465
+ function fieldText(type, value, locale, timeZone) {
466
+ if (value === null || value === void 0) return "";
467
+ const v = value;
468
+ switch (type) {
469
+ case "list":
470
+ return Array.isArray(value) ? value.join(", ") : String(value);
471
+ case "time_range":
472
+ return formatTimeRange(
473
+ String(v.start ?? ""),
474
+ typeof v.end === "string" ? v.end : void 0,
475
+ locale,
476
+ timeZone
477
+ );
478
+ case "money_cents":
479
+ return formatMoney(
480
+ Number(v.amount ?? 0),
481
+ String(v.currency ?? "USD"),
482
+ locale
483
+ );
484
+ case "address":
485
+ return ["line1", "line2", "city", "region", "postal_code", "country"].map((k) => v[k]).filter((p) => typeof p === "string" && p).join(", ");
486
+ case "weekly_hours": {
487
+ const entries = Array.isArray(v.entries) ? v.entries : [];
488
+ return entries.map((e) => `${weekday(e.day, locale)} ${e.start}\u2013${e.end}`).join(", ");
489
+ }
490
+ default:
491
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
492
+ }
493
+ }
494
+ var DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
495
+ function weekday(day, locale) {
496
+ const i = DAYS.indexOf(day);
497
+ if (i < 0) return day;
498
+ try {
499
+ return new Intl.DateTimeFormat(locale, {
500
+ weekday: "short",
501
+ timeZone: "UTC"
502
+ }).format(new Date(Date.UTC(2024, 0, 1 + i)));
503
+ } catch {
504
+ return day;
505
+ }
506
+ }
507
+ function outcomeText(t, outcome, status) {
508
+ if (outcome === "confirmed") return t("proposalConfirmed");
509
+ if (outcome === "declined") return t("proposalDeclined");
510
+ if (outcome === "failed") return t("proposalFailed");
511
+ if (outcome === "expired") return t("proposalExpired");
512
+ if (status === "confirmed") return t("proposalConfirmed");
513
+ if (status === "declined") return t("proposalDeclined");
514
+ if (status === "expired") return t("proposalExpired");
515
+ if (status !== "pending") return t("proposalSettled");
516
+ return null;
517
+ }
518
+ function ProposalCard(props) {
519
+ const card = cardOf(props.card);
520
+ const proposal = card.proposal;
521
+ const { t, locale } = useCopy();
522
+ const { store, handlers, timeZone } = useChat();
523
+ const { colors, space, font } = useTheme();
524
+ const edit = useLink(proposal?.edit_deep_link, props.card);
525
+ if (!proposal) return null;
526
+ const settled = outcomeText(t, props.card.outcome, proposal.status);
527
+ const actionable = settled === null && !props.restored;
528
+ const decide = (decision) => {
529
+ const onProposal = handlers.current?.onProposal;
530
+ if (!onProposal) return;
531
+ store.decideProposal(
532
+ props.card.id,
533
+ () => onProposal(props.card, decision)
534
+ ).catch(
535
+ (error) => handlers.current?.onWarning?.(`Vinaverse proposal: ${String(error)}`, {
536
+ code: "proposal_failed"
537
+ })
538
+ );
539
+ };
540
+ const fields = proposal.fields.map((f) => {
541
+ const after = fieldText(f.type, f.after, locale, timeZone);
542
+ const before = f.before === void 0 || f.before === null ? null : fieldText(f.type, f.before, locale, timeZone);
543
+ return {
544
+ key: f.key,
545
+ label: f.label,
546
+ value: before !== null && before !== after ? `${before} \u2192 ${after}` : after
547
+ };
548
+ });
549
+ return /* @__PURE__ */ jsxs(
550
+ CardFrame,
551
+ {
552
+ label: [
553
+ proposal.title,
554
+ proposal.summary,
555
+ ...fields.map((f) => `${f.label}: ${f.value}`),
556
+ settled
557
+ ].filter(Boolean).join(". "),
558
+ restored: props.restored,
559
+ testID: `vinaverse-proposal-${card.id}`,
560
+ children: [
561
+ /* @__PURE__ */ jsx(Title, { children: proposal.title }),
562
+ /* @__PURE__ */ jsx(Body, { muted: true, children: proposal.summary }),
563
+ /* @__PURE__ */ jsx(View, { style: { gap: space.xs }, children: fields.map((f) => /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", gap: space.sm }, children: [
564
+ /* @__PURE__ */ jsx(
565
+ Text,
566
+ {
567
+ style: {
568
+ color: colors.textMuted,
569
+ fontSize: font.size.sm,
570
+ minWidth: 72
571
+ },
572
+ children: f.label
573
+ }
574
+ ),
575
+ /* @__PURE__ */ jsx(
576
+ Text,
577
+ {
578
+ style: { color: colors.text, fontSize: font.size.sm, flex: 1 },
579
+ children: f.value
580
+ }
581
+ )
582
+ ] }, f.key)) }),
583
+ settled ? /* @__PURE__ */ jsx(
584
+ Text,
585
+ {
586
+ style: {
587
+ color: props.card.outcome === "confirmed" || proposal.status === "confirmed" ? colors.success : colors.textMuted,
588
+ fontSize: font.size.sm,
589
+ fontWeight: "600"
590
+ },
591
+ children: settled
592
+ }
593
+ ) : null,
594
+ actionable ? /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.sm }, children: [
595
+ /* @__PURE__ */ jsx(
596
+ Button,
597
+ {
598
+ label: t("proposalConfirm"),
599
+ variant: "primary",
600
+ busy: props.busy,
601
+ disabled: props.busy,
602
+ onPress: () => decide("confirm"),
603
+ testID: "vinaverse-proposal-confirm"
604
+ }
605
+ ),
606
+ /* @__PURE__ */ jsx(
607
+ Button,
608
+ {
609
+ label: t("proposalDecline"),
610
+ disabled: props.busy,
611
+ onPress: () => decide("decline"),
612
+ testID: "vinaverse-proposal-decline"
613
+ }
614
+ ),
615
+ edit ? /* @__PURE__ */ jsx(
616
+ Button,
617
+ {
618
+ label: t("proposalChange"),
619
+ variant: "text",
620
+ role: "link",
621
+ onPress: edit
622
+ }
623
+ ) : null
624
+ ] }) : null
625
+ ]
626
+ }
627
+ );
628
+ }
629
+ var KNOWN = /* @__PURE__ */ new Set([
630
+ "single_choice",
631
+ "multi_choice",
632
+ "image_choice",
633
+ "yes_no",
634
+ "list_choice",
635
+ "rate_each",
636
+ "short_text",
637
+ "long_text",
638
+ "date",
639
+ "number",
640
+ "scale",
641
+ "photo"
642
+ ]);
643
+ var ONE_TAP = /* @__PURE__ */ new Set(["single_choice", "yes_no", "list_choice", "scale"]);
644
+ function QuestionCard(props) {
645
+ const held = props.card;
646
+ const question = held.question;
647
+ const { store, handlers } = useChat();
648
+ const { t, locale } = useCopy();
649
+ const { colors, font, space } = useTheme();
650
+ const needsSocket = useConversationSelector(
651
+ store,
652
+ (v) => v.answersNeedWebSocket
653
+ );
654
+ if (!question) return null;
655
+ const send = (answer, questionId = question.question_id) => {
656
+ try {
657
+ store.answer(held.id, questionId, answer);
658
+ } catch (error) {
659
+ handlers.current?.onWarning?.(`Vinaverse answer: ${String(error)}`, {
660
+ code: "answer_failed"
661
+ });
662
+ }
663
+ };
664
+ const locked = held.locked.includes(question.question_id);
665
+ const state = question.state;
666
+ const open = state === "open" && !locked && !props.restored;
667
+ const summary = state === "answered" ? answerText(question, locale, t("questionOther")) : null;
668
+ return /* @__PURE__ */ jsxs(
669
+ CardFrame,
670
+ {
671
+ label: [question.prompt, summary].filter(Boolean).join(". "),
672
+ restored: props.restored,
673
+ testID: `vinaverse-question-${held.id}`,
674
+ children: [
675
+ question.progress ? /* @__PURE__ */ jsx(Text, { style: { color: colors.textMuted, fontSize: font.size.xs }, children: t("questionStep", {
676
+ step: question.progress.step,
677
+ total: question.progress.total
678
+ }) }) : null,
679
+ /* @__PURE__ */ jsx(Title, { children: question.prompt }),
680
+ question.help ? /* @__PURE__ */ jsx(Body, { muted: true, children: question.help }) : null,
681
+ state === "answered" ? /* @__PURE__ */ jsxs(View, { style: { gap: space.xxs }, children: [
682
+ /* @__PURE__ */ jsx(Body, { muted: true, children: t("questionAnswered") }),
683
+ summary ? /* @__PURE__ */ jsx(Body, { children: summary }) : null
684
+ ] }) : state === "superseded" ? /* @__PURE__ */ jsx(Body, { muted: true, children: t("questionReplaced") }) : !KNOWN.has(question.kind) || needsSocket ? /* @__PURE__ */ jsx(Body, { muted: true, children: t("typeAnswerBelow") }) : question.kind === "rate_each" ? /* @__PURE__ */ jsx(
685
+ RateEach,
686
+ {
687
+ question,
688
+ locked: held.locked,
689
+ enabled: !props.restored,
690
+ send
691
+ }
692
+ ) : /* @__PURE__ */ jsx(
693
+ Answer,
694
+ {
695
+ question,
696
+ enabled: open,
697
+ send,
698
+ held
699
+ },
700
+ question.question_id
701
+ )
702
+ ]
703
+ }
704
+ );
705
+ }
706
+ function Answer(props) {
707
+ const { question, send, enabled } = props;
708
+ const { t } = useCopy();
709
+ const { space } = useTheme();
710
+ const { handlers } = useChat();
711
+ const [draft, setDraft] = useState({});
712
+ const kind = question.kind;
713
+ const oneTap = ONE_TAP.has(kind) && !question.allows_other;
714
+ const problem = answerProblem(question, draft);
715
+ const submit = (answer = draft) => {
716
+ if (answerProblem(question, answer) === null)
717
+ send(answer);
718
+ };
719
+ const input = (() => {
720
+ switch (kind) {
721
+ case "short_text":
722
+ case "long_text":
723
+ return /* @__PURE__ */ jsx(
724
+ TextAnswer,
725
+ {
726
+ question,
727
+ enabled,
728
+ value: draft.text ?? "",
729
+ onChange: (text) => setDraft({ text })
730
+ }
731
+ );
732
+ case "number":
733
+ return /* @__PURE__ */ jsx(
734
+ NumberAnswer,
735
+ {
736
+ question,
737
+ enabled,
738
+ value: draft.number,
739
+ onChange: (n) => setDraft({ number: n })
740
+ }
741
+ );
742
+ case "scale":
743
+ return /* @__PURE__ */ jsx(
744
+ ScaleAnswer,
745
+ {
746
+ question,
747
+ enabled,
748
+ onPick: (n) => submit({ number: n })
749
+ }
750
+ );
751
+ case "date":
752
+ return /* @__PURE__ */ jsx(
753
+ DateAnswer,
754
+ {
755
+ question,
756
+ enabled,
757
+ value: draft.date,
758
+ onChange: (date) => setDraft({ date })
759
+ }
760
+ );
761
+ case "photo":
762
+ return /* @__PURE__ */ jsx(
763
+ Button,
764
+ {
765
+ label: t("questionAddPhoto"),
766
+ disabled: !enabled || !handlers.current?.onPickPhoto,
767
+ onPress: () => {
768
+ handlers.current?.onPickPhoto?.(question, props.held).then((picked) => {
769
+ if (picked) send({ upload_ref: picked.upload_ref });
770
+ }).catch(
771
+ (error) => handlers.current?.onWarning?.(
772
+ `Vinaverse photo: ${String(error)}`,
773
+ { code: "photo_failed" }
774
+ )
775
+ );
776
+ }
777
+ }
778
+ );
779
+ default:
780
+ return /* @__PURE__ */ jsx(
781
+ ChoiceAnswer,
782
+ {
783
+ question,
784
+ enabled,
785
+ draft,
786
+ onChange: (next) => oneTap ? submit(next) : setDraft(next)
787
+ }
788
+ );
789
+ }
790
+ })();
791
+ const tapToAnswer = oneTap || kind === "scale" || kind === "photo";
792
+ return /* @__PURE__ */ jsxs(View, { style: { gap: space.sm }, children: [
793
+ input,
794
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.sm }, children: [
795
+ tapToAnswer ? null : /* @__PURE__ */ jsx(
796
+ Button,
797
+ {
798
+ label: t("questionSubmit"),
799
+ variant: "primary",
800
+ disabled: !enabled || problem !== null,
801
+ onPress: () => submit(),
802
+ testID: "vinaverse-question-submit"
803
+ }
804
+ ),
805
+ question.required ? null : /* @__PURE__ */ jsx(
806
+ Button,
807
+ {
808
+ label: t("questionSkip"),
809
+ variant: "text",
810
+ disabled: !enabled,
811
+ onPress: () => send({ skipped: true })
812
+ }
813
+ )
814
+ ] })
815
+ ] });
816
+ }
817
+ function ChoiceAnswer(props) {
818
+ const { question, draft, onChange, enabled } = props;
819
+ const { t } = useCopy();
820
+ const { slots } = useChat();
821
+ const { colors, radius, space, font } = useTheme();
822
+ const [search, setSearch] = useState("");
823
+ const multi = question.kind === "multi_choice" || question.kind === "image_choice";
824
+ const values = draft.values ?? [];
825
+ const other = draft.other_text;
826
+ const none = question.none_of_the_above;
827
+ const list = question.kind === "list_choice";
828
+ const options = useMemo(() => {
829
+ const all = question.options ?? [];
830
+ const q = search.trim().toLowerCase();
831
+ return list && q ? all.filter((o) => o.label.toLowerCase().includes(q)) : all;
832
+ }, [question.options, search, list]);
833
+ const shown = list ? options.slice(0, 50) : options;
834
+ const pick = (value) => {
835
+ if (none && value === none.value) return onChange({ values: [value] });
836
+ const rest = values.filter((v) => v !== none?.value);
837
+ const next = multi ? rest.includes(value) ? rest.filter((v) => v !== value) : [...rest, value] : [value];
838
+ const keepOther = other !== void 0 && (multi || question.kind === "single_choice");
839
+ onChange({
840
+ ...next.length ? { values: next } : {},
841
+ ...keepOther ? { other_text: other } : {}
842
+ });
843
+ };
844
+ const Img = slots.Image ?? Image;
845
+ return /* @__PURE__ */ jsxs(View, { style: { gap: space.xs }, children: [
846
+ list ? /* @__PURE__ */ jsx(
847
+ TextInput,
848
+ {
849
+ value: search,
850
+ onChangeText: setSearch,
851
+ placeholder: t("questionSearch"),
852
+ placeholderTextColor: colors.textMuted,
853
+ accessibilityLabel: t("questionSearch"),
854
+ editable: enabled,
855
+ style: {
856
+ borderWidth: 1,
857
+ borderColor: colors.border,
858
+ borderRadius: radius.md,
859
+ paddingHorizontal: space.md,
860
+ minHeight: 44,
861
+ color: colors.text,
862
+ fontSize: font.size.md
863
+ }
864
+ }
865
+ ) : null,
866
+ shown.map((o) => {
867
+ const image = question.kind === "image_choice" ? safeImage(o.image_url) : null;
868
+ return /* @__PURE__ */ jsx(
869
+ Button,
870
+ {
871
+ label: o.label,
872
+ role: multi ? "checkbox" : "radio",
873
+ selected: values.includes(o.value),
874
+ disabled: !enabled,
875
+ ...o.description ? { hint: o.description } : {},
876
+ onPress: () => pick(o.value),
877
+ children: image ? /* @__PURE__ */ jsxs(View, { style: { alignItems: "center", gap: space.xs }, children: [
878
+ /* @__PURE__ */ jsx(
879
+ Img,
880
+ {
881
+ source: { uri: image },
882
+ style: { width: 96, height: 96, borderRadius: radius.md },
883
+ accessible: false
884
+ }
885
+ ),
886
+ /* @__PURE__ */ jsx(
887
+ ChoiceLabel,
888
+ {
889
+ selected: values.includes(o.value),
890
+ text: o.label
891
+ }
892
+ )
893
+ ] }) : /* @__PURE__ */ jsx(ChoiceLabel, { selected: values.includes(o.value), text: o.label })
894
+ },
895
+ o.value
896
+ );
897
+ }),
898
+ none ? /* @__PURE__ */ jsx(
899
+ Button,
900
+ {
901
+ label: none.label,
902
+ role: multi ? "checkbox" : "radio",
903
+ selected: values.includes(none.value),
904
+ disabled: !enabled,
905
+ onPress: () => pick(none.value)
906
+ }
907
+ ) : null,
908
+ question.allows_other ? /* @__PURE__ */ jsx(
909
+ OtherText,
910
+ {
911
+ max: question.other_max_length ?? 500,
912
+ enabled,
913
+ value: other,
914
+ onChange: (text) => {
915
+ const kept = values.filter((v) => v !== none?.value);
916
+ const withValues = question.kind === "single_choice" || multi;
917
+ onChange({
918
+ ...withValues && kept.length ? { values: question.kind === "single_choice" ? [] : kept } : {},
919
+ ...text === void 0 ? {} : { other_text: text }
920
+ });
921
+ }
922
+ }
923
+ ) : null
924
+ ] });
925
+ }
926
+ function ChoiceLabel(props) {
927
+ const { colors, font } = useTheme();
928
+ return /* @__PURE__ */ jsx(
929
+ Text,
930
+ {
931
+ style: {
932
+ color: props.selected ? colors.onPrimary : colors.text,
933
+ fontSize: font.size.md,
934
+ textAlign: "center"
935
+ },
936
+ children: props.text
937
+ }
938
+ );
939
+ }
940
+ function OtherText(props) {
941
+ const { t } = useCopy();
942
+ const { colors, radius, space, font } = useTheme();
943
+ const on = props.value !== void 0;
944
+ return /* @__PURE__ */ jsxs(View, { style: { gap: space.xs }, children: [
945
+ /* @__PURE__ */ jsx(
946
+ Button,
947
+ {
948
+ label: t("questionOther"),
949
+ role: "checkbox",
950
+ selected: on,
951
+ disabled: !props.enabled,
952
+ onPress: () => props.onChange(on ? void 0 : "")
953
+ }
954
+ ),
955
+ on ? /* @__PURE__ */ jsxs(Fragment, { children: [
956
+ /* @__PURE__ */ jsx(
957
+ TextInput,
958
+ {
959
+ value: props.value,
960
+ onChangeText: (text) => props.onChange(text),
961
+ accessibilityLabel: t("questionOther"),
962
+ testID: "vinaverse-question-other",
963
+ editable: props.enabled,
964
+ style: {
965
+ borderWidth: 1,
966
+ borderColor: colors.border,
967
+ borderRadius: radius.md,
968
+ paddingHorizontal: space.md,
969
+ minHeight: 44,
970
+ color: colors.text,
971
+ fontSize: font.size.md
972
+ }
973
+ }
974
+ ),
975
+ /* @__PURE__ */ jsx(CharactersLeft, { max: props.max, text: props.value ?? "" })
976
+ ] }) : null
977
+ ] });
978
+ }
979
+ function CharactersLeft(props) {
980
+ const { t } = useCopy();
981
+ const { colors, font } = useTheme();
982
+ const left = props.max - codePoints(props.text);
983
+ return /* @__PURE__ */ jsx(
984
+ Text,
985
+ {
986
+ accessibilityLiveRegion: left < 20 ? "polite" : "none",
987
+ style: {
988
+ color: left < 0 ? colors.danger : colors.textMuted,
989
+ fontSize: font.size.xs
990
+ },
991
+ children: t("questionCharactersLeft", { count: left })
992
+ }
993
+ );
994
+ }
995
+ function TextAnswer(props) {
996
+ const { colors, radius, space, font } = useTheme();
997
+ const long = props.question.kind === "long_text";
998
+ const max = props.question.text?.max_length ?? (long ? 2e3 : 200);
999
+ return /* @__PURE__ */ jsxs(View, { style: { gap: space.xs }, children: [
1000
+ /* @__PURE__ */ jsx(
1001
+ TextInput,
1002
+ {
1003
+ value: props.value,
1004
+ onChangeText: props.onChange,
1005
+ accessibilityLabel: props.question.prompt,
1006
+ editable: props.enabled,
1007
+ multiline: long,
1008
+ testID: "vinaverse-question-text",
1009
+ style: {
1010
+ borderWidth: 1,
1011
+ borderColor: colors.border,
1012
+ borderRadius: radius.md,
1013
+ paddingHorizontal: space.md,
1014
+ paddingVertical: space.sm,
1015
+ minHeight: long ? 96 : 44,
1016
+ color: colors.text,
1017
+ fontSize: font.size.md,
1018
+ textAlignVertical: long ? "top" : "center"
1019
+ }
1020
+ }
1021
+ ),
1022
+ /* @__PURE__ */ jsx(CharactersLeft, { max, text: props.value })
1023
+ ] });
1024
+ }
1025
+ function NumberAnswer(props) {
1026
+ const range = props.question.number ?? { min: 0, max: 100, step: 1 };
1027
+ const { colors, space, font } = useTheme();
1028
+ const value = props.value ?? range.min;
1029
+ const clamp = (n) => Math.min(range.max, Math.max(range.min, Number(n.toFixed(6))));
1030
+ const shown = `${value}${props.question.number?.unit ? ` ${props.question.number.unit}` : ""}`;
1031
+ return /* @__PURE__ */ jsxs(
1032
+ View,
1033
+ {
1034
+ accessible: true,
1035
+ accessibilityRole: "adjustable",
1036
+ accessibilityLabel: props.question.prompt,
1037
+ accessibilityValue: {
1038
+ min: range.min,
1039
+ max: range.max,
1040
+ now: value,
1041
+ text: shown
1042
+ },
1043
+ accessibilityActions: [{ name: "increment" }, { name: "decrement" }],
1044
+ testID: "vinaverse-question-number",
1045
+ onAccessibilityAction: (e) => props.onChange(
1046
+ clamp(
1047
+ value + (e.nativeEvent.actionName === "increment" ? range.step : -range.step)
1048
+ )
1049
+ ),
1050
+ style: { flexDirection: "row", alignItems: "center", gap: space.md },
1051
+ children: [
1052
+ /* @__PURE__ */ jsx(
1053
+ Button,
1054
+ {
1055
+ label: "\u2212",
1056
+ disabled: !props.enabled || value <= range.min,
1057
+ onPress: () => props.onChange(clamp(value - range.step))
1058
+ }
1059
+ ),
1060
+ /* @__PURE__ */ jsx(
1061
+ Text,
1062
+ {
1063
+ style: {
1064
+ color: colors.text,
1065
+ fontSize: font.size.lg,
1066
+ minWidth: 64,
1067
+ textAlign: "center"
1068
+ },
1069
+ children: shown
1070
+ }
1071
+ ),
1072
+ /* @__PURE__ */ jsx(
1073
+ Button,
1074
+ {
1075
+ label: "+",
1076
+ disabled: !props.enabled || value >= range.max,
1077
+ onPress: () => props.onChange(clamp(value + range.step))
1078
+ }
1079
+ )
1080
+ ]
1081
+ }
1082
+ );
1083
+ }
1084
+ function ScaleAnswer(props) {
1085
+ const scale = props.question.scale;
1086
+ const { colors, space, font } = useTheme();
1087
+ if (!scale) return null;
1088
+ const points = [];
1089
+ for (let n = scale.min; n <= scale.max && points.length < 21; n += scale.step)
1090
+ points.push(n);
1091
+ return /* @__PURE__ */ jsxs(View, { style: { gap: space.xs }, children: [
1092
+ /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.xs }, children: points.map((n) => /* @__PURE__ */ jsx(
1093
+ Button,
1094
+ {
1095
+ label: String(n),
1096
+ role: "radio",
1097
+ disabled: !props.enabled,
1098
+ onPress: () => props.onPick(n)
1099
+ },
1100
+ n
1101
+ )) }),
1102
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", justifyContent: "space-between" }, children: [
1103
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.textMuted, fontSize: font.size.xs }, children: scale.min_label }),
1104
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.textMuted, fontSize: font.size.xs }, children: scale.max_label })
1105
+ ] })
1106
+ ] });
1107
+ }
1108
+ var iso = (y, m, d) => `${String(y).padStart(4, "0")}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
1109
+ function DateAnswer(props) {
1110
+ const { locale } = useCopy();
1111
+ const { colors, space, font } = useTheme();
1112
+ const bounds = props.question.date;
1113
+ const start = props.value ?? bounds?.min ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1114
+ const [month, setMonth] = useState(() => ({
1115
+ y: Number(start.slice(0, 4)),
1116
+ m: Number(start.slice(5, 7)) - 1
1117
+ }));
1118
+ const days = new Date(Date.UTC(month.y, month.m + 1, 0)).getUTCDate();
1119
+ const title = (() => {
1120
+ try {
1121
+ return new Intl.DateTimeFormat(locale, {
1122
+ month: "long",
1123
+ year: "numeric",
1124
+ timeZone: "UTC"
1125
+ }).format(new Date(Date.UTC(month.y, month.m, 1)));
1126
+ } catch {
1127
+ return `${month.y}-${month.m + 1}`;
1128
+ }
1129
+ })();
1130
+ const move = (by) => {
1131
+ const d = new Date(Date.UTC(month.y, month.m + by, 1));
1132
+ setMonth({ y: d.getUTCFullYear(), m: d.getUTCMonth() });
1133
+ };
1134
+ const outside = (date) => bounds?.min !== void 0 && date < bounds.min || bounds?.max !== void 0 && date > bounds.max;
1135
+ return /* @__PURE__ */ jsxs(View, { style: { gap: space.xs }, children: [
1136
+ /* @__PURE__ */ jsxs(
1137
+ View,
1138
+ {
1139
+ style: {
1140
+ flexDirection: "row",
1141
+ alignItems: "center",
1142
+ justifyContent: "space-between"
1143
+ },
1144
+ children: [
1145
+ /* @__PURE__ */ jsx(
1146
+ Button,
1147
+ {
1148
+ label: "\u2039",
1149
+ variant: "text",
1150
+ disabled: !props.enabled,
1151
+ onPress: () => move(-1)
1152
+ }
1153
+ ),
1154
+ /* @__PURE__ */ jsx(
1155
+ Text,
1156
+ {
1157
+ accessibilityRole: "header",
1158
+ style: { color: colors.text, fontSize: font.size.md },
1159
+ children: title
1160
+ }
1161
+ ),
1162
+ /* @__PURE__ */ jsx(
1163
+ Button,
1164
+ {
1165
+ label: "\u203A",
1166
+ variant: "text",
1167
+ disabled: !props.enabled,
1168
+ onPress: () => move(1)
1169
+ }
1170
+ )
1171
+ ]
1172
+ }
1173
+ ),
1174
+ /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.xxs }, children: Array.from({ length: days }, (_, i) => {
1175
+ const date = iso(month.y, month.m, i + 1);
1176
+ return /* @__PURE__ */ jsx(
1177
+ Button,
1178
+ {
1179
+ label: String(i + 1),
1180
+ hint: formatDate(date, locale),
1181
+ role: "radio",
1182
+ selected: props.value === date,
1183
+ disabled: !props.enabled || outside(date),
1184
+ onPress: () => props.onChange(date)
1185
+ },
1186
+ date
1187
+ );
1188
+ }) }),
1189
+ props.value ? /* @__PURE__ */ jsx(Body, { muted: true, children: formatDate(props.value, locale) }) : null
1190
+ ] });
1191
+ }
1192
+ function RateEach(props) {
1193
+ const { question } = props;
1194
+ const { t } = useCopy();
1195
+ const { colors, space, font } = useTheme();
1196
+ const answered = new Map(
1197
+ (question.row_answers ?? []).map((r) => [
1198
+ r.question_id,
1199
+ (r.answer.values ?? [])[0]
1200
+ ])
1201
+ );
1202
+ const open = (question.rows ?? []).filter(
1203
+ (r) => !props.locked.includes(r.question_id)
1204
+ );
1205
+ return /* @__PURE__ */ jsxs(View, { style: { gap: space.md }, children: [
1206
+ (question.rows ?? []).map((row) => {
1207
+ const done = props.locked.includes(row.question_id);
1208
+ return /* @__PURE__ */ jsxs(
1209
+ View,
1210
+ {
1211
+ accessibilityLabel: row.label,
1212
+ style: { gap: space.xs },
1213
+ children: [
1214
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontSize: font.size.sm }, children: row.label }),
1215
+ /* @__PURE__ */ jsx(
1216
+ View,
1217
+ {
1218
+ style: { flexDirection: "row", flexWrap: "wrap", gap: space.xs },
1219
+ children: (question.options ?? []).map((o) => /* @__PURE__ */ jsx(
1220
+ Button,
1221
+ {
1222
+ label: o.label,
1223
+ role: "radio",
1224
+ selected: answered.get(row.question_id) === o.value,
1225
+ disabled: !props.enabled || done,
1226
+ onPress: () => props.send({ values: [o.value] }, row.question_id)
1227
+ },
1228
+ o.value
1229
+ ))
1230
+ }
1231
+ )
1232
+ ]
1233
+ },
1234
+ row.question_id
1235
+ );
1236
+ }),
1237
+ question.required || open.length === 0 ? null : /* @__PURE__ */ jsx(
1238
+ Button,
1239
+ {
1240
+ label: t("questionSkip"),
1241
+ variant: "text",
1242
+ disabled: !props.enabled,
1243
+ onPress: () => {
1244
+ for (const row of open)
1245
+ props.send({ skipped: true }, row.question_id);
1246
+ }
1247
+ }
1248
+ )
1249
+ ] });
1250
+ }
1251
+ function answerText(question, locale, otherLabel) {
1252
+ const answer = question.answer;
1253
+ if (!answer || answer.skipped) return null;
1254
+ const labels = new Map(
1255
+ (question.options ?? []).map((o) => [o.value, o.label])
1256
+ );
1257
+ if (question.none_of_the_above)
1258
+ labels.set(
1259
+ question.none_of_the_above.value,
1260
+ question.none_of_the_above.label
1261
+ );
1262
+ const parts = [
1263
+ ...(answer.values ?? []).map((v) => labels.get(v) ?? v),
1264
+ ...answer.other_text ? [`${otherLabel}: ${answer.other_text}`] : [],
1265
+ ...answer.text ? [answer.text] : [],
1266
+ ...answer.number !== void 0 ? [String(answer.number)] : [],
1267
+ ...answer.date ? [formatDate(answer.date, locale)] : [],
1268
+ ...answer.upload_ref ? ["\u{1F4F7}"] : []
1269
+ ];
1270
+ return parts.length ? parts.join(", ") : null;
1271
+ }
1272
+ function TimeSlotsCard(props) {
1273
+ const card = cardOf(props.card);
1274
+ const { locale } = useCopy();
1275
+ const { timeZone } = useChat();
1276
+ const { colors, radius, space, font } = useTheme();
1277
+ const slots = (card.slots ?? []).map(
1278
+ (s) => formatTimeRange(s.start, s.end, locale, timeZone)
1279
+ );
1280
+ return /* @__PURE__ */ jsxs(
1281
+ CardFrame,
1282
+ {
1283
+ label: [card.title, ...slots].join(", "),
1284
+ restored: props.restored,
1285
+ testID: `vinaverse-card-${card.id}`,
1286
+ children: [
1287
+ /* @__PURE__ */ jsx(Title, { children: card.title }),
1288
+ /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.xs }, children: slots.map((s) => /* @__PURE__ */ jsx(
1289
+ Text,
1290
+ {
1291
+ style: {
1292
+ color: colors.text,
1293
+ borderWidth: 1,
1294
+ borderColor: colors.border,
1295
+ borderRadius: radius.pill,
1296
+ paddingHorizontal: space.md,
1297
+ paddingVertical: space.xs,
1298
+ fontSize: font.size.sm
1299
+ },
1300
+ children: s
1301
+ },
1302
+ s
1303
+ )) })
1304
+ ]
1305
+ }
1306
+ );
1307
+ }
1308
+ var DEFAULTS = {
1309
+ proposal: ProposalCard,
1310
+ question: QuestionCard,
1311
+ next_step: NextStepCard,
1312
+ time_slots: TimeSlotsCard,
1313
+ professional: ListingCard,
1314
+ workspace: ListingCard,
1315
+ links: LinksCard
1316
+ };
1317
+ function defaultCard(kind) {
1318
+ return DEFAULTS[kind] ?? InfoCard;
1319
+ }
1320
+ var CardView = /* @__PURE__ */ memo(function CardView2(props) {
1321
+ const { slots } = useChat();
1322
+ const { card, busy } = props.item;
1323
+ const Default = defaultCard(card.kind);
1324
+ const Custom = slots.cards?.[card.kind];
1325
+ const read = useMemo(() => readCard(card.card), [card]);
1326
+ const own = {
1327
+ card,
1328
+ read,
1329
+ busy,
1330
+ restored: props.item.restored === true,
1331
+ grouped: props.grouped === true
1332
+ };
1333
+ return Custom ? /* @__PURE__ */ jsx(Custom, { ...own, Default }) : /* @__PURE__ */ jsx(Default, { ...own });
1334
+ });
1335
+ var keyOf = (item) => item.key;
1336
+ var kindOf = (item) => item.card.kind;
1337
+ function CardCarousel(props) {
1338
+ const { width } = useWindowDimensions();
1339
+ const { t } = useCopy();
1340
+ const size2 = Math.min(Math.round(width * 0.8), 360);
1341
+ return /* @__PURE__ */ jsx(
1342
+ View,
1343
+ {
1344
+ accessibilityRole: "list",
1345
+ accessibilityLabel: t("cardsInRow", {
1346
+ count: String(props.items.length)
1347
+ }),
1348
+ children: /* @__PURE__ */ jsx(
1349
+ FlashList,
1350
+ {
1351
+ horizontal: true,
1352
+ data: props.items,
1353
+ keyExtractor: keyOf,
1354
+ getItemType: kindOf,
1355
+ renderItem: ({ item }) => /* @__PURE__ */ jsx(View, { style: { width: size2 }, children: /* @__PURE__ */ jsx(CardView, { item, grouped: true }) }),
1356
+ snapToInterval: size2,
1357
+ decelerationRate: "fast",
1358
+ showsHorizontalScrollIndicator: false
1359
+ }
1360
+ )
1361
+ }
1362
+ );
1363
+ }
1364
+ var ActivityLine = /* @__PURE__ */ memo(function ActivityLine2() {
1365
+ const { store } = useChat();
1366
+ const { t } = useCopy();
1367
+ const { colors, space, font } = useTheme();
1368
+ const activity = useConversationSelector(store, (v) => v.activity);
1369
+ const waiting = useConversationSelector(
1370
+ store,
1371
+ (v) => v.responding && v.items.at(-1)?.type === "message" && v.items.at(-1).role === "person"
1372
+ );
1373
+ const name = useConversationSelector(store, (v) => v.agent.name);
1374
+ const running = activity.filter((a) => a.state === "running").at(-1);
1375
+ if (!running && !waiting) return null;
1376
+ const text = running?.label ?? t("working", { agent: name ?? t("agentFallbackName") });
1377
+ return /* @__PURE__ */ jsx(
1378
+ View,
1379
+ {
1380
+ accessibilityLiveRegion: "polite",
1381
+ style: { paddingHorizontal: space.lg, paddingVertical: space.sm },
1382
+ children: /* @__PURE__ */ jsx(Text, { style: { color: colors.textMuted, fontSize: font.size.sm }, children: text })
1383
+ }
1384
+ );
1385
+ });
1386
+ var Avatar = /* @__PURE__ */ memo(function Avatar2(props) {
1387
+ const { colors, size: size2, font } = useTheme();
1388
+ const { slots } = useChat();
1389
+ const box = {
1390
+ width: size2.avatar,
1391
+ height: size2.avatar,
1392
+ borderRadius: size2.avatar / 2
1393
+ };
1394
+ if (props.uri && !props.person) {
1395
+ const Img = slots.Image ?? Image;
1396
+ return /* @__PURE__ */ jsx(
1397
+ Img,
1398
+ {
1399
+ source: { uri: props.uri },
1400
+ style: box,
1401
+ accessibilityIgnoresInvertColors: true,
1402
+ accessible: false,
1403
+ importantForAccessibility: "no"
1404
+ }
1405
+ );
1406
+ }
1407
+ return /* @__PURE__ */ jsx(
1408
+ View,
1409
+ {
1410
+ accessible: false,
1411
+ importantForAccessibility: "no-hide-descendants",
1412
+ style: {
1413
+ ...box,
1414
+ backgroundColor: props.person ? colors.staffBubble : colors.primary,
1415
+ alignItems: "center",
1416
+ justifyContent: "center"
1417
+ },
1418
+ children: /* @__PURE__ */ jsx(
1419
+ Text,
1420
+ {
1421
+ style: {
1422
+ color: props.person ? colors.staffText : colors.onPrimary,
1423
+ fontSize: font.size.sm,
1424
+ fontWeight: "600"
1425
+ },
1426
+ children: props.person ? "\u25C9" : (props.name.trim()[0] ?? "?").toUpperCase()
1427
+ }
1428
+ )
1429
+ }
1430
+ );
1431
+ });
1432
+ function DefaultHeader(props) {
1433
+ const { colors, space, font } = useTheme();
1434
+ const { t } = useCopy();
1435
+ const person = props.takeoverBy !== null;
1436
+ const subtitle = person ? t("staffLabel", { by: props.takeoverBy || t("staffFallbackRole") }) : t("agentLabel");
1437
+ return /* @__PURE__ */ jsxs(
1438
+ View,
1439
+ {
1440
+ accessibilityRole: "header",
1441
+ accessible: true,
1442
+ accessibilityLabel: `${props.name}, ${subtitle}`,
1443
+ style: {
1444
+ flexDirection: "row",
1445
+ alignItems: "center",
1446
+ gap: space.sm,
1447
+ paddingHorizontal: space.lg,
1448
+ paddingVertical: space.sm,
1449
+ backgroundColor: colors.background,
1450
+ borderBottomWidth: 1,
1451
+ borderBottomColor: colors.divider
1452
+ },
1453
+ children: [
1454
+ /* @__PURE__ */ jsx(Avatar, { name: props.name, uri: props.avatarUrl, person }),
1455
+ /* @__PURE__ */ jsxs(View, { style: { flexShrink: 1 }, children: [
1456
+ /* @__PURE__ */ jsx(
1457
+ Text,
1458
+ {
1459
+ numberOfLines: 1,
1460
+ style: {
1461
+ color: colors.text,
1462
+ fontSize: font.size.md,
1463
+ fontWeight: "600",
1464
+ ...font.family ? { fontFamily: font.family } : {}
1465
+ },
1466
+ children: props.name
1467
+ }
1468
+ ),
1469
+ /* @__PURE__ */ jsx(
1470
+ Text,
1471
+ {
1472
+ numberOfLines: 1,
1473
+ style: {
1474
+ color: person ? colors.staffAccent : colors.textMuted,
1475
+ fontSize: font.size.xs
1476
+ },
1477
+ children: subtitle
1478
+ }
1479
+ )
1480
+ ] })
1481
+ ]
1482
+ }
1483
+ );
1484
+ }
1485
+ var ChatHeader = /* @__PURE__ */ memo(function ChatHeader2() {
1486
+ const { store, slots, agentAvatar } = useChat();
1487
+ const { t } = useCopy();
1488
+ const agent = useConversationSelector(store, (v) => v.agent);
1489
+ const takeover = useConversationSelector(store, (v) => v.takeover);
1490
+ const Header = slots.Header;
1491
+ const props = {
1492
+ name: agent.name ?? t("agentFallbackName"),
1493
+ avatarUrl: agentAvatar ?? agent.avatarUrl,
1494
+ takeoverBy: takeover ? takeover.by ?? "" : null
1495
+ };
1496
+ return Header ? /* @__PURE__ */ jsx(Header, { ...props, Default: DefaultHeader }) : /* @__PURE__ */ jsx(DefaultHeader, { ...props });
1497
+ });
1498
+ var DisclosureBar = /* @__PURE__ */ memo(function DisclosureBar2() {
1499
+ const { store } = useChat();
1500
+ const { t } = useCopy();
1501
+ const theme = useTheme();
1502
+ const { space, font } = theme;
1503
+ const disclosure = useConversationSelector(store, (v) => v.disclosure);
1504
+ const [open, setOpen] = useState(false);
1505
+ const legible = contrastRatio(
1506
+ theme.colors.disclosureText,
1507
+ theme.colors.disclosureBackground
1508
+ ) >= 4.5;
1509
+ const pair = legible ? theme.colors : colors(theme.scheme);
1510
+ const text = disclosure ?? t("disclosureFallback");
1511
+ return /* @__PURE__ */ jsx(
1512
+ Pressable,
1513
+ {
1514
+ accessibilityRole: "text",
1515
+ accessibilityLabel: text,
1516
+ onPress: () => setOpen((o) => !o),
1517
+ testID: "vinaverse-disclosure",
1518
+ style: {
1519
+ backgroundColor: pair.disclosureBackground,
1520
+ paddingHorizontal: space.lg,
1521
+ paddingVertical: space.xs
1522
+ },
1523
+ children: /* @__PURE__ */ jsx(
1524
+ Text,
1525
+ {
1526
+ numberOfLines: open ? void 0 : 2,
1527
+ style: {
1528
+ color: pair.disclosureText,
1529
+ // never below the disclosure's minimum, whatever the tenant's type scale
1530
+ fontSize: Math.max(font.size.xs, size.disclosureFontMin),
1531
+ lineHeight: Math.max(font.size.xs, size.disclosureFontMin) * font.lineHeight
1532
+ },
1533
+ children: text
1534
+ }
1535
+ )
1536
+ }
1537
+ );
1538
+ });
1539
+ var ChatSkeleton = /* @__PURE__ */ memo(function ChatSkeleton2() {
1540
+ const { colors, radius, space } = useTheme();
1541
+ const row = (width, end) => /* @__PURE__ */ jsx(
1542
+ View,
1543
+ {
1544
+ style: {
1545
+ alignSelf: end ? "flex-end" : "flex-start",
1546
+ width,
1547
+ height: 40,
1548
+ borderRadius: radius.lg,
1549
+ backgroundColor: colors.surface,
1550
+ marginHorizontal: space.lg,
1551
+ marginTop: space.md
1552
+ }
1553
+ }
1554
+ );
1555
+ return /* @__PURE__ */ jsxs(
1556
+ View,
1557
+ {
1558
+ accessible: true,
1559
+ accessibilityRole: "progressbar",
1560
+ accessibilityState: { busy: true },
1561
+ testID: "vinaverse-skeleton",
1562
+ children: [
1563
+ row("62%", false),
1564
+ row("45%", true),
1565
+ row("70%", false)
1566
+ ]
1567
+ }
1568
+ );
1569
+ });
1570
+ function DefaultBubble(props) {
1571
+ const { colors, radius, space, font } = useTheme();
1572
+ const role = props.item.role;
1573
+ const [background, foreground] = role === "person" ? [colors.personBubble, colors.personText] : role === "staff" ? [colors.staffBubble, colors.staffText] : [colors.agentBubble, colors.agentText];
1574
+ return /* @__PURE__ */ jsx(
1575
+ View,
1576
+ {
1577
+ style: {
1578
+ backgroundColor: background,
1579
+ borderRadius: radius.lg,
1580
+ paddingHorizontal: space.md,
1581
+ paddingVertical: space.sm
1582
+ },
1583
+ children: /* @__PURE__ */ jsx(
1584
+ Text,
1585
+ {
1586
+ selectable: true,
1587
+ style: {
1588
+ color: foreground,
1589
+ fontSize: font.size.md,
1590
+ lineHeight: font.size.md * font.lineHeight,
1591
+ ...font.family ? { fontFamily: font.family } : {}
1592
+ },
1593
+ children: props.text
1594
+ }
1595
+ )
1596
+ }
1597
+ );
1598
+ }
1599
+ function useAuthor(item) {
1600
+ const { t } = useCopy();
1601
+ const { store } = useChat();
1602
+ const name = store.getSnapshot().agent.name ?? t("agentFallbackName");
1603
+ const by = store.getSnapshot().takeover?.by;
1604
+ if (item.role === "person") return t("you");
1605
+ if (item.role === "staff")
1606
+ return t("staffLabel", { by: by || t("staffFallbackRole") });
1607
+ return `${name} \xB7 ${t("agentLabel")}`;
1608
+ }
1609
+ var MessageBubble = /* @__PURE__ */ memo(
1610
+ function MessageBubble2(props) {
1611
+ const { item } = props;
1612
+ return /* @__PURE__ */ jsx(Frame, { item, text: item.text, showAuthor: props.showAuthor });
1613
+ }
1614
+ );
1615
+ var StreamingBubble = /* @__PURE__ */ memo(
1616
+ function StreamingBubble2(props) {
1617
+ const { store } = useChat();
1618
+ const text = useStreamingText(store, props.item.key) ?? "";
1619
+ return /* @__PURE__ */ jsx(Frame, { item: props.item, text, showAuthor: props.showAuthor });
1620
+ }
1621
+ );
1622
+ function Frame(props) {
1623
+ const { item, text } = props;
1624
+ const { store, slots } = useChat();
1625
+ const { t } = useCopy();
1626
+ const { colors, space, font } = useTheme();
1627
+ const author = useAuthor(item);
1628
+ const mine = item.role === "person";
1629
+ const status = item.state === "sending" ? t("messageSending") : item.state === "held" ? t("messageHeld") : item.state === "not_sent" ? item.rejection?.message || t("messageNotSent") : item.state === "delivered" ? t("messageDelivered") : item.partial ? t("messageCutOff") : item.refusal && mine ? t("messageRefused") : null;
1630
+ const Bubble = slots.MessageBubble;
1631
+ const bubble = Bubble ? /* @__PURE__ */ jsx(Bubble, { item, text, Default: DefaultBubble }) : /* @__PURE__ */ jsx(DefaultBubble, { item, text });
1632
+ return /* @__PURE__ */ jsxs(
1633
+ View,
1634
+ {
1635
+ accessible: true,
1636
+ accessibilityLabel: [author, text, status].filter(Boolean).join(". "),
1637
+ style: {
1638
+ alignSelf: mine ? "flex-end" : "flex-start",
1639
+ maxWidth: "85%",
1640
+ marginHorizontal: space.lg,
1641
+ marginTop: props.showAuthor ? space.md : space.xs,
1642
+ opacity: item.state === "sending" || item.state === "held" || item.refusal ? 0.75 : 1
1643
+ },
1644
+ children: [
1645
+ props.showAuthor && !mine ? /* @__PURE__ */ jsx(
1646
+ Text,
1647
+ {
1648
+ style: {
1649
+ color: item.role === "staff" ? colors.staffAccent : colors.textMuted,
1650
+ fontSize: font.size.xs,
1651
+ marginBottom: space.xxs
1652
+ },
1653
+ children: author
1654
+ }
1655
+ ) : null,
1656
+ bubble,
1657
+ status ? /* @__PURE__ */ jsxs(
1658
+ View,
1659
+ {
1660
+ style: {
1661
+ flexDirection: "row",
1662
+ alignItems: "center",
1663
+ justifyContent: mine ? "flex-end" : "flex-start",
1664
+ gap: space.xs,
1665
+ marginTop: space.xxs
1666
+ },
1667
+ children: [
1668
+ /* @__PURE__ */ jsx(
1669
+ Text,
1670
+ {
1671
+ style: {
1672
+ color: item.state === "not_sent" ? colors.danger : colors.textMuted,
1673
+ fontSize: font.size.xs
1674
+ },
1675
+ children: status
1676
+ }
1677
+ ),
1678
+ item.state === "not_sent" ? /* @__PURE__ */ jsx(
1679
+ Button,
1680
+ {
1681
+ label: t("tryAgain"),
1682
+ variant: "text",
1683
+ onPress: () => {
1684
+ store.retry(item.key);
1685
+ }
1686
+ }
1687
+ ) : null
1688
+ ]
1689
+ }
1690
+ ) : null
1691
+ ]
1692
+ }
1693
+ );
1694
+ }
1695
+ function FailureActionButton(props) {
1696
+ const { handlers, failureAction } = useChat();
1697
+ const { t } = useCopy();
1698
+ if (!failureAction) return null;
1699
+ return /* @__PURE__ */ jsx(
1700
+ Button,
1701
+ {
1702
+ label: failureAction.label ?? t("talkToPerson"),
1703
+ variant: "text",
1704
+ onPress: () => handlers.current.failureAction?.onPress(props.state)
1705
+ }
1706
+ );
1707
+ }
1708
+ function FailureSlot(props) {
1709
+ const { slot: Slot, state, Default } = props;
1710
+ return Slot ? /* @__PURE__ */ jsx(Slot, { state, Default }) : /* @__PURE__ */ jsx(Default, { state });
1711
+ }
1712
+ var SystemRow = /* @__PURE__ */ memo(function SystemRow2(props) {
1713
+ const { t } = useCopy();
1714
+ const { store, slots } = useChat();
1715
+ const conversationId = useConversationSelector(
1716
+ store,
1717
+ (v) => v.conversationId
1718
+ );
1719
+ const { event, data } = props.item;
1720
+ if (event === "turn_failed") {
1721
+ const key = props.item.key;
1722
+ const state = {
1723
+ kind: "turn_failed",
1724
+ reason: typeof data.code === "string" ? data.code : null,
1725
+ retry: data.retry === true ? () => void store.retryTurn(key) : void 0,
1726
+ conversationId
1727
+ };
1728
+ return /* @__PURE__ */ jsx(
1729
+ FailureSlot,
1730
+ {
1731
+ slot: slots.Error,
1732
+ state,
1733
+ Default: DefaultTurnFailed
1734
+ }
1735
+ );
1736
+ }
1737
+ const agent = store.getSnapshot().agent.name ?? t("agentFallbackName");
1738
+ const text = (() => {
1739
+ switch (event) {
1740
+ case "takeover_started":
1741
+ return t("takeoverStarted", {
1742
+ by: data.by || t("staffFallbackRole")
1743
+ });
1744
+ case "takeover_ended":
1745
+ return t("takeoverEnded", { agent });
1746
+ case "handoff_created":
1747
+ return data.expected_response ? t("handoffCreated", { expected: data.expected_response }) : t("handoffCreatedNoTime");
1748
+ case "flow_completed":
1749
+ return data.result === "ok" ? t("flowSent") : data.result === "failed" ? t("flowNotSent") : null;
1750
+ default:
1751
+ return null;
1752
+ }
1753
+ })();
1754
+ if (!text) return null;
1755
+ return /* @__PURE__ */ jsx(Line, { text });
1756
+ });
1757
+ function DefaultTurnFailed(props) {
1758
+ const { t } = useCopy();
1759
+ const { failureAction } = useChat();
1760
+ const text = t("turnFailed");
1761
+ if (!failureAction) return /* @__PURE__ */ jsx(Line, { text, danger: true });
1762
+ return /* @__PURE__ */ jsxs(View, { style: { alignItems: "center" }, children: [
1763
+ /* @__PURE__ */ jsx(Line, { text, danger: true }),
1764
+ /* @__PURE__ */ jsx(FailureActionButton, { state: props.state })
1765
+ ] });
1766
+ }
1767
+ function Line(props) {
1768
+ const { colors, space, font } = useTheme();
1769
+ return /* @__PURE__ */ jsx(
1770
+ View,
1771
+ {
1772
+ accessible: true,
1773
+ accessibilityLabel: props.text,
1774
+ style: {
1775
+ alignItems: "center",
1776
+ paddingHorizontal: space.xl,
1777
+ paddingVertical: space.sm
1778
+ },
1779
+ children: /* @__PURE__ */ jsx(
1780
+ Text,
1781
+ {
1782
+ style: {
1783
+ color: props.danger ? colors.danger : colors.textMuted,
1784
+ fontSize: font.size.sm,
1785
+ textAlign: "center"
1786
+ },
1787
+ children: props.text
1788
+ }
1789
+ )
1790
+ }
1791
+ );
1792
+ }
1793
+ var Welcome = /* @__PURE__ */ memo(function Welcome2(props) {
1794
+ const { colors, radius, space, font } = useTheme();
1795
+ return /* @__PURE__ */ jsxs(
1796
+ View,
1797
+ {
1798
+ style: { padding: space.lg, gap: space.md },
1799
+ testID: "vinaverse-welcome",
1800
+ children: [
1801
+ /* @__PURE__ */ jsx(
1802
+ Text,
1803
+ {
1804
+ style: {
1805
+ alignSelf: "flex-start",
1806
+ maxWidth: "85%",
1807
+ color: colors.agentText,
1808
+ backgroundColor: colors.agentBubble,
1809
+ borderRadius: radius.lg,
1810
+ paddingHorizontal: space.md,
1811
+ paddingVertical: space.sm,
1812
+ fontSize: font.size.md,
1813
+ lineHeight: font.size.md * font.lineHeight
1814
+ },
1815
+ children: props.welcome.text
1816
+ }
1817
+ ),
1818
+ props.welcome.chips.length ? /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.sm }, children: props.welcome.chips.map((chip) => /* @__PURE__ */ jsx(
1819
+ Button,
1820
+ {
1821
+ label: chip.label,
1822
+ onPress: () => props.onChip(chip.message)
1823
+ },
1824
+ chip.label
1825
+ )) }) : null
1826
+ ]
1827
+ }
1828
+ );
1829
+ });
1830
+ var keyOf2 = (item) => item.key;
1831
+ function itemType(item) {
1832
+ if (item.type === "card") return `card:${item.card.kind}`;
1833
+ if (item.type === "message")
1834
+ return item.state === "streaming" ? "streaming" : `message:${item.role}`;
1835
+ return "system";
1836
+ }
1837
+ function authorOf(item) {
1838
+ return item?.type === "message" ? item.role : null;
1839
+ }
1840
+ function carousels(items, previous) {
1841
+ const groups = /* @__PURE__ */ new Map();
1842
+ for (const item of items) {
1843
+ if (item.type !== "card") continue;
1844
+ const group = item.card.card.group;
1845
+ if (group?.layout !== "carousel" || typeof group.id !== "string") continue;
1846
+ const id = `${item.card.turnId ?? ""}\0${group.id}`;
1847
+ const members = groups.get(id);
1848
+ if (members) members.push(item);
1849
+ else groups.set(id, [item]);
1850
+ }
1851
+ const runs = /* @__PURE__ */ new Map();
1852
+ const inside = /* @__PURE__ */ new Set();
1853
+ for (const run of groups.values()) {
1854
+ if (run.length < 2) continue;
1855
+ const key = run[0]?.key;
1856
+ const before = previous.get(key);
1857
+ runs.set(
1858
+ key,
1859
+ before && before.length === run.length && before.every((c, n) => c === run[n]) ? before : run
1860
+ );
1861
+ for (const card of run.slice(1)) inside.add(card.key);
1862
+ }
1863
+ return { runs, inside };
1864
+ }
1865
+ var Row = /* @__PURE__ */ memo(function Row2(props) {
1866
+ const { item } = props;
1867
+ const { slots } = useChat();
1868
+ const Group = slots.CardGroup;
1869
+ if (props.inside) return null;
1870
+ if (props.run && Group) return /* @__PURE__ */ jsx(Group, { items: props.run });
1871
+ if (item.type === "card") return /* @__PURE__ */ jsx(CardView, { item });
1872
+ if (item.type === "system") return /* @__PURE__ */ jsx(SystemRow, { item });
1873
+ return item.state === "streaming" ? /* @__PURE__ */ jsx(StreamingBubble, { item, showAuthor: props.showAuthor }) : /* @__PURE__ */ jsx(MessageBubble, { item, showAuthor: props.showAuthor });
1874
+ });
1875
+ var MessageList = /* @__PURE__ */ memo(function MessageList2() {
1876
+ const { store, slots, starters, handlers, imagePolicy } = useChat();
1877
+ const { t } = useCopy();
1878
+ const { space } = useTheme();
1879
+ const reducedMotion = useReducedMotion();
1880
+ const items = useConversationSelector(store, (v) => v.items);
1881
+ usePrefetchPhotos(items, handlers.current?.prefetchImage, imagePolicy?.hosts);
1882
+ const opening = useConversationSelector(
1883
+ store,
1884
+ (v) => v.status === "starting" || v.status === "connecting"
1885
+ );
1886
+ const agentName = useConversationSelector(store, (v) => v.agent.name);
1887
+ const served = useConversationSelector(store, (v) => v.welcome);
1888
+ const welcome = useMemo(
1889
+ () => served ?? (starters?.length ? {
1890
+ text: t("welcomeFallback", {
1891
+ agent: agentName ?? t("agentFallbackName")
1892
+ }),
1893
+ chips: [...starters]
1894
+ } : null),
1895
+ [served, starters, t, agentName]
1896
+ );
1897
+ useAnnounceReplies(items);
1898
+ const Group = slots.CardGroup;
1899
+ const runsRef = useRef(/* @__PURE__ */ new Map());
1900
+ const grouping = useMemo(
1901
+ () => Group ? carousels(items, runsRef.current) : null,
1902
+ [items, Group]
1903
+ );
1904
+ runsRef.current = grouping?.runs ?? /* @__PURE__ */ new Map();
1905
+ const itemsRef = useRef(items);
1906
+ itemsRef.current = items;
1907
+ const groupingRef = useRef(grouping);
1908
+ groupingRef.current = grouping;
1909
+ const renderItem = useCallback(
1910
+ ({ item, index }) => /* @__PURE__ */ jsx(
1911
+ Row,
1912
+ {
1913
+ item,
1914
+ showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1]),
1915
+ run: groupingRef.current?.runs.get(item.key),
1916
+ inside: groupingRef.current?.inside.has(item.key) === true
1917
+ }
1918
+ ),
1919
+ []
1920
+ );
1921
+ const onChip = useCallback(
1922
+ (message) => {
1923
+ store.send(message);
1924
+ const chip = welcome?.chips.find((c) => c.message === message);
1925
+ if (chip) handlers.current?.onChip?.(chip);
1926
+ },
1927
+ [store, welcome, handlers]
1928
+ );
1929
+ const Empty = slots.EmptyState;
1930
+ const empty = opening ? /* @__PURE__ */ jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsx(
1931
+ Empty,
1932
+ {
1933
+ agentName: agentName ?? t("agentFallbackName"),
1934
+ welcome,
1935
+ onChip
1936
+ }
1937
+ ) : welcome ? /* @__PURE__ */ jsx(Welcome, { welcome, onChip }) : null;
1938
+ const props = useMemo(
1939
+ () => ({
1940
+ data: items,
1941
+ renderItem,
1942
+ keyExtractor: keyOf2,
1943
+ getItemType: itemType,
1944
+ maintainVisibleContentPosition: {
1945
+ startRenderingFromBottom: true,
1946
+ autoscrollToBottomThreshold: 0.2,
1947
+ animateAutoScrollToBottom: !reducedMotion
1948
+ },
1949
+ ListFooterComponent: /* @__PURE__ */ jsx(ActivityLine, {}),
1950
+ ListEmptyComponent: empty,
1951
+ keyboardShouldPersistTaps: "handled",
1952
+ contentContainerStyle: { paddingVertical: space.sm }
1953
+ }),
1954
+ [items, renderItem, reducedMotion, empty, space.sm]
1955
+ );
1956
+ const List = slots.ListComponent;
1957
+ return List ? /* @__PURE__ */ jsx(List, { ...props }) : /* @__PURE__ */ jsx(FlashList, { ...props });
1958
+ });
1959
+ function usePrefetchPhotos(items, prefetch, hosts) {
1960
+ const done = useRef(/* @__PURE__ */ new Set());
1961
+ useEffect(() => {
1962
+ if (!prefetch) return;
1963
+ for (const item of items) {
1964
+ if (item.type !== "card") continue;
1965
+ const card = item.card.card;
1966
+ const url = allowedImage(card.images?.[0]?.url ?? card.image_url, hosts);
1967
+ if (url && !done.current.has(url)) {
1968
+ done.current.add(url);
1969
+ prefetch(url);
1970
+ }
1971
+ }
1972
+ }, [items, prefetch, hosts]);
1973
+ }
1974
+ function useAnnounceReplies(items) {
1975
+ const seen = useRef(void 0);
1976
+ const { t } = useCopy();
1977
+ const { store, handlers } = useChat();
1978
+ useEffect(() => {
1979
+ const last = items.at(-1);
1980
+ if (seen.current === void 0) {
1981
+ seen.current = last?.key ?? "";
1982
+ return;
1983
+ }
1984
+ if (last?.type !== "message" || last.role === "person") return;
1985
+ if (last.state !== "sent" || last.key === seen.current) return;
1986
+ seen.current = last.key;
1987
+ const author = last.role === "staff" ? t("staffLabel", {
1988
+ by: store.getSnapshot().takeover?.by || t("staffFallbackRole")
1989
+ }) : store.getSnapshot().agent.name ?? t("agentFallbackName");
1990
+ (handlers.current?.announce ?? announce)(`${author}: ${last.text}`);
1991
+ }, [items, t, store, handlers]);
1992
+ }
1993
+
1994
+ export { ActivityLine, Avatar, Button, CardActions, CardCarousel, CardFrame, CardView, ChatContextProvider, ChatHeader, ChatSkeleton, DefaultBubble, DefaultHeader, DefaultTurnFailed, DisclosureBar, FailureActionButton, FailureSlot, InfoCard, LinksCard, ListingCard, MessageBubble, MessageList, NextStepCard, ProposalCard, QuestionCard, Row, StreamingBubble, SystemRow, TimeSlotsCard, Welcome, allowedImage, answerText, carousels, defaultCard, fieldText, itemType, monogram, ratingText, useChat };
1995
+ //# sourceMappingURL=chunk-KSHDF3MG.js.map
1996
+ //# sourceMappingURL=chunk-KSHDF3MG.js.map