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