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

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 (45) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +302 -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 +273 -0
  10. package/dist/chat.cjs.map +1 -0
  11. package/dist/chat.d.cts +490 -0
  12. package/dist/chat.d.ts +490 -0
  13. package/dist/chat.js +4 -0
  14. package/dist/chat.js.map +1 -0
  15. package/dist/chunk-43I4ZAEK.js +2607 -0
  16. package/dist/chunk-43I4ZAEK.js.map +1 -0
  17. package/dist/chunk-6QYDPGRS.cjs +97 -0
  18. package/dist/chunk-6QYDPGRS.cjs.map +1 -0
  19. package/dist/chunk-7KPCO443.js +74 -0
  20. package/dist/chunk-7KPCO443.js.map +1 -0
  21. package/dist/chunk-EFEXTVIT.js +88 -0
  22. package/dist/chunk-EFEXTVIT.js.map +1 -0
  23. package/dist/chunk-MG32E4JI.cjs +268 -0
  24. package/dist/chunk-MG32E4JI.cjs.map +1 -0
  25. package/dist/chunk-NMKJ7LIJ.cjs +76 -0
  26. package/dist/chunk-NMKJ7LIJ.cjs.map +1 -0
  27. package/dist/chunk-NRYNK7E4.cjs +2699 -0
  28. package/dist/chunk-NRYNK7E4.cjs.map +1 -0
  29. package/dist/chunk-POJP52GE.js +261 -0
  30. package/dist/chunk-POJP52GE.js.map +1 -0
  31. package/dist/index.cjs +303 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +11 -0
  34. package/dist/index.d.ts +11 -0
  35. package/dist/index.js +6 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/launcher.cjs +86 -0
  38. package/dist/launcher.cjs.map +1 -0
  39. package/dist/launcher.d.cts +122 -0
  40. package/dist/launcher.d.ts +122 -0
  41. package/dist/launcher.js +5 -0
  42. package/dist/launcher.js.map +1 -0
  43. package/dist/theme-BhiEGv64.d.cts +35 -0
  44. package/dist/theme-BhiEGv64.d.ts +35 -0
  45. package/package.json +242 -3
@@ -0,0 +1,2699 @@
1
+ 'use strict';
2
+
3
+ var chunk6QYDPGRS_cjs = require('./chunk-6QYDPGRS.cjs');
4
+ var agent = require('@vinaverse/react/agent');
5
+ var flashList = require('@shopify/flash-list');
6
+ var react$1 = require('@vinaverse/react');
7
+ var reactNative = require('react-native');
8
+ var agent$1 = require('@vinaverse/client/agent');
9
+ var react = require('react');
10
+ var jsxRuntime = require('react/jsx-runtime');
11
+ var conversation = require('@vinaverse/client/conversation');
12
+ var tokens = require('@vinaverse/tokens');
13
+
14
+ var ChatContext = /* @__PURE__ */ react.createContext(
15
+ null
16
+ );
17
+ function ChatContextProvider(props) {
18
+ return /* @__PURE__ */ jsxRuntime.jsx(ChatContext.Provider, { value: props.value, children: props.children });
19
+ }
20
+ function useChat() {
21
+ const value = react.useContext(ChatContext);
22
+ if (!value)
23
+ throw new Error(
24
+ "Vinaverse parts render inside <VinaverseChat> or <VinaverseChatProvider>."
25
+ );
26
+ return value;
27
+ }
28
+ var Button = /* @__PURE__ */ react.memo(function Button2(props) {
29
+ const { colors, radius, space, size: size2, font } = chunk6QYDPGRS_cjs.useTheme();
30
+ const variant = props.variant ?? "secondary";
31
+ const inactive = props.disabled === true || props.busy === true;
32
+ const filled = variant === "primary" || props.selected === true;
33
+ const background = filled ? colors.primary : variant === "secondary" ? colors.surface : "transparent";
34
+ const foreground = filled ? colors.onPrimary : colors.primary;
35
+ const checkable = props.role === "radio" || props.role === "checkbox";
36
+ return /* @__PURE__ */ jsxRuntime.jsxs(
37
+ reactNative.Pressable,
38
+ {
39
+ accessibilityRole: props.role ?? "button",
40
+ accessibilityLabel: props.label,
41
+ ...props.hint ? { accessibilityHint: props.hint } : {},
42
+ accessibilityState: {
43
+ disabled: inactive,
44
+ busy: props.busy === true,
45
+ ...checkable ? props.role === "radio" ? { selected: props.selected === true } : { checked: props.selected === true } : {}
46
+ },
47
+ disabled: inactive,
48
+ onPress: props.onPress,
49
+ ...props.testID ? { testID: props.testID } : {},
50
+ style: ({ pressed }) => ({
51
+ minHeight: size2.touchTarget,
52
+ minWidth: size2.touchTarget,
53
+ paddingHorizontal: space.md,
54
+ paddingVertical: space.sm,
55
+ borderRadius: radius.md,
56
+ borderWidth: variant === "text" ? 0 : 1,
57
+ borderColor: filled ? colors.primary : colors.border,
58
+ backgroundColor: background,
59
+ opacity: inactive ? 0.55 : pressed ? 0.8 : 1,
60
+ alignItems: "center",
61
+ justifyContent: "center",
62
+ flexDirection: "row",
63
+ gap: space.xs
64
+ }),
65
+ children: [
66
+ props.busy ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: foreground }) : null,
67
+ props.children ?? /* @__PURE__ */ jsxRuntime.jsx(
68
+ reactNative.Text,
69
+ {
70
+ style: {
71
+ color: foreground,
72
+ fontSize: font.size.md,
73
+ fontWeight: "500",
74
+ ...font.family ? { fontFamily: font.family } : {}
75
+ },
76
+ children: props.label
77
+ }
78
+ )
79
+ ]
80
+ }
81
+ );
82
+ });
83
+ function cardOf(held) {
84
+ return held.card;
85
+ }
86
+ function CardFrame(props) {
87
+ const { colors, radius, space } = chunk6QYDPGRS_cjs.useTheme();
88
+ return /* @__PURE__ */ jsxRuntime.jsx(
89
+ reactNative.View,
90
+ {
91
+ accessibilityLabel: props.label,
92
+ ...props.restored ? {
93
+ pointerEvents: "none",
94
+ accessibilityState: { disabled: true }
95
+ } : {},
96
+ ...props.testID ? { testID: props.testID } : {},
97
+ style: {
98
+ marginHorizontal: 16,
99
+ marginTop: space.sm,
100
+ padding: space.md,
101
+ gap: space.sm,
102
+ borderRadius: radius.lg,
103
+ borderWidth: 1,
104
+ borderColor: colors.divider,
105
+ backgroundColor: colors.background,
106
+ opacity: props.restored ? 0.7 : 1,
107
+ maxWidth: 520,
108
+ alignSelf: "stretch"
109
+ },
110
+ children: props.children
111
+ }
112
+ );
113
+ }
114
+ function Title(props) {
115
+ const { colors, font } = chunk6QYDPGRS_cjs.useTheme();
116
+ return /* @__PURE__ */ jsxRuntime.jsx(
117
+ reactNative.Text,
118
+ {
119
+ accessibilityRole: "header",
120
+ numberOfLines: props.numberOfLines,
121
+ style: {
122
+ color: colors.text,
123
+ fontSize: font.size.md,
124
+ fontWeight: "600",
125
+ ...font.family ? { fontFamily: font.family } : {}
126
+ },
127
+ children: props.children
128
+ }
129
+ );
130
+ }
131
+ function Body(props) {
132
+ const { colors, font } = chunk6QYDPGRS_cjs.useTheme();
133
+ return /* @__PURE__ */ jsxRuntime.jsx(
134
+ reactNative.Text,
135
+ {
136
+ style: {
137
+ color: props.danger ? colors.danger : props.muted ? colors.textMuted : colors.text,
138
+ fontSize: props.muted ? font.size.sm : font.size.md,
139
+ lineHeight: (props.muted ? font.size.sm : font.size.md) * font.lineHeight,
140
+ ...font.family ? { fontFamily: font.family } : {}
141
+ },
142
+ children: props.children
143
+ }
144
+ );
145
+ }
146
+ function useLink(link, held) {
147
+ const { store, handlers } = useChat();
148
+ if (!link || !store.checkLink(link)) return null;
149
+ return () => handlers.current?.onOpenLink?.(link, held);
150
+ }
151
+ function ratingText(valuePpm, locale) {
152
+ const value = valuePpm / 1e6;
153
+ try {
154
+ return new Intl.NumberFormat(locale, {
155
+ minimumFractionDigits: 1,
156
+ maximumFractionDigits: 1
157
+ }).format(value);
158
+ } catch {
159
+ return value.toFixed(1);
160
+ }
161
+ }
162
+ function allowedImage(url, hosts) {
163
+ const safe = safeImage(url);
164
+ if (safe === null || !hosts) return safe;
165
+ const host = /^https:\/\/([^/:?#]+)/i.exec(safe)?.[1]?.toLowerCase();
166
+ return host && hosts.some((h) => h.toLowerCase() === host) ? safe : null;
167
+ }
168
+ function safeImage(url) {
169
+ return url && /^https:\/\//i.test(url) ? url : null;
170
+ }
171
+ function InfoCard(props) {
172
+ const card = cardOf(props.card);
173
+ const { locale, t } = react$1.useCopy();
174
+ const { slots, timeZone, handlers } = useChat();
175
+ const { colors, radius, space, font } = chunk6QYDPGRS_cjs.useTheme();
176
+ const image = safeImage(card.image_url);
177
+ const Img = slots.Image ?? reactNative.Image;
178
+ const money = card.price ?? card.price_from;
179
+ const rating = card.rating ? ratingText(card.rating.value_ppm, locale) : null;
180
+ const amount = money ? react$1.formatMoney(money.amount, money.currency, locale) : null;
181
+ const price = amount && !card.price ? t("priceFrom", { price: amount }) : amount;
182
+ const when = card.starts_at ? react$1.formatTimeRange(card.starts_at, void 0, locale, timeZone) : null;
183
+ const status = typeof card.status === "string" && card.status ? card.status : null;
184
+ const ref = card.ref;
185
+ const onOpenTarget = handlers.current?.onOpenTarget;
186
+ const open = ref && onOpenTarget && !props.restored ? () => onOpenTarget({ type: ref.type, id: ref.id }, props.card) : null;
187
+ const label = [
188
+ card.title,
189
+ card.subtitle,
190
+ status,
191
+ rating && `\u2605 ${rating}`,
192
+ price,
193
+ when
194
+ ].filter(Boolean).join(", ");
195
+ const top = /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flexDirection: "row", gap: space.md }, children: [
196
+ image ? /* @__PURE__ */ jsxRuntime.jsx(
197
+ Img,
198
+ {
199
+ source: { uri: image },
200
+ style: {
201
+ width: 64,
202
+ height: 64,
203
+ borderRadius: radius.md,
204
+ backgroundColor: colors.surface
205
+ },
206
+ accessible: false
207
+ }
208
+ ) : null,
209
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1, gap: space.xxs }, children: [
210
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { numberOfLines: 2, children: card.title }),
211
+ card.subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: card.subtitle }) : null,
212
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors.textMuted, fontSize: font.size.sm }, children: [
213
+ rating && `\u2605 ${rating}${card.rating ? ` (${card.rating.count})` : ""}`,
214
+ price,
215
+ when
216
+ ].filter(Boolean).join(" \xB7 ") })
217
+ ] })
218
+ ] });
219
+ return /* @__PURE__ */ jsxRuntime.jsxs(
220
+ CardFrame,
221
+ {
222
+ label,
223
+ restored: props.restored,
224
+ testID: `vinaverse-card-${card.id}`,
225
+ children: [
226
+ open ? /* @__PURE__ */ jsxRuntime.jsx(
227
+ reactNative.Pressable,
228
+ {
229
+ accessibilityRole: "link",
230
+ accessibilityLabel: card.title,
231
+ onPress: open,
232
+ testID: `vinaverse-open-${card.id}`,
233
+ children: top
234
+ }
235
+ ) : top,
236
+ card.body ? /* @__PURE__ */ jsxRuntime.jsx(Body, { children: card.body }) : null,
237
+ /* @__PURE__ */ jsxRuntime.jsx(
238
+ Badges,
239
+ {
240
+ badges: status ? [status, ...card.badges ?? []] : card.badges
241
+ }
242
+ ),
243
+ props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(CardActions, { ...props })
244
+ ]
245
+ }
246
+ );
247
+ }
248
+ function Badges(props) {
249
+ const { colors, radius, space, font } = chunk6QYDPGRS_cjs.useTheme();
250
+ if (!props.badges?.length) return null;
251
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.xs }, children: props.badges.map((b) => /* @__PURE__ */ jsxRuntime.jsx(
252
+ reactNative.Text,
253
+ {
254
+ style: {
255
+ color: colors.text,
256
+ backgroundColor: colors.surface,
257
+ borderRadius: radius.pill,
258
+ paddingHorizontal: space.sm,
259
+ paddingVertical: space.xxs,
260
+ fontSize: font.size.xs
261
+ },
262
+ children: b
263
+ },
264
+ b
265
+ )) });
266
+ }
267
+ function CardActions(props) {
268
+ const card = cardOf(props.card);
269
+ const { space } = chunk6QYDPGRS_cjs.useTheme();
270
+ if (!card.actions?.length) return null;
271
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.sm }, children: card.actions.map((a) => /* @__PURE__ */ jsxRuntime.jsx(
272
+ Action,
273
+ {
274
+ held: props.card,
275
+ action: a.action,
276
+ label: a.label,
277
+ link: a.deep_link
278
+ },
279
+ `${a.action}:${a.label}`
280
+ )) });
281
+ }
282
+ function Action(props) {
283
+ const { store, handlers } = useChat();
284
+ const open = useLink(props.link, props.held);
285
+ if (props.link && !open) return null;
286
+ return /* @__PURE__ */ jsxRuntime.jsx(
287
+ Button,
288
+ {
289
+ label: props.label,
290
+ role: open ? "link" : "button",
291
+ onPress: () => {
292
+ if (open) return open();
293
+ store.cardAction(props.held.id, props.action);
294
+ handlers.current?.onCardAction?.(props.held, props.action);
295
+ }
296
+ }
297
+ );
298
+ }
299
+ function LinksCard(props) {
300
+ const card = cardOf(props.card);
301
+ const { store, handlers } = useChat();
302
+ const links = (card.links ?? []).filter(
303
+ (l) => /^https:\/\//i.test(l.url) || store.checkLink(l.url)
304
+ );
305
+ return /* @__PURE__ */ jsxRuntime.jsxs(
306
+ CardFrame,
307
+ {
308
+ label: [card.title, ...links.map((l) => l.label)].join(", "),
309
+ restored: props.restored,
310
+ testID: `vinaverse-card-${card.id}`,
311
+ children: [
312
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { children: card.title }),
313
+ links.map((l) => /* @__PURE__ */ jsxRuntime.jsx(
314
+ Button,
315
+ {
316
+ label: l.label,
317
+ role: "link",
318
+ variant: "secondary",
319
+ disabled: props.restored,
320
+ onPress: () => {
321
+ const open = handlers.current?.onOpenLink;
322
+ if (open) open(l.url, props.card);
323
+ else void reactNative.Linking.openURL(l.url);
324
+ }
325
+ },
326
+ l.url
327
+ ))
328
+ ]
329
+ }
330
+ );
331
+ }
332
+ function ListingCard(props) {
333
+ const card = cardOf(props.card);
334
+ const { locale, t } = react$1.useCopy();
335
+ const { slots, timeZone, handlers, imagePolicy } = useChat();
336
+ const { colors, radius, space, font } = chunk6QYDPGRS_cjs.useTheme();
337
+ const [failed, setFailed] = react.useState(null);
338
+ const photoUrl = allowedImage(
339
+ card.images?.[0]?.url ?? card.image_url,
340
+ imagePolicy?.hosts
341
+ );
342
+ const hero = photoUrl === failed ? null : photoUrl;
343
+ const Img = slots.Image ?? reactNative.Image;
344
+ const rating = card.rating ? `\u2605 ${ratingText(card.rating.value_ppm, locale)} (${card.rating.count})` : t("ratingNone") || null;
345
+ const price = card.price_from ? t("priceFrom", {
346
+ price: react$1.formatMoney(
347
+ card.price_from.amount,
348
+ card.price_from.currency,
349
+ locale
350
+ )
351
+ }) : null;
352
+ const next = card.next_available ? t("nextAvailable", {
353
+ // "today, 10:00 AM", "tomorrow, 10:00 AM", else the date (the copy's `today` and `tomorrow`).
354
+ time: react$1.formatDayTime(
355
+ card.next_available,
356
+ { today: t("today"), tomorrow: t("tomorrow") },
357
+ locale,
358
+ timeZone
359
+ )
360
+ }) : null;
361
+ const ref = card.ref;
362
+ const onOpenTarget = handlers.current?.onOpenTarget;
363
+ const open = ref && onOpenTarget && !props.restored ? () => onOpenTarget({ type: ref.type, id: ref.id }, props.card) : null;
364
+ const asked = imagePolicy?.aspectRatio;
365
+ const size2 = card.images?.[0];
366
+ const range = imagePolicy?.naturalAspect ?? { min: 1, max: 2 };
367
+ const own = asked === "natural" && !props.grouped && size2?.width && size2.height ? Math.min(range.max, Math.max(range.min, size2.width / size2.height)) : null;
368
+ const photo = {
369
+ width: "100%",
370
+ aspectRatio: own ?? (typeof asked === "number" ? asked : 1.6),
371
+ borderRadius: radius.md
372
+ };
373
+ const top = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
374
+ hero ? /* @__PURE__ */ jsxRuntime.jsx(
375
+ Img,
376
+ {
377
+ source: { uri: hero },
378
+ onError: () => setFailed(hero),
379
+ style: { ...photo, backgroundColor: colors.surface },
380
+ accessible: false,
381
+ testID: `vinaverse-photo-${card.id}`
382
+ }
383
+ ) : 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(
384
+ reactNative.View,
385
+ {
386
+ accessible: false,
387
+ importantForAccessibility: "no-hide-descendants",
388
+ testID: `vinaverse-monogram-${card.id}`,
389
+ style: {
390
+ ...photo,
391
+ backgroundColor: colors.surface,
392
+ alignItems: "center",
393
+ justifyContent: "center"
394
+ },
395
+ children: /* @__PURE__ */ jsxRuntime.jsx(
396
+ reactNative.Text,
397
+ {
398
+ style: {
399
+ color: colors.textMuted,
400
+ fontSize: font.size.xl,
401
+ fontWeight: "600"
402
+ },
403
+ children: monogram(card.title)
404
+ }
405
+ )
406
+ }
407
+ ),
408
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space.xxs }, children: [
409
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { numberOfLines: 2, children: card.title }),
410
+ card.subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: card.subtitle }) : null,
411
+ rating || price ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: [rating, price].filter(Boolean).join(" \xB7 ") }) : null
412
+ ] })
413
+ ] });
414
+ return /* @__PURE__ */ jsxRuntime.jsxs(
415
+ CardFrame,
416
+ {
417
+ label: [card.title, card.subtitle, rating, price, next].filter(Boolean).join(", "),
418
+ restored: props.restored,
419
+ testID: `vinaverse-card-${card.id}`,
420
+ children: [
421
+ open ? /* @__PURE__ */ jsxRuntime.jsx(
422
+ reactNative.Pressable,
423
+ {
424
+ accessibilityRole: "link",
425
+ accessibilityLabel: card.title,
426
+ onPress: open,
427
+ style: { gap: space.sm },
428
+ testID: `vinaverse-open-${card.id}`,
429
+ children: top
430
+ }
431
+ ) : top,
432
+ card.body ? /* @__PURE__ */ jsxRuntime.jsx(Body, { children: card.body }) : null,
433
+ /* @__PURE__ */ jsxRuntime.jsx(Badges, { badges: next ? [next, ...card.badges ?? []] : card.badges }),
434
+ props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(CardActions, { ...props })
435
+ ]
436
+ }
437
+ );
438
+ }
439
+ function monogram(title) {
440
+ return title.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => [...w][0]?.toUpperCase() ?? "").join("");
441
+ }
442
+ function NextStepCard(props) {
443
+ const card = cardOf(props.card);
444
+ const action = card.actions?.[0];
445
+ const open = useLink(action?.deep_link, props.card);
446
+ return /* @__PURE__ */ jsxRuntime.jsxs(
447
+ CardFrame,
448
+ {
449
+ label: [card.title, card.body].filter(Boolean).join(". "),
450
+ restored: props.restored,
451
+ testID: `vinaverse-card-${card.id}`,
452
+ children: [
453
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { children: card.title }),
454
+ card.body ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: card.body }) : null,
455
+ action && open && !props.restored ? /* @__PURE__ */ jsxRuntime.jsx(
456
+ Button,
457
+ {
458
+ label: action.label,
459
+ variant: "primary",
460
+ role: "link",
461
+ onPress: open
462
+ }
463
+ ) : null
464
+ ]
465
+ }
466
+ );
467
+ }
468
+ function fieldText(type, value, locale, timeZone) {
469
+ if (value === null || value === void 0) return "";
470
+ const v = value;
471
+ switch (type) {
472
+ case "list":
473
+ return Array.isArray(value) ? value.join(", ") : String(value);
474
+ case "time_range":
475
+ return react$1.formatTimeRange(
476
+ String(v.start ?? ""),
477
+ typeof v.end === "string" ? v.end : void 0,
478
+ locale,
479
+ timeZone
480
+ );
481
+ case "money_cents":
482
+ return react$1.formatMoney(
483
+ Number(v.amount ?? 0),
484
+ String(v.currency ?? "USD"),
485
+ locale
486
+ );
487
+ case "address":
488
+ return ["line1", "line2", "city", "region", "postal_code", "country"].map((k) => v[k]).filter((p) => typeof p === "string" && p).join(", ");
489
+ case "weekly_hours": {
490
+ const entries = Array.isArray(v.entries) ? v.entries : [];
491
+ return entries.map((e) => `${weekday(e.day, locale)} ${e.start}\u2013${e.end}`).join(", ");
492
+ }
493
+ default:
494
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
495
+ }
496
+ }
497
+ var DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
498
+ function weekday(day, locale) {
499
+ const i = DAYS.indexOf(day);
500
+ if (i < 0) return day;
501
+ try {
502
+ return new Intl.DateTimeFormat(locale, {
503
+ weekday: "short",
504
+ timeZone: "UTC"
505
+ }).format(new Date(Date.UTC(2024, 0, 1 + i)));
506
+ } catch {
507
+ return day;
508
+ }
509
+ }
510
+ function outcomeText(t, outcome, status) {
511
+ if (outcome === "confirmed") return t("proposalConfirmed");
512
+ if (outcome === "declined") return t("proposalDeclined");
513
+ if (outcome === "failed") return t("proposalFailed");
514
+ if (outcome === "expired") return t("proposalExpired");
515
+ if (status === "confirmed") return t("proposalConfirmed");
516
+ if (status === "declined") return t("proposalDeclined");
517
+ if (status === "expired") return t("proposalExpired");
518
+ if (status !== "pending") return t("proposalSettled");
519
+ return null;
520
+ }
521
+ function ProposalCard(props) {
522
+ const card = cardOf(props.card);
523
+ const proposal = card.proposal;
524
+ const { t, locale } = react$1.useCopy();
525
+ const { store, handlers, timeZone } = useChat();
526
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
527
+ const edit = useLink(proposal?.edit_deep_link, props.card);
528
+ if (!proposal) return null;
529
+ const settled = outcomeText(t, props.card.outcome, proposal.status);
530
+ const actionable = settled === null && !props.restored;
531
+ const decide = (decision) => {
532
+ const onProposal = handlers.current?.onProposal;
533
+ if (!onProposal) return;
534
+ store.decideProposal(
535
+ props.card.id,
536
+ () => onProposal(props.card, decision)
537
+ ).catch(
538
+ (error) => handlers.current?.onWarning?.(`Vinaverse proposal: ${String(error)}`, {
539
+ code: "proposal_failed"
540
+ })
541
+ );
542
+ };
543
+ const fields = proposal.fields.map((f) => {
544
+ const after = fieldText(f.type, f.after, locale, timeZone);
545
+ const before = f.before === void 0 || f.before === null ? null : fieldText(f.type, f.before, locale, timeZone);
546
+ return {
547
+ key: f.key,
548
+ label: f.label,
549
+ value: before !== null && before !== after ? `${before} \u2192 ${after}` : after
550
+ };
551
+ });
552
+ return /* @__PURE__ */ jsxRuntime.jsxs(
553
+ CardFrame,
554
+ {
555
+ label: [
556
+ proposal.title,
557
+ proposal.summary,
558
+ ...fields.map((f) => `${f.label}: ${f.value}`),
559
+ settled
560
+ ].filter(Boolean).join(". "),
561
+ restored: props.restored,
562
+ testID: `vinaverse-proposal-${card.id}`,
563
+ children: [
564
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { children: proposal.title }),
565
+ /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: proposal.summary }),
566
+ /* @__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: [
567
+ /* @__PURE__ */ jsxRuntime.jsx(
568
+ reactNative.Text,
569
+ {
570
+ style: {
571
+ color: colors.textMuted,
572
+ fontSize: font.size.sm,
573
+ minWidth: 72
574
+ },
575
+ children: f.label
576
+ }
577
+ ),
578
+ /* @__PURE__ */ jsxRuntime.jsx(
579
+ reactNative.Text,
580
+ {
581
+ style: { color: colors.text, fontSize: font.size.sm, flex: 1 },
582
+ children: f.value
583
+ }
584
+ )
585
+ ] }, f.key)) }),
586
+ settled ? /* @__PURE__ */ jsxRuntime.jsx(
587
+ reactNative.Text,
588
+ {
589
+ style: {
590
+ color: props.card.outcome === "confirmed" || proposal.status === "confirmed" ? colors.success : colors.textMuted,
591
+ fontSize: font.size.sm,
592
+ fontWeight: "600"
593
+ },
594
+ children: settled
595
+ }
596
+ ) : null,
597
+ actionable ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.sm }, children: [
598
+ /* @__PURE__ */ jsxRuntime.jsx(
599
+ Button,
600
+ {
601
+ label: t("proposalConfirm"),
602
+ variant: "primary",
603
+ busy: props.busy,
604
+ disabled: props.busy,
605
+ onPress: () => decide("confirm"),
606
+ testID: "vinaverse-proposal-confirm"
607
+ }
608
+ ),
609
+ /* @__PURE__ */ jsxRuntime.jsx(
610
+ Button,
611
+ {
612
+ label: t("proposalDecline"),
613
+ disabled: props.busy,
614
+ onPress: () => decide("decline"),
615
+ testID: "vinaverse-proposal-decline"
616
+ }
617
+ ),
618
+ edit ? /* @__PURE__ */ jsxRuntime.jsx(
619
+ Button,
620
+ {
621
+ label: t("proposalChange"),
622
+ variant: "text",
623
+ role: "link",
624
+ onPress: edit
625
+ }
626
+ ) : null
627
+ ] }) : null
628
+ ]
629
+ }
630
+ );
631
+ }
632
+ var KNOWN = /* @__PURE__ */ new Set([
633
+ "single_choice",
634
+ "multi_choice",
635
+ "image_choice",
636
+ "yes_no",
637
+ "list_choice",
638
+ "rate_each",
639
+ "short_text",
640
+ "long_text",
641
+ "date",
642
+ "number",
643
+ "scale",
644
+ "photo"
645
+ ]);
646
+ var ONE_TAP = /* @__PURE__ */ new Set(["single_choice", "yes_no", "list_choice", "scale"]);
647
+ function QuestionCard(props) {
648
+ const held = props.card;
649
+ const question = held.question;
650
+ const { store, handlers } = useChat();
651
+ const { t, locale } = react$1.useCopy();
652
+ const { colors, font, space } = chunk6QYDPGRS_cjs.useTheme();
653
+ const needsSocket = react$1.useConversationSelector(
654
+ store,
655
+ (v) => v.answersNeedWebSocket
656
+ );
657
+ if (!question) return null;
658
+ const send = (answer, questionId = question.question_id) => {
659
+ try {
660
+ store.answer(held.id, questionId, answer);
661
+ } catch (error) {
662
+ handlers.current?.onWarning?.(`Vinaverse answer: ${String(error)}`, {
663
+ code: "answer_failed"
664
+ });
665
+ }
666
+ };
667
+ const locked = held.locked.includes(question.question_id);
668
+ const state = question.state;
669
+ const open = state === "open" && !locked && !props.restored;
670
+ const summary = state === "answered" ? answerText(question, locale, t("questionOther")) : null;
671
+ return /* @__PURE__ */ jsxRuntime.jsxs(
672
+ CardFrame,
673
+ {
674
+ label: [question.prompt, summary].filter(Boolean).join(". "),
675
+ restored: props.restored,
676
+ testID: `vinaverse-question-${held.id}`,
677
+ children: [
678
+ question.progress ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors.textMuted, fontSize: font.size.xs }, children: t("questionStep", {
679
+ step: question.progress.step,
680
+ total: question.progress.total
681
+ }) }) : null,
682
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { children: question.prompt }),
683
+ question.help ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: question.help }) : null,
684
+ state === "answered" ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space.xxs }, children: [
685
+ /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: t("questionAnswered") }),
686
+ summary ? /* @__PURE__ */ jsxRuntime.jsx(Body, { children: summary }) : null
687
+ ] }) : 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(
688
+ RateEach,
689
+ {
690
+ question,
691
+ locked: held.locked,
692
+ enabled: !props.restored,
693
+ send
694
+ }
695
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
696
+ Answer,
697
+ {
698
+ question,
699
+ enabled: open,
700
+ send,
701
+ held
702
+ },
703
+ question.question_id
704
+ )
705
+ ]
706
+ }
707
+ );
708
+ }
709
+ function Answer(props) {
710
+ const { question, send, enabled } = props;
711
+ const { t } = react$1.useCopy();
712
+ const { space } = chunk6QYDPGRS_cjs.useTheme();
713
+ const { handlers } = useChat();
714
+ const [draft, setDraft] = react.useState({});
715
+ const kind = question.kind;
716
+ const oneTap = ONE_TAP.has(kind) && !question.allows_other;
717
+ const problem = conversation.answerProblem(question, draft);
718
+ const submit = (answer = draft) => {
719
+ if (conversation.answerProblem(question, answer) === null)
720
+ send(answer);
721
+ };
722
+ const input = (() => {
723
+ switch (kind) {
724
+ case "short_text":
725
+ case "long_text":
726
+ return /* @__PURE__ */ jsxRuntime.jsx(
727
+ TextAnswer,
728
+ {
729
+ question,
730
+ enabled,
731
+ value: draft.text ?? "",
732
+ onChange: (text) => setDraft({ text })
733
+ }
734
+ );
735
+ case "number":
736
+ return /* @__PURE__ */ jsxRuntime.jsx(
737
+ NumberAnswer,
738
+ {
739
+ question,
740
+ enabled,
741
+ value: draft.number,
742
+ onChange: (n) => setDraft({ number: n })
743
+ }
744
+ );
745
+ case "scale":
746
+ return /* @__PURE__ */ jsxRuntime.jsx(
747
+ ScaleAnswer,
748
+ {
749
+ question,
750
+ enabled,
751
+ onPick: (n) => submit({ number: n })
752
+ }
753
+ );
754
+ case "date":
755
+ return /* @__PURE__ */ jsxRuntime.jsx(
756
+ DateAnswer,
757
+ {
758
+ question,
759
+ enabled,
760
+ value: draft.date,
761
+ onChange: (date) => setDraft({ date })
762
+ }
763
+ );
764
+ case "photo":
765
+ return /* @__PURE__ */ jsxRuntime.jsx(
766
+ Button,
767
+ {
768
+ label: t("questionAddPhoto"),
769
+ disabled: !enabled || !handlers.current?.onPickPhoto,
770
+ onPress: () => {
771
+ handlers.current?.onPickPhoto?.(question, props.held).then((picked) => {
772
+ if (picked) send({ upload_ref: picked.upload_ref });
773
+ }).catch(
774
+ (error) => handlers.current?.onWarning?.(
775
+ `Vinaverse photo: ${String(error)}`,
776
+ { code: "photo_failed" }
777
+ )
778
+ );
779
+ }
780
+ }
781
+ );
782
+ default:
783
+ return /* @__PURE__ */ jsxRuntime.jsx(
784
+ ChoiceAnswer,
785
+ {
786
+ question,
787
+ enabled,
788
+ draft,
789
+ onChange: (next) => oneTap ? submit(next) : setDraft(next)
790
+ }
791
+ );
792
+ }
793
+ })();
794
+ const tapToAnswer = oneTap || kind === "scale" || kind === "photo";
795
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space.sm }, children: [
796
+ input,
797
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.sm }, children: [
798
+ tapToAnswer ? null : /* @__PURE__ */ jsxRuntime.jsx(
799
+ Button,
800
+ {
801
+ label: t("questionSubmit"),
802
+ variant: "primary",
803
+ disabled: !enabled || problem !== null,
804
+ onPress: () => submit(),
805
+ testID: "vinaverse-question-submit"
806
+ }
807
+ ),
808
+ question.required ? null : /* @__PURE__ */ jsxRuntime.jsx(
809
+ Button,
810
+ {
811
+ label: t("questionSkip"),
812
+ variant: "text",
813
+ disabled: !enabled,
814
+ onPress: () => send({ skipped: true })
815
+ }
816
+ )
817
+ ] })
818
+ ] });
819
+ }
820
+ function ChoiceAnswer(props) {
821
+ const { question, draft, onChange, enabled } = props;
822
+ const { t } = react$1.useCopy();
823
+ const { slots } = useChat();
824
+ const { colors, radius, space, font } = chunk6QYDPGRS_cjs.useTheme();
825
+ const [search, setSearch] = react.useState("");
826
+ const multi = question.kind === "multi_choice" || question.kind === "image_choice";
827
+ const values = draft.values ?? [];
828
+ const other = draft.other_text;
829
+ const none = question.none_of_the_above;
830
+ const list = question.kind === "list_choice";
831
+ const options = react.useMemo(() => {
832
+ const all = question.options ?? [];
833
+ const q = search.trim().toLowerCase();
834
+ return list && q ? all.filter((o) => o.label.toLowerCase().includes(q)) : all;
835
+ }, [question.options, search, list]);
836
+ const shown = list ? options.slice(0, 50) : options;
837
+ const pick = (value) => {
838
+ if (none && value === none.value) return onChange({ values: [value] });
839
+ const rest = values.filter((v) => v !== none?.value);
840
+ const next = multi ? rest.includes(value) ? rest.filter((v) => v !== value) : [...rest, value] : [value];
841
+ const keepOther = other !== void 0 && (multi || question.kind === "single_choice");
842
+ onChange({
843
+ ...next.length ? { values: next } : {},
844
+ ...keepOther ? { other_text: other } : {}
845
+ });
846
+ };
847
+ const Img = slots.Image ?? reactNative.Image;
848
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space.xs }, children: [
849
+ list ? /* @__PURE__ */ jsxRuntime.jsx(
850
+ reactNative.TextInput,
851
+ {
852
+ value: search,
853
+ onChangeText: setSearch,
854
+ placeholder: t("questionSearch"),
855
+ placeholderTextColor: colors.textMuted,
856
+ accessibilityLabel: t("questionSearch"),
857
+ editable: enabled,
858
+ style: {
859
+ borderWidth: 1,
860
+ borderColor: colors.border,
861
+ borderRadius: radius.md,
862
+ paddingHorizontal: space.md,
863
+ minHeight: 44,
864
+ color: colors.text,
865
+ fontSize: font.size.md
866
+ }
867
+ }
868
+ ) : null,
869
+ shown.map((o) => {
870
+ const image = question.kind === "image_choice" ? safeImage(o.image_url) : null;
871
+ return /* @__PURE__ */ jsxRuntime.jsx(
872
+ Button,
873
+ {
874
+ label: o.label,
875
+ role: multi ? "checkbox" : "radio",
876
+ selected: values.includes(o.value),
877
+ disabled: !enabled,
878
+ ...o.description ? { hint: o.description } : {},
879
+ onPress: () => pick(o.value),
880
+ children: image ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { alignItems: "center", gap: space.xs }, children: [
881
+ /* @__PURE__ */ jsxRuntime.jsx(
882
+ Img,
883
+ {
884
+ source: { uri: image },
885
+ style: { width: 96, height: 96, borderRadius: radius.md },
886
+ accessible: false
887
+ }
888
+ ),
889
+ /* @__PURE__ */ jsxRuntime.jsx(
890
+ ChoiceLabel,
891
+ {
892
+ selected: values.includes(o.value),
893
+ text: o.label
894
+ }
895
+ )
896
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(ChoiceLabel, { selected: values.includes(o.value), text: o.label })
897
+ },
898
+ o.value
899
+ );
900
+ }),
901
+ none ? /* @__PURE__ */ jsxRuntime.jsx(
902
+ Button,
903
+ {
904
+ label: none.label,
905
+ role: multi ? "checkbox" : "radio",
906
+ selected: values.includes(none.value),
907
+ disabled: !enabled,
908
+ onPress: () => pick(none.value)
909
+ }
910
+ ) : null,
911
+ question.allows_other ? /* @__PURE__ */ jsxRuntime.jsx(
912
+ OtherText,
913
+ {
914
+ max: question.other_max_length ?? 500,
915
+ enabled,
916
+ value: other,
917
+ onChange: (text) => {
918
+ const kept = values.filter((v) => v !== none?.value);
919
+ const withValues = question.kind === "single_choice" || multi;
920
+ onChange({
921
+ ...withValues && kept.length ? { values: question.kind === "single_choice" ? [] : kept } : {},
922
+ ...text === void 0 ? {} : { other_text: text }
923
+ });
924
+ }
925
+ }
926
+ ) : null
927
+ ] });
928
+ }
929
+ function ChoiceLabel(props) {
930
+ const { colors, font } = chunk6QYDPGRS_cjs.useTheme();
931
+ return /* @__PURE__ */ jsxRuntime.jsx(
932
+ reactNative.Text,
933
+ {
934
+ style: {
935
+ color: props.selected ? colors.onPrimary : colors.text,
936
+ fontSize: font.size.md,
937
+ textAlign: "center"
938
+ },
939
+ children: props.text
940
+ }
941
+ );
942
+ }
943
+ function OtherText(props) {
944
+ const { t } = react$1.useCopy();
945
+ const { colors, radius, space, font } = chunk6QYDPGRS_cjs.useTheme();
946
+ const on = props.value !== void 0;
947
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space.xs }, children: [
948
+ /* @__PURE__ */ jsxRuntime.jsx(
949
+ Button,
950
+ {
951
+ label: t("questionOther"),
952
+ role: "checkbox",
953
+ selected: on,
954
+ disabled: !props.enabled,
955
+ onPress: () => props.onChange(on ? void 0 : "")
956
+ }
957
+ ),
958
+ on ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
959
+ /* @__PURE__ */ jsxRuntime.jsx(
960
+ reactNative.TextInput,
961
+ {
962
+ value: props.value,
963
+ onChangeText: (text) => props.onChange(text),
964
+ accessibilityLabel: t("questionOther"),
965
+ testID: "vinaverse-question-other",
966
+ editable: props.enabled,
967
+ style: {
968
+ borderWidth: 1,
969
+ borderColor: colors.border,
970
+ borderRadius: radius.md,
971
+ paddingHorizontal: space.md,
972
+ minHeight: 44,
973
+ color: colors.text,
974
+ fontSize: font.size.md
975
+ }
976
+ }
977
+ ),
978
+ /* @__PURE__ */ jsxRuntime.jsx(CharactersLeft, { max: props.max, text: props.value ?? "" })
979
+ ] }) : null
980
+ ] });
981
+ }
982
+ function CharactersLeft(props) {
983
+ const { t } = react$1.useCopy();
984
+ const { colors, font } = chunk6QYDPGRS_cjs.useTheme();
985
+ const left = props.max - conversation.codePoints(props.text);
986
+ return /* @__PURE__ */ jsxRuntime.jsx(
987
+ reactNative.Text,
988
+ {
989
+ accessibilityLiveRegion: left < 20 ? "polite" : "none",
990
+ style: {
991
+ color: left < 0 ? colors.danger : colors.textMuted,
992
+ fontSize: font.size.xs
993
+ },
994
+ children: t("questionCharactersLeft", { count: left })
995
+ }
996
+ );
997
+ }
998
+ function TextAnswer(props) {
999
+ const { colors, radius, space, font } = chunk6QYDPGRS_cjs.useTheme();
1000
+ const long = props.question.kind === "long_text";
1001
+ const max = props.question.text?.max_length ?? (long ? 2e3 : 200);
1002
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space.xs }, children: [
1003
+ /* @__PURE__ */ jsxRuntime.jsx(
1004
+ reactNative.TextInput,
1005
+ {
1006
+ value: props.value,
1007
+ onChangeText: props.onChange,
1008
+ accessibilityLabel: props.question.prompt,
1009
+ editable: props.enabled,
1010
+ multiline: long,
1011
+ testID: "vinaverse-question-text",
1012
+ style: {
1013
+ borderWidth: 1,
1014
+ borderColor: colors.border,
1015
+ borderRadius: radius.md,
1016
+ paddingHorizontal: space.md,
1017
+ paddingVertical: space.sm,
1018
+ minHeight: long ? 96 : 44,
1019
+ color: colors.text,
1020
+ fontSize: font.size.md,
1021
+ textAlignVertical: long ? "top" : "center"
1022
+ }
1023
+ }
1024
+ ),
1025
+ /* @__PURE__ */ jsxRuntime.jsx(CharactersLeft, { max, text: props.value })
1026
+ ] });
1027
+ }
1028
+ function NumberAnswer(props) {
1029
+ const range = props.question.number ?? { min: 0, max: 100, step: 1 };
1030
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
1031
+ const value = props.value ?? range.min;
1032
+ const clamp = (n) => Math.min(range.max, Math.max(range.min, Number(n.toFixed(6))));
1033
+ const shown = `${value}${props.question.number?.unit ? ` ${props.question.number.unit}` : ""}`;
1034
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1035
+ reactNative.View,
1036
+ {
1037
+ accessible: true,
1038
+ accessibilityRole: "adjustable",
1039
+ accessibilityLabel: props.question.prompt,
1040
+ accessibilityValue: {
1041
+ min: range.min,
1042
+ max: range.max,
1043
+ now: value,
1044
+ text: shown
1045
+ },
1046
+ accessibilityActions: [{ name: "increment" }, { name: "decrement" }],
1047
+ testID: "vinaverse-question-number",
1048
+ onAccessibilityAction: (e) => props.onChange(
1049
+ clamp(
1050
+ value + (e.nativeEvent.actionName === "increment" ? range.step : -range.step)
1051
+ )
1052
+ ),
1053
+ style: { flexDirection: "row", alignItems: "center", gap: space.md },
1054
+ children: [
1055
+ /* @__PURE__ */ jsxRuntime.jsx(
1056
+ Button,
1057
+ {
1058
+ label: "\u2212",
1059
+ disabled: !props.enabled || value <= range.min,
1060
+ onPress: () => props.onChange(clamp(value - range.step))
1061
+ }
1062
+ ),
1063
+ /* @__PURE__ */ jsxRuntime.jsx(
1064
+ reactNative.Text,
1065
+ {
1066
+ style: {
1067
+ color: colors.text,
1068
+ fontSize: font.size.lg,
1069
+ minWidth: 64,
1070
+ textAlign: "center"
1071
+ },
1072
+ children: shown
1073
+ }
1074
+ ),
1075
+ /* @__PURE__ */ jsxRuntime.jsx(
1076
+ Button,
1077
+ {
1078
+ label: "+",
1079
+ disabled: !props.enabled || value >= range.max,
1080
+ onPress: () => props.onChange(clamp(value + range.step))
1081
+ }
1082
+ )
1083
+ ]
1084
+ }
1085
+ );
1086
+ }
1087
+ function ScaleAnswer(props) {
1088
+ const scale = props.question.scale;
1089
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
1090
+ if (!scale) return null;
1091
+ const points = [];
1092
+ for (let n = scale.min; n <= scale.max && points.length < 21; n += scale.step)
1093
+ points.push(n);
1094
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space.xs }, children: [
1095
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.xs }, children: points.map((n) => /* @__PURE__ */ jsxRuntime.jsx(
1096
+ Button,
1097
+ {
1098
+ label: String(n),
1099
+ role: "radio",
1100
+ disabled: !props.enabled,
1101
+ onPress: () => props.onPick(n)
1102
+ },
1103
+ n
1104
+ )) }),
1105
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flexDirection: "row", justifyContent: "space-between" }, children: [
1106
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors.textMuted, fontSize: font.size.xs }, children: scale.min_label }),
1107
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors.textMuted, fontSize: font.size.xs }, children: scale.max_label })
1108
+ ] })
1109
+ ] });
1110
+ }
1111
+ var iso = (y, m, d) => `${String(y).padStart(4, "0")}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
1112
+ function DateAnswer(props) {
1113
+ const { locale } = react$1.useCopy();
1114
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
1115
+ const bounds = props.question.date;
1116
+ const start = props.value ?? bounds?.min ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1117
+ const [month, setMonth] = react.useState(() => ({
1118
+ y: Number(start.slice(0, 4)),
1119
+ m: Number(start.slice(5, 7)) - 1
1120
+ }));
1121
+ const days = new Date(Date.UTC(month.y, month.m + 1, 0)).getUTCDate();
1122
+ const title = (() => {
1123
+ try {
1124
+ return new Intl.DateTimeFormat(locale, {
1125
+ month: "long",
1126
+ year: "numeric",
1127
+ timeZone: "UTC"
1128
+ }).format(new Date(Date.UTC(month.y, month.m, 1)));
1129
+ } catch {
1130
+ return `${month.y}-${month.m + 1}`;
1131
+ }
1132
+ })();
1133
+ const move = (by) => {
1134
+ const d = new Date(Date.UTC(month.y, month.m + by, 1));
1135
+ setMonth({ y: d.getUTCFullYear(), m: d.getUTCMonth() });
1136
+ };
1137
+ const outside = (date) => bounds?.min !== void 0 && date < bounds.min || bounds?.max !== void 0 && date > bounds.max;
1138
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space.xs }, children: [
1139
+ /* @__PURE__ */ jsxRuntime.jsxs(
1140
+ reactNative.View,
1141
+ {
1142
+ style: {
1143
+ flexDirection: "row",
1144
+ alignItems: "center",
1145
+ justifyContent: "space-between"
1146
+ },
1147
+ children: [
1148
+ /* @__PURE__ */ jsxRuntime.jsx(
1149
+ Button,
1150
+ {
1151
+ label: "\u2039",
1152
+ variant: "text",
1153
+ disabled: !props.enabled,
1154
+ onPress: () => move(-1)
1155
+ }
1156
+ ),
1157
+ /* @__PURE__ */ jsxRuntime.jsx(
1158
+ reactNative.Text,
1159
+ {
1160
+ accessibilityRole: "header",
1161
+ style: { color: colors.text, fontSize: font.size.md },
1162
+ children: title
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsxRuntime.jsx(
1166
+ Button,
1167
+ {
1168
+ label: "\u203A",
1169
+ variant: "text",
1170
+ disabled: !props.enabled,
1171
+ onPress: () => move(1)
1172
+ }
1173
+ )
1174
+ ]
1175
+ }
1176
+ ),
1177
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.xxs }, children: Array.from({ length: days }, (_, i) => {
1178
+ const date = iso(month.y, month.m, i + 1);
1179
+ return /* @__PURE__ */ jsxRuntime.jsx(
1180
+ Button,
1181
+ {
1182
+ label: String(i + 1),
1183
+ hint: react$1.formatDate(date, locale),
1184
+ role: "radio",
1185
+ selected: props.value === date,
1186
+ disabled: !props.enabled || outside(date),
1187
+ onPress: () => props.onChange(date)
1188
+ },
1189
+ date
1190
+ );
1191
+ }) }),
1192
+ props.value ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: react$1.formatDate(props.value, locale) }) : null
1193
+ ] });
1194
+ }
1195
+ function RateEach(props) {
1196
+ const { question } = props;
1197
+ const { t } = react$1.useCopy();
1198
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
1199
+ const answered = new Map(
1200
+ (question.row_answers ?? []).map((r) => [
1201
+ r.question_id,
1202
+ (r.answer.values ?? [])[0]
1203
+ ])
1204
+ );
1205
+ const open = (question.rows ?? []).filter(
1206
+ (r) => !props.locked.includes(r.question_id)
1207
+ );
1208
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space.md }, children: [
1209
+ (question.rows ?? []).map((row) => {
1210
+ const done = props.locked.includes(row.question_id);
1211
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1212
+ reactNative.View,
1213
+ {
1214
+ accessibilityLabel: row.label,
1215
+ style: { gap: space.xs },
1216
+ children: [
1217
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors.text, fontSize: font.size.sm }, children: row.label }),
1218
+ /* @__PURE__ */ jsxRuntime.jsx(
1219
+ reactNative.View,
1220
+ {
1221
+ style: { flexDirection: "row", flexWrap: "wrap", gap: space.xs },
1222
+ children: (question.options ?? []).map((o) => /* @__PURE__ */ jsxRuntime.jsx(
1223
+ Button,
1224
+ {
1225
+ label: o.label,
1226
+ role: "radio",
1227
+ selected: answered.get(row.question_id) === o.value,
1228
+ disabled: !props.enabled || done,
1229
+ onPress: () => props.send({ values: [o.value] }, row.question_id)
1230
+ },
1231
+ o.value
1232
+ ))
1233
+ }
1234
+ )
1235
+ ]
1236
+ },
1237
+ row.question_id
1238
+ );
1239
+ }),
1240
+ question.required || open.length === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(
1241
+ Button,
1242
+ {
1243
+ label: t("questionSkip"),
1244
+ variant: "text",
1245
+ disabled: !props.enabled,
1246
+ onPress: () => {
1247
+ for (const row of open)
1248
+ props.send({ skipped: true }, row.question_id);
1249
+ }
1250
+ }
1251
+ )
1252
+ ] });
1253
+ }
1254
+ function answerText(question, locale, otherLabel) {
1255
+ const answer = question.answer;
1256
+ if (!answer || answer.skipped) return null;
1257
+ const labels = new Map(
1258
+ (question.options ?? []).map((o) => [o.value, o.label])
1259
+ );
1260
+ if (question.none_of_the_above)
1261
+ labels.set(
1262
+ question.none_of_the_above.value,
1263
+ question.none_of_the_above.label
1264
+ );
1265
+ const parts = [
1266
+ ...(answer.values ?? []).map((v) => labels.get(v) ?? v),
1267
+ ...answer.other_text ? [`${otherLabel}: ${answer.other_text}`] : [],
1268
+ ...answer.text ? [answer.text] : [],
1269
+ ...answer.number !== void 0 ? [String(answer.number)] : [],
1270
+ ...answer.date ? [react$1.formatDate(answer.date, locale)] : [],
1271
+ ...answer.upload_ref ? ["\u{1F4F7}"] : []
1272
+ ];
1273
+ return parts.length ? parts.join(", ") : null;
1274
+ }
1275
+ function TimeSlotsCard(props) {
1276
+ const card = cardOf(props.card);
1277
+ const { locale } = react$1.useCopy();
1278
+ const { timeZone } = useChat();
1279
+ const { colors, radius, space, font } = chunk6QYDPGRS_cjs.useTheme();
1280
+ const slots = (card.slots ?? []).map(
1281
+ (s) => react$1.formatTimeRange(s.start, s.end, locale, timeZone)
1282
+ );
1283
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1284
+ CardFrame,
1285
+ {
1286
+ label: [card.title, ...slots].join(", "),
1287
+ restored: props.restored,
1288
+ testID: `vinaverse-card-${card.id}`,
1289
+ children: [
1290
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { children: card.title }),
1291
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space.xs }, children: slots.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
1292
+ reactNative.Text,
1293
+ {
1294
+ style: {
1295
+ color: colors.text,
1296
+ borderWidth: 1,
1297
+ borderColor: colors.border,
1298
+ borderRadius: radius.pill,
1299
+ paddingHorizontal: space.md,
1300
+ paddingVertical: space.xs,
1301
+ fontSize: font.size.sm
1302
+ },
1303
+ children: s
1304
+ },
1305
+ s
1306
+ )) })
1307
+ ]
1308
+ }
1309
+ );
1310
+ }
1311
+ var DEFAULTS = {
1312
+ proposal: ProposalCard,
1313
+ question: QuestionCard,
1314
+ next_step: NextStepCard,
1315
+ time_slots: TimeSlotsCard,
1316
+ professional: ListingCard,
1317
+ workspace: ListingCard,
1318
+ links: LinksCard
1319
+ };
1320
+ function defaultCard(kind) {
1321
+ return DEFAULTS[kind] ?? InfoCard;
1322
+ }
1323
+ var CardView = /* @__PURE__ */ react.memo(function CardView2(props) {
1324
+ const { slots } = useChat();
1325
+ const { card, busy } = props.item;
1326
+ const Default = defaultCard(card.kind);
1327
+ const Custom = slots.cards?.[card.kind];
1328
+ const read = react.useMemo(() => agent$1.readCard(card.card), [card]);
1329
+ const own = {
1330
+ card,
1331
+ read,
1332
+ busy,
1333
+ restored: props.item.restored === true,
1334
+ grouped: props.grouped === true
1335
+ };
1336
+ return Custom ? /* @__PURE__ */ jsxRuntime.jsx(Custom, { ...own, Default }) : /* @__PURE__ */ jsxRuntime.jsx(Default, { ...own });
1337
+ });
1338
+ var keyOf = (item) => item.key;
1339
+ var kindOf = (item) => item.card.kind;
1340
+ function CardCarousel(props) {
1341
+ const { width } = reactNative.useWindowDimensions();
1342
+ const { t } = react$1.useCopy();
1343
+ const size2 = Math.min(Math.round(width * 0.8), 360);
1344
+ return /* @__PURE__ */ jsxRuntime.jsx(
1345
+ reactNative.View,
1346
+ {
1347
+ accessibilityRole: "list",
1348
+ accessibilityLabel: t("cardsInRow", {
1349
+ count: String(props.items.length)
1350
+ }),
1351
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1352
+ flashList.FlashList,
1353
+ {
1354
+ horizontal: true,
1355
+ data: props.items,
1356
+ keyExtractor: keyOf,
1357
+ getItemType: kindOf,
1358
+ renderItem: ({ item }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { width: size2 }, children: /* @__PURE__ */ jsxRuntime.jsx(CardView, { item, grouped: true }) }),
1359
+ snapToInterval: size2,
1360
+ decelerationRate: "fast",
1361
+ showsHorizontalScrollIndicator: false
1362
+ }
1363
+ )
1364
+ }
1365
+ );
1366
+ }
1367
+ var ActivityLine = /* @__PURE__ */ react.memo(function ActivityLine2() {
1368
+ const { store } = useChat();
1369
+ const { t } = react$1.useCopy();
1370
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
1371
+ const activity = react$1.useConversationSelector(store, (v) => v.activity);
1372
+ const waiting = react$1.useConversationSelector(
1373
+ store,
1374
+ (v) => v.responding && v.items.at(-1)?.type === "message" && v.items.at(-1).role === "person"
1375
+ );
1376
+ const name = react$1.useConversationSelector(store, (v) => v.agent.name);
1377
+ const running = activity.filter((a) => a.state === "running").at(-1);
1378
+ if (!running && !waiting) return null;
1379
+ const text = running?.label ?? t("working", { agent: name ?? t("agentFallbackName") });
1380
+ return /* @__PURE__ */ jsxRuntime.jsx(
1381
+ reactNative.View,
1382
+ {
1383
+ accessibilityLiveRegion: "polite",
1384
+ style: { paddingHorizontal: space.lg, paddingVertical: space.sm },
1385
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: colors.textMuted, fontSize: font.size.sm }, children: text })
1386
+ }
1387
+ );
1388
+ });
1389
+ var Avatar = /* @__PURE__ */ react.memo(function Avatar2(props) {
1390
+ const { colors, size: size2, font } = chunk6QYDPGRS_cjs.useTheme();
1391
+ const { slots } = useChat();
1392
+ const box = {
1393
+ width: size2.avatar,
1394
+ height: size2.avatar,
1395
+ borderRadius: size2.avatar / 2
1396
+ };
1397
+ if (props.uri && !props.person) {
1398
+ const Img = slots.Image ?? reactNative.Image;
1399
+ return /* @__PURE__ */ jsxRuntime.jsx(
1400
+ Img,
1401
+ {
1402
+ source: { uri: props.uri },
1403
+ style: box,
1404
+ accessibilityIgnoresInvertColors: true,
1405
+ accessible: false,
1406
+ importantForAccessibility: "no"
1407
+ }
1408
+ );
1409
+ }
1410
+ return /* @__PURE__ */ jsxRuntime.jsx(
1411
+ reactNative.View,
1412
+ {
1413
+ accessible: false,
1414
+ importantForAccessibility: "no-hide-descendants",
1415
+ style: {
1416
+ ...box,
1417
+ backgroundColor: props.person ? colors.staffBubble : colors.primary,
1418
+ alignItems: "center",
1419
+ justifyContent: "center"
1420
+ },
1421
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1422
+ reactNative.Text,
1423
+ {
1424
+ style: {
1425
+ color: props.person ? colors.staffText : colors.onPrimary,
1426
+ fontSize: font.size.sm,
1427
+ fontWeight: "600"
1428
+ },
1429
+ children: props.person ? "\u25C9" : (props.name.trim()[0] ?? "?").toUpperCase()
1430
+ }
1431
+ )
1432
+ }
1433
+ );
1434
+ });
1435
+ function DefaultHeader(props) {
1436
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
1437
+ const { t } = react$1.useCopy();
1438
+ const person = props.takeoverBy !== null;
1439
+ const subtitle = person ? t("staffLabel", { by: props.takeoverBy || t("staffFallbackRole") }) : t("agentLabel");
1440
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1441
+ reactNative.View,
1442
+ {
1443
+ accessibilityRole: "header",
1444
+ accessible: true,
1445
+ accessibilityLabel: `${props.name}, ${subtitle}`,
1446
+ style: {
1447
+ flexDirection: "row",
1448
+ alignItems: "center",
1449
+ gap: space.sm,
1450
+ paddingHorizontal: space.lg,
1451
+ paddingVertical: space.sm,
1452
+ backgroundColor: colors.background,
1453
+ borderBottomWidth: 1,
1454
+ borderBottomColor: colors.divider
1455
+ },
1456
+ children: [
1457
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: props.name, uri: props.avatarUrl, person }),
1458
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flexShrink: 1 }, children: [
1459
+ /* @__PURE__ */ jsxRuntime.jsx(
1460
+ reactNative.Text,
1461
+ {
1462
+ numberOfLines: 1,
1463
+ style: {
1464
+ color: colors.text,
1465
+ fontSize: font.size.md,
1466
+ fontWeight: "600",
1467
+ ...font.family ? { fontFamily: font.family } : {}
1468
+ },
1469
+ children: props.name
1470
+ }
1471
+ ),
1472
+ /* @__PURE__ */ jsxRuntime.jsx(
1473
+ reactNative.Text,
1474
+ {
1475
+ numberOfLines: 1,
1476
+ style: {
1477
+ color: person ? colors.staffAccent : colors.textMuted,
1478
+ fontSize: font.size.xs
1479
+ },
1480
+ children: subtitle
1481
+ }
1482
+ )
1483
+ ] })
1484
+ ]
1485
+ }
1486
+ );
1487
+ }
1488
+ var ChatHeader = /* @__PURE__ */ react.memo(function ChatHeader2() {
1489
+ const { store, slots, agentAvatar } = useChat();
1490
+ const { t } = react$1.useCopy();
1491
+ const agent = react$1.useConversationSelector(store, (v) => v.agent);
1492
+ const takeover = react$1.useConversationSelector(store, (v) => v.takeover);
1493
+ const Header = slots.Header;
1494
+ const props = {
1495
+ name: agent.name ?? t("agentFallbackName"),
1496
+ avatarUrl: agentAvatar ?? agent.avatarUrl,
1497
+ takeoverBy: takeover ? takeover.by ?? "" : null
1498
+ };
1499
+ return Header ? /* @__PURE__ */ jsxRuntime.jsx(Header, { ...props, Default: DefaultHeader }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultHeader, { ...props });
1500
+ });
1501
+ function DefaultComposer(props) {
1502
+ const { t, direction } = react$1.useCopy();
1503
+ const { colors, radius, space, font, size: size2 } = chunk6QYDPGRS_cjs.useTheme();
1504
+ const [text, setText] = react.useState("");
1505
+ const length = conversation.codePoints(text);
1506
+ const canSend = text.trim().length > 0 && length <= conversation.MAX_TEXT;
1507
+ const send = () => {
1508
+ if (!canSend) return;
1509
+ props.onSend(text);
1510
+ setText("");
1511
+ };
1512
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1513
+ reactNative.View,
1514
+ {
1515
+ style: {
1516
+ flexDirection: "row",
1517
+ alignItems: "flex-end",
1518
+ gap: space.sm,
1519
+ paddingHorizontal: space.lg,
1520
+ paddingVertical: space.sm,
1521
+ borderTopWidth: 1,
1522
+ borderTopColor: colors.divider,
1523
+ backgroundColor: colors.background
1524
+ },
1525
+ children: [
1526
+ /* @__PURE__ */ jsxRuntime.jsx(
1527
+ reactNative.TextInput,
1528
+ {
1529
+ value: text,
1530
+ onChangeText: setText,
1531
+ placeholder: props.placeholder,
1532
+ placeholderTextColor: colors.textMuted,
1533
+ accessibilityLabel: props.placeholder,
1534
+ multiline: true,
1535
+ maxLength: conversation.MAX_TEXT * 2,
1536
+ testID: "vinaverse-composer-input",
1537
+ style: {
1538
+ flex: 1,
1539
+ minHeight: size2.touchTarget,
1540
+ maxHeight: 140,
1541
+ paddingHorizontal: space.md,
1542
+ paddingVertical: space.sm,
1543
+ borderRadius: radius.lg,
1544
+ borderWidth: 1,
1545
+ borderColor: colors.border,
1546
+ color: colors.text,
1547
+ backgroundColor: colors.surface,
1548
+ fontSize: font.size.md,
1549
+ textAlign: direction === "rtl" ? "right" : "left",
1550
+ writingDirection: direction,
1551
+ ...font.family ? { fontFamily: font.family } : {}
1552
+ }
1553
+ }
1554
+ ),
1555
+ props.responding && !text ? /* @__PURE__ */ jsxRuntime.jsx(
1556
+ Button,
1557
+ {
1558
+ label: t("stop"),
1559
+ onPress: props.onStop,
1560
+ testID: "vinaverse-stop"
1561
+ }
1562
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1563
+ Button,
1564
+ {
1565
+ label: t("send"),
1566
+ variant: "primary",
1567
+ disabled: !canSend,
1568
+ onPress: send,
1569
+ testID: "vinaverse-send"
1570
+ }
1571
+ )
1572
+ ]
1573
+ }
1574
+ );
1575
+ }
1576
+ var Composer = /* @__PURE__ */ react.memo(function Composer2(props) {
1577
+ const { store, slots } = useChat();
1578
+ const { t } = react$1.useCopy();
1579
+ const takeover = react$1.useConversationSelector(store, (v) => v.takeover);
1580
+ const name = react$1.useConversationSelector(store, (v) => v.agent.name);
1581
+ const responding = react$1.useConversationSelector(store, (v) => v.responding);
1582
+ const { onError } = props;
1583
+ const onSend = react.useCallback(
1584
+ (text) => {
1585
+ try {
1586
+ store.send(text);
1587
+ } catch (error) {
1588
+ onError?.(error);
1589
+ }
1590
+ },
1591
+ [store, onError]
1592
+ );
1593
+ const onStop = react.useCallback(() => store.cancel(), [store]);
1594
+ const placeholder = takeover ? t("composerPlaceholderTakeover", {
1595
+ by: takeover.by || t("staffFallbackRole")
1596
+ }) : t("composerPlaceholder", { agent: name ?? t("agentFallbackName") });
1597
+ const Slot = slots.Composer;
1598
+ const own = { placeholder, responding, onSend, onStop };
1599
+ return Slot ? /* @__PURE__ */ jsxRuntime.jsx(Slot, { ...own, Default: DefaultComposer }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultComposer, { ...own });
1600
+ });
1601
+ var DisclosureBar = /* @__PURE__ */ react.memo(function DisclosureBar2() {
1602
+ const { store } = useChat();
1603
+ const { t } = react$1.useCopy();
1604
+ const theme = chunk6QYDPGRS_cjs.useTheme();
1605
+ const { space, font } = theme;
1606
+ const disclosure = react$1.useConversationSelector(store, (v) => v.disclosure);
1607
+ const [open, setOpen] = react.useState(false);
1608
+ const legible = tokens.contrastRatio(
1609
+ theme.colors.disclosureText,
1610
+ theme.colors.disclosureBackground
1611
+ ) >= 4.5;
1612
+ const pair = legible ? theme.colors : tokens.colors(theme.scheme);
1613
+ const text = disclosure ?? t("disclosureFallback");
1614
+ return /* @__PURE__ */ jsxRuntime.jsx(
1615
+ reactNative.Pressable,
1616
+ {
1617
+ accessibilityRole: "text",
1618
+ accessibilityLabel: text,
1619
+ onPress: () => setOpen((o) => !o),
1620
+ testID: "vinaverse-disclosure",
1621
+ style: {
1622
+ backgroundColor: pair.disclosureBackground,
1623
+ paddingHorizontal: space.lg,
1624
+ paddingVertical: space.xs
1625
+ },
1626
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1627
+ reactNative.Text,
1628
+ {
1629
+ numberOfLines: open ? void 0 : 2,
1630
+ style: {
1631
+ color: pair.disclosureText,
1632
+ // never below the disclosure's minimum, whatever the tenant's type scale
1633
+ fontSize: Math.max(font.size.xs, tokens.size.disclosureFontMin),
1634
+ lineHeight: Math.max(font.size.xs, tokens.size.disclosureFontMin) * font.lineHeight
1635
+ },
1636
+ children: text
1637
+ }
1638
+ )
1639
+ }
1640
+ );
1641
+ });
1642
+ var ChatSkeleton = /* @__PURE__ */ react.memo(function ChatSkeleton2() {
1643
+ const { colors, radius, space } = chunk6QYDPGRS_cjs.useTheme();
1644
+ const row = (width, end) => /* @__PURE__ */ jsxRuntime.jsx(
1645
+ reactNative.View,
1646
+ {
1647
+ style: {
1648
+ alignSelf: end ? "flex-end" : "flex-start",
1649
+ width,
1650
+ height: 40,
1651
+ borderRadius: radius.lg,
1652
+ backgroundColor: colors.surface,
1653
+ marginHorizontal: space.lg,
1654
+ marginTop: space.md
1655
+ }
1656
+ }
1657
+ );
1658
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1659
+ reactNative.View,
1660
+ {
1661
+ accessible: true,
1662
+ accessibilityRole: "progressbar",
1663
+ accessibilityState: { busy: true },
1664
+ testID: "vinaverse-skeleton",
1665
+ children: [
1666
+ row("62%", false),
1667
+ row("45%", true),
1668
+ row("70%", false)
1669
+ ]
1670
+ }
1671
+ );
1672
+ });
1673
+ function DefaultBubble(props) {
1674
+ const { colors, radius, space, font } = chunk6QYDPGRS_cjs.useTheme();
1675
+ const role = props.item.role;
1676
+ const [background, foreground] = role === "person" ? [colors.personBubble, colors.personText] : role === "staff" ? [colors.staffBubble, colors.staffText] : [colors.agentBubble, colors.agentText];
1677
+ return /* @__PURE__ */ jsxRuntime.jsx(
1678
+ reactNative.View,
1679
+ {
1680
+ style: {
1681
+ backgroundColor: background,
1682
+ borderRadius: radius.lg,
1683
+ paddingHorizontal: space.md,
1684
+ paddingVertical: space.sm
1685
+ },
1686
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1687
+ reactNative.Text,
1688
+ {
1689
+ selectable: true,
1690
+ style: {
1691
+ color: foreground,
1692
+ fontSize: font.size.md,
1693
+ lineHeight: font.size.md * font.lineHeight,
1694
+ ...font.family ? { fontFamily: font.family } : {}
1695
+ },
1696
+ children: props.text
1697
+ }
1698
+ )
1699
+ }
1700
+ );
1701
+ }
1702
+ function useAuthor(item) {
1703
+ const { t } = react$1.useCopy();
1704
+ const { store } = useChat();
1705
+ const name = store.getSnapshot().agent.name ?? t("agentFallbackName");
1706
+ const by = store.getSnapshot().takeover?.by;
1707
+ if (item.role === "person") return t("you");
1708
+ if (item.role === "staff")
1709
+ return t("staffLabel", { by: by || t("staffFallbackRole") });
1710
+ return `${name} \xB7 ${t("agentLabel")}`;
1711
+ }
1712
+ var MessageBubble = /* @__PURE__ */ react.memo(
1713
+ function MessageBubble2(props) {
1714
+ const { item } = props;
1715
+ return /* @__PURE__ */ jsxRuntime.jsx(Frame, { item, text: item.text, showAuthor: props.showAuthor });
1716
+ }
1717
+ );
1718
+ var StreamingBubble = /* @__PURE__ */ react.memo(
1719
+ function StreamingBubble2(props) {
1720
+ const { store } = useChat();
1721
+ const text = react$1.useStreamingText(store, props.item.key) ?? "";
1722
+ return /* @__PURE__ */ jsxRuntime.jsx(Frame, { item: props.item, text, showAuthor: props.showAuthor });
1723
+ }
1724
+ );
1725
+ function Frame(props) {
1726
+ const { item, text } = props;
1727
+ const { store, slots } = useChat();
1728
+ const { t } = react$1.useCopy();
1729
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
1730
+ const author = useAuthor(item);
1731
+ const mine = item.role === "person";
1732
+ 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") : null;
1733
+ const Bubble = slots.MessageBubble;
1734
+ const bubble = Bubble ? /* @__PURE__ */ jsxRuntime.jsx(Bubble, { item, text, Default: DefaultBubble }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultBubble, { item, text });
1735
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1736
+ reactNative.View,
1737
+ {
1738
+ accessible: true,
1739
+ accessibilityLabel: [author, text, status].filter(Boolean).join(". "),
1740
+ style: {
1741
+ alignSelf: mine ? "flex-end" : "flex-start",
1742
+ maxWidth: "85%",
1743
+ marginHorizontal: space.lg,
1744
+ marginTop: props.showAuthor ? space.md : space.xs,
1745
+ opacity: item.state === "sending" || item.state === "held" ? 0.75 : 1
1746
+ },
1747
+ children: [
1748
+ props.showAuthor && !mine ? /* @__PURE__ */ jsxRuntime.jsx(
1749
+ reactNative.Text,
1750
+ {
1751
+ style: {
1752
+ color: item.role === "staff" ? colors.staffAccent : colors.textMuted,
1753
+ fontSize: font.size.xs,
1754
+ marginBottom: space.xxs
1755
+ },
1756
+ children: author
1757
+ }
1758
+ ) : null,
1759
+ bubble,
1760
+ status ? /* @__PURE__ */ jsxRuntime.jsxs(
1761
+ reactNative.View,
1762
+ {
1763
+ style: {
1764
+ flexDirection: "row",
1765
+ alignItems: "center",
1766
+ justifyContent: mine ? "flex-end" : "flex-start",
1767
+ gap: space.xs,
1768
+ marginTop: space.xxs
1769
+ },
1770
+ children: [
1771
+ /* @__PURE__ */ jsxRuntime.jsx(
1772
+ reactNative.Text,
1773
+ {
1774
+ style: {
1775
+ color: item.state === "not_sent" ? colors.danger : colors.textMuted,
1776
+ fontSize: font.size.xs
1777
+ },
1778
+ children: status
1779
+ }
1780
+ ),
1781
+ item.state === "not_sent" ? /* @__PURE__ */ jsxRuntime.jsx(
1782
+ Button,
1783
+ {
1784
+ label: t("tryAgain"),
1785
+ variant: "text",
1786
+ onPress: () => {
1787
+ store.retry(item.key);
1788
+ }
1789
+ }
1790
+ ) : null
1791
+ ]
1792
+ }
1793
+ ) : null
1794
+ ]
1795
+ }
1796
+ );
1797
+ }
1798
+ function FailureActionButton(props) {
1799
+ const { handlers, failureAction } = useChat();
1800
+ const { t } = react$1.useCopy();
1801
+ if (!failureAction) return null;
1802
+ return /* @__PURE__ */ jsxRuntime.jsx(
1803
+ Button,
1804
+ {
1805
+ label: failureAction.label ?? t("talkToPerson"),
1806
+ variant: "text",
1807
+ onPress: () => handlers.current.failureAction?.onPress(props.state)
1808
+ }
1809
+ );
1810
+ }
1811
+ function FailureSlot(props) {
1812
+ const { slot: Slot, state, Default } = props;
1813
+ return Slot ? /* @__PURE__ */ jsxRuntime.jsx(Slot, { state, Default }) : /* @__PURE__ */ jsxRuntime.jsx(Default, { state });
1814
+ }
1815
+ var SystemRow = /* @__PURE__ */ react.memo(function SystemRow2(props) {
1816
+ const { t } = react$1.useCopy();
1817
+ const { store, slots } = useChat();
1818
+ const conversationId = react$1.useConversationSelector(
1819
+ store,
1820
+ (v) => v.conversationId
1821
+ );
1822
+ const { event, data } = props.item;
1823
+ if (event === "turn_failed") {
1824
+ const key = props.item.key;
1825
+ const state = {
1826
+ kind: "turn_failed",
1827
+ reason: typeof data.code === "string" ? data.code : null,
1828
+ retry: data.retry === true ? () => void store.retryTurn(key) : void 0,
1829
+ conversationId
1830
+ };
1831
+ return /* @__PURE__ */ jsxRuntime.jsx(
1832
+ FailureSlot,
1833
+ {
1834
+ slot: slots.Error,
1835
+ state,
1836
+ Default: DefaultTurnFailed
1837
+ }
1838
+ );
1839
+ }
1840
+ const agent = store.getSnapshot().agent.name ?? t("agentFallbackName");
1841
+ const text = (() => {
1842
+ switch (event) {
1843
+ case "takeover_started":
1844
+ return t("takeoverStarted", {
1845
+ by: data.by || t("staffFallbackRole")
1846
+ });
1847
+ case "takeover_ended":
1848
+ return t("takeoverEnded", { agent });
1849
+ case "handoff_created":
1850
+ return data.expected_response ? t("handoffCreated", { expected: data.expected_response }) : t("handoffCreatedNoTime");
1851
+ case "flow_completed":
1852
+ return data.result === "ok" ? t("flowSent") : data.result === "failed" ? t("flowNotSent") : null;
1853
+ default:
1854
+ return null;
1855
+ }
1856
+ })();
1857
+ if (!text) return null;
1858
+ return /* @__PURE__ */ jsxRuntime.jsx(Line, { text });
1859
+ });
1860
+ function DefaultTurnFailed(props) {
1861
+ const { t } = react$1.useCopy();
1862
+ const { failureAction } = useChat();
1863
+ const text = t("turnFailed");
1864
+ if (!failureAction) return /* @__PURE__ */ jsxRuntime.jsx(Line, { text, danger: true });
1865
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { alignItems: "center" }, children: [
1866
+ /* @__PURE__ */ jsxRuntime.jsx(Line, { text, danger: true }),
1867
+ /* @__PURE__ */ jsxRuntime.jsx(FailureActionButton, { state: props.state })
1868
+ ] });
1869
+ }
1870
+ function Line(props) {
1871
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
1872
+ return /* @__PURE__ */ jsxRuntime.jsx(
1873
+ reactNative.View,
1874
+ {
1875
+ accessible: true,
1876
+ accessibilityLabel: props.text,
1877
+ style: {
1878
+ alignItems: "center",
1879
+ paddingHorizontal: space.xl,
1880
+ paddingVertical: space.sm
1881
+ },
1882
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1883
+ reactNative.Text,
1884
+ {
1885
+ style: {
1886
+ color: props.danger ? colors.danger : colors.textMuted,
1887
+ fontSize: font.size.sm,
1888
+ textAlign: "center"
1889
+ },
1890
+ children: props.text
1891
+ }
1892
+ )
1893
+ }
1894
+ );
1895
+ }
1896
+ var Welcome = /* @__PURE__ */ react.memo(function Welcome2(props) {
1897
+ const { colors, radius, space, font } = chunk6QYDPGRS_cjs.useTheme();
1898
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1899
+ reactNative.View,
1900
+ {
1901
+ style: { padding: space.lg, gap: space.md },
1902
+ testID: "vinaverse-welcome",
1903
+ children: [
1904
+ /* @__PURE__ */ jsxRuntime.jsx(
1905
+ reactNative.Text,
1906
+ {
1907
+ style: {
1908
+ alignSelf: "flex-start",
1909
+ maxWidth: "85%",
1910
+ color: colors.agentText,
1911
+ backgroundColor: colors.agentBubble,
1912
+ borderRadius: radius.lg,
1913
+ paddingHorizontal: space.md,
1914
+ paddingVertical: space.sm,
1915
+ fontSize: font.size.md,
1916
+ lineHeight: font.size.md * font.lineHeight
1917
+ },
1918
+ children: props.welcome.text
1919
+ }
1920
+ ),
1921
+ 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(
1922
+ Button,
1923
+ {
1924
+ label: chip.label,
1925
+ onPress: () => props.onChip(chip.message)
1926
+ },
1927
+ chip.label
1928
+ )) }) : null
1929
+ ]
1930
+ }
1931
+ );
1932
+ });
1933
+ var keyOf2 = (item) => item.key;
1934
+ function itemType(item) {
1935
+ if (item.type === "card") return `card:${item.card.kind}`;
1936
+ if (item.type === "message")
1937
+ return item.state === "streaming" ? "streaming" : `message:${item.role}`;
1938
+ return "system";
1939
+ }
1940
+ function authorOf(item) {
1941
+ return item?.type === "message" ? item.role : null;
1942
+ }
1943
+ function carousels(items, previous) {
1944
+ const groups = /* @__PURE__ */ new Map();
1945
+ for (const item of items) {
1946
+ if (item.type !== "card") continue;
1947
+ const group = item.card.card.group;
1948
+ if (group?.layout !== "carousel" || typeof group.id !== "string") continue;
1949
+ const id = `${item.card.turnId ?? ""}\0${group.id}`;
1950
+ const members = groups.get(id);
1951
+ if (members) members.push(item);
1952
+ else groups.set(id, [item]);
1953
+ }
1954
+ const runs = /* @__PURE__ */ new Map();
1955
+ const inside = /* @__PURE__ */ new Set();
1956
+ for (const run of groups.values()) {
1957
+ if (run.length < 2) continue;
1958
+ const key = run[0]?.key;
1959
+ const before = previous.get(key);
1960
+ runs.set(
1961
+ key,
1962
+ before && before.length === run.length && before.every((c, n) => c === run[n]) ? before : run
1963
+ );
1964
+ for (const card of run.slice(1)) inside.add(card.key);
1965
+ }
1966
+ return { runs, inside };
1967
+ }
1968
+ var Row = /* @__PURE__ */ react.memo(function Row2(props) {
1969
+ const { item } = props;
1970
+ const { slots } = useChat();
1971
+ const Group = slots.CardGroup;
1972
+ if (props.inside) return null;
1973
+ if (props.run && Group) return /* @__PURE__ */ jsxRuntime.jsx(Group, { items: props.run });
1974
+ if (item.type === "card") return /* @__PURE__ */ jsxRuntime.jsx(CardView, { item });
1975
+ if (item.type === "system") return /* @__PURE__ */ jsxRuntime.jsx(SystemRow, { item });
1976
+ return item.state === "streaming" ? /* @__PURE__ */ jsxRuntime.jsx(StreamingBubble, { item, showAuthor: props.showAuthor }) : /* @__PURE__ */ jsxRuntime.jsx(MessageBubble, { item, showAuthor: props.showAuthor });
1977
+ });
1978
+ var MessageList = /* @__PURE__ */ react.memo(function MessageList2() {
1979
+ const { store, slots, starters, handlers, imagePolicy } = useChat();
1980
+ const { t } = react$1.useCopy();
1981
+ const { space } = chunk6QYDPGRS_cjs.useTheme();
1982
+ const reducedMotion = chunk6QYDPGRS_cjs.useReducedMotion();
1983
+ const items = react$1.useConversationSelector(store, (v) => v.items);
1984
+ usePrefetchPhotos(items, handlers.current?.prefetchImage, imagePolicy?.hosts);
1985
+ const opening = react$1.useConversationSelector(
1986
+ store,
1987
+ (v) => v.status === "starting" || v.status === "connecting"
1988
+ );
1989
+ const agentName = react$1.useConversationSelector(store, (v) => v.agent.name);
1990
+ const served = react$1.useConversationSelector(store, (v) => v.welcome);
1991
+ const welcome = react.useMemo(
1992
+ () => served ?? (starters?.length ? {
1993
+ text: t("welcomeFallback", {
1994
+ agent: agentName ?? t("agentFallbackName")
1995
+ }),
1996
+ chips: [...starters]
1997
+ } : null),
1998
+ [served, starters, t, agentName]
1999
+ );
2000
+ useAnnounceReplies(items);
2001
+ const Group = slots.CardGroup;
2002
+ const runsRef = react.useRef(/* @__PURE__ */ new Map());
2003
+ const grouping = react.useMemo(
2004
+ () => Group ? carousels(items, runsRef.current) : null,
2005
+ [items, Group]
2006
+ );
2007
+ runsRef.current = grouping?.runs ?? /* @__PURE__ */ new Map();
2008
+ const itemsRef = react.useRef(items);
2009
+ itemsRef.current = items;
2010
+ const groupingRef = react.useRef(grouping);
2011
+ groupingRef.current = grouping;
2012
+ const renderItem = react.useCallback(
2013
+ ({ item, index }) => /* @__PURE__ */ jsxRuntime.jsx(
2014
+ Row,
2015
+ {
2016
+ item,
2017
+ showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1]),
2018
+ run: groupingRef.current?.runs.get(item.key),
2019
+ inside: groupingRef.current?.inside.has(item.key) === true
2020
+ }
2021
+ ),
2022
+ []
2023
+ );
2024
+ const onChip = react.useCallback(
2025
+ (message) => {
2026
+ store.send(message);
2027
+ const chip = welcome?.chips.find((c) => c.message === message);
2028
+ if (chip) handlers.current?.onChip?.(chip);
2029
+ },
2030
+ [store, welcome, handlers]
2031
+ );
2032
+ const Empty = slots.EmptyState;
2033
+ const empty = opening ? /* @__PURE__ */ jsxRuntime.jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsxRuntime.jsx(
2034
+ Empty,
2035
+ {
2036
+ agentName: agentName ?? t("agentFallbackName"),
2037
+ welcome,
2038
+ onChip
2039
+ }
2040
+ ) : welcome ? /* @__PURE__ */ jsxRuntime.jsx(Welcome, { welcome, onChip }) : null;
2041
+ const props = react.useMemo(
2042
+ () => ({
2043
+ data: items,
2044
+ renderItem,
2045
+ keyExtractor: keyOf2,
2046
+ getItemType: itemType,
2047
+ maintainVisibleContentPosition: {
2048
+ startRenderingFromBottom: true,
2049
+ autoscrollToBottomThreshold: 0.2,
2050
+ animateAutoScrollToBottom: !reducedMotion
2051
+ },
2052
+ ListFooterComponent: /* @__PURE__ */ jsxRuntime.jsx(ActivityLine, {}),
2053
+ ListEmptyComponent: empty,
2054
+ keyboardShouldPersistTaps: "handled",
2055
+ contentContainerStyle: { paddingVertical: space.sm }
2056
+ }),
2057
+ [items, renderItem, reducedMotion, empty, space.sm]
2058
+ );
2059
+ const List = slots.ListComponent;
2060
+ return List ? /* @__PURE__ */ jsxRuntime.jsx(List, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(flashList.FlashList, { ...props });
2061
+ });
2062
+ function usePrefetchPhotos(items, prefetch, hosts) {
2063
+ const done = react.useRef(/* @__PURE__ */ new Set());
2064
+ react.useEffect(() => {
2065
+ if (!prefetch) return;
2066
+ for (const item of items) {
2067
+ if (item.type !== "card") continue;
2068
+ const card = item.card.card;
2069
+ const url = allowedImage(card.images?.[0]?.url ?? card.image_url, hosts);
2070
+ if (url && !done.current.has(url)) {
2071
+ done.current.add(url);
2072
+ prefetch(url);
2073
+ }
2074
+ }
2075
+ }, [items, prefetch, hosts]);
2076
+ }
2077
+ function useAnnounceReplies(items) {
2078
+ const seen = react.useRef(void 0);
2079
+ const { t } = react$1.useCopy();
2080
+ const { store, handlers } = useChat();
2081
+ react.useEffect(() => {
2082
+ const last = items.at(-1);
2083
+ if (seen.current === void 0) {
2084
+ seen.current = last?.key ?? "";
2085
+ return;
2086
+ }
2087
+ if (last?.type !== "message" || last.role === "person") return;
2088
+ if (last.state !== "sent" || last.key === seen.current) return;
2089
+ seen.current = last.key;
2090
+ const author = last.role === "staff" ? t("staffLabel", {
2091
+ by: store.getSnapshot().takeover?.by || t("staffFallbackRole")
2092
+ }) : store.getSnapshot().agent.name ?? t("agentFallbackName");
2093
+ (handlers.current?.announce ?? chunk6QYDPGRS_cjs.announce)(`${author}: ${last.text}`);
2094
+ }, [items, t, store, handlers]);
2095
+ }
2096
+ var CONNECTING_DELAY = tokens.motion.bannerDelay;
2097
+ var StatusBanner = /* @__PURE__ */ react.memo(function StatusBanner2() {
2098
+ const { store, slots } = useChat();
2099
+ const { t } = react$1.useCopy();
2100
+ const banner = react$1.useConversationSelector(store, (v) => v.banner);
2101
+ const status = react$1.useConversationSelector(store, (v) => v.status);
2102
+ const conversationId = react$1.useConversationSelector(
2103
+ store,
2104
+ (v) => v.conversationId
2105
+ );
2106
+ const unavailable = react.useMemo(
2107
+ () => ({
2108
+ kind: "unavailable",
2109
+ reason: status,
2110
+ retry: () => store.start(),
2111
+ conversationId
2112
+ }),
2113
+ [store, status, conversationId]
2114
+ );
2115
+ const [late, setLate] = react.useState(false);
2116
+ react.useEffect(() => {
2117
+ setLate(false);
2118
+ if (banner !== "connecting") return;
2119
+ const timer = setTimeout(() => setLate(true), CONNECTING_DELAY);
2120
+ return () => clearTimeout(timer);
2121
+ }, [banner]);
2122
+ if (banner === null || banner === "connecting" && !late) return null;
2123
+ if (banner === "unavailable")
2124
+ return /* @__PURE__ */ jsxRuntime.jsx(
2125
+ FailureSlot,
2126
+ {
2127
+ slot: slots.Unavailable,
2128
+ state: unavailable,
2129
+ Default: DefaultUnavailable
2130
+ }
2131
+ );
2132
+ const text = {
2133
+ connecting: t("bannerConnecting"),
2134
+ reconnecting: t("bannerReconnecting"),
2135
+ offline: t("bannerOffline"),
2136
+ continued_elsewhere: t("bannerContinuedElsewhere")
2137
+ }[banner];
2138
+ const action = banner === "continued_elsewhere" ? /* @__PURE__ */ jsxRuntime.jsx(
2139
+ Button,
2140
+ {
2141
+ label: t("continueHere"),
2142
+ variant: "text",
2143
+ onPress: () => void store.continueHere()
2144
+ }
2145
+ ) : banner === "offline" ? /* @__PURE__ */ jsxRuntime.jsx(
2146
+ Button,
2147
+ {
2148
+ label: t("tryAgain"),
2149
+ variant: "text",
2150
+ onPress: () => store.start()
2151
+ }
2152
+ ) : null;
2153
+ return /* @__PURE__ */ jsxRuntime.jsx(Banner, { text, children: action });
2154
+ });
2155
+ function DefaultUnavailable(props) {
2156
+ const { t } = react$1.useCopy();
2157
+ const { retry } = props.state;
2158
+ return /* @__PURE__ */ jsxRuntime.jsxs(Banner, { text: t("bannerUnavailable"), children: [
2159
+ retry ? /* @__PURE__ */ jsxRuntime.jsx(Button, { label: t("tryAgain"), variant: "text", onPress: retry }) : null,
2160
+ /* @__PURE__ */ jsxRuntime.jsx(FailureActionButton, { state: props.state })
2161
+ ] });
2162
+ }
2163
+ function Banner(props) {
2164
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
2165
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2166
+ reactNative.View,
2167
+ {
2168
+ accessibilityLiveRegion: "polite",
2169
+ accessibilityRole: "alert",
2170
+ style: {
2171
+ flexDirection: "row",
2172
+ alignItems: "center",
2173
+ justifyContent: "space-between",
2174
+ gap: space.sm,
2175
+ paddingHorizontal: space.lg,
2176
+ paddingVertical: space.xs,
2177
+ backgroundColor: colors.surface,
2178
+ borderBottomWidth: 1,
2179
+ borderBottomColor: colors.divider
2180
+ },
2181
+ children: [
2182
+ /* @__PURE__ */ jsxRuntime.jsx(
2183
+ reactNative.Text,
2184
+ {
2185
+ style: { color: colors.text, fontSize: font.size.sm, flexShrink: 1 },
2186
+ children: props.text
2187
+ }
2188
+ ),
2189
+ props.children
2190
+ ]
2191
+ }
2192
+ );
2193
+ }
2194
+ function VinaverseChatProvider(props) {
2195
+ const {
2196
+ conversation,
2197
+ components,
2198
+ agentAvatar,
2199
+ timeZone,
2200
+ starters,
2201
+ imagePolicy
2202
+ } = props;
2203
+ const handlers = react.useRef(props);
2204
+ handlers.current = props;
2205
+ const slots = useStableSlots(components);
2206
+ const hasAction = props.failureAction !== void 0;
2207
+ const actionLabel = props.failureAction?.label;
2208
+ const failureAction = react.useMemo(
2209
+ () => hasAction ? { label: actionLabel } : null,
2210
+ [hasAction, actionLabel]
2211
+ );
2212
+ const value = react.useMemo(
2213
+ () => ({
2214
+ store: conversation,
2215
+ handlers,
2216
+ slots,
2217
+ agentAvatar,
2218
+ timeZone,
2219
+ failureAction,
2220
+ starters,
2221
+ imagePolicy
2222
+ }),
2223
+ [
2224
+ conversation,
2225
+ slots,
2226
+ agentAvatar,
2227
+ timeZone,
2228
+ failureAction,
2229
+ starters,
2230
+ imagePolicy
2231
+ ]
2232
+ );
2233
+ chunk6QYDPGRS_cjs.useNativeLifecycle(conversation);
2234
+ react.useEffect(() => {
2235
+ conversation.start();
2236
+ }, [conversation]);
2237
+ const { onWarning, theme, copy } = props;
2238
+ react.useEffect(() => {
2239
+ if (!onWarning) return;
2240
+ for (const problem of chunk6QYDPGRS_cjs.themeProblems(theme))
2241
+ onWarning(`Vinaverse theme: ${problem}`, { code: "theme" });
2242
+ for (const key of react$1.bannedWordsIn(copy ?? {}))
2243
+ onWarning(
2244
+ `Vinaverse copy: "${key}" says assistant, bot or crew; the copy says agent.`,
2245
+ { code: "copy" }
2246
+ );
2247
+ }, [onWarning, theme, copy]);
2248
+ return /* @__PURE__ */ jsxRuntime.jsx(react$1.CopyProvider, { locale: props.locale, copy: props.copy, children: /* @__PURE__ */ jsxRuntime.jsx(chunk6QYDPGRS_cjs.ThemeProvider, { theme: props.theme, colorScheme: props.colorScheme, children: /* @__PURE__ */ jsxRuntime.jsx(ChatContextProvider, { value, children: props.children }) }) });
2249
+ }
2250
+ function VinaverseChat(props) {
2251
+ return /* @__PURE__ */ jsxRuntime.jsx(VinaverseChatProvider, { ...props, children: /* @__PURE__ */ jsxRuntime.jsx(
2252
+ Layout,
2253
+ {
2254
+ hideHeader: props.hideHeader === true,
2255
+ keyboardVerticalOffset: props.keyboardVerticalOffset ?? 0,
2256
+ keyboardBehavior: props.keyboardBehavior ?? (reactNative.Platform.OS === "ios" ? "padding" : "none"),
2257
+ style: props.style,
2258
+ onError: props.onWarning,
2259
+ suggestion: props.suggestion ?? null,
2260
+ suggestionTestID: props.suggestionTestID ?? "vinaverse-suggestion",
2261
+ footer: props.footer
2262
+ }
2263
+ ) });
2264
+ }
2265
+ function Layout(props) {
2266
+ const { colors } = chunk6QYDPGRS_cjs.useTheme();
2267
+ const { direction } = react$1.useCopy();
2268
+ const { onError } = props;
2269
+ const reportError = react.useMemo(
2270
+ () => onError ? (e) => onError(String(e), { code: "send_failed" }) : void 0,
2271
+ [onError]
2272
+ );
2273
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2274
+ reactNative.KeyboardAvoidingView,
2275
+ {
2276
+ ...props.keyboardBehavior === "none" ? { enabled: false } : { behavior: props.keyboardBehavior },
2277
+ keyboardVerticalOffset: props.keyboardVerticalOffset,
2278
+ style: [
2279
+ { flex: 1, backgroundColor: colors.background, direction },
2280
+ props.style
2281
+ ],
2282
+ children: [
2283
+ props.hideHeader ? null : /* @__PURE__ */ jsxRuntime.jsx(ChatHeader, {}),
2284
+ /* @__PURE__ */ jsxRuntime.jsx(DisclosureBar, {}),
2285
+ /* @__PURE__ */ jsxRuntime.jsx(StatusBanner, {}),
2286
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(MessageList, {}) }),
2287
+ props.suggestion ? /* @__PURE__ */ jsxRuntime.jsx(Suggestion, { chip: props.suggestion, testID: props.suggestionTestID }) : null,
2288
+ /* @__PURE__ */ jsxRuntime.jsx(Composer, { ...reportError ? { onError: reportError } : {} }),
2289
+ props.footer
2290
+ ]
2291
+ }
2292
+ );
2293
+ }
2294
+ function Suggestion(props) {
2295
+ const { store, handlers } = useChat();
2296
+ const { space } = chunk6QYDPGRS_cjs.useTheme();
2297
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { paddingHorizontal: space.lg, alignItems: "flex-start" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2298
+ Button,
2299
+ {
2300
+ label: props.chip.label,
2301
+ testID: props.testID,
2302
+ onPress: () => {
2303
+ store.send(props.chip.message);
2304
+ handlers.current?.onChip?.(props.chip);
2305
+ }
2306
+ }
2307
+ ) });
2308
+ }
2309
+ function useStableSlots(slots) {
2310
+ const held = react.useRef({});
2311
+ const next = slots ?? {};
2312
+ if (!sameSlots(held.current, next)) held.current = next;
2313
+ return held.current;
2314
+ }
2315
+ function sameSlots(a, b) {
2316
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
2317
+ for (const key of keys) {
2318
+ if (key === "cards") {
2319
+ const ca = a.cards ?? {};
2320
+ const cb = b.cards ?? {};
2321
+ const kinds = /* @__PURE__ */ new Set([...Object.keys(ca), ...Object.keys(cb)]);
2322
+ for (const kind of kinds) if (ca[kind] !== cb[kind]) return false;
2323
+ } else if (a[key] !== b[key]) return false;
2324
+ }
2325
+ return true;
2326
+ }
2327
+ function AgentChat(props) {
2328
+ const {
2329
+ agentKey,
2330
+ place,
2331
+ entryPoint,
2332
+ askAbout,
2333
+ onContact,
2334
+ onOpenPrivacy,
2335
+ components,
2336
+ agentName,
2337
+ opener,
2338
+ openerTestID,
2339
+ ...chat
2340
+ } = props;
2341
+ const runtime = agent.useAgentRuntime();
2342
+ chunk6QYDPGRS_cjs.useAgentSweep(runtime);
2343
+ const { conversation, state, startNew } = agent.useAgentConversation(agentKey, {
2344
+ place: place ?? null,
2345
+ entryPoint: entryPoint ?? null
2346
+ });
2347
+ const store = conversation.store;
2348
+ const spoke = react$1.useConversationSelector(
2349
+ store,
2350
+ (v) => v.items.some((i) => i.type === "message" && i.role === "person")
2351
+ );
2352
+ const { Ended, Unreachable, UnavailableScreen, Footer, ...slots } = components ?? {};
2353
+ const copy = react.useMemo(
2354
+ () => agentName ? { ...props.copy, agentFallbackName: agentName } : props.copy,
2355
+ [agentName, props.copy]
2356
+ );
2357
+ const events = {
2358
+ onOpenTarget: chat.onOpenTarget ? (target, card) => {
2359
+ runtime.emit({
2360
+ type: "target_opened",
2361
+ key: conversation.key,
2362
+ cardId: card.id,
2363
+ kind: target.type
2364
+ });
2365
+ chat.onOpenTarget?.(target, card);
2366
+ } : void 0,
2367
+ onOpenLink: chat.onOpenLink ? (link, card) => {
2368
+ runtime.emit({
2369
+ type: "link_opened",
2370
+ key: conversation.key,
2371
+ cardId: card.id,
2372
+ kind: card.kind
2373
+ });
2374
+ chat.onOpenLink?.(link, card);
2375
+ } : void 0,
2376
+ onChip: (chip) => {
2377
+ runtime.emit({ type: "chip_tapped", key: conversation.key });
2378
+ chat.onChip?.(chip);
2379
+ }
2380
+ };
2381
+ const shared = {
2382
+ ...chat,
2383
+ ...events.onOpenTarget ? { onOpenTarget: events.onOpenTarget } : {},
2384
+ ...events.onOpenLink ? { onOpenLink: events.onOpenLink } : {},
2385
+ onChip: events.onChip,
2386
+ ...copy ? { copy } : {},
2387
+ ...onContact && !chat.failureAction ? { failureAction: { onPress: () => onContact() } } : {},
2388
+ conversation: store,
2389
+ components: slots
2390
+ };
2391
+ if (state.failure === "ended") {
2392
+ const own = {
2393
+ onStartNew: startNew,
2394
+ reason: state.reason,
2395
+ code: state.code,
2396
+ onContact
2397
+ };
2398
+ return /* @__PURE__ */ jsxRuntime.jsx(VinaverseChatProvider, { ...shared, children: /* @__PURE__ */ jsxRuntime.jsx(Centered, { children: Ended ? /* @__PURE__ */ jsxRuntime.jsx(Ended, { ...own, Default: DefaultEnded }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultEnded, { ...own }) }) });
2399
+ }
2400
+ if (state.failure === "unreachable") {
2401
+ const own = {
2402
+ onRetry: conversation.retry,
2403
+ reason: state.reason,
2404
+ onContact
2405
+ };
2406
+ return /* @__PURE__ */ jsxRuntime.jsx(VinaverseChatProvider, { ...shared, children: /* @__PURE__ */ jsxRuntime.jsx(Centered, { children: Unreachable ? /* @__PURE__ */ jsxRuntime.jsx(Unreachable, { ...own, Default: DefaultUnreachable }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultUnreachable, { ...own }) }) });
2407
+ }
2408
+ if (state.failure === "unavailable") {
2409
+ const own = {
2410
+ reason: state.reason,
2411
+ code: state.code,
2412
+ onContact,
2413
+ onRetry: conversation.retry
2414
+ };
2415
+ return /* @__PURE__ */ jsxRuntime.jsx(VinaverseChatProvider, { ...shared, children: /* @__PURE__ */ jsxRuntime.jsx(Centered, { children: UnavailableScreen ? /* @__PURE__ */ jsxRuntime.jsx(UnavailableScreen, { ...own, Default: DefaultUnavailableScreen }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultUnavailableScreen, { ...own }) }) });
2416
+ }
2417
+ const offered = opener && !spoke ? opener : null;
2418
+ return /* @__PURE__ */ jsxRuntime.jsx(
2419
+ VinaverseChat,
2420
+ {
2421
+ ...shared,
2422
+ suggestion: offered ?? (state.moved && askAbout ? { label: askAbout, message: askAbout } : null),
2423
+ ...offered ? { suggestionTestID: openerTestID ?? "vinaverse-opener" } : {},
2424
+ footer: Footer === null ? null : /* @__PURE__ */ jsxRuntime.jsx(
2425
+ FooterFor,
2426
+ {
2427
+ store,
2428
+ state,
2429
+ rate: conversation.rate,
2430
+ canRate: conversation.canRate,
2431
+ Footer,
2432
+ onOpenPrivacy
2433
+ }
2434
+ )
2435
+ }
2436
+ );
2437
+ }
2438
+ function FooterFor(props) {
2439
+ const newest = react$1.useConversationSelector(props.store, newestWholeReply);
2440
+ const turnId = newest && props.canRate(newest) ? newest : null;
2441
+ const own = {
2442
+ turnId,
2443
+ rating: turnId ? props.state.ratings.get(turnId) : void 0,
2444
+ onRate: (rating) => {
2445
+ if (turnId) props.rate(turnId, rating);
2446
+ },
2447
+ onOpenPrivacy: props.onOpenPrivacy
2448
+ };
2449
+ const { Footer } = props;
2450
+ return Footer ? /* @__PURE__ */ jsxRuntime.jsx(Footer, { ...own, Default: AgentFooter }) : /* @__PURE__ */ jsxRuntime.jsx(AgentFooter, { ...own });
2451
+ }
2452
+ function newestWholeReply(view) {
2453
+ for (let i = view.items.length - 1; i >= 0; i--) {
2454
+ const item = view.items[i];
2455
+ if (item?.type !== "message" || item.role !== "agent") continue;
2456
+ const whole = item.state === "sent" && !item.partial && !item.refusal && item.key.startsWith("a:");
2457
+ return whole ? item.key.slice(2) : null;
2458
+ }
2459
+ return null;
2460
+ }
2461
+ function Centered(props) {
2462
+ const { colors, space } = chunk6QYDPGRS_cjs.useTheme();
2463
+ return /* @__PURE__ */ jsxRuntime.jsx(
2464
+ reactNative.View,
2465
+ {
2466
+ style: {
2467
+ flex: 1,
2468
+ alignItems: "center",
2469
+ justifyContent: "center",
2470
+ padding: space.xl,
2471
+ backgroundColor: colors.background
2472
+ },
2473
+ children: props.children
2474
+ }
2475
+ );
2476
+ }
2477
+ function Notice(props) {
2478
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
2479
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2480
+ reactNative.View,
2481
+ {
2482
+ accessibilityRole: "alert",
2483
+ style: { alignItems: "center", gap: space.md },
2484
+ children: [
2485
+ /* @__PURE__ */ jsxRuntime.jsx(
2486
+ reactNative.Text,
2487
+ {
2488
+ style: {
2489
+ color: colors.text,
2490
+ fontSize: font.size.lg,
2491
+ fontWeight: "600",
2492
+ textAlign: "center"
2493
+ },
2494
+ children: props.title
2495
+ }
2496
+ ),
2497
+ /* @__PURE__ */ jsxRuntime.jsx(
2498
+ reactNative.Text,
2499
+ {
2500
+ style: {
2501
+ color: colors.textMuted,
2502
+ fontSize: font.size.md,
2503
+ textAlign: "center"
2504
+ },
2505
+ children: props.body
2506
+ }
2507
+ ),
2508
+ props.action && props.onPress ? /* @__PURE__ */ jsxRuntime.jsx(
2509
+ Button,
2510
+ {
2511
+ label: props.action,
2512
+ variant: "primary",
2513
+ onPress: props.onPress
2514
+ }
2515
+ ) : null
2516
+ ]
2517
+ }
2518
+ );
2519
+ }
2520
+ function DefaultEnded(props) {
2521
+ const { t } = react$1.useCopy();
2522
+ return /* @__PURE__ */ jsxRuntime.jsx(
2523
+ Notice,
2524
+ {
2525
+ title: t("agentEnded"),
2526
+ body: t("agentEndedBody"),
2527
+ action: t("agentStartOver"),
2528
+ onPress: props.onStartNew
2529
+ }
2530
+ );
2531
+ }
2532
+ function DefaultUnavailableScreen(props) {
2533
+ const { t } = react$1.useCopy();
2534
+ const name = agent.useAgentName(t("agentFallbackName"));
2535
+ const title = t("agentOff", { agent: name });
2536
+ return /* @__PURE__ */ jsxRuntime.jsx(
2537
+ Notice,
2538
+ {
2539
+ title: title.charAt(0).toUpperCase() + title.slice(1),
2540
+ body: t("agentOffBody"),
2541
+ ...props.onContact ? { action: t("talkToPerson"), onPress: props.onContact } : {}
2542
+ }
2543
+ );
2544
+ }
2545
+ function DefaultUnreachable(props) {
2546
+ const { t } = react$1.useCopy();
2547
+ const name = agent.useAgentName(t("agentFallbackName"));
2548
+ return /* @__PURE__ */ jsxRuntime.jsx(
2549
+ Notice,
2550
+ {
2551
+ title: t("agentUnreachable", { agent: name }),
2552
+ body: t("agentUnreachableBody"),
2553
+ action: t("tryAgain"),
2554
+ onPress: props.onRetry
2555
+ }
2556
+ );
2557
+ }
2558
+ function AgentFooter(props) {
2559
+ const { t } = react$1.useCopy();
2560
+ const { colors, space, font } = chunk6QYDPGRS_cjs.useTheme();
2561
+ const muted = { color: colors.textMuted, fontSize: font.size.sm };
2562
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2563
+ reactNative.View,
2564
+ {
2565
+ style: {
2566
+ flexDirection: "row",
2567
+ flexWrap: "wrap",
2568
+ alignItems: "center",
2569
+ gap: space.xs,
2570
+ paddingHorizontal: space.lg
2571
+ },
2572
+ children: [
2573
+ props.turnId === null ? null : props.rating ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: muted, children: t("helpfulThanks") }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2574
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: muted, children: t("helpful") }),
2575
+ /* @__PURE__ */ jsxRuntime.jsx(
2576
+ Button,
2577
+ {
2578
+ label: t("helpfulYes"),
2579
+ variant: "text",
2580
+ onPress: () => props.onRate("up")
2581
+ }
2582
+ ),
2583
+ /* @__PURE__ */ jsxRuntime.jsx(
2584
+ Button,
2585
+ {
2586
+ label: t("helpfulNo"),
2587
+ variant: "text",
2588
+ onPress: () => props.onRate("down")
2589
+ }
2590
+ )
2591
+ ] }),
2592
+ props.onOpenPrivacy ? /* @__PURE__ */ jsxRuntime.jsx(
2593
+ Button,
2594
+ {
2595
+ label: t("privacy"),
2596
+ variant: "text",
2597
+ role: "link",
2598
+ onPress: props.onOpenPrivacy
2599
+ }
2600
+ ) : null
2601
+ ]
2602
+ }
2603
+ );
2604
+ }
2605
+
2606
+ Object.defineProperty(exports, "AgentRuntimeProvider", {
2607
+ enumerable: true,
2608
+ get: function () { return agent.AgentRuntimeProvider; }
2609
+ });
2610
+ Object.defineProperty(exports, "forgetGreeting", {
2611
+ enumerable: true,
2612
+ get: function () { return agent.forgetGreeting; }
2613
+ });
2614
+ Object.defineProperty(exports, "greetedKey", {
2615
+ enumerable: true,
2616
+ get: function () { return agent.greetedKey; }
2617
+ });
2618
+ Object.defineProperty(exports, "useAgentConversation", {
2619
+ enumerable: true,
2620
+ get: function () { return agent.useAgentConversation; }
2621
+ });
2622
+ Object.defineProperty(exports, "useAgentFeatures", {
2623
+ enumerable: true,
2624
+ get: function () { return agent.useAgentFeatures; }
2625
+ });
2626
+ Object.defineProperty(exports, "useAgentLauncher", {
2627
+ enumerable: true,
2628
+ get: function () { return agent.useAgentLauncher; }
2629
+ });
2630
+ Object.defineProperty(exports, "useAgentName", {
2631
+ enumerable: true,
2632
+ get: function () { return agent.useAgentName; }
2633
+ });
2634
+ Object.defineProperty(exports, "useAgentProfile", {
2635
+ enumerable: true,
2636
+ get: function () { return agent.useAgentProfile; }
2637
+ });
2638
+ Object.defineProperty(exports, "useAgentRuntime", {
2639
+ enumerable: true,
2640
+ get: function () { return agent.useAgentRuntime; }
2641
+ });
2642
+ Object.defineProperty(exports, "useAgentStatus", {
2643
+ enumerable: true,
2644
+ get: function () { return agent.useAgentStatus; }
2645
+ });
2646
+ Object.defineProperty(exports, "useAgentStatusState", {
2647
+ enumerable: true,
2648
+ get: function () { return agent.useAgentStatusState; }
2649
+ });
2650
+ exports.ActivityLine = ActivityLine;
2651
+ exports.AgentChat = AgentChat;
2652
+ exports.AgentFooter = AgentFooter;
2653
+ exports.Avatar = Avatar;
2654
+ exports.Button = Button;
2655
+ exports.CardActions = CardActions;
2656
+ exports.CardCarousel = CardCarousel;
2657
+ exports.CardFrame = CardFrame;
2658
+ exports.CardView = CardView;
2659
+ exports.ChatHeader = ChatHeader;
2660
+ exports.ChatSkeleton = ChatSkeleton;
2661
+ exports.Composer = Composer;
2662
+ exports.DefaultBubble = DefaultBubble;
2663
+ exports.DefaultComposer = DefaultComposer;
2664
+ exports.DefaultEnded = DefaultEnded;
2665
+ exports.DefaultHeader = DefaultHeader;
2666
+ exports.DefaultTurnFailed = DefaultTurnFailed;
2667
+ exports.DefaultUnavailable = DefaultUnavailable;
2668
+ exports.DefaultUnavailableScreen = DefaultUnavailableScreen;
2669
+ exports.DefaultUnreachable = DefaultUnreachable;
2670
+ exports.DisclosureBar = DisclosureBar;
2671
+ exports.FailureActionButton = FailureActionButton;
2672
+ exports.InfoCard = InfoCard;
2673
+ exports.LinksCard = LinksCard;
2674
+ exports.ListingCard = ListingCard;
2675
+ exports.MessageBubble = MessageBubble;
2676
+ exports.MessageList = MessageList;
2677
+ exports.NextStepCard = NextStepCard;
2678
+ exports.ProposalCard = ProposalCard;
2679
+ exports.QuestionCard = QuestionCard;
2680
+ exports.Row = Row;
2681
+ exports.StatusBanner = StatusBanner;
2682
+ exports.StreamingBubble = StreamingBubble;
2683
+ exports.SystemRow = SystemRow;
2684
+ exports.TimeSlotsCard = TimeSlotsCard;
2685
+ exports.VinaverseChat = VinaverseChat;
2686
+ exports.VinaverseChatProvider = VinaverseChatProvider;
2687
+ exports.Welcome = Welcome;
2688
+ exports.allowedImage = allowedImage;
2689
+ exports.answerText = answerText;
2690
+ exports.carousels = carousels;
2691
+ exports.defaultCard = defaultCard;
2692
+ exports.fieldText = fieldText;
2693
+ exports.itemType = itemType;
2694
+ exports.monogram = monogram;
2695
+ exports.newestWholeReply = newestWholeReply;
2696
+ exports.ratingText = ratingText;
2697
+ exports.useChat = useChat;
2698
+ //# sourceMappingURL=chunk-NRYNK7E4.cjs.map
2699
+ //# sourceMappingURL=chunk-NRYNK7E4.cjs.map