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

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