@vinaverse/react-native 0.1.0-alpha.4 → 0.1.0-alpha.6

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