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