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