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