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