@guideai/react-native 0.0.0-stage → 0.2.0

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/ui.mjs ADDED
@@ -0,0 +1,1525 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import React, { useRef, useState, useEffect, useCallback, useMemo, useContext, createContext } from "react";
3
+ import { View, StyleSheet, Text, ScrollView, Linking, KeyboardAvoidingView, Pressable, Alert, FlatList, ActivityIndicator, TextInput, Platform, StatusBar, Animated, Easing, Dimensions, PanResponder, Modal } from "react-native";
4
+ import { r as requireConfigured, b as useGuideAI, c as useOnboarding, E as EP } from "./useOnboarding-DAEGoYSs.js";
5
+ function parseInline(text) {
6
+ const spans = [];
7
+ const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)|(__[^_]+__)|(\*[^*\n]+\*)|(_[^_\n]+_)|(\[[^\]]+\]\([^)]+\))/g;
8
+ let cursor = 0;
9
+ let match;
10
+ while ((match = pattern.exec(text)) !== null) {
11
+ if (match.index > cursor) {
12
+ spans.push({ text: text.slice(cursor, match.index) });
13
+ }
14
+ const token = match[0];
15
+ if (token.startsWith("`")) {
16
+ spans.push({ text: token.slice(1, -1), code: true });
17
+ } else if (token.startsWith("**") || token.startsWith("__")) {
18
+ spans.push({ text: token.slice(2, -2), bold: true });
19
+ } else if (token.startsWith("[")) {
20
+ const split = token.indexOf("](");
21
+ spans.push({
22
+ text: token.slice(1, split),
23
+ href: token.slice(split + 2, -1)
24
+ });
25
+ } else {
26
+ spans.push({ text: token.slice(1, -1), italic: true });
27
+ }
28
+ cursor = match.index + token.length;
29
+ }
30
+ if (cursor < text.length) spans.push({ text: text.slice(cursor) });
31
+ return spans.length ? spans : [{ text }];
32
+ }
33
+ function tableCells(line) {
34
+ return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => parseInline(cell.trim()));
35
+ }
36
+ const TABLE_DIVIDER = /^\s*\|?[\s:-]*-[\s|:-]*\|?\s*$/;
37
+ function parseMarkdown(source) {
38
+ var _a, _b;
39
+ const blocks = [];
40
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
41
+ let index = 0;
42
+ while (index < lines.length) {
43
+ const line = lines[index];
44
+ const trimmed = line.trim();
45
+ if (!trimmed) {
46
+ index += 1;
47
+ continue;
48
+ }
49
+ const fence = /^```(\w+)?/.exec(trimmed);
50
+ if (fence) {
51
+ const language = fence[1];
52
+ const body = [];
53
+ index += 1;
54
+ while (index < lines.length && !lines[index].trim().startsWith("```")) {
55
+ body.push(lines[index]);
56
+ index += 1;
57
+ }
58
+ index += 1;
59
+ blocks.push({ type: "code", text: body.join("\n"), language });
60
+ continue;
61
+ }
62
+ if (trimmed.includes("|") && index + 1 < lines.length && TABLE_DIVIDER.test(lines[index + 1])) {
63
+ const header = tableCells(trimmed);
64
+ const rows = [];
65
+ index += 2;
66
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
67
+ rows.push(tableCells(lines[index]));
68
+ index += 1;
69
+ }
70
+ blocks.push({ type: "table", header, rows });
71
+ continue;
72
+ }
73
+ const heading = /^(#{1,6})\s+(.*)$/.exec(trimmed);
74
+ if (heading) {
75
+ const level = Math.min(heading[1].length, 3);
76
+ blocks.push({ type: "heading", level, spans: parseInline(heading[2]) });
77
+ index += 1;
78
+ continue;
79
+ }
80
+ if (/^(\*\s*){3,}$|^(-\s*){3,}$|^(_\s*){3,}$/.test(trimmed)) {
81
+ blocks.push({ type: "rule" });
82
+ index += 1;
83
+ continue;
84
+ }
85
+ const quote = /^>\s?(.*)$/.exec(trimmed);
86
+ if (quote) {
87
+ blocks.push({ type: "quote", spans: parseInline(quote[1]) });
88
+ index += 1;
89
+ continue;
90
+ }
91
+ const indent = (_b = (_a = /^(\s*)/.exec(line)) == null ? void 0 : _a[1].length) != null ? _b : 0;
92
+ const depth = Math.floor(indent / 2);
93
+ const bullet = /^[-*+]\s+(.*)$/.exec(trimmed);
94
+ if (bullet) {
95
+ blocks.push({ type: "bullet", spans: parseInline(bullet[1]), depth });
96
+ index += 1;
97
+ continue;
98
+ }
99
+ const ordered = /^(\d+)[.)]\s+(.*)$/.exec(trimmed);
100
+ if (ordered) {
101
+ blocks.push({
102
+ type: "ordered",
103
+ spans: parseInline(ordered[2]),
104
+ index: Number(ordered[1]),
105
+ depth
106
+ });
107
+ index += 1;
108
+ continue;
109
+ }
110
+ const paragraph = [trimmed];
111
+ index += 1;
112
+ while (index < lines.length) {
113
+ const nextLine = lines[index].trim();
114
+ if (!nextLine || /^(#{1,6})\s/.test(nextLine) || /^[-*+]\s/.test(nextLine) || /^\d+[.)]\s/.test(nextLine) || nextLine.startsWith("```") || nextLine.startsWith(">") || nextLine.includes("|")) break;
115
+ paragraph.push(nextLine);
116
+ index += 1;
117
+ }
118
+ blocks.push({ type: "paragraph", spans: parseInline(paragraph.join(" ")) });
119
+ }
120
+ return blocks;
121
+ }
122
+ function Spans({ spans, style, accentColor }) {
123
+ return /* @__PURE__ */ jsx(Fragment, { children: spans.map((span, index) => {
124
+ if (span.href) {
125
+ return /* @__PURE__ */ jsx(
126
+ Text,
127
+ {
128
+ style: [style, { color: accentColor, textDecorationLine: "underline" }],
129
+ onPress: () => {
130
+ void Linking.openURL(span.href);
131
+ },
132
+ accessibilityRole: "link",
133
+ children: span.text
134
+ },
135
+ index
136
+ );
137
+ }
138
+ return /* @__PURE__ */ jsx(
139
+ Text,
140
+ {
141
+ style: [
142
+ style,
143
+ span.bold ? styles$3.bold : null,
144
+ span.italic ? styles$3.italic : null,
145
+ span.code ? styles$3.inlineCode : null
146
+ ],
147
+ children: span.text
148
+ },
149
+ index
150
+ );
151
+ }) });
152
+ }
153
+ function Markdown({
154
+ children,
155
+ color = "#0f172a",
156
+ accentColor = "#2563eb",
157
+ fontSize = 15
158
+ }) {
159
+ const blocks = React.useMemo(() => parseMarkdown(children), [children]);
160
+ const body = { color, fontSize, lineHeight: fontSize * 1.45 };
161
+ return /* @__PURE__ */ jsx(View, { style: styles$3.container, children: blocks.map((block, index) => /* @__PURE__ */ jsx(
162
+ Block,
163
+ {
164
+ block,
165
+ body,
166
+ color,
167
+ accentColor,
168
+ fontSize
169
+ },
170
+ index
171
+ )) });
172
+ }
173
+ function Block({ block, body, color, accentColor, fontSize }) {
174
+ switch (block.type) {
175
+ case "heading": {
176
+ const sizes = { 1: fontSize + 6, 2: fontSize + 3, 3: fontSize + 1 };
177
+ return /* @__PURE__ */ jsx(Text, { style: [styles$3.heading, { color, fontSize: sizes[block.level] }], children: /* @__PURE__ */ jsx(
178
+ Spans,
179
+ {
180
+ spans: block.spans,
181
+ accentColor,
182
+ style: { color, fontSize: sizes[block.level], fontWeight: "700" }
183
+ }
184
+ ) });
185
+ }
186
+ case "paragraph":
187
+ return /* @__PURE__ */ jsx(Text, { style: styles$3.paragraph, children: /* @__PURE__ */ jsx(Spans, { spans: block.spans, style: body, accentColor }) });
188
+ case "bullet":
189
+ return /* @__PURE__ */ jsxs(View, { style: [styles$3.listRow, { marginLeft: block.depth * 14 }], children: [
190
+ /* @__PURE__ */ jsx(Text, { style: [body, { color: accentColor }], children: "•" }),
191
+ /* @__PURE__ */ jsx(Text, { style: styles$3.listText, children: /* @__PURE__ */ jsx(Spans, { spans: block.spans, style: body, accentColor }) })
192
+ ] });
193
+ case "ordered":
194
+ return /* @__PURE__ */ jsxs(View, { style: [styles$3.listRow, { marginLeft: block.depth * 14 }], children: [
195
+ /* @__PURE__ */ jsxs(Text, { style: [body, { color: accentColor, fontVariant: ["tabular-nums"] }], children: [
196
+ block.index,
197
+ "."
198
+ ] }),
199
+ /* @__PURE__ */ jsx(Text, { style: styles$3.listText, children: /* @__PURE__ */ jsx(Spans, { spans: block.spans, style: body, accentColor }) })
200
+ ] });
201
+ case "code":
202
+ return /* @__PURE__ */ jsx(View, { style: styles$3.codeBlock, children: /* @__PURE__ */ jsx(Text, { style: [styles$3.codeText, { fontSize: fontSize - 2 }], children: block.text }) });
203
+ case "quote":
204
+ return /* @__PURE__ */ jsx(View, { style: [styles$3.quote, { borderLeftColor: accentColor }], children: /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx(
205
+ Spans,
206
+ {
207
+ spans: block.spans,
208
+ accentColor,
209
+ style: { ...body, color: "#475569", fontStyle: "italic" }
210
+ }
211
+ ) }) });
212
+ case "rule":
213
+ return /* @__PURE__ */ jsx(View, { style: styles$3.rule });
214
+ case "table": {
215
+ const columns = Math.max(block.header.length, ...block.rows.map((r) => r.length), 1);
216
+ const scrolls = columns > 3;
217
+ const table = /* @__PURE__ */ jsxs(View, { style: [styles$3.table, scrolls ? { minWidth: columns * 116 } : null], children: [
218
+ /* @__PURE__ */ jsx(View, { style: [styles$3.tableRow, styles$3.tableHeaderRow], children: block.header.map((cell, index) => /* @__PURE__ */ jsx(View, { style: styles$3.tableCell, children: /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx(
219
+ Spans,
220
+ {
221
+ spans: cell,
222
+ accentColor,
223
+ style: { ...body, fontWeight: "700", fontSize: fontSize - 1 }
224
+ }
225
+ ) }) }, index)) }),
226
+ block.rows.map((row, rowIndex) => /* @__PURE__ */ jsx(View, { style: styles$3.tableRow, children: row.map((cell, cellIndex) => /* @__PURE__ */ jsx(View, { style: styles$3.tableCell, children: /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx(
227
+ Spans,
228
+ {
229
+ spans: cell,
230
+ accentColor,
231
+ style: { ...body, fontSize: fontSize - 1 }
232
+ }
233
+ ) }) }, cellIndex)) }, rowIndex))
234
+ ] });
235
+ return scrolls ? /* @__PURE__ */ jsx(
236
+ ScrollView,
237
+ {
238
+ horizontal: true,
239
+ showsHorizontalScrollIndicator: false,
240
+ alwaysBounceHorizontal: false,
241
+ children: table
242
+ }
243
+ ) : table;
244
+ }
245
+ default:
246
+ return null;
247
+ }
248
+ }
249
+ const mono = { fontFamily: "Menlo" };
250
+ const styles$3 = StyleSheet.create({
251
+ container: { gap: 6 },
252
+ heading: { fontWeight: "700", marginTop: 4 },
253
+ paragraph: {},
254
+ listRow: { flexDirection: "row", gap: 8, alignItems: "flex-start" },
255
+ listText: { flex: 1 },
256
+ bold: { fontWeight: "700" },
257
+ italic: { fontStyle: "italic" },
258
+ inlineCode: {
259
+ ...mono,
260
+ backgroundColor: "rgba(100,116,139,0.14)",
261
+ color: "#0f172a"
262
+ },
263
+ codeBlock: {
264
+ backgroundColor: "#0f172a",
265
+ borderRadius: 10,
266
+ padding: 12,
267
+ marginVertical: 2
268
+ },
269
+ codeText: { ...mono, color: "#e2e8f0" },
270
+ quote: { borderLeftWidth: 3, paddingLeft: 10, paddingVertical: 2 },
271
+ rule: { height: StyleSheet.hairlineWidth, backgroundColor: "#cbd5e1", marginVertical: 6 },
272
+ table: {
273
+ borderWidth: StyleSheet.hairlineWidth,
274
+ borderColor: "#cbd5e1",
275
+ borderRadius: 8,
276
+ overflow: "hidden"
277
+ },
278
+ tableRow: {
279
+ flexDirection: "row",
280
+ borderBottomWidth: StyleSheet.hairlineWidth,
281
+ borderBottomColor: "#e2e8f0"
282
+ },
283
+ tableHeaderRow: { backgroundColor: "rgba(100,116,139,0.10)" },
284
+ tableCell: { flex: 1, padding: 8 }
285
+ });
286
+ const darkTheme = {
287
+ background: "#0B0713",
288
+ surface: "rgba(255,255,255,0.06)",
289
+ surfaceRaised: "#1C1626",
290
+ text: "#F5F3FF",
291
+ textMuted: "#8B85A0",
292
+ accent: "#7C5CFF",
293
+ onAccent: "#FFFFFF",
294
+ userBubble: "#7C5CFF",
295
+ onUserBubble: "#FFFFFF",
296
+ error: "rgba(248,113,113,0.16)",
297
+ border: "rgba(255,255,255,0.10)",
298
+ radius: 22,
299
+ fontSize: 15
300
+ };
301
+ const lightTheme = {
302
+ background: "#FFFFFF",
303
+ surface: "#F1F5F9",
304
+ surfaceRaised: "#FFFFFF",
305
+ text: "#0F172A",
306
+ textMuted: "#64748B",
307
+ accent: "#7C5CFF",
308
+ onAccent: "#FFFFFF",
309
+ userBubble: "#7C5CFF",
310
+ onUserBubble: "#FFFFFF",
311
+ error: "#FEE2E2",
312
+ border: "#E2E8F0",
313
+ radius: 22,
314
+ fontSize: 15
315
+ };
316
+ function resolveTheme(overrides, base = lightTheme) {
317
+ return overrides ? { ...base, ...overrides } : base;
318
+ }
319
+ function PersonIcon({ color }) {
320
+ return /* @__PURE__ */ jsxs(View, { style: styles$2.iconWrap, children: [
321
+ /* @__PURE__ */ jsx(View, { style: [styles$2.iconBand, { borderColor: color }] }),
322
+ /* @__PURE__ */ jsx(View, { style: [styles$2.iconCup, styles$2.iconCupLeft, { backgroundColor: color }] }),
323
+ /* @__PURE__ */ jsx(View, { style: [styles$2.iconCup, styles$2.iconCupRight, { backgroundColor: color }] })
324
+ ] });
325
+ }
326
+ function Orb({ theme, active }) {
327
+ const pulse = useRef(new Animated.Value(0)).current;
328
+ React.useEffect(() => {
329
+ if (!active) {
330
+ pulse.stopAnimation();
331
+ Animated.timing(pulse, { toValue: 0, duration: 240, useNativeDriver: true }).start();
332
+ return;
333
+ }
334
+ const loop = Animated.loop(
335
+ Animated.sequence([
336
+ Animated.timing(pulse, {
337
+ toValue: 1,
338
+ duration: 900,
339
+ easing: Easing.inOut(Easing.quad),
340
+ useNativeDriver: true
341
+ }),
342
+ Animated.timing(pulse, {
343
+ toValue: 0,
344
+ duration: 900,
345
+ easing: Easing.inOut(Easing.quad),
346
+ useNativeDriver: true
347
+ })
348
+ ])
349
+ );
350
+ loop.start();
351
+ return () => loop.stop();
352
+ }, [active, pulse]);
353
+ const scale = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.18] });
354
+ const glow = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.35, 0.7] });
355
+ return /* @__PURE__ */ jsxs(View, { style: styles$2.orbWrap, children: [
356
+ /* @__PURE__ */ jsx(
357
+ Animated.View,
358
+ {
359
+ style: [
360
+ styles$2.orbGlow,
361
+ { backgroundColor: theme.accent, opacity: glow, transform: [{ scale }] }
362
+ ]
363
+ }
364
+ ),
365
+ /* @__PURE__ */ jsx(View, { style: [styles$2.orbCore, { backgroundColor: theme.accent }] })
366
+ ] });
367
+ }
368
+ function Bubble$1({ message, theme, onTalkToHuman }) {
369
+ if (message.role === "user") {
370
+ return /* @__PURE__ */ jsx(View, { style: [styles$2.row, styles$2.rowUser], children: /* @__PURE__ */ jsx(View, { style: [
371
+ styles$2.bubble,
372
+ { backgroundColor: theme.userBubble, borderRadius: theme.radius },
373
+ message.error ? { backgroundColor: theme.error } : null
374
+ ], children: /* @__PURE__ */ jsx(Text, { style: {
375
+ color: theme.onUserBubble,
376
+ fontSize: theme.fontSize,
377
+ lineHeight: theme.fontSize * 1.4
378
+ }, children: message.content }) }) });
379
+ }
380
+ return /* @__PURE__ */ jsx(View, { style: [styles$2.row, styles$2.rowAssistant], children: /* @__PURE__ */ jsxs(View, { style: [
381
+ styles$2.assistantBody,
382
+ message.error ? { backgroundColor: theme.error, borderRadius: theme.radius, padding: 12 } : null
383
+ ], children: [
384
+ message.content ? /* @__PURE__ */ jsx(Markdown, { color: theme.text, accentColor: theme.accent, fontSize: theme.fontSize, children: message.content }) : (
385
+ // A streaming message with no text yet would be an empty box.
386
+ /* @__PURE__ */ jsx(Text, { style: { color: theme.textMuted, fontSize: theme.fontSize }, children: message.streaming ? "…" : "" })
387
+ ),
388
+ message.error && onTalkToHuman ? /* @__PURE__ */ jsxs(
389
+ Pressable,
390
+ {
391
+ onPress: onTalkToHuman,
392
+ accessibilityRole: "button",
393
+ accessibilityLabel: "Talk to a person",
394
+ style: [styles$2.errorAction, { borderColor: theme.accent }],
395
+ hitSlop: 6,
396
+ children: [
397
+ /* @__PURE__ */ jsx(PersonIcon, { color: theme.accent }),
398
+ /* @__PURE__ */ jsx(Text, { style: [styles$2.humanText, {
399
+ color: theme.accent,
400
+ fontSize: theme.fontSize - 1
401
+ }], children: "Talk to a human" })
402
+ ]
403
+ }
404
+ ) : null
405
+ ] }) });
406
+ }
407
+ function resolveTopInset(explicit) {
408
+ var _a;
409
+ if (explicit !== void 0) return explicit;
410
+ if (Platform.OS === "android") return (_a = StatusBar.currentHeight) != null ? _a : 24;
411
+ return 8;
412
+ }
413
+ function useHumanHandover(onTalkToHuman, available) {
414
+ const [reachable, setReachable] = useState(false);
415
+ useEffect(() => {
416
+ if (!onTalkToHuman || available !== void 0) return;
417
+ let cancelled = false;
418
+ void (async () => {
419
+ try {
420
+ const { api } = requireConfigured();
421
+ const status = await api.get(
422
+ EP.supportStatus()
423
+ );
424
+ if (!cancelled) setReachable(!!(status == null ? void 0 : status.enabled));
425
+ } catch {
426
+ }
427
+ })();
428
+ return () => {
429
+ cancelled = true;
430
+ };
431
+ }, [onTalkToHuman, available]);
432
+ if (!onTalkToHuman) return false;
433
+ return available != null ? available : reachable;
434
+ }
435
+ function GuideAIChat({
436
+ theme: overrides,
437
+ placeholder = "Ask me anything",
438
+ greeting = "Ask me anything",
439
+ emptyState,
440
+ showMic = false,
441
+ leadingAccessory,
442
+ title,
443
+ onClose,
444
+ onTalkToHuman,
445
+ humanHandoverAvailable,
446
+ topInset
447
+ }) {
448
+ const { harness, debug } = requireConfigured();
449
+ const {
450
+ messages,
451
+ input,
452
+ setInput,
453
+ send,
454
+ busy,
455
+ canSend,
456
+ status
457
+ } = useGuideAI({ harness, trace: debug });
458
+ const theme = resolveTheme(overrides);
459
+ const listRef = useRef(null);
460
+ const showHuman = useHumanHandover(onTalkToHuman, humanHandoverAvailable);
461
+ const { checklist, dismiss } = useOnboarding();
462
+ const nextSetupStep = checklist == null ? void 0 : checklist.steps.find((step) => step.state === "available");
463
+ const headerTop = resolveTopInset(topInset);
464
+ const submit = () => {
465
+ void send();
466
+ };
467
+ return /* @__PURE__ */ jsxs(
468
+ KeyboardAvoidingView,
469
+ {
470
+ style: [styles$2.container, { backgroundColor: theme.background }],
471
+ behavior: Platform.OS === "ios" ? "padding" : void 0,
472
+ children: [
473
+ title || onClose || showHuman ? /* @__PURE__ */ jsxs(View, { style: [
474
+ styles$2.header,
475
+ { borderBottomColor: theme.border, paddingTop: headerTop + 14 }
476
+ ], children: [
477
+ /* @__PURE__ */ jsx(View, { style: styles$2.headerSide, children: title ? /* @__PURE__ */ jsx(
478
+ Text,
479
+ {
480
+ numberOfLines: 1,
481
+ style: [styles$2.title, { color: theme.text, fontSize: theme.fontSize + 1 }],
482
+ children: title
483
+ }
484
+ ) : null }),
485
+ /* @__PURE__ */ jsxs(View, { style: styles$2.headerActions, children: [
486
+ showHuman ? /* @__PURE__ */ jsxs(
487
+ Pressable,
488
+ {
489
+ onPress: onTalkToHuman,
490
+ accessibilityRole: "button",
491
+ accessibilityLabel: "Talk to a person",
492
+ style: [styles$2.human, {
493
+ backgroundColor: theme.surface,
494
+ borderColor: theme.border
495
+ }],
496
+ hitSlop: 8,
497
+ children: [
498
+ /* @__PURE__ */ jsx(PersonIcon, { color: theme.accent }),
499
+ /* @__PURE__ */ jsx(Text, { style: [styles$2.humanText, {
500
+ color: theme.accent,
501
+ fontSize: theme.fontSize - 2
502
+ }], children: "Talk to human" })
503
+ ]
504
+ }
505
+ ) : null,
506
+ onClose ? /* @__PURE__ */ jsx(
507
+ Pressable,
508
+ {
509
+ onPress: onClose,
510
+ accessibilityRole: "button",
511
+ accessibilityLabel: "Close assistant",
512
+ style: [styles$2.close, { backgroundColor: theme.surface }],
513
+ hitSlop: 10,
514
+ children: /* @__PURE__ */ jsx(Text, { style: [styles$2.closeIcon, { color: theme.text }], children: "✕" })
515
+ }
516
+ ) : null
517
+ ] })
518
+ ] }) : null,
519
+ checklist && !checklist.completed && !checklist.dismissed ? /* @__PURE__ */ jsxs(View, { style: { margin: 14, padding: 14, borderRadius: 12, borderWidth: 1, borderColor: theme.border, backgroundColor: theme.surface }, accessibilityLabel: "Setup checklist", children: [
520
+ /* @__PURE__ */ jsx(Text, { style: { color: theme.text, fontWeight: "700", fontSize: theme.fontSize + 2 }, children: checklist.title }),
521
+ /* @__PURE__ */ jsx(Text, { style: { color: theme.textMuted, marginTop: 3 }, children: checklist.goal }),
522
+ /* @__PURE__ */ jsxs(Text, { style: { color: theme.textMuted, marginTop: 8 }, children: [
523
+ checklist.completed_required,
524
+ " of ",
525
+ checklist.total_required,
526
+ " required steps complete"
527
+ ] }),
528
+ checklist.steps.map((step) => /* @__PURE__ */ jsxs(Text, { style: { color: step.state === "locked" ? theme.textMuted : theme.text, marginTop: 5 }, children: [
529
+ step.state === "complete" ? "✓" : step.state === "locked" ? "–" : "○",
530
+ " ",
531
+ step.title
532
+ ] }, step.id)),
533
+ nextSetupStep ? /* @__PURE__ */ jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: `Help with ${nextSetupStep.title}`, onPress: () => void send(`Help me with ${nextSetupStep.title}`), style: { alignSelf: "flex-start", marginTop: 10, padding: 10, borderRadius: 8, backgroundColor: theme.accent }, children: /* @__PURE__ */ jsx(Text, { style: { color: theme.onAccent, fontWeight: "600" }, children: "Continue setup" }) }) : null,
534
+ checklist.allow_dismissal ? /* @__PURE__ */ jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "I don't need this setup", onPress: () => Alert.alert("Stop showing setup?", "This checklist will not appear again on future visits.", [
535
+ { text: "Keep setup", style: "cancel" },
536
+ { text: "I don't need it", onPress: () => {
537
+ void dismiss().catch(() => Alert.alert("Could not dismiss setup", "Please try again."));
538
+ } }
539
+ ]), style: { alignSelf: "flex-start", marginTop: 10, padding: 10 }, children: /* @__PURE__ */ jsx(Text, { style: { color: theme.textMuted, textDecorationLine: "underline" }, children: "I don't need this setup" }) }) : null
540
+ ] }) : null,
541
+ messages.length === 0 ? /* @__PURE__ */ jsx(View, { style: styles$2.empty, children: emptyState != null ? emptyState : /* @__PURE__ */ jsxs(View, { style: styles$2.greetingRow, children: [
542
+ /* @__PURE__ */ jsx(Orb, { theme, active: busy }),
543
+ /* @__PURE__ */ jsx(Text, { style: [
544
+ styles$2.greeting,
545
+ { color: theme.textMuted, fontSize: theme.fontSize + 3 }
546
+ ], children: greeting })
547
+ ] }) }) : /* @__PURE__ */ jsx(
548
+ FlatList,
549
+ {
550
+ ref: listRef,
551
+ data: messages,
552
+ keyExtractor: (message) => message.id,
553
+ renderItem: ({ item }) => /* @__PURE__ */ jsx(Bubble$1, { message: item, theme, onTalkToHuman }),
554
+ contentContainerStyle: styles$2.list,
555
+ onContentSizeChange: () => {
556
+ var _a;
557
+ return (_a = listRef.current) == null ? void 0 : _a.scrollToEnd({ animated: true });
558
+ },
559
+ keyboardShouldPersistTaps: "handled"
560
+ }
561
+ ),
562
+ status ? /* @__PURE__ */ jsxs(View, { style: styles$2.status, children: [
563
+ /* @__PURE__ */ jsx(ActivityIndicator, { size: "small", color: theme.accent }),
564
+ /* @__PURE__ */ jsx(Text, { style: { color: theme.textMuted, fontSize: theme.fontSize - 2 }, children: status })
565
+ ] }) : null,
566
+ /* @__PURE__ */ jsxs(View, { style: styles$2.composerRow, children: [
567
+ /* @__PURE__ */ jsxs(View, { style: [
568
+ styles$2.composer,
569
+ {
570
+ backgroundColor: theme.surface,
571
+ borderRadius: theme.radius + 6,
572
+ borderColor: theme.border
573
+ }
574
+ ], children: [
575
+ leadingAccessory ? /* @__PURE__ */ jsx(View, { style: styles$2.leading, children: leadingAccessory }) : null,
576
+ /* @__PURE__ */ jsx(
577
+ TextInput,
578
+ {
579
+ style: [styles$2.input, { color: theme.text, fontSize: theme.fontSize }],
580
+ value: input,
581
+ onChangeText: setInput,
582
+ placeholder: checklist && !checklist.completed && checklist.chat_policy === "setup_only" ? "Ask about your setup" : placeholder,
583
+ placeholderTextColor: theme.textMuted,
584
+ editable: !busy,
585
+ onSubmitEditing: submit,
586
+ returnKeyType: "send",
587
+ multiline: true,
588
+ accessibilityLabel: placeholder
589
+ }
590
+ ),
591
+ /* @__PURE__ */ jsx(
592
+ Pressable,
593
+ {
594
+ onPress: submit,
595
+ disabled: !canSend,
596
+ accessibilityRole: "button",
597
+ accessibilityLabel: "Send",
598
+ style: [styles$2.send, { backgroundColor: canSend ? theme.accent : theme.border }],
599
+ hitSlop: 6,
600
+ children: /* @__PURE__ */ jsx(Text, { style: [
601
+ styles$2.sendIcon,
602
+ { color: canSend ? theme.onAccent : theme.textMuted }
603
+ ], children: "↑" })
604
+ }
605
+ )
606
+ ] }),
607
+ showMic ? /* @__PURE__ */ jsx(
608
+ Pressable,
609
+ {
610
+ accessibilityRole: "button",
611
+ accessibilityLabel: "Voice input",
612
+ style: [styles$2.mic, { backgroundColor: theme.surface, borderColor: theme.border }],
613
+ children: /* @__PURE__ */ jsx(Text, { style: { color: theme.textMuted, fontSize: 18 }, children: "🎤" })
614
+ }
615
+ ) : null
616
+ ] })
617
+ ]
618
+ }
619
+ );
620
+ }
621
+ const styles$2 = StyleSheet.create({
622
+ container: { flex: 1 },
623
+ header: {
624
+ flexDirection: "row",
625
+ alignItems: "center",
626
+ justifyContent: "space-between",
627
+ // paddingTop is set inline from the status-bar inset; only the
628
+ // bottom half of the navbar's breathing room lives here.
629
+ paddingHorizontal: 16,
630
+ paddingBottom: 14,
631
+ borderBottomWidth: StyleSheet.hairlineWidth
632
+ },
633
+ headerSide: { flex: 1, minWidth: 0 },
634
+ headerActions: { flexDirection: "row", alignItems: "center", gap: 8 },
635
+ title: { fontWeight: "600" },
636
+ human: {
637
+ flexDirection: "row",
638
+ alignItems: "center",
639
+ gap: 6,
640
+ paddingHorizontal: 10,
641
+ paddingVertical: 6,
642
+ borderRadius: 999,
643
+ borderWidth: StyleSheet.hairlineWidth
644
+ },
645
+ humanText: { fontWeight: "600" },
646
+ errorAction: {
647
+ flexDirection: "row",
648
+ alignItems: "center",
649
+ gap: 6,
650
+ alignSelf: "flex-start",
651
+ marginTop: 10,
652
+ paddingHorizontal: 12,
653
+ paddingVertical: 7,
654
+ borderRadius: 999,
655
+ borderWidth: StyleSheet.hairlineWidth
656
+ },
657
+ iconWrap: { width: 14, height: 14, alignItems: "center", justifyContent: "center" },
658
+ iconBand: {
659
+ position: "absolute",
660
+ top: 1,
661
+ width: 12,
662
+ height: 9,
663
+ borderWidth: 1.6,
664
+ borderBottomWidth: 0,
665
+ borderTopLeftRadius: 6,
666
+ borderTopRightRadius: 6
667
+ },
668
+ iconCup: { position: "absolute", bottom: 1, width: 3.5, height: 6, borderRadius: 1.5 },
669
+ iconCupLeft: { left: 0 },
670
+ iconCupRight: { right: 0 },
671
+ close: {
672
+ width: 32,
673
+ height: 32,
674
+ borderRadius: 16,
675
+ alignItems: "center",
676
+ justifyContent: "center"
677
+ },
678
+ closeIcon: { fontSize: 15, fontWeight: "600", lineHeight: 18 },
679
+ list: { padding: 16, paddingBottom: 8, gap: 14 },
680
+ empty: {
681
+ // Centred, not pinned near the top: before the first message the
682
+ // greeting is the only thing on screen, and hanging it under the
683
+ // navbar leaves the rest of the display empty.
684
+ flex: 1,
685
+ alignItems: "center",
686
+ justifyContent: "center",
687
+ paddingHorizontal: 24,
688
+ paddingBottom: 24
689
+ },
690
+ greetingRow: { flexDirection: "row", alignItems: "center", gap: 14 },
691
+ greeting: { fontWeight: "400", textAlign: "center", flexShrink: 1 },
692
+ orbWrap: { width: 34, height: 34, alignItems: "center", justifyContent: "center" },
693
+ orbGlow: { position: "absolute", width: 34, height: 34, borderRadius: 17 },
694
+ orbCore: { width: 20, height: 20, borderRadius: 10 },
695
+ row: { flexDirection: "row" },
696
+ rowUser: { justifyContent: "flex-end" },
697
+ rowAssistant: { justifyContent: "flex-start" },
698
+ // The user's bubble hugs its text: a three-word question drawn as a
699
+ // full-width block reads as a paragraph, not a chat message. It is
700
+ // capped at 80% so a long one still leaves the reply side legible.
701
+ bubble: { maxWidth: "80%", paddingHorizontal: 16, paddingVertical: 11 },
702
+ // The assistant's body does NOT hug its content, and that is the whole
703
+ // point of `width` here rather than `maxWidth` alone.
704
+ //
705
+ // A View with only maxWidth shrink-wraps — React Native's equivalent of
706
+ // width: fit-content. For a paragraph that looks fine, because the text
707
+ // fills the measure anyway. For anything that lays itself out against
708
+ // its parent — a table whose cells are `flex: 1`, a fenced code block,
709
+ // a numbered list — the parent has collapsed to nothing by the time the
710
+ // child is measured, so the table renders as an empty sliver and every
711
+ // reply ends up a different width.
712
+ //
713
+ // Fixing it here rather than on the table keeps every block type
714
+ // consistent: one column, the same measure for prose, code and tables.
715
+ assistantBody: { width: "80%" },
716
+ status: {
717
+ flexDirection: "row",
718
+ alignItems: "center",
719
+ gap: 8,
720
+ paddingHorizontal: 22,
721
+ paddingBottom: 8
722
+ },
723
+ composerRow: {
724
+ flexDirection: "row",
725
+ alignItems: "flex-end",
726
+ gap: 10,
727
+ paddingHorizontal: 14,
728
+ paddingBottom: Platform.OS === "ios" ? 10 : 14,
729
+ paddingTop: 4
730
+ },
731
+ composer: {
732
+ flex: 1,
733
+ flexDirection: "row",
734
+ alignItems: "center",
735
+ paddingLeft: 8,
736
+ paddingRight: 6,
737
+ paddingVertical: 6,
738
+ borderWidth: StyleSheet.hairlineWidth,
739
+ gap: 6
740
+ },
741
+ leading: { paddingLeft: 4 },
742
+ input: {
743
+ flex: 1,
744
+ paddingHorizontal: 8,
745
+ paddingVertical: Platform.OS === "ios" ? 8 : 4,
746
+ maxHeight: 120
747
+ },
748
+ send: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center" },
749
+ sendIcon: { fontSize: 17, fontWeight: "700", lineHeight: 20 },
750
+ mic: {
751
+ width: 46,
752
+ height: 46,
753
+ borderRadius: 23,
754
+ alignItems: "center",
755
+ justifyContent: "center",
756
+ borderWidth: StyleSheet.hairlineWidth
757
+ }
758
+ });
759
+ const POLL_INTERVAL_MS = 3e3;
760
+ const CONVERSATION_KEY = "guideai_support_conversation";
761
+ function useStoredConversation() {
762
+ const { runtime } = requireConfigured();
763
+ const [id, setId] = useState(() => {
764
+ try {
765
+ return runtime.storage.getItem(CONVERSATION_KEY);
766
+ } catch {
767
+ return null;
768
+ }
769
+ });
770
+ const persist = useCallback((next) => {
771
+ setId(next);
772
+ try {
773
+ if (next) runtime.storage.setItem(CONVERSATION_KEY, next);
774
+ else runtime.storage.removeItem(CONVERSATION_KEY);
775
+ } catch {
776
+ }
777
+ }, [runtime]);
778
+ return [id, persist];
779
+ }
780
+ function Bubble({ message, theme }) {
781
+ if (message.sender === "system") {
782
+ return /* @__PURE__ */ jsx(Text, { style: [styles$1.systemNote, {
783
+ color: theme.textMuted,
784
+ fontSize: theme.fontSize - 2
785
+ }], children: message.body });
786
+ }
787
+ const fromVisitor = message.sender === "visitor";
788
+ return /* @__PURE__ */ jsx(View, { style: [styles$1.row, fromVisitor ? styles$1.rowUser : styles$1.rowAgent], children: /* @__PURE__ */ jsxs(View, { style: styles$1.column, children: [
789
+ !fromVisitor && message.sender_name ? /* @__PURE__ */ jsx(Text, { style: [styles$1.senderName, {
790
+ color: theme.textMuted,
791
+ fontSize: theme.fontSize - 3
792
+ }], children: message.sender_name }) : null,
793
+ /* @__PURE__ */ jsx(View, { style: [
794
+ styles$1.bubble,
795
+ {
796
+ borderRadius: theme.radius,
797
+ backgroundColor: fromVisitor ? theme.userBubble : theme.surface
798
+ }
799
+ ], children: /* @__PURE__ */ jsx(Text, { style: {
800
+ color: fromVisitor ? theme.onUserBubble : theme.text,
801
+ fontSize: theme.fontSize,
802
+ lineHeight: theme.fontSize * 1.4
803
+ }, children: message.body }) })
804
+ ] }) });
805
+ }
806
+ function GuideAISupportChat({
807
+ theme: overrides,
808
+ onClose,
809
+ onBackToAssistant,
810
+ topInset,
811
+ openingContext
812
+ }) {
813
+ var _a;
814
+ const { api } = requireConfigured();
815
+ const theme = resolveTheme(overrides);
816
+ const headerTop = topInset != null ? topInset : Platform.OS === "android" ? (_a = StatusBar.currentHeight) != null ? _a : 24 : 8;
817
+ const [conversationId, setConversationId] = useStoredConversation();
818
+ const [messages, setMessages] = useState([]);
819
+ const [agentsOnline, setAgentsOnline] = useState(null);
820
+ const [input, setInput] = useState("");
821
+ const [busy, setBusy] = useState(false);
822
+ const [failed, setFailed] = useState(false);
823
+ const [name, setName] = useState("");
824
+ const [email, setEmail] = useState("");
825
+ const [issue, setIssue] = useState("");
826
+ const listRef = useRef(null);
827
+ const cursor = useRef(null);
828
+ const seenIds = useRef(/* @__PURE__ */ new Set());
829
+ const mounted = useRef(true);
830
+ useEffect(() => () => {
831
+ mounted.current = false;
832
+ }, []);
833
+ const poll = useCallback(async (id) => {
834
+ var _a2;
835
+ try {
836
+ const path = EP.supportMsgs(id);
837
+ const result = await api.get(
838
+ cursor.current ? `${path}?since=${encodeURIComponent(cursor.current)}` : path
839
+ );
840
+ if (!mounted.current) return;
841
+ setAgentsOnline(result.agents_online);
842
+ const fresh = ((_a2 = result.messages) != null ? _a2 : []).filter((m) => !seenIds.current.has(m.id));
843
+ if (fresh.length === 0) return;
844
+ for (const message of fresh) {
845
+ seenIds.current.add(message.id);
846
+ if (!cursor.current || message.created_at > cursor.current) {
847
+ cursor.current = message.created_at;
848
+ }
849
+ }
850
+ setMessages((previous) => [...previous, ...fresh]);
851
+ } catch {
852
+ }
853
+ }, [api]);
854
+ useEffect(() => {
855
+ if (!conversationId) return;
856
+ void poll(conversationId);
857
+ const timer = setInterval(() => {
858
+ void poll(conversationId);
859
+ }, POLL_INTERVAL_MS);
860
+ return () => clearInterval(timer);
861
+ }, [conversationId, poll]);
862
+ const startConversation = useCallback(async () => {
863
+ const text = issue.trim();
864
+ if (!text || busy) return;
865
+ setBusy(true);
866
+ setFailed(false);
867
+ try {
868
+ const result = await api.post(
869
+ EP.supportConvs(),
870
+ {
871
+ message: openingContext ? `${text}
872
+
873
+ ---
874
+ ${openingContext}` : text,
875
+ visitor_name: name.trim() || void 0,
876
+ visitor_email: email.trim() || void 0
877
+ }
878
+ );
879
+ if (!mounted.current) return;
880
+ setAgentsOnline(result.agents_online);
881
+ setConversationId(result.conversation_id);
882
+ } catch {
883
+ if (mounted.current) setFailed(true);
884
+ } finally {
885
+ if (mounted.current) setBusy(false);
886
+ }
887
+ }, [api, busy, email, issue, name, openingContext, setConversationId]);
888
+ const send = useCallback(async () => {
889
+ const text = input.trim();
890
+ if (!text || !conversationId || busy) return;
891
+ setInput("");
892
+ setBusy(true);
893
+ try {
894
+ await api.post(EP.supportMsgs(conversationId), { body: text });
895
+ await poll(conversationId);
896
+ } catch {
897
+ if (mounted.current) {
898
+ setInput(text);
899
+ setFailed(true);
900
+ }
901
+ } finally {
902
+ if (mounted.current) setBusy(false);
903
+ }
904
+ }, [api, busy, conversationId, input, poll]);
905
+ const presence = agentsOnline === null ? "Connecting…" : agentsOnline > 0 ? `${agentsOnline} agent${agentsOnline === 1 ? "" : "s"} online` : "No agents online — leave a message and we will reply by email";
906
+ return /* @__PURE__ */ jsxs(
907
+ KeyboardAvoidingView,
908
+ {
909
+ style: [styles$1.container, { backgroundColor: theme.background }],
910
+ behavior: Platform.OS === "ios" ? "padding" : "height",
911
+ children: [
912
+ /* @__PURE__ */ jsxs(View, { style: [
913
+ styles$1.header,
914
+ { borderBottomColor: theme.border, paddingTop: headerTop + 14 }
915
+ ], children: [
916
+ /* @__PURE__ */ jsxs(View, { style: styles$1.headerSide, children: [
917
+ /* @__PURE__ */ jsx(
918
+ Text,
919
+ {
920
+ numberOfLines: 1,
921
+ style: [styles$1.title, { color: theme.text, fontSize: theme.fontSize + 1 }],
922
+ children: "Support"
923
+ }
924
+ ),
925
+ /* @__PURE__ */ jsxs(View, { style: styles$1.presenceRow, children: [
926
+ /* @__PURE__ */ jsx(View, { style: [styles$1.presenceDot, {
927
+ backgroundColor: agentsOnline ? "#22c55e" : theme.textMuted
928
+ }] }),
929
+ /* @__PURE__ */ jsx(
930
+ Text,
931
+ {
932
+ numberOfLines: 1,
933
+ style: { color: theme.textMuted, fontSize: theme.fontSize - 3 },
934
+ children: presence
935
+ }
936
+ )
937
+ ] })
938
+ ] }),
939
+ /* @__PURE__ */ jsxs(View, { style: styles$1.headerActions, children: [
940
+ onBackToAssistant ? /* @__PURE__ */ jsx(
941
+ Pressable,
942
+ {
943
+ onPress: onBackToAssistant,
944
+ accessibilityRole: "button",
945
+ accessibilityLabel: "Back to the assistant",
946
+ style: [styles$1.backBtn, { borderColor: theme.border }],
947
+ hitSlop: 8,
948
+ children: /* @__PURE__ */ jsx(Text, { style: { color: theme.accent, fontSize: theme.fontSize - 2, fontWeight: "600" }, children: "Assistant" })
949
+ }
950
+ ) : null,
951
+ onClose ? /* @__PURE__ */ jsx(
952
+ Pressable,
953
+ {
954
+ onPress: onClose,
955
+ accessibilityRole: "button",
956
+ accessibilityLabel: "Close support",
957
+ style: [styles$1.close, { backgroundColor: theme.surface }],
958
+ hitSlop: 10,
959
+ children: /* @__PURE__ */ jsx(Text, { style: [styles$1.closeIcon, { color: theme.text }], children: "✕" })
960
+ }
961
+ ) : null
962
+ ] })
963
+ ] }),
964
+ !conversationId ? (
965
+ // ── Intake ──
966
+ // Name and email are optional; the issue is not. Asking for
967
+ // everything before a visitor may speak is how support forms lose
968
+ // people who just want to ask something.
969
+ /* @__PURE__ */ jsxs(
970
+ ScrollView,
971
+ {
972
+ style: styles$1.intakeScroll,
973
+ contentContainerStyle: styles$1.intake,
974
+ keyboardShouldPersistTaps: "handled",
975
+ children: [
976
+ /* @__PURE__ */ jsx(Text, { style: [styles$1.intakeTitle, { color: theme.text, fontSize: theme.fontSize + 2 }], children: "Talk to a person" }),
977
+ /* @__PURE__ */ jsx(Text, { style: { color: theme.textMuted, fontSize: theme.fontSize - 1, lineHeight: 20 }, children: "Tell us what you need and we will connect you with someone." }),
978
+ /* @__PURE__ */ jsx(
979
+ TextInput,
980
+ {
981
+ style: [styles$1.field, {
982
+ color: theme.text,
983
+ borderColor: theme.border,
984
+ backgroundColor: theme.surface,
985
+ fontSize: theme.fontSize
986
+ }],
987
+ value: name,
988
+ onChangeText: setName,
989
+ placeholder: "Your name (optional)",
990
+ placeholderTextColor: theme.textMuted,
991
+ autoCapitalize: "words"
992
+ }
993
+ ),
994
+ /* @__PURE__ */ jsx(
995
+ TextInput,
996
+ {
997
+ style: [styles$1.field, {
998
+ color: theme.text,
999
+ borderColor: theme.border,
1000
+ backgroundColor: theme.surface,
1001
+ fontSize: theme.fontSize
1002
+ }],
1003
+ value: email,
1004
+ onChangeText: setEmail,
1005
+ placeholder: "Email (optional)",
1006
+ placeholderTextColor: theme.textMuted,
1007
+ autoCapitalize: "none",
1008
+ keyboardType: "email-address"
1009
+ }
1010
+ ),
1011
+ /* @__PURE__ */ jsx(
1012
+ TextInput,
1013
+ {
1014
+ style: [styles$1.field, styles$1.fieldMultiline, {
1015
+ color: theme.text,
1016
+ borderColor: theme.border,
1017
+ backgroundColor: theme.surface,
1018
+ fontSize: theme.fontSize
1019
+ }],
1020
+ value: issue,
1021
+ onChangeText: setIssue,
1022
+ placeholder: "How can we help?",
1023
+ placeholderTextColor: theme.textMuted,
1024
+ multiline: true
1025
+ }
1026
+ ),
1027
+ failed ? /* @__PURE__ */ jsx(Text, { style: { color: theme.textMuted, fontSize: theme.fontSize - 2 }, children: "We could not start the conversation. Please try again." }) : null,
1028
+ /* @__PURE__ */ jsx(
1029
+ Pressable,
1030
+ {
1031
+ onPress: () => {
1032
+ void startConversation();
1033
+ },
1034
+ disabled: !issue.trim() || busy,
1035
+ accessibilityRole: "button",
1036
+ style: [styles$1.primaryBtn, {
1037
+ backgroundColor: issue.trim() && !busy ? theme.accent : theme.border
1038
+ }],
1039
+ children: busy ? /* @__PURE__ */ jsx(ActivityIndicator, { size: "small", color: theme.onAccent }) : /* @__PURE__ */ jsx(Text, { style: {
1040
+ color: issue.trim() ? theme.onAccent : theme.textMuted,
1041
+ fontSize: theme.fontSize,
1042
+ fontWeight: "700"
1043
+ }, children: "Start conversation" })
1044
+ }
1045
+ )
1046
+ ]
1047
+ }
1048
+ )
1049
+ ) : (
1050
+ // ── Thread ──
1051
+ /* @__PURE__ */ jsxs(Fragment, { children: [
1052
+ /* @__PURE__ */ jsx(
1053
+ FlatList,
1054
+ {
1055
+ ref: listRef,
1056
+ style: styles$1.list,
1057
+ data: messages,
1058
+ keyExtractor: (message) => message.id,
1059
+ renderItem: ({ item }) => /* @__PURE__ */ jsx(Bubble, { message: item, theme }),
1060
+ contentContainerStyle: [
1061
+ styles$1.listContent,
1062
+ // Centre the waiting state instead of pinning it under the
1063
+ // header: an empty thread is most of the screen.
1064
+ messages.length === 0 ? styles$1.listContentEmpty : null
1065
+ ],
1066
+ onContentSizeChange: () => {
1067
+ var _a2;
1068
+ return (_a2 = listRef.current) == null ? void 0 : _a2.scrollToEnd({ animated: true });
1069
+ },
1070
+ keyboardShouldPersistTaps: "handled",
1071
+ ListEmptyComponent: (
1072
+ // This is most of the screen while a visitor waits, so it
1073
+ // says what is happening and what to expect rather than
1074
+ // leaving them looking at one grey line.
1075
+ /* @__PURE__ */ jsxs(View, { style: styles$1.waiting, children: [
1076
+ /* @__PURE__ */ jsx(ActivityIndicator, { size: "small", color: theme.accent }),
1077
+ /* @__PURE__ */ jsx(Text, { style: [styles$1.waitingTitle, {
1078
+ color: theme.text,
1079
+ fontSize: theme.fontSize
1080
+ }], children: agentsOnline ? "Connecting you to an agent…" : "Message sent" }),
1081
+ /* @__PURE__ */ jsx(Text, { style: [styles$1.waitingBody, {
1082
+ color: theme.textMuted,
1083
+ fontSize: theme.fontSize - 2
1084
+ }], children: agentsOnline ? "Someone will be with you shortly. You can keep typing in the meantime." : "Nobody is online right now, but your message is saved and we will reply by email." })
1085
+ ] })
1086
+ )
1087
+ }
1088
+ ),
1089
+ /* @__PURE__ */ jsx(View, { style: [styles$1.composerRow, { borderTopColor: theme.border }], children: /* @__PURE__ */ jsxs(View, { style: [styles$1.composer, {
1090
+ backgroundColor: theme.surface,
1091
+ borderRadius: theme.radius + 6,
1092
+ borderColor: theme.border
1093
+ }], children: [
1094
+ /* @__PURE__ */ jsx(
1095
+ TextInput,
1096
+ {
1097
+ style: [styles$1.input, { color: theme.text, fontSize: theme.fontSize }],
1098
+ value: input,
1099
+ onChangeText: setInput,
1100
+ placeholder: "Write a message",
1101
+ placeholderTextColor: theme.textMuted,
1102
+ onSubmitEditing: () => {
1103
+ void send();
1104
+ },
1105
+ returnKeyType: "send",
1106
+ multiline: true
1107
+ }
1108
+ ),
1109
+ /* @__PURE__ */ jsx(
1110
+ Pressable,
1111
+ {
1112
+ onPress: () => {
1113
+ void send();
1114
+ },
1115
+ disabled: !input.trim() || busy,
1116
+ accessibilityRole: "button",
1117
+ accessibilityLabel: "Send",
1118
+ style: [styles$1.send, {
1119
+ backgroundColor: input.trim() && !busy ? theme.accent : theme.border
1120
+ }],
1121
+ hitSlop: 6,
1122
+ children: /* @__PURE__ */ jsx(Text, { style: [styles$1.sendIcon, {
1123
+ color: input.trim() && !busy ? theme.onAccent : theme.textMuted
1124
+ }], children: "↑" })
1125
+ }
1126
+ )
1127
+ ] }) })
1128
+ ] })
1129
+ )
1130
+ ]
1131
+ }
1132
+ );
1133
+ }
1134
+ const styles$1 = StyleSheet.create({
1135
+ container: { flex: 1 },
1136
+ header: {
1137
+ flexDirection: "row",
1138
+ alignItems: "center",
1139
+ justifyContent: "space-between",
1140
+ paddingHorizontal: 16,
1141
+ paddingBottom: 14,
1142
+ borderBottomWidth: StyleSheet.hairlineWidth
1143
+ },
1144
+ headerSide: { flex: 1, minWidth: 0 },
1145
+ headerActions: { flexDirection: "row", alignItems: "center", gap: 8 },
1146
+ title: { fontWeight: "600" },
1147
+ presenceRow: { flexDirection: "row", alignItems: "center", gap: 6, marginTop: 3 },
1148
+ presenceDot: { width: 7, height: 7, borderRadius: 4 },
1149
+ backBtn: {
1150
+ paddingHorizontal: 12,
1151
+ paddingVertical: 7,
1152
+ borderRadius: 999,
1153
+ borderWidth: StyleSheet.hairlineWidth
1154
+ },
1155
+ close: {
1156
+ width: 32,
1157
+ height: 32,
1158
+ borderRadius: 16,
1159
+ alignItems: "center",
1160
+ justifyContent: "center"
1161
+ },
1162
+ closeIcon: { fontSize: 15, fontWeight: "600", lineHeight: 18 },
1163
+ // The form has to own the space under the header — without flex it sits
1164
+ // jammed at the top with the rest of the sheet blank — and it has to
1165
+ // scroll, or the keyboard buries the button on a short phone.
1166
+ intakeScroll: { flex: 1 },
1167
+ intake: { padding: 20, gap: 14, paddingBottom: 32 },
1168
+ intakeTitle: { fontWeight: "700" },
1169
+ field: {
1170
+ borderWidth: StyleSheet.hairlineWidth,
1171
+ borderRadius: 12,
1172
+ paddingHorizontal: 14,
1173
+ // 48pt tall: a text field below the 44pt touch minimum is hard to hit
1174
+ // and reads as disabled.
1175
+ minHeight: 48,
1176
+ paddingVertical: Platform.OS === "ios" ? 14 : 10
1177
+ },
1178
+ fieldMultiline: { minHeight: 110, textAlignVertical: "top", paddingTop: 14 },
1179
+ primaryBtn: {
1180
+ borderRadius: 999,
1181
+ minHeight: 50,
1182
+ alignItems: "center",
1183
+ justifyContent: "center",
1184
+ marginTop: 6
1185
+ },
1186
+ // `style` fills the sheet; `contentContainerStyle` lays out the rows.
1187
+ // Putting padding on the container and nothing on the list is what left
1188
+ // the composer floating in the middle of the screen.
1189
+ list: { flex: 1 },
1190
+ listContent: { padding: 16, paddingBottom: 12, gap: 14 },
1191
+ listContentEmpty: { flexGrow: 1, justifyContent: "center" },
1192
+ row: { flexDirection: "row" },
1193
+ rowUser: { justifyContent: "flex-end" },
1194
+ rowAgent: { justifyContent: "flex-start" },
1195
+ // ONE width cap, on the column. It used to be here and on the bubble
1196
+ // too, and nested percentages multiply: an agent's reply was 80% of 80%
1197
+ // — 64% of the sheet — while the visitor's own messages ran to 80%.
1198
+ column: { maxWidth: "82%" },
1199
+ bubble: { paddingHorizontal: 15, paddingVertical: 11 },
1200
+ senderName: { marginBottom: 4, marginLeft: 8, fontWeight: "600" },
1201
+ systemNote: { textAlign: "center", marginVertical: 6, paddingHorizontal: 24 },
1202
+ waiting: { alignItems: "center", gap: 10, paddingHorizontal: 32 },
1203
+ waitingTitle: { fontWeight: "600", marginTop: 4 },
1204
+ waitingBody: { textAlign: "center", lineHeight: 19 },
1205
+ composerRow: {
1206
+ flexDirection: "row",
1207
+ alignItems: "flex-end",
1208
+ gap: 10,
1209
+ paddingHorizontal: 14,
1210
+ paddingBottom: Platform.OS === "ios" ? 12 : 14,
1211
+ paddingTop: 10,
1212
+ // A hairline above the composer: without it the input floats against
1213
+ // the last message with nothing marking where the thread ends.
1214
+ borderTopWidth: StyleSheet.hairlineWidth
1215
+ },
1216
+ composer: {
1217
+ flex: 1,
1218
+ flexDirection: "row",
1219
+ alignItems: "flex-end",
1220
+ paddingLeft: 10,
1221
+ paddingRight: 6,
1222
+ paddingVertical: 5,
1223
+ borderWidth: StyleSheet.hairlineWidth,
1224
+ gap: 6
1225
+ },
1226
+ input: {
1227
+ flex: 1,
1228
+ paddingHorizontal: 6,
1229
+ minHeight: 38,
1230
+ maxHeight: 120,
1231
+ paddingVertical: Platform.OS === "ios" ? 9 : 6
1232
+ },
1233
+ // 38pt, matching the input's resting height so the two line up, and
1234
+ // paired with hitSlop to clear the 44pt touch minimum.
1235
+ send: { width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center" },
1236
+ sendIcon: { fontSize: 18, fontWeight: "700", lineHeight: 21 }
1237
+ });
1238
+ const SheetContext = createContext(null);
1239
+ function useGuideAISheet() {
1240
+ const controls = useContext(SheetContext);
1241
+ if (!controls) {
1242
+ throw new Error(
1243
+ "useGuideAISheet() must be used inside <GuideAILauncher>. Mount it once at your app root, above your navigator."
1244
+ );
1245
+ }
1246
+ return controls;
1247
+ }
1248
+ function Nudge({ text, side, bottom, horizontal, theme, onDismiss }) {
1249
+ const enter = useRef(new Animated.Value(0)).current;
1250
+ useEffect(() => {
1251
+ Animated.timing(enter, {
1252
+ toValue: 1,
1253
+ duration: 260,
1254
+ easing: Easing.out(Easing.back(1.4)),
1255
+ useNativeDriver: true
1256
+ }).start();
1257
+ }, [enter]);
1258
+ const translate = enter.interpolate({
1259
+ inputRange: [0, 1],
1260
+ // Slides in from the button it belongs to, so the eye is led to it.
1261
+ outputRange: [side === "right" ? 14 : -14, 0]
1262
+ });
1263
+ return /* @__PURE__ */ jsx(
1264
+ Animated.View,
1265
+ {
1266
+ pointerEvents: "box-none",
1267
+ style: [
1268
+ styles.nudge,
1269
+ {
1270
+ bottom: bottom + 14,
1271
+ backgroundColor: theme.surfaceRaised,
1272
+ borderColor: theme.border,
1273
+ opacity: enter,
1274
+ transform: [{ translateX: translate }]
1275
+ },
1276
+ side === "right" ? { right: horizontal + 64 } : { left: horizontal + 64 }
1277
+ ],
1278
+ children: /* @__PURE__ */ jsx(Pressable, { onPress: onDismiss, accessibilityRole: "button", accessibilityLabel: text, children: /* @__PURE__ */ jsx(Text, { style: [styles.nudgeText, { color: theme.text, fontSize: theme.fontSize - 1 }], children: text }) })
1279
+ }
1280
+ );
1281
+ }
1282
+ function GuideAILauncher({
1283
+ hideButton = false,
1284
+ position = "bottom-right",
1285
+ offset,
1286
+ icon,
1287
+ label = "Open assistant",
1288
+ draggable = true,
1289
+ nudge = "Need a hand? Ask me anything",
1290
+ nudgeDelay = 1200,
1291
+ nudgeDuration = 5e3,
1292
+ children,
1293
+ ...chatProps
1294
+ }) {
1295
+ var _a, _b, _c;
1296
+ const [isOpen, setIsOpen] = useState(false);
1297
+ const { checklist } = useOnboarding();
1298
+ const autoOpenedFor = useRef(null);
1299
+ const [view, setView] = useState("assistant");
1300
+ const theme = resolveTheme(chatProps.theme);
1301
+ useEffect(() => {
1302
+ if (checklist && !checklist.completed && !checklist.dismissed && checklist.start_mode === "automatic" && autoOpenedFor.current !== checklist.id) {
1303
+ autoOpenedFor.current = checklist.id;
1304
+ setIsOpen(true);
1305
+ }
1306
+ }, [checklist == null ? void 0 : checklist.id, checklist == null ? void 0 : checklist.completed, checklist == null ? void 0 : checklist.dismissed, checklist == null ? void 0 : checklist.start_mode]);
1307
+ const effectiveNudge = checklist && !checklist.completed && !checklist.dismissed ? `Setup: ${checklist.total_required - checklist.completed_required} required steps remaining` : nudge;
1308
+ const open = useCallback(() => setIsOpen(true), []);
1309
+ const close = useCallback(() => setIsOpen(false), []);
1310
+ const toggle = useCallback(() => setIsOpen((previous) => !previous), []);
1311
+ const controls = useMemo(
1312
+ () => ({ open, close, toggle, isOpen }),
1313
+ [open, close, toggle, isOpen]
1314
+ );
1315
+ const bottom = (_a = offset == null ? void 0 : offset.bottom) != null ? _a : 28;
1316
+ const horizontal = (_b = offset == null ? void 0 : offset.horizontal) != null ? _b : 20;
1317
+ const FAB_SIZE = 56;
1318
+ const screen = Dimensions.get("window");
1319
+ const pan = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;
1320
+ const panValue = useRef({ x: 0, y: 0 });
1321
+ const [dragging, setDragging] = useState(false);
1322
+ useEffect(() => {
1323
+ const id = pan.addListener((value) => {
1324
+ panValue.current = value;
1325
+ });
1326
+ return () => pan.removeListener(id);
1327
+ }, [pan]);
1328
+ const responder = useMemo(() => PanResponder.create({
1329
+ // Claim the gesture only once it is clearly a drag, so a tap still
1330
+ // reaches the button's onPress.
1331
+ onMoveShouldSetPanResponder: (_event, gesture) => draggable && (Math.abs(gesture.dx) > 4 || Math.abs(gesture.dy) > 4),
1332
+ onPanResponderGrant: () => {
1333
+ setDragging(true);
1334
+ pan.setOffset({ ...panValue.current });
1335
+ pan.setValue({ x: 0, y: 0 });
1336
+ },
1337
+ onPanResponderMove: Animated.event(
1338
+ [null, { dx: pan.x, dy: pan.y }],
1339
+ // PanResponder has no native-driver equivalent; the values feed a
1340
+ // transform, which stays smooth enough for one small view.
1341
+ { useNativeDriver: false }
1342
+ ),
1343
+ onPanResponderRelease: () => {
1344
+ pan.flattenOffset();
1345
+ setDragging(false);
1346
+ const restingX = position === "bottom-right" ? screen.width - horizontal - FAB_SIZE : horizontal;
1347
+ const currentX = restingX + panValue.current.x;
1348
+ screen.height - bottom - FAB_SIZE + panValue.current.y;
1349
+ const snapLeft = currentX + FAB_SIZE / 2 < screen.width / 2;
1350
+ const targetX = (snapLeft ? horizontal : screen.width - horizontal - FAB_SIZE) - restingX;
1351
+ const minY = -(screen.height - bottom - FAB_SIZE - 60);
1352
+ const maxY = bottom - 12;
1353
+ const targetY = Math.min(Math.max(panValue.current.y, minY), maxY);
1354
+ Animated.spring(pan, {
1355
+ toValue: { x: targetX, y: targetY },
1356
+ useNativeDriver: false,
1357
+ friction: 7,
1358
+ tension: 60
1359
+ }).start();
1360
+ }
1361
+ }), [draggable, pan, position, screen.width, screen.height, horizontal, bottom]);
1362
+ const [nudgeVisible, setNudgeVisible] = useState(false);
1363
+ const nudgeShown = useRef(false);
1364
+ useEffect(() => {
1365
+ if (!effectiveNudge || hideButton || nudgeShown.current) return;
1366
+ const show = setTimeout(() => {
1367
+ nudgeShown.current = true;
1368
+ setNudgeVisible(true);
1369
+ }, nudgeDelay);
1370
+ return () => clearTimeout(show);
1371
+ }, [effectiveNudge, hideButton, nudgeDelay]);
1372
+ useEffect(() => {
1373
+ if (!nudgeVisible) return;
1374
+ const hide = setTimeout(() => setNudgeVisible(false), nudgeDuration);
1375
+ return () => clearTimeout(hide);
1376
+ }, [nudgeVisible, nudgeDuration]);
1377
+ useEffect(() => {
1378
+ if (isOpen) setNudgeVisible(false);
1379
+ }, [isOpen]);
1380
+ useEffect(() => {
1381
+ if (dragging) setNudgeVisible(false);
1382
+ }, [dragging]);
1383
+ return /* @__PURE__ */ jsxs(SheetContext.Provider, { value: controls, children: [
1384
+ children,
1385
+ nudgeVisible && effectiveNudge && !hideButton ? /* @__PURE__ */ jsx(
1386
+ Nudge,
1387
+ {
1388
+ text: effectiveNudge,
1389
+ side: position === "bottom-right" ? "right" : "left",
1390
+ bottom,
1391
+ horizontal,
1392
+ theme,
1393
+ onDismiss: () => setNudgeVisible(false)
1394
+ }
1395
+ ) : null,
1396
+ !hideButton ? /* @__PURE__ */ jsx(
1397
+ Animated.View,
1398
+ {
1399
+ ...draggable ? responder.panHandlers : {},
1400
+ style: [
1401
+ styles.fabWrap,
1402
+ { bottom },
1403
+ position === "bottom-right" ? { right: horizontal } : { left: horizontal },
1404
+ { transform: pan.getTranslateTransform() }
1405
+ ],
1406
+ children: /* @__PURE__ */ jsx(
1407
+ Pressable,
1408
+ {
1409
+ onPress: open,
1410
+ accessibilityRole: "button",
1411
+ accessibilityLabel: checklist && !checklist.completed ? `Open setup checklist, ${checklist.total_required - checklist.completed_required} steps remaining` : label,
1412
+ style: [
1413
+ styles.fab,
1414
+ { backgroundColor: theme.accent },
1415
+ // Lifts under the finger, so a drag reads as picked up.
1416
+ dragging ? styles.fabDragging : null
1417
+ ],
1418
+ children: icon != null ? icon : /* @__PURE__ */ jsx(View, { style: [styles.fabOrb, { backgroundColor: theme.onAccent }] })
1419
+ }
1420
+ )
1421
+ }
1422
+ ) : null,
1423
+ /* @__PURE__ */ jsx(
1424
+ Modal,
1425
+ {
1426
+ visible: isOpen,
1427
+ animationType: "slide",
1428
+ presentationStyle: "pageSheet",
1429
+ statusBarTranslucent: Platform.OS === "android",
1430
+ onRequestClose: close,
1431
+ children: /* @__PURE__ */ jsxs(View, { style: { flex: 1, backgroundColor: theme.background }, children: [
1432
+ /* @__PURE__ */ jsx(View, { style: view === "assistant" ? styles.pane : styles.paneHidden, children: /* @__PURE__ */ jsx(
1433
+ GuideAIChat,
1434
+ {
1435
+ ...chatProps,
1436
+ onClose: close,
1437
+ onTalkToHuman: (_c = chatProps.onTalkToHuman) != null ? _c : (() => setView("support"))
1438
+ }
1439
+ ) }),
1440
+ view === "support" ? /* @__PURE__ */ jsx(View, { style: styles.pane, children: /* @__PURE__ */ jsx(
1441
+ GuideAISupportChat,
1442
+ {
1443
+ theme: chatProps.theme,
1444
+ topInset: chatProps.topInset,
1445
+ onClose: close,
1446
+ onBackToAssistant: () => setView("assistant")
1447
+ }
1448
+ ) }) : null
1449
+ ] })
1450
+ }
1451
+ )
1452
+ ] });
1453
+ }
1454
+ function GuideAIButton({
1455
+ icon,
1456
+ label = "Open assistant",
1457
+ theme: overrides,
1458
+ size = 56
1459
+ }) {
1460
+ const { open } = useGuideAISheet();
1461
+ const theme = resolveTheme(overrides);
1462
+ return /* @__PURE__ */ jsx(
1463
+ Pressable,
1464
+ {
1465
+ onPress: open,
1466
+ accessibilityRole: "button",
1467
+ accessibilityLabel: label,
1468
+ style: {
1469
+ width: size,
1470
+ height: size,
1471
+ borderRadius: size / 2,
1472
+ backgroundColor: theme.accent,
1473
+ alignItems: "center",
1474
+ justifyContent: "center"
1475
+ },
1476
+ children: icon != null ? icon : /* @__PURE__ */ jsx(View, { style: [styles.fabOrb, { backgroundColor: theme.onAccent }] })
1477
+ }
1478
+ );
1479
+ }
1480
+ const styles = StyleSheet.create({
1481
+ pane: { ...StyleSheet.absoluteFillObject },
1482
+ paneHidden: { ...StyleSheet.absoluteFillObject, display: "none" },
1483
+ fabWrap: { position: "absolute" },
1484
+ fabDragging: { opacity: 0.9, transform: [{ scale: 1.08 }] },
1485
+ fab: {
1486
+ width: 56,
1487
+ height: 56,
1488
+ borderRadius: 28,
1489
+ alignItems: "center",
1490
+ justifyContent: "center",
1491
+ // Raised enough to read as floating above content, not part of it.
1492
+ shadowColor: "#000",
1493
+ shadowOpacity: 0.3,
1494
+ shadowRadius: 12,
1495
+ shadowOffset: { width: 0, height: 6 },
1496
+ elevation: 8
1497
+ },
1498
+ fabOrb: { width: 18, height: 18, borderRadius: 9 },
1499
+ nudge: {
1500
+ position: "absolute",
1501
+ maxWidth: 220,
1502
+ paddingHorizontal: 14,
1503
+ paddingVertical: 10,
1504
+ borderRadius: 16,
1505
+ borderWidth: StyleSheet.hairlineWidth,
1506
+ shadowColor: "#000",
1507
+ shadowOpacity: 0.25,
1508
+ shadowRadius: 10,
1509
+ shadowOffset: { width: 0, height: 4 },
1510
+ elevation: 6
1511
+ },
1512
+ nudgeText: { lineHeight: 19 }
1513
+ });
1514
+ export {
1515
+ GuideAIButton,
1516
+ GuideAIChat,
1517
+ GuideAILauncher,
1518
+ GuideAISupportChat,
1519
+ Markdown,
1520
+ darkTheme,
1521
+ lightTheme,
1522
+ resolveTheme,
1523
+ useGuideAISheet
1524
+ };
1525
+ //# sourceMappingURL=ui.mjs.map