opendesk-react-native 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,636 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenDeskChat = OpenDeskChat;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const react_native_1 = require("react-native");
7
+ const react_2 = require("../react");
8
+ const BotFace_1 = require("./BotFace");
9
+ const icons_1 = require("./icons");
10
+ const strings_1 = require("./strings");
11
+ const theme_1 = require("./theme");
12
+ const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
13
+ const GROUP_GAP_MS = 5 * 60 * 1000;
14
+ /**
15
+ * A drop-in support screen that mirrors openDesk's web widget: the home screen, the thread
16
+ * with AI streaming, actions and handoff, and the composer with attachments.
17
+ */
18
+ function OpenDeskChat({ initialConversationId, onClose, pickAttachment, accentColor, theme: themeSetting, strings: stringOverrides, keyboardVerticalOffset = 0, }) {
19
+ const [conversationId, setConversationId] = (0, react_1.useState)(initialConversationId ?? null);
20
+ const [composing, setComposing] = (0, react_1.useState)(false);
21
+ const config = (0, react_2.useOpenDeskState)((state) => state.config);
22
+ const systemScheme = (0, react_native_1.useColorScheme)();
23
+ const setting = themeSetting ?? config?.theme ?? 'auto';
24
+ const scheme = setting === 'auto' ? (systemScheme === 'dark' ? 'dark' : 'light') : setting;
25
+ const theme = (0, react_1.useMemo)(() => (0, theme_1.makeTheme)(config, scheme, accentColor), [config, scheme, accentColor]);
26
+ const strings = (0, react_1.useMemo)(() => ({ ...strings_1.defaultStrings, ...stringOverrides }), [stringOverrides]);
27
+ const styles = (0, react_1.useMemo)(() => createStyles(theme), [theme]);
28
+ const ui = (0, react_1.useMemo)(() => ({ theme, strings, styles, bot: config?.bot ?? null }), [theme, strings, styles, config?.bot]);
29
+ const inThread = conversationId !== null || composing;
30
+ return ((0, jsx_runtime_1.jsx)(react_native_1.KeyboardAvoidingView, { style: styles.root, behavior: react_native_1.Platform.OS === 'ios' ? 'padding' : undefined, keyboardVerticalOffset: keyboardVerticalOffset, children: inThread ? ((0, jsx_runtime_1.jsx)(Thread, { ui: ui, conversationId: conversationId, pickAttachment: pickAttachment, onBack: () => {
31
+ setConversationId(null);
32
+ setComposing(false);
33
+ }, onClose: onClose, onStarted: (id) => {
34
+ setComposing(false);
35
+ setConversationId(id);
36
+ } })) : ((0, jsx_runtime_1.jsx)(Home, { ui: ui, onClose: onClose, onOpen: setConversationId, onNew: () => setComposing(true) })) }));
37
+ }
38
+ function subtitleFor(config, strings) {
39
+ if (config?.subtitle) {
40
+ return config.subtitle;
41
+ }
42
+ return config?.ai_answers && config.bot ? (0, strings_1.format)(strings.replyTimeAi, { name: config.bot.name }) : strings.replyTime;
43
+ }
44
+ function Home({ ui, onClose, onOpen, onNew, }) {
45
+ const { theme, strings, styles } = ui;
46
+ const { conversations, ready, error } = (0, react_2.useConversations)();
47
+ const unread = (0, react_2.useOpenDeskState)((state) => state.unread);
48
+ const config = (0, react_2.useOpenDeskState)((state) => state.config);
49
+ const name = config?.title || config?.workspace_name || strings.support;
50
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.flex, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.head, styles.headHome], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.headRow, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.ws, children: [(0, jsx_runtime_1.jsx)(Glyph, { ui: ui, name: name, url: config?.avatar_url ?? null }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.wsName, numberOfLines: 1, children: name })] }), onClose ? (0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.closeChat, onPress: onClose, icon: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3 }) }) : null] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.greet, children: config?.greeting || strings.greeting }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.subRow, children: [(0, jsx_runtime_1.jsx)(AvatarStack, { ui: ui, config: config }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.sub, children: subtitleFor(config, strings) })] })] }), (0, jsx_runtime_1.jsxs)(react_native_1.ScrollView, { style: styles.flex, contentContainerStyle: styles.homeBody, children: [!ready ? (error ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.empty, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.emptyTitle, children: strings.unavailable }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.emptyText, children: strings.unavailableHint })] })) : ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.card, styles.skeletonCard], children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.skel, { width: '60%' }] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.skel, { width: '85%' }] })] }))) : null, ready && conversations.length > 0 ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.card, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.cardHead, children: strings.yourConversations }), conversations.map((conversation, index) => ((0, jsx_runtime_1.jsx)(ConversationRow, { ui: ui, conversation: conversation, unread: unread[conversation.id] ?? 0, first: index === 0, onOpen: onOpen }, conversation.id)))] })) : null, config?.open === false ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.away, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.awayText, children: config.offline_message || strings.offlineDefault }) })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { accessibilityRole: "button", onPress: onNew, style: ({ pressed }) => [styles.primary, pressed && styles.pressed], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.primaryText, children: strings.sendMessage }), (0, jsx_runtime_1.jsx)(icons_1.ArrowIcon, { color: theme.accentFg, size: 15 })] }), config?.help_center_url ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.card, children: (0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { accessibilityRole: "link", onPress: () => react_native_1.Linking.openURL(config.help_center_url), style: ({ pressed }) => [styles.linkRow, pressed && { backgroundColor: theme.bg3 }], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.linkText, children: strings.searchHelp }), (0, jsx_runtime_1.jsx)(icons_1.ArrowIcon, { color: theme.fg3, size: 14, angle: -45 })] }) })) : null] }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "link", onPress: () => react_native_1.Linking.openURL('https://opendesk.sh'), style: styles.foot, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.footText, children: strings.poweredBy }) })] }));
51
+ }
52
+ const ConversationRow = (0, react_1.memo)(function ConversationRow({ ui, conversation, unread, first, onOpen, }) {
53
+ const { theme, strings, styles } = ui;
54
+ const open = conversation.status === 'open' || conversation.status === 'pending';
55
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { accessibilityRole: "button", onPress: () => onOpen(conversation.id), style: ({ pressed }) => [styles.conv, !first && styles.convBorder, pressed && { backgroundColor: theme.bg3 }], children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.dot, open && { backgroundColor: theme.accent }] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.convMain, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.convTop, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.convSubject, unread > 0 && styles.bold], numberOfLines: 1, children: conversation.subject || strings.conversation }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.time, children: relativeTime(conversation.last_message_at) })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.convTop, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.convPrev, numberOfLines: 1, children: conversation.last_message_preview || ' ' }), unread > 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.unread, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.unreadText, children: unread }) })) : null] })] })] }));
56
+ });
57
+ function Thread({ ui, conversationId, pickAttachment, onBack, onClose, onStarted, }) {
58
+ const { theme, strings, styles } = ui;
59
+ const client = (0, react_2.useOpenDesk)();
60
+ const config = (0, react_2.useOpenDeskState)((state) => state.config);
61
+ const { messages, aiReplies, typing, conversation } = (0, react_2.useConversation)(conversationId);
62
+ const [draft, setDraft] = (0, react_1.useState)('');
63
+ const [attachments, setAttachments] = (0, react_1.useState)([]);
64
+ const [uploadingName, setUploadingName] = (0, react_1.useState)(null);
65
+ const [topicId, setTopicId] = (0, react_1.useState)(null);
66
+ const [sending, setSending] = (0, react_1.useState)(false);
67
+ const [focused, setFocused] = (0, react_1.useState)(false);
68
+ const [inputHeight, setInputHeight] = (0, react_1.useState)(30);
69
+ const [error, setError] = (0, react_1.useState)(null);
70
+ const topics = config?.topics ?? [];
71
+ const topic = topics.find((t) => t.id === topicId) ?? null;
72
+ const needsTopic = conversationId === null && topics.length > 0 && topic === null;
73
+ const name = config?.title || config?.workspace_name || strings.support;
74
+ const lastMessage = messages[messages.length - 1];
75
+ const lastAiIndex = lastIndexWhere(messages, (m) => m.ai === true);
76
+ const handedOff = conversation?.ai_handoff === true && !messages.slice(lastAiIndex + 1).some((m) => m.author.type === 'user');
77
+ const handoffMessageId = conversation && !conversation.ai_handoff && aiReplies.length === 0 && lastMessage?.ai && lastMessage.direction === 'outbound'
78
+ ? lastMessage.id
79
+ : null;
80
+ const rows = (0, react_1.useMemo)(() => buildRows(messages, aiReplies, typing, handedOff, strings), [messages, aiReplies, typing, handedOff, strings]);
81
+ const decide = (0, react_1.useCallback)((message, decision) => {
82
+ if (message.ai_action) {
83
+ client.decideAction(message.ticket_id, message.ai_action.id, decision).catch(() => undefined);
84
+ }
85
+ }, [client]);
86
+ const requestHuman = (0, react_1.useCallback)((message) => {
87
+ client.requestHuman(message.ticket_id).catch(() => setError(strings.handoffFailed));
88
+ }, [client, strings.handoffFailed]);
89
+ const ready = (draft.trim().length > 0 || attachments.length > 0) && !sending && uploadingName === null && !needsTopic;
90
+ const send = (0, react_1.useCallback)(async () => {
91
+ if (!ready) {
92
+ return;
93
+ }
94
+ const body = draft.trim();
95
+ const attachmentIds = attachments.map((attachment) => attachment.id);
96
+ setError(null);
97
+ setDraft('');
98
+ setAttachments([]);
99
+ try {
100
+ if (conversationId) {
101
+ await client.sendMessage(conversationId, { ...(body ? { body } : {}), attachment_ids: attachmentIds });
102
+ }
103
+ else {
104
+ setSending(true);
105
+ const started = await client.startConversation({
106
+ ...(body ? { body } : {}),
107
+ attachment_ids: attachmentIds,
108
+ ...(topicId ? { topic_id: topicId } : {}),
109
+ });
110
+ onStarted(started.id);
111
+ }
112
+ }
113
+ catch (caught) {
114
+ setDraft(body);
115
+ setAttachments(attachments);
116
+ setError(caught instanceof Error ? `${strings.notSent} ${caught.message}` : strings.notSent);
117
+ }
118
+ finally {
119
+ setSending(false);
120
+ }
121
+ }, [attachments, client, conversationId, draft, onStarted, ready, strings.notSent, topicId]);
122
+ const attach = (0, react_1.useCallback)(async () => {
123
+ if (!pickAttachment) {
124
+ return;
125
+ }
126
+ setError(null);
127
+ let file;
128
+ try {
129
+ file = await pickAttachment();
130
+ }
131
+ catch {
132
+ return;
133
+ }
134
+ if (!file) {
135
+ return;
136
+ }
137
+ const fileName = 'name' in file && typeof file.name === 'string' ? file.name : 'file';
138
+ const size = 'size' in file && typeof file.size === 'number' ? file.size : undefined;
139
+ const allowed = config?.attachment_extensions;
140
+ const dot = fileName.lastIndexOf('.');
141
+ if (allowed && allowed.length > 0 && (dot < 1 || !allowed.includes(fileName.slice(dot).toLowerCase()))) {
142
+ setError((0, strings_1.format)(strings.fileTypeRefused, { name: fileName }));
143
+ return;
144
+ }
145
+ if (size !== undefined && size > MAX_UPLOAD_BYTES) {
146
+ setError((0, strings_1.format)(strings.fileTooLarge, { name: fileName }));
147
+ return;
148
+ }
149
+ setUploadingName(fileName);
150
+ try {
151
+ const uploaded = await client.upload(file);
152
+ setAttachments((current) => [...current, uploaded]);
153
+ }
154
+ catch (caught) {
155
+ setError(caught instanceof Error && caught.message ? caught.message : (0, strings_1.format)(strings.uploadFailed, { name: fileName }));
156
+ }
157
+ finally {
158
+ setUploadingName(null);
159
+ }
160
+ }, [client, config?.attachment_extensions, pickAttachment, strings]);
161
+ const showAttach = Boolean(pickAttachment) && config?.attachments !== false && (config?.attachment_extensions?.length ?? 1) > 0;
162
+ const bot = config?.ai_answers ? config.bot : null;
163
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.flex, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.head, children: [(0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.back, onPress: onBack, icon: (0, jsx_runtime_1.jsx)(icons_1.ChevronIcon, { color: theme.fg3, direction: "left" }) }), bot ? ((0, jsx_runtime_1.jsx)(BotFace_1.BotFace, { size: 26, look: bot.look, avatarUrl: bot.avatar_url })) : ((0, jsx_runtime_1.jsx)(Glyph, { ui: ui, name: name, url: config?.avatar_url ?? null })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.headMain, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.title, numberOfLines: 1, children: conversation?.subject || name }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.titleSub, numberOfLines: 1, children: subtitleFor(config, strings) })] }), onClose ? (0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.closeChat, onPress: onClose, icon: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3 }) }) : null] }), rows.length === 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { style: styles.flex, contentContainerStyle: styles.emptyScroll, keyboardShouldPersistTaps: "handled", children: needsTopic ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.card, styles.picker], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.pickerTitle, children: strings.topicsTitle }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.pickerHint, children: strings.topicsHint }), topics.map((item, index) => ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { accessibilityRole: "button", onPress: () => setTopicId(item.id), style: ({ pressed }) => [styles.topic, index > 0 && styles.convBorder, pressed && { backgroundColor: theme.bg2 }], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.topicText, children: item.label }), (0, jsx_runtime_1.jsx)(icons_1.ChevronIcon, { color: theme.fg4, size: 14, direction: "right" })] }, item.id)))] })) : ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.empty, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.emptyTitle, children: config?.greeting || strings.greeting }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.emptyText, children: config?.intro || strings.emptyHint })] })) })) : ((0, jsx_runtime_1.jsx)(react_native_1.FlatList, { inverted: true, style: styles.flex, data: rows, keyExtractor: (row) => row.key, contentContainerStyle: styles.thread, keyboardShouldPersistTaps: "handled", renderItem: ({ item }) => ((0, jsx_runtime_1.jsx)(ThreadRow, { ui: ui, row: item, showHandoff: item.kind === 'message' && item.message.id === handoffMessageId, onDecide: decide, onRequestHuman: requestHuman })) })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.composer, children: [topic && conversationId === null ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.topicRow, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.topicChip, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.topicChipText, numberOfLines: 1, children: topic.label }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: strings.changeTopic, onPress: () => setTopicId(null), style: styles.topicChipX, children: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3, size: 10 }) })] }) })) : null, attachments.length > 0 || uploadingName ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.pendingFiles, children: [attachments.map((attachment) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.chip, styles.pendingChip], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipText, numberOfLines: 1, children: attachment.filename }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: (0, strings_1.format)(strings.removeFile, { name: attachment.filename }), onPress: () => setAttachments((current) => current.filter((a) => a.id !== attachment.id)), hitSlop: 8, children: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3, size: 10 }) })] }, attachment.id))), uploadingName ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.chip, styles.pendingChip], children: [(0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { size: "small", color: theme.fg3, style: styles.chipSpinner }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipText, numberOfLines: 1, children: uploadingName })] })) : null] })) : null, error ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.err, children: error }) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.cbar, showAttach && styles.cbarLead, focused && styles.cbarFocus, needsTopic && styles.cbarOff], children: [showAttach ? ((0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.attach, onPress: attach, disabled: uploadingName !== null || needsTopic, icon: (0, jsx_runtime_1.jsx)(icons_1.PaperclipIcon, { color: theme.fg3 }) })) : null, (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { style: [styles.input, { height: inputHeight }], value: draft, onContentSizeChange: (event) => setInputHeight(Math.min(120, Math.max(30, Math.ceil(event.nativeEvent.contentSize.height)))), ...(react_native_1.Platform.OS === 'web' ? { rows: 1 } : {}), editable: !needsTopic, onFocus: () => setFocused(true), onBlur: () => setFocused(false), onChangeText: (text) => {
164
+ setDraft(text);
165
+ if (conversationId) {
166
+ client.typing(conversationId);
167
+ }
168
+ }, placeholder: needsTopic ? strings.placeholderTopic : strings.placeholder, placeholderTextColor: theme.fg4, accessibilityLabel: strings.placeholder, multiline: true }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: strings.send, disabled: !ready, onPress: send, style: [styles.send, ready && styles.sendReady], children: (0, jsx_runtime_1.jsx)(icons_1.ArrowIcon, { color: theme.accentFg, size: 14, angle: -90 }) })] })] })] }));
169
+ }
170
+ const ThreadRow = (0, react_1.memo)(function ThreadRow({ ui, row, showHandoff, onDecide, onRequestHuman, }) {
171
+ const { theme, strings, styles } = ui;
172
+ switch (row.kind) {
173
+ case 'day':
174
+ return (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.day, children: row.label });
175
+ case 'typing':
176
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.typing, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.typingBubble, children: (0, jsx_runtime_1.jsx)(Dots, { color: theme.fg3 }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.typingText, children: (0, strings_1.format)(strings.typing, { name: row.name }) })] }));
177
+ case 'handed':
178
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.handed, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.handedIc, children: (0, jsx_runtime_1.jsx)(icons_1.PersonIcon, { color: theme.fg2 }) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.flex, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.handedTitle, children: strings.handedOff }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.handedText, children: strings.handedOffHint })] })] }));
179
+ case 'reply':
180
+ return (0, jsx_runtime_1.jsx)(Bubble, { ui: ui, reply: row.reply, first: true, last: true, gap: true, showHandoff: false, onDecide: onDecide, onRequestHuman: onRequestHuman });
181
+ case 'message':
182
+ return ((0, jsx_runtime_1.jsx)(Bubble, { ui: ui, message: row.message, first: row.first, last: row.last, gap: row.gap, showHandoff: showHandoff, onDecide: onDecide, onRequestHuman: onRequestHuman }));
183
+ }
184
+ });
185
+ /**
186
+ * One bubble for a stored message or an AI answer still streaming. Both share one element
187
+ * tree, so when the stored message replaces the stream (same row key) nothing remounts.
188
+ */
189
+ function Bubble({ ui, message, reply, first, last, gap, showHandoff, onDecide, onRequestHuman, }) {
190
+ const { theme, strings, styles, bot: fallbackBot } = ui;
191
+ const mine = message?.direction === 'inbound';
192
+ const ai = reply !== undefined || message?.ai === true;
193
+ const author = message ? message.author.user?.name || message.author.bot?.name : reply?.bot?.name;
194
+ const bot = message ? message.author.bot : reply?.bot;
195
+ const avatar = message ? (message.author.user?.avatar_url ?? bot?.avatar_url) : bot?.avatar_url;
196
+ const botLook = bot?.look ?? fallbackBot?.look;
197
+ const text = message ? (message.body_text ?? '') : (reply?.text ?? '');
198
+ const local = message?.id.startsWith('local-') === true;
199
+ const action = message?.ai_action;
200
+ const tools = reply?.tools ?? [];
201
+ const corners = mine
202
+ ? { borderBottomRightRadius: 4, ...(first ? {} : { borderTopRightRadius: 4 }) }
203
+ : { borderBottomLeftRadius: 4, ...(first ? {} : { borderTopLeftRadius: 4 }) };
204
+ const images = (message?.attachments ?? []).filter((a) => a.content_type.startsWith('image/'));
205
+ const files = (message?.attachments ?? []).filter((a) => !a.content_type.startsWith('image/'));
206
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.row, mine ? styles.rowMe : styles.rowThem, gap && styles.rowGap], children: [!mine && first && author ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.who, children: [ai ? ((0, jsx_runtime_1.jsx)(BotFace_1.BotFace, { size: 18, look: botLook, avatarUrl: avatar })) : avatar ? ((0, jsx_runtime_1.jsx)(Avatar, { ui: ui, size: 18, name: author, url: avatar, tint: theme.bg2 })) : null, (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.whoName, children: author }), ai ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.aiPill, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.aiPillText, children: strings.ai }) })) : null] })) : null, tools.length > 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.tools, children: tools.map((tool, index) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.tool, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.toolIc, tool.status === 'error' && { backgroundColor: theme.bg2 }], children: (0, jsx_runtime_1.jsx)(icons_1.SparkIcon, { color: tool.status === 'error' ? theme.fg3 : theme.ai, size: 8 }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.toolText, tool.status === 'running' && { color: theme.fg2 }], children: (0, strings_1.format)(tool.status === 'running' ? strings.checking : tool.status === 'error' ? strings.toolFailed : strings.checked, {
207
+ tool: tool.name.replace(/_/g, ' '),
208
+ }) })] }, `${tool.name}-${index}`))) })) : null, text || reply ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
209
+ styles.msg,
210
+ mine ? styles.msgMe : ai ? styles.msgAi : styles.msgThem,
211
+ corners,
212
+ local && styles.sendingMsg,
213
+ ], children: text ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.msgText, mine && { color: theme.accentFg }], children: text }) : (0, jsx_runtime_1.jsx)(Dots, { color: theme.ai }) })) : null, images.map((attachment) => ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: () => react_native_1.Linking.openURL(attachment.url), style: styles.fileGap, children: (0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: attachment.url }, style: styles.thumb, accessibilityLabel: attachment.filename, resizeMode: "cover" }) }, attachment.id))), files.map((attachment) => ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: () => react_native_1.Linking.openURL(attachment.url), style: [styles.chip, styles.fileGap], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipText, numberOfLines: 1, children: attachment.filename }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipSize, children: fileSize(attachment.size_bytes) })] }, attachment.id))), message && action ? (0, jsx_runtime_1.jsx)(ActionCard, { ui: ui, message: message, onDecide: onDecide }) : null, message && showHandoff ? ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { accessibilityRole: "button", onPress: () => onRequestHuman(message), style: ({ pressed }) => [styles.handoff, pressed && { backgroundColor: theme.bg2 }], children: [(0, jsx_runtime_1.jsx)(icons_1.PersonIcon, { color: theme.fg, size: 14 }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.handoffText, children: strings.talkToPerson })] })) : null, last && message ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.meta, children: local ? strings.sending : clockTime(message.created_at) }) : null] }));
214
+ }
215
+ function ActionCard({ ui, message, onDecide, }) {
216
+ const { theme, strings, styles } = ui;
217
+ const action = message.ai_action;
218
+ const pending = action.status === 'pending';
219
+ const label = {
220
+ pending: strings.actionPending,
221
+ confirmed: strings.actionConfirmed,
222
+ declined: strings.actionDeclined,
223
+ expired: strings.actionExpired,
224
+ failed: strings.actionFailed,
225
+ }[action.status];
226
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.action, pending && styles.actionPending], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.actionTop, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.actionTool, children: [(0, jsx_runtime_1.jsx)(icons_1.SparkIcon, { color: theme.ai, size: 8 }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.actionToolText, numberOfLines: 1, children: action.tool.replace(/_/g, ' ') })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.actionNeed, action.status === 'failed' && { color: theme.danger }], children: label })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.actionTitle, children: action.title }), action.summary ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.actionSum, children: action.summary }) : null, pending ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.actionBtns, children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: () => onDecide(message, 'decline'), style: [styles.btn, styles.btnGhost], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.btnGhostText, children: strings.cancel }) }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: () => onDecide(message, 'confirm'), style: [styles.btn, styles.btnSolid], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.btnSolidText, children: strings.confirm }) })] })) : null] }));
227
+ }
228
+ function IconButton({ ui, label, onPress, icon, disabled, }) {
229
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: onPress, disabled: disabled, hitSlop: 6, style: ({ pressed }) => [ui.styles.iconBtn, pressed && { backgroundColor: ui.theme.bg2 }, disabled && ui.styles.disabled], children: icon }));
230
+ }
231
+ function Glyph({ ui, name, url }) {
232
+ const { styles } = ui;
233
+ return url ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: url }, style: styles.wsGlyph })) : ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.wsGlyph, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.wsGlyphText, children: name.trim().charAt(0).toUpperCase() }) }));
234
+ }
235
+ function Avatar({ ui, size, name, url, round, tint, style, }) {
236
+ const radius = round ? size / 2 : Math.min(ui.theme.radius.av, size / 2);
237
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
238
+ { width: size, height: size, borderRadius: radius, backgroundColor: tint, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' },
239
+ style,
240
+ ], children: url ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: url }, style: { width: size, height: size } })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { fontSize: size * 0.4, fontWeight: '600', color: ui.theme.fg2 }, children: initials(name) })) }));
241
+ }
242
+ function AvatarStack({ ui, config }) {
243
+ const { theme } = ui;
244
+ const ring = { borderWidth: 2, borderColor: theme.headBg };
245
+ const team = (config?.team ?? []).slice(0, 3);
246
+ const bot = config?.ai_answers ? config.bot : null;
247
+ if (!bot && team.length === 0) {
248
+ return null;
249
+ }
250
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flexDirection: 'row', alignItems: 'center' }, children: [bot ? (0, jsx_runtime_1.jsx)(BotFace_1.BotFace, { size: 22, look: bot.look, avatarUrl: bot.avatar_url }) : null, team.map((member, index) => ((0, jsx_runtime_1.jsx)(Avatar, { ui: ui, size: 22, name: member.name, url: member.avatar_url, tint: theme.bg2, style: [ring, { marginLeft: index === 0 && bot ? 1 : -7 }] }, `${member.name}-${index}`)))] }));
251
+ }
252
+ /** Three dots that pulse in turn, like the widget's typing indicator. */
253
+ function Dots({ color }) {
254
+ const values = (0, react_1.useRef)([0, 1, 2].map(() => new react_native_1.Animated.Value(0.3))).current;
255
+ (0, react_1.useEffect)(() => {
256
+ const native = react_native_1.Platform.OS !== 'web';
257
+ const loop = react_native_1.Animated.loop(react_native_1.Animated.stagger(160, values.map((value) => react_native_1.Animated.sequence([
258
+ react_native_1.Animated.timing(value, { toValue: 1, duration: 280, useNativeDriver: native }),
259
+ react_native_1.Animated.timing(value, { toValue: 0.3, duration: 280, useNativeDriver: native }),
260
+ ]))));
261
+ loop.start();
262
+ return () => loop.stop();
263
+ }, [values]);
264
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexDirection: 'row', alignItems: 'center', height: 19.5, gap: 3 }, children: values.map((opacity, index) => ((0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: { width: 5, height: 5, borderRadius: 3, backgroundColor: color, opacity } }, index))) }));
265
+ }
266
+ /** Newest first for the inverted list: typing, streaming answers, then messages grouped by author with day labels. */
267
+ function buildRows(messages, aiReplies, typing, handedOff, strings) {
268
+ const rows = [];
269
+ const authorKey = (m) => m.direction === 'inbound' ? 'me' : `${m.author.type}:${m.author.user?.id ?? m.author.bot?.id ?? ''}:${m.ai ? 'ai' : ''}`;
270
+ messages.forEach((message, index) => {
271
+ const previous = messages[index - 1];
272
+ const next = messages[index + 1];
273
+ const day = dayLabel(message.created_at, strings);
274
+ const newDay = !previous || dayLabel(previous.created_at, strings) !== day;
275
+ if (newDay) {
276
+ rows.push({ kind: 'day', key: `day-${day}`, label: day });
277
+ }
278
+ const startsGroup = newDay || authorKey(previous) !== authorKey(message) || Date.parse(message.created_at) - Date.parse(previous.created_at) > GROUP_GAP_MS;
279
+ const endsGroup = !next ||
280
+ authorKey(next) !== authorKey(message) ||
281
+ Date.parse(next.created_at) - Date.parse(message.created_at) > GROUP_GAP_MS ||
282
+ dayLabel(next.created_at, strings) !== day;
283
+ rows.push({
284
+ kind: 'message',
285
+ key: message.client_id ?? message.id,
286
+ message,
287
+ first: startsGroup,
288
+ last: endsGroup,
289
+ gap: startsGroup && !newDay,
290
+ });
291
+ });
292
+ if (handedOff) {
293
+ rows.push({ kind: 'handed', key: 'handed' });
294
+ }
295
+ aiReplies.forEach((reply) => rows.push({ kind: 'reply', key: `ai-${reply.id}`, reply }));
296
+ if (typing) {
297
+ rows.push({ kind: 'typing', key: 'typing', name: typing });
298
+ }
299
+ return rows.reverse();
300
+ }
301
+ function lastIndexWhere(items, predicate) {
302
+ for (let i = items.length - 1; i >= 0; i--) {
303
+ if (predicate(items[i])) {
304
+ return i;
305
+ }
306
+ }
307
+ return -1;
308
+ }
309
+ function initials(name) {
310
+ return name
311
+ .split(/\s+/)
312
+ .filter(Boolean)
313
+ .slice(0, 2)
314
+ .map((part) => part.charAt(0).toUpperCase())
315
+ .join('');
316
+ }
317
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
318
+ function dayLabel(iso, strings) {
319
+ const date = new Date(iso);
320
+ const today = new Date();
321
+ const yesterday = new Date(today.getTime() - 86400000);
322
+ if (date.toDateString() === today.toDateString()) {
323
+ return strings.today;
324
+ }
325
+ if (date.toDateString() === yesterday.toDateString()) {
326
+ return strings.yesterday;
327
+ }
328
+ return `${months[date.getMonth()]} ${date.getDate()}`;
329
+ }
330
+ function clockTime(iso) {
331
+ const date = new Date(iso);
332
+ const hours = date.getHours();
333
+ return `${hours % 12 || 12}:${String(date.getMinutes()).padStart(2, '0')} ${hours < 12 ? 'AM' : 'PM'}`;
334
+ }
335
+ function relativeTime(iso) {
336
+ const seconds = Math.round((Date.now() - Date.parse(iso)) / 1000);
337
+ if (!Number.isFinite(seconds)) {
338
+ return '';
339
+ }
340
+ if (seconds < 45) {
341
+ return 'now';
342
+ }
343
+ const minutes = Math.round(seconds / 60);
344
+ if (minutes < 60) {
345
+ return `${minutes}m ago`;
346
+ }
347
+ const hours = Math.round(minutes / 60);
348
+ if (hours < 24) {
349
+ return `${hours}h ago`;
350
+ }
351
+ const days = Math.round(hours / 24);
352
+ if (days < 7) {
353
+ return `${days}d ago`;
354
+ }
355
+ const date = new Date(iso);
356
+ return `${months[date.getMonth()]} ${date.getDate()}`;
357
+ }
358
+ function fileSize(bytes) {
359
+ if (bytes < 1024) {
360
+ return `${bytes} B`;
361
+ }
362
+ if (bytes < 1024 * 1024) {
363
+ return `${Math.round(bytes / 1024)} KB`;
364
+ }
365
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
366
+ }
367
+ function createStyles(t) {
368
+ const r = t.radius;
369
+ return react_native_1.StyleSheet.create({
370
+ root: { flex: 1, backgroundColor: t.bg },
371
+ flex: { flex: 1 },
372
+ bold: { fontWeight: '600' },
373
+ pressed: { opacity: 0.92, transform: [{ scale: 0.985 }] },
374
+ disabled: { opacity: 0.4 },
375
+ head: {
376
+ flexDirection: 'row',
377
+ alignItems: 'center',
378
+ gap: 10,
379
+ paddingHorizontal: 12,
380
+ paddingTop: 12,
381
+ paddingBottom: 11,
382
+ borderBottomWidth: 1,
383
+ borderBottomColor: t.line,
384
+ backgroundColor: t.bg,
385
+ },
386
+ headHome: { flexDirection: 'column', alignItems: 'stretch', gap: 0, padding: 16, paddingBottom: 20, backgroundColor: t.headBg },
387
+ headRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 },
388
+ headMain: { flex: 1, minWidth: 0 },
389
+ ws: { flexDirection: 'row', alignItems: 'center', gap: 9, flexShrink: 1 },
390
+ wsGlyph: {
391
+ width: 26,
392
+ height: 26,
393
+ borderRadius: Math.min(r.av, 13),
394
+ backgroundColor: t.accent,
395
+ alignItems: 'center',
396
+ justifyContent: 'center',
397
+ overflow: 'hidden',
398
+ },
399
+ wsGlyphText: { color: t.accentFg, fontSize: 12, fontWeight: '700' },
400
+ wsName: { fontSize: 13.5, fontWeight: '600', letterSpacing: -0.14, color: t.fg, flexShrink: 1 },
401
+ greet: { marginTop: 18, fontSize: 22, lineHeight: 28, fontWeight: '600', letterSpacing: -0.55, color: t.fg },
402
+ subRow: { marginTop: 8, flexDirection: 'row', alignItems: 'center', gap: 8 },
403
+ sub: { flex: 1, color: t.fg3, fontSize: 13, lineHeight: 18 },
404
+ title: { fontSize: 14, fontWeight: '600', letterSpacing: -0.14, color: t.fg },
405
+ titleSub: { fontSize: 12, color: t.fg3, marginTop: 1 },
406
+ iconBtn: { width: 30, height: 30, borderRadius: Math.min(r.ctl, 15), alignItems: 'center', justifyContent: 'center' },
407
+ homeBody: { padding: 16, gap: 12 },
408
+ card: { borderWidth: 1, borderColor: t.line, borderRadius: r.card, backgroundColor: t.bg, overflow: 'hidden' },
409
+ cardHead: {
410
+ paddingHorizontal: 14,
411
+ paddingTop: 12,
412
+ paddingBottom: 6,
413
+ fontSize: 11,
414
+ fontWeight: '600',
415
+ letterSpacing: 0.66,
416
+ textTransform: 'uppercase',
417
+ color: t.fg4,
418
+ },
419
+ conv: { flexDirection: 'row', alignItems: 'flex-start', gap: 10, paddingVertical: 10, paddingHorizontal: 14 },
420
+ convBorder: { borderTopWidth: 1, borderTopColor: t.line },
421
+ dot: { width: 7, height: 7, borderRadius: 4, marginTop: 6, backgroundColor: t.line2 },
422
+ convMain: { flex: 1, minWidth: 0 },
423
+ convTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8 },
424
+ convSubject: { flex: 1, fontSize: 13, fontWeight: '500', color: t.fg },
425
+ convPrev: { flex: 1, fontSize: 12.5, color: t.fg3, marginTop: 1 },
426
+ time: { fontFamily: t.mono, fontSize: 10.5, color: t.fg4 },
427
+ unread: { minWidth: 18, height: 18, borderRadius: 9, paddingHorizontal: 5, backgroundColor: t.danger, alignItems: 'center', justifyContent: 'center' },
428
+ unreadText: { color: '#ffffff', fontSize: 10.5, fontWeight: '600' },
429
+ primary: {
430
+ height: 44,
431
+ borderRadius: r.ctl,
432
+ backgroundColor: t.accent,
433
+ flexDirection: 'row',
434
+ alignItems: 'center',
435
+ justifyContent: 'space-between',
436
+ paddingHorizontal: 16,
437
+ shadowColor: '#000000',
438
+ shadowOpacity: 0.08,
439
+ shadowRadius: 2,
440
+ shadowOffset: { width: 0, height: 1 },
441
+ elevation: 1,
442
+ },
443
+ primaryText: { color: t.accentFg, fontSize: 13.5, fontWeight: '500' },
444
+ linkRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, paddingHorizontal: 14 },
445
+ linkText: { fontSize: 13, fontWeight: '500', color: t.fg },
446
+ away: { borderWidth: 1, borderColor: t.line, borderRadius: r.card, backgroundColor: t.bg3, padding: 12 },
447
+ awayText: { fontSize: 12.5, lineHeight: 18, color: t.fg2 },
448
+ foot: { height: 30, alignItems: 'center', justifyContent: 'center', borderTopWidth: 1, borderTopColor: t.line },
449
+ footText: { fontSize: 11, color: t.fg4 },
450
+ skeletonCard: { paddingVertical: 4 },
451
+ skel: { height: 12, borderRadius: 4, marginVertical: 10, marginHorizontal: 14, backgroundColor: t.bg2 },
452
+ emptyScroll: { flexGrow: 1 },
453
+ empty: { paddingVertical: 32, paddingHorizontal: 24, alignItems: 'center' },
454
+ emptyTitle: { fontSize: 14, fontWeight: '600', color: t.fg, marginBottom: 4, textAlign: 'center' },
455
+ emptyText: { fontSize: 13, lineHeight: 19, color: t.fg3, textAlign: 'center' },
456
+ picker: { margin: 14 },
457
+ pickerTitle: { paddingHorizontal: 14, paddingTop: 14, fontSize: 14, fontWeight: '600', color: t.fg },
458
+ pickerHint: { paddingHorizontal: 14, marginTop: 2, marginBottom: 12, fontSize: 12.5, lineHeight: 18, color: t.fg3 },
459
+ topic: {
460
+ minHeight: 42,
461
+ flexDirection: 'row',
462
+ alignItems: 'center',
463
+ justifyContent: 'space-between',
464
+ gap: 10,
465
+ paddingVertical: 10,
466
+ paddingHorizontal: 14,
467
+ },
468
+ topicText: { flex: 1, fontSize: 13.5, lineHeight: 19, fontWeight: '500', color: t.fg },
469
+ thread: { paddingTop: 16, paddingHorizontal: 14, paddingBottom: 8 },
470
+ day: { alignSelf: 'center', fontFamily: t.mono, fontSize: 10.5, color: t.fg4, marginTop: 10, marginBottom: 6 },
471
+ row: { maxWidth: '84%', marginTop: 2 },
472
+ rowMe: { alignSelf: 'flex-end', alignItems: 'flex-end' },
473
+ rowThem: { alignSelf: 'flex-start', alignItems: 'flex-start' },
474
+ rowGap: { marginTop: 12 },
475
+ who: { flexDirection: 'row', alignItems: 'center', gap: 6, marginHorizontal: 2, marginBottom: 4 },
476
+ whoName: { fontSize: 11.5, fontWeight: '500', color: t.fg2 },
477
+ aiPill: { height: 16, paddingHorizontal: 5, borderRadius: 4, backgroundColor: t.aiSoft, justifyContent: 'center' },
478
+ aiPillText: { color: t.ai, fontSize: 9.5, fontWeight: '600', letterSpacing: 0.38 },
479
+ msg: { paddingVertical: 8, paddingHorizontal: 12, maxWidth: '100%', borderRadius: r.bubble },
480
+ msgMe: { backgroundColor: t.accent },
481
+ msgThem: { backgroundColor: t.bg2 },
482
+ msgAi: { backgroundColor: t.bg3, borderWidth: 1, borderColor: t.line },
483
+ msgText: { fontSize: 13.5, lineHeight: 19.5, color: t.fg },
484
+ sendingMsg: { opacity: 0.7 },
485
+ meta: { fontFamily: t.mono, fontSize: 10, color: t.fg4, marginTop: 3, marginHorizontal: 4 },
486
+ tools: { gap: 4, marginBottom: 6 },
487
+ tool: { flexDirection: 'row', alignItems: 'center', gap: 7 },
488
+ toolIc: { width: 16, height: 16, borderRadius: 8, backgroundColor: t.aiSoft, alignItems: 'center', justifyContent: 'center' },
489
+ toolText: { fontSize: 12, lineHeight: 18, color: t.fg3 },
490
+ fileGap: { marginTop: 4 },
491
+ thumb: { width: 220, height: 160, borderRadius: r.card, borderWidth: 1, borderColor: t.line, backgroundColor: t.bg2 },
492
+ chip: {
493
+ flexDirection: 'row',
494
+ alignItems: 'center',
495
+ gap: 6,
496
+ maxWidth: 240,
497
+ height: 30,
498
+ paddingHorizontal: 10,
499
+ borderRadius: r.ctl,
500
+ borderWidth: 1,
501
+ borderColor: t.line,
502
+ backgroundColor: t.bg,
503
+ },
504
+ chipText: { flexShrink: 1, fontSize: 12, color: t.fg },
505
+ chipSize: { fontFamily: t.mono, fontSize: 10.5, color: t.fg4 },
506
+ chipSpinner: { transform: [{ scale: 0.6 }], width: 14, height: 14 },
507
+ handoff: {
508
+ marginTop: 6,
509
+ flexDirection: 'row',
510
+ alignItems: 'center',
511
+ gap: 6,
512
+ height: 30,
513
+ paddingHorizontal: 11,
514
+ borderRadius: r.ctl,
515
+ borderWidth: 1,
516
+ borderColor: t.line2,
517
+ backgroundColor: t.bg,
518
+ },
519
+ handoffText: { fontSize: 12.5, fontWeight: '500', color: t.fg },
520
+ handed: {
521
+ marginTop: 6,
522
+ marginBottom: 14,
523
+ flexDirection: 'row',
524
+ alignItems: 'flex-start',
525
+ gap: 10,
526
+ paddingVertical: 11,
527
+ paddingHorizontal: 12,
528
+ borderRadius: r.card,
529
+ backgroundColor: t.bg3,
530
+ borderWidth: 1,
531
+ borderColor: t.line,
532
+ },
533
+ handedIc: {
534
+ width: 28,
535
+ height: 28,
536
+ borderRadius: Math.min(r.av, 14),
537
+ backgroundColor: t.bg,
538
+ borderWidth: 1,
539
+ borderColor: t.line,
540
+ alignItems: 'center',
541
+ justifyContent: 'center',
542
+ },
543
+ handedTitle: { fontSize: 13, lineHeight: 18, fontWeight: '600', color: t.fg },
544
+ handedText: { fontSize: 12, lineHeight: 17, color: t.fg3 },
545
+ typing: { alignSelf: 'flex-start', flexDirection: 'row', alignItems: 'center', gap: 8, paddingTop: 10, paddingBottom: 4 },
546
+ typingBubble: {
547
+ paddingHorizontal: 12,
548
+ height: 36,
549
+ justifyContent: 'center',
550
+ borderRadius: r.bubble,
551
+ borderBottomLeftRadius: 4,
552
+ backgroundColor: t.bg2,
553
+ },
554
+ typingText: { fontSize: 12, color: t.fg3 },
555
+ action: {
556
+ marginTop: 6,
557
+ width: 272,
558
+ maxWidth: '100%',
559
+ paddingTop: 11,
560
+ paddingHorizontal: 12,
561
+ paddingBottom: 12,
562
+ borderRadius: r.card,
563
+ borderWidth: 1,
564
+ borderColor: t.line,
565
+ backgroundColor: t.bg,
566
+ },
567
+ actionPending: { borderColor: t.aiLine },
568
+ actionTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8, minHeight: 20 },
569
+ actionTool: { flexDirection: 'row', alignItems: 'center', gap: 5, flexShrink: 1 },
570
+ actionToolText: { fontSize: 11.5, fontWeight: '500', color: t.ai },
571
+ actionNeed: { fontSize: 11, color: t.fg4 },
572
+ actionTitle: { marginTop: 6, fontSize: 13.5, lineHeight: 19, fontWeight: '600', letterSpacing: -0.14, color: t.fg },
573
+ actionSum: { marginTop: 2, fontSize: 12.5, lineHeight: 18, color: t.fg2 },
574
+ actionBtns: { flexDirection: 'row', justifyContent: 'flex-end', gap: 6, marginTop: 10 },
575
+ btn: { height: 30, paddingHorizontal: 12, borderRadius: r.ctl, alignItems: 'center', justifyContent: 'center' },
576
+ btnSolid: { backgroundColor: t.accent },
577
+ btnSolidText: { color: t.accentFg, fontSize: 12.5, fontWeight: '500' },
578
+ btnGhost: { borderWidth: 1, borderColor: t.line2 },
579
+ btnGhostText: { color: t.fg, fontSize: 12.5, fontWeight: '500' },
580
+ composer: { paddingTop: 10, paddingHorizontal: 12, paddingBottom: 12, gap: 6, backgroundColor: t.bg },
581
+ topicRow: { alignItems: 'center' },
582
+ topicChip: {
583
+ flexDirection: 'row',
584
+ alignItems: 'center',
585
+ gap: 4,
586
+ height: 26,
587
+ paddingLeft: 10,
588
+ paddingRight: 4,
589
+ borderRadius: 999,
590
+ backgroundColor: t.bg2,
591
+ borderWidth: 1,
592
+ borderColor: t.line,
593
+ },
594
+ topicChipText: { fontSize: 12, fontWeight: '500', color: t.fg },
595
+ topicChipX: { width: 20, height: 20, borderRadius: 10, alignItems: 'center', justifyContent: 'center' },
596
+ pendingFiles: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 },
597
+ pendingChip: { height: 26 },
598
+ err: { color: t.danger, fontSize: 12 },
599
+ cbar: {
600
+ flexDirection: 'row',
601
+ alignItems: 'flex-end',
602
+ gap: 4,
603
+ padding: 5,
604
+ paddingLeft: 12,
605
+ borderWidth: 1,
606
+ borderColor: t.line,
607
+ borderRadius: r.bar,
608
+ backgroundColor: t.bg,
609
+ },
610
+ cbarLead: { paddingLeft: 5 },
611
+ cbarFocus: { borderColor: t.fg4 },
612
+ cbarOff: { backgroundColor: t.bg3 },
613
+ input: {
614
+ flex: 1,
615
+ minHeight: 30,
616
+ maxHeight: 120,
617
+ paddingVertical: 5,
618
+ paddingHorizontal: 0,
619
+ fontSize: 14,
620
+ lineHeight: 20,
621
+ color: t.fg,
622
+ ...(react_native_1.Platform.OS === 'web' ? { outlineStyle: 'none' } : {}),
623
+ },
624
+ send: {
625
+ width: 30,
626
+ height: 30,
627
+ borderRadius: Math.min(r.ctl, 15),
628
+ alignItems: 'center',
629
+ justifyContent: 'center',
630
+ backgroundColor: t.accent,
631
+ opacity: 0.28,
632
+ transform: [{ scale: 0.9 }],
633
+ },
634
+ sendReady: { opacity: 1, transform: [{ scale: 1 }] },
635
+ });
636
+ }