@vinaverse/react-native 0.1.0-alpha.5 → 0.1.0-alpha.7

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