@mobileai/react-native 0.9.9 → 0.9.11

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.
Files changed (74) hide show
  1. package/README.md +11 -0
  2. package/lib/module/components/AIAgent.js +513 -36
  3. package/lib/module/components/AIAgent.js.map +1 -1
  4. package/lib/module/components/AgentChatBar.js +320 -13
  5. package/lib/module/components/AgentChatBar.js.map +1 -1
  6. package/lib/module/config/endpoints.js +22 -0
  7. package/lib/module/config/endpoints.js.map +1 -0
  8. package/lib/module/core/systemPrompt.js +126 -100
  9. package/lib/module/core/systemPrompt.js.map +1 -1
  10. package/lib/module/services/AudioInputService.js +9 -0
  11. package/lib/module/services/AudioInputService.js.map +1 -1
  12. package/lib/module/services/flags/FlagService.js +1 -1
  13. package/lib/module/services/flags/FlagService.js.map +1 -1
  14. package/lib/module/services/telemetry/TelemetryService.js +39 -13
  15. package/lib/module/services/telemetry/TelemetryService.js.map +1 -1
  16. package/lib/module/services/telemetry/device.js +80 -10
  17. package/lib/module/services/telemetry/device.js.map +1 -1
  18. package/lib/module/support/EscalationSocket.js +46 -7
  19. package/lib/module/support/EscalationSocket.js.map +1 -1
  20. package/lib/module/support/SupportChatModal.js +516 -0
  21. package/lib/module/support/SupportChatModal.js.map +1 -0
  22. package/lib/module/support/TicketStore.js +93 -0
  23. package/lib/module/support/TicketStore.js.map +1 -0
  24. package/lib/module/support/escalateTool.js +39 -13
  25. package/lib/module/support/escalateTool.js.map +1 -1
  26. package/lib/module/support/index.js.map +1 -1
  27. package/lib/typescript/src/components/AIAgent.d.ts +24 -1
  28. package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
  29. package/lib/typescript/src/components/AgentChatBar.d.ts +24 -2
  30. package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
  31. package/lib/typescript/src/config/endpoints.d.ts +18 -0
  32. package/lib/typescript/src/config/endpoints.d.ts.map +1 -0
  33. package/lib/typescript/src/core/systemPrompt.d.ts +4 -13
  34. package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
  35. package/lib/typescript/src/core/types.d.ts +1 -1
  36. package/lib/typescript/src/core/types.d.ts.map +1 -1
  37. package/lib/typescript/src/hooks/useAction.d.ts +2 -2
  38. package/lib/typescript/src/hooks/useAction.d.ts.map +1 -1
  39. package/lib/typescript/src/index.d.ts +1 -1
  40. package/lib/typescript/src/index.d.ts.map +1 -1
  41. package/lib/typescript/src/services/AudioInputService.d.ts.map +1 -1
  42. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +1 -1
  43. package/lib/typescript/src/services/telemetry/device.d.ts +15 -4
  44. package/lib/typescript/src/services/telemetry/device.d.ts.map +1 -1
  45. package/lib/typescript/src/support/EscalationSocket.d.ts +7 -1
  46. package/lib/typescript/src/support/EscalationSocket.d.ts.map +1 -1
  47. package/lib/typescript/src/support/SupportChatModal.d.ts +19 -0
  48. package/lib/typescript/src/support/SupportChatModal.d.ts.map +1 -0
  49. package/lib/typescript/src/support/TicketStore.d.ts +34 -0
  50. package/lib/typescript/src/support/TicketStore.d.ts.map +1 -0
  51. package/lib/typescript/src/support/escalateTool.d.ts +15 -1
  52. package/lib/typescript/src/support/escalateTool.d.ts.map +1 -1
  53. package/lib/typescript/src/support/index.d.ts +1 -1
  54. package/lib/typescript/src/support/index.d.ts.map +1 -1
  55. package/lib/typescript/src/support/types.d.ts +15 -0
  56. package/lib/typescript/src/support/types.d.ts.map +1 -1
  57. package/package.json +5 -1
  58. package/src/components/AIAgent.tsx +507 -36
  59. package/src/components/AgentChatBar.tsx +355 -9
  60. package/src/config/endpoints.ts +22 -0
  61. package/src/core/systemPrompt.ts +126 -100
  62. package/src/core/types.ts +1 -1
  63. package/src/hooks/useAction.ts +2 -2
  64. package/src/index.ts +1 -0
  65. package/src/services/AudioInputService.ts +9 -0
  66. package/src/services/flags/FlagService.ts +1 -1
  67. package/src/services/telemetry/TelemetryService.ts +40 -13
  68. package/src/services/telemetry/device.ts +88 -11
  69. package/src/support/EscalationSocket.ts +47 -8
  70. package/src/support/SupportChatModal.tsx +527 -0
  71. package/src/support/TicketStore.ts +100 -0
  72. package/src/support/escalateTool.ts +47 -13
  73. package/src/support/index.ts +1 -0
  74. package/src/support/types.ts +14 -0
@@ -14,11 +14,13 @@
14
14
  * - Auto-reconnect on unexpected close (max 3 attempts, exponential backoff)
15
15
  */
16
16
 
17
- export type SocketReplyHandler = (reply: string) => void;
17
+ export type SocketReplyHandler = (reply: string, ticketId?: string) => void;
18
18
 
19
19
  interface EscalationSocketOptions {
20
20
  onReply: SocketReplyHandler;
21
21
  onError?: (error: Event) => void;
22
+ onTypingChange?: (isTyping: boolean) => void;
23
+ onTicketClosed?: (ticketId?: string) => void;
22
24
  maxReconnectAttempts?: number;
23
25
  }
24
26
 
@@ -31,11 +33,15 @@ export class EscalationSocket {
31
33
 
32
34
  private readonly onReply: SocketReplyHandler;
33
35
  private readonly onError?: (error: Event) => void;
36
+ private readonly onTypingChange?: (isTyping: boolean) => void;
37
+ private readonly onTicketClosed?: (ticketId?: string) => void;
34
38
  private readonly maxReconnectAttempts: number;
35
39
 
36
40
  constructor(options: EscalationSocketOptions) {
37
41
  this.onReply = options.onReply;
38
42
  this.onError = options.onError;
43
+ this.onTypingChange = options.onTypingChange;
44
+ this.onTicketClosed = options.onTicketClosed;
39
45
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 3;
40
46
  }
41
47
 
@@ -45,6 +51,22 @@ export class EscalationSocket {
45
51
  this.openConnection();
46
52
  }
47
53
 
54
+ sendText(text: string): boolean {
55
+ if (this.ws?.readyState === 1) { // WebSocket.OPEN
56
+ this.ws.send(JSON.stringify({ type: 'user_message', content: text }));
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+
62
+ sendTypingStatus(isTyping: boolean): boolean {
63
+ if (this.ws?.readyState === 1) {
64
+ this.ws.send(JSON.stringify({ type: isTyping ? 'typing_start' : 'typing_stop' }));
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+
48
70
  disconnect(): void {
49
71
  this.intentionalClose = true;
50
72
  if (this.reconnectTimer) {
@@ -68,16 +90,33 @@ export class EscalationSocket {
68
90
  }
69
91
 
70
92
  this.ws.onopen = () => {
71
- console.log('[EscalationSocket] Connected:', this.wsUrl);
93
+ console.log('[EscalationSocket] Connected to:', this.wsUrl);
72
94
  this.reconnectAttempts = 0;
73
95
  };
74
96
 
75
97
  this.ws.onmessage = (event) => {
76
98
  try {
77
- const msg = JSON.parse(String(event.data));
78
- if (msg.type === 'ping') return; // heartbeat — ignore
99
+ const rawData = String(event.data);
100
+ console.log('[EscalationSocket] Message received:', rawData);
101
+ const msg = JSON.parse(rawData);
102
+ if (msg.type === 'ping') {
103
+ console.log('[EscalationSocket] Heartbeat ping received');
104
+ return;
105
+ }
79
106
  if (msg.type === 'reply' && msg.reply) {
80
- this.onReply(msg.reply);
107
+ console.log('[EscalationSocket] Human reply received:', msg.reply);
108
+ this.onTypingChange?.(false);
109
+ this.onReply(msg.reply, msg.ticketId);
110
+ } else if (msg.type === 'typing_start') {
111
+ this.onTypingChange?.(true);
112
+ } else if (msg.type === 'typing_stop') {
113
+ this.onTypingChange?.(false);
114
+ } else if (msg.type === 'ticket_closed') {
115
+ console.log('[EscalationSocket] Ticket closed by agent');
116
+ this.onTypingChange?.(false);
117
+ this.onTicketClosed?.(msg.ticketId);
118
+ this.intentionalClose = true;
119
+ this.ws?.close();
81
120
  }
82
121
  } catch {
83
122
  // Non-JSON message — ignore
@@ -85,13 +124,13 @@ export class EscalationSocket {
85
124
  };
86
125
 
87
126
  this.ws.onerror = (event) => {
88
- console.error('[EscalationSocket] Error:', event);
127
+ console.error('[EscalationSocket] ❌ WebSocket error. URL was:', this.wsUrl, event);
89
128
  this.onError?.(event);
90
129
  };
91
130
 
92
- this.ws.onclose = () => {
131
+ this.ws.onclose = (event) => {
132
+ console.warn(`[EscalationSocket] Connection closed. Code=${event.code} Reason="${event.reason}" Intentional=${this.intentionalClose}`);
93
133
  if (this.intentionalClose) return;
94
- console.warn('[EscalationSocket] Connection closed unexpectedly');
95
134
  this.scheduleReconnect();
96
135
  };
97
136
  }
@@ -0,0 +1,527 @@
1
+ /**
2
+ * SupportChatModal — full-screen chat modal for human support conversations.
3
+ * Shows message history (bubbles with timestamps/avatars), typing indicator, and reply input.
4
+ * Supports native swipe-down-to-dismiss on iOS pageSheet.
5
+ */
6
+
7
+ import { useState, useEffect, useRef } from 'react';
8
+ import {
9
+ Modal,
10
+ View,
11
+ Text,
12
+ TextInput,
13
+ Pressable,
14
+ ScrollView,
15
+ StyleSheet,
16
+ Platform,
17
+ StatusBar,
18
+ Keyboard,
19
+ } from 'react-native';
20
+ import type { AIMessage } from '../core/types';
21
+ import { CloseIcon, SendArrowIcon, LoadingDots } from '../components/Icons';
22
+
23
+ // ─── Props ─────────────────────────────────────────────────────
24
+
25
+ interface SupportChatModalProps {
26
+ visible: boolean;
27
+ messages: AIMessage[];
28
+ onSend: (message: string) => void;
29
+ onClose: () => void;
30
+ isAgentTyping?: boolean;
31
+ isThinking?: boolean;
32
+ /** Optional: externally controlled scroll trigger. Pass when messages update externally. */
33
+ scrollToEndTrigger?: number;
34
+ }
35
+
36
+ // ─── Helpers ───────────────────────────────────────────────────
37
+
38
+ function formatRelativeTime(timestamp: number): string {
39
+ const diff = Date.now() - timestamp;
40
+ if (diff < 60000) return 'just now';
41
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
42
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
43
+ return new Date(timestamp).toLocaleDateString();
44
+ }
45
+
46
+ function shouldShowDateSeparator(prev: AIMessage | undefined, curr: AIMessage): boolean {
47
+ if (!prev) return true;
48
+ const prevDay = new Date(prev.timestamp).toDateString();
49
+ const currDay = new Date(curr.timestamp).toDateString();
50
+ return prevDay !== currDay;
51
+ }
52
+
53
+ function formatDateSeparator(timestamp: number): string {
54
+ const now = new Date();
55
+ const date = new Date(timestamp);
56
+ if (date.toDateString() === now.toDateString()) return 'Today';
57
+ const yesterday = new Date(now);
58
+ yesterday.setDate(yesterday.getDate() - 1);
59
+ if (date.toDateString() === yesterday.toDateString()) return 'Yesterday';
60
+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
61
+ }
62
+
63
+ // ─── Agent Avatar ──────────────────────────────────────────────
64
+
65
+ function AgentAvatar() {
66
+ return (
67
+ <View style={s.agentAvatar}>
68
+ <View style={s.avatarHead} />
69
+ <View style={s.avatarBody} />
70
+ </View>
71
+ );
72
+ }
73
+
74
+ // ─── Main Component ────────────────────────────────────────────
75
+
76
+ export function SupportChatModal({
77
+ visible,
78
+ messages,
79
+ onSend,
80
+ onClose,
81
+ isAgentTyping = false,
82
+ isThinking = false,
83
+ scrollToEndTrigger = 0,
84
+ }: SupportChatModalProps) {
85
+ const [text, setText] = useState('');
86
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
87
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
+ const scrollRef = useRef<any>(null);
89
+
90
+ // Scroll to bottom when new messages arrive or typing indicator changes
91
+ useEffect(() => {
92
+ if (messages.length > 0 || isAgentTyping) {
93
+ setTimeout(() => scrollRef.current?.scrollToEnd?.({ animated: true }), 150);
94
+ }
95
+ }, [messages.length, isAgentTyping]);
96
+
97
+ // Scroll when externally triggered (e.g., after message update in parent)
98
+ useEffect(() => {
99
+ if (scrollToEndTrigger && scrollToEndTrigger > 0) {
100
+ setTimeout(() => scrollRef.current?.scrollToEnd?.({ animated: true }), 200);
101
+ }
102
+ }, [scrollToEndTrigger]);
103
+
104
+ // Manually track keyboard height — reliable inside iOS pageSheet modals
105
+ // where KeyboardAvoidingView miscalculates the offset from screen origin.
106
+ useEffect(() => {
107
+ const showSub = Keyboard.addListener('keyboardWillShow', (e) => {
108
+ setKeyboardHeight(e.endCoordinates.height);
109
+ setTimeout(() => scrollRef.current?.scrollToEnd?.({ animated: true }), 100);
110
+ });
111
+ const hideSub = Keyboard.addListener('keyboardWillHide', () => {
112
+ setKeyboardHeight(0);
113
+ });
114
+ return () => {
115
+ showSub.remove();
116
+ hideSub.remove();
117
+ };
118
+ }, []);
119
+
120
+ const handleSend = () => {
121
+ if (!text.trim() || isThinking) return;
122
+ onSend(text.trim());
123
+ setText('');
124
+ };
125
+
126
+ const isEmpty = messages.length === 0 && !isAgentTyping;
127
+
128
+ return (
129
+ <Modal
130
+ visible={visible}
131
+ animationType="slide"
132
+ presentationStyle="pageSheet"
133
+ onRequestClose={onClose}
134
+ >
135
+ <StatusBar barStyle="light-content" />
136
+ <View style={[s.container, keyboardHeight > 0 && { paddingBottom: keyboardHeight }]}>
137
+ {/* Drag grip indicator */}
138
+ <View style={s.dragHandle}>
139
+ <View style={s.dragGrip} />
140
+ </View>
141
+
142
+ {/* ── Header ── */}
143
+ <View style={s.header}>
144
+ <Pressable onPress={onClose} style={s.headerBtn} hitSlop={12}>
145
+ <CloseIcon size={20} color="rgba(255,255,255,0.7)" />
146
+ </Pressable>
147
+ <View style={s.headerCenter}>
148
+ <Text style={s.headerTitle}>Support Chat</Text>
149
+ <View style={s.headerStatus}>
150
+ <View style={s.statusDot} />
151
+ <Text style={s.headerSubtitle}>Agent online</Text>
152
+ </View>
153
+ </View>
154
+ <View style={s.headerBtn} />
155
+ </View>
156
+
157
+ {/* ── Messages ── */}
158
+ {isEmpty ? (
159
+ <View style={s.emptyState}>
160
+ <View style={s.emptyIcon}>
161
+ <View style={s.emptyBubble} />
162
+ <View style={s.emptyTail} />
163
+ </View>
164
+ <Text style={s.emptyTitle}>No messages yet</Text>
165
+ <Text style={s.emptySubtitle}>Start the conversation below</Text>
166
+ </View>
167
+ ) : (
168
+ <ScrollView
169
+ ref={scrollRef}
170
+ style={s.messagesList}
171
+ contentContainerStyle={s.messagesContent}
172
+ showsVerticalScrollIndicator={false}
173
+ keyboardShouldPersistTaps="handled"
174
+ >
175
+ {messages.map((msg, i) => {
176
+ const isUser = msg.role === 'user';
177
+ const prev = messages[i - 1];
178
+ const showDate = shouldShowDateSeparator(prev, msg);
179
+
180
+ return (
181
+ <View key={msg.id}>
182
+ {/* Date separator */}
183
+ {showDate && (
184
+ <View style={s.dateSeparator}>
185
+ <View style={s.dateLine} />
186
+ <Text style={s.dateText}>{formatDateSeparator(msg.timestamp)}</Text>
187
+ <View style={s.dateLine} />
188
+ </View>
189
+ )}
190
+
191
+ {/* Message row */}
192
+ <View style={[s.messageRow, isUser && s.messageRowUser]}>
193
+ {/* Agent avatar (left side) */}
194
+ {!isUser && <AgentAvatar />}
195
+
196
+ <View style={s.bubbleColumn}>
197
+ <View
198
+ style={[
199
+ s.bubble,
200
+ isUser ? s.bubbleUser : s.bubbleAgent,
201
+ ]}
202
+ >
203
+ <Text style={[s.bubbleText, !isUser && s.bubbleTextAgent]}>
204
+ {msg.content}
205
+ </Text>
206
+ </View>
207
+ <Text style={[s.timestamp, isUser && s.timestampUser]}>
208
+ {formatRelativeTime(msg.timestamp)}
209
+ </Text>
210
+ </View>
211
+ </View>
212
+ </View>
213
+ );
214
+ })}
215
+
216
+ {/* Typing indicator */}
217
+ {isAgentTyping && (
218
+ <View style={s.messageRow}>
219
+ <AgentAvatar />
220
+ <View style={s.bubbleColumn}>
221
+ <View style={[s.bubble, s.bubbleAgent, s.typingBubble]}>
222
+ <LoadingDots size={20} color="rgba(255,255,255,0.6)" />
223
+ </View>
224
+ </View>
225
+ </View>
226
+ )}
227
+ </ScrollView>
228
+ )}
229
+
230
+ {/* ── Input Row ── */}
231
+ <View style={s.inputRow}>
232
+ <TextInput
233
+ style={s.input}
234
+ placeholder="Type a message..."
235
+ placeholderTextColor="rgba(255,255,255,0.35)"
236
+ value={text}
237
+ onChangeText={setText}
238
+ onSubmitEditing={handleSend}
239
+ returnKeyType="send"
240
+ editable={!isThinking}
241
+ />
242
+ <Pressable
243
+ style={[s.sendBtn, text.trim() && !isThinking ? s.sendBtnActive : s.sendBtnInactive]}
244
+ onPress={handleSend}
245
+ disabled={!text.trim() || isThinking}
246
+ >
247
+ <SendArrowIcon size={18} color={text.trim() && !isThinking ? '#fff' : 'rgba(255,255,255,0.3)'} />
248
+ </Pressable>
249
+ </View>
250
+ </View>
251
+ </Modal>
252
+ );
253
+ }
254
+
255
+ // ─── Styles ────────────────────────────────────────────────────
256
+
257
+ const s = StyleSheet.create({
258
+ container: {
259
+ flex: 1,
260
+ backgroundColor: '#0f0f1e',
261
+ },
262
+
263
+ // ── Drag Handle ──
264
+ dragHandle: {
265
+ alignItems: 'center',
266
+ paddingTop: Platform.OS === 'ios' ? 52 : 16,
267
+ paddingBottom: 6,
268
+ },
269
+ dragGrip: {
270
+ width: 36,
271
+ height: 5,
272
+ borderRadius: 3,
273
+ backgroundColor: 'rgba(255,255,255,0.2)',
274
+ },
275
+
276
+ // ── Header ──
277
+ header: {
278
+ flexDirection: 'row',
279
+ alignItems: 'center',
280
+ justifyContent: 'space-between',
281
+ paddingHorizontal: 12,
282
+ paddingTop: 10,
283
+ paddingBottom: 14,
284
+ backgroundColor: 'rgba(255,255,255,0.03)',
285
+ borderBottomWidth: StyleSheet.hairlineWidth,
286
+ borderBottomColor: 'rgba(255,255,255,0.08)',
287
+ },
288
+ headerBtn: {
289
+ width: 36,
290
+ height: 36,
291
+ alignItems: 'center',
292
+ justifyContent: 'center',
293
+ },
294
+ headerCenter: {
295
+ alignItems: 'center',
296
+ },
297
+ headerTitle: {
298
+ color: '#fff',
299
+ fontSize: 17,
300
+ fontWeight: '700',
301
+ letterSpacing: 0.3,
302
+ },
303
+ headerStatus: {
304
+ flexDirection: 'row',
305
+ alignItems: 'center',
306
+ gap: 5,
307
+ marginTop: 3,
308
+ },
309
+ statusDot: {
310
+ width: 7,
311
+ height: 7,
312
+ borderRadius: 4,
313
+ backgroundColor: '#34C759',
314
+ },
315
+ headerSubtitle: {
316
+ color: 'rgba(255,255,255,0.5)',
317
+ fontSize: 12,
318
+ fontWeight: '500',
319
+ },
320
+
321
+ // ── Messages ──
322
+ messagesList: {
323
+ flex: 1,
324
+ },
325
+ messagesContent: {
326
+ paddingHorizontal: 16,
327
+ paddingVertical: 12,
328
+ paddingBottom: 16,
329
+ },
330
+
331
+ // ── Date Separator ──
332
+ dateSeparator: {
333
+ flexDirection: 'row',
334
+ alignItems: 'center',
335
+ paddingVertical: 16,
336
+ gap: 12,
337
+ },
338
+ dateLine: {
339
+ flex: 1,
340
+ height: StyleSheet.hairlineWidth,
341
+ backgroundColor: 'rgba(255,255,255,0.08)',
342
+ },
343
+ dateText: {
344
+ color: 'rgba(255,255,255,0.3)',
345
+ fontSize: 11,
346
+ fontWeight: '600',
347
+ textTransform: 'uppercase',
348
+ letterSpacing: 0.5,
349
+ },
350
+
351
+ // ── Message Row ──
352
+ messageRow: {
353
+ flexDirection: 'row',
354
+ alignItems: 'flex-end',
355
+ marginBottom: 4,
356
+ gap: 8,
357
+ },
358
+ messageRowUser: {
359
+ justifyContent: 'flex-end',
360
+ },
361
+ bubbleColumn: {
362
+ maxWidth: '72%',
363
+ },
364
+
365
+ // ── Bubble ──
366
+ bubble: {
367
+ borderRadius: 18,
368
+ paddingHorizontal: 14,
369
+ paddingVertical: 10,
370
+ },
371
+ bubbleUser: {
372
+ backgroundColor: '#7B68EE',
373
+ borderBottomRightRadius: 6,
374
+ elevation: 2,
375
+ shadowColor: '#7B68EE',
376
+ shadowOffset: { width: 0, height: 2 },
377
+ shadowOpacity: 0.25,
378
+ shadowRadius: 4,
379
+ },
380
+ bubbleAgent: {
381
+ backgroundColor: 'rgba(255,255,255,0.08)',
382
+ borderBottomLeftRadius: 6,
383
+ },
384
+ bubbleText: {
385
+ fontSize: 15,
386
+ lineHeight: 21,
387
+ color: '#fff',
388
+ },
389
+ bubbleTextAgent: {
390
+ color: 'rgba(255,255,255,0.9)',
391
+ },
392
+
393
+ // ── Timestamp ──
394
+ timestamp: {
395
+ color: 'rgba(255,255,255,0.25)',
396
+ fontSize: 11,
397
+ marginTop: 4,
398
+ marginLeft: 4,
399
+ marginBottom: 6,
400
+ },
401
+ timestampUser: {
402
+ textAlign: 'right',
403
+ marginRight: 4,
404
+ marginLeft: 0,
405
+ },
406
+
407
+ // ── Agent Avatar ──
408
+ agentAvatar: {
409
+ width: 30,
410
+ height: 30,
411
+ borderRadius: 15,
412
+ backgroundColor: '#7B68EE',
413
+ alignItems: 'center',
414
+ justifyContent: 'center',
415
+ marginBottom: 14,
416
+ },
417
+ avatarHead: {
418
+ width: 10,
419
+ height: 10,
420
+ borderRadius: 5,
421
+ backgroundColor: 'rgba(255,255,255,0.9)',
422
+ marginTop: 2,
423
+ },
424
+ avatarBody: {
425
+ width: 16,
426
+ height: 6,
427
+ borderTopLeftRadius: 8,
428
+ borderTopRightRadius: 8,
429
+ backgroundColor: 'rgba(255,255,255,0.9)',
430
+ marginTop: 1,
431
+ },
432
+
433
+ // ── Typing Indicator ──
434
+ typingBubble: {
435
+ flexDirection: 'row',
436
+ alignItems: 'center',
437
+ paddingVertical: 12,
438
+ paddingHorizontal: 16,
439
+ minWidth: 60,
440
+ },
441
+
442
+ // ── Empty State ──
443
+ emptyState: {
444
+ flex: 1,
445
+ alignItems: 'center',
446
+ justifyContent: 'center',
447
+ paddingBottom: 60,
448
+ },
449
+ emptyIcon: {
450
+ width: 64,
451
+ height: 64,
452
+ alignItems: 'center',
453
+ justifyContent: 'center',
454
+ marginBottom: 20,
455
+ },
456
+ emptyBubble: {
457
+ width: 48,
458
+ height: 36,
459
+ borderRadius: 12,
460
+ backgroundColor: 'rgba(255,255,255,0.08)',
461
+ borderWidth: 1,
462
+ borderColor: 'rgba(255,255,255,0.12)',
463
+ },
464
+ emptyTail: {
465
+ position: 'absolute',
466
+ bottom: 10,
467
+ left: 16,
468
+ width: 0,
469
+ height: 0,
470
+ borderTopWidth: 8,
471
+ borderTopColor: 'rgba(255,255,255,0.08)',
472
+ borderRightWidth: 8,
473
+ borderRightColor: 'transparent',
474
+ },
475
+ emptyTitle: {
476
+ color: 'rgba(255,255,255,0.5)',
477
+ fontSize: 17,
478
+ fontWeight: '600',
479
+ marginBottom: 6,
480
+ },
481
+ emptySubtitle: {
482
+ color: 'rgba(255,255,255,0.25)',
483
+ fontSize: 14,
484
+ },
485
+
486
+ // ── Input Row ──
487
+ inputRow: {
488
+ flexDirection: 'row',
489
+ alignItems: 'center',
490
+ gap: 10,
491
+ paddingHorizontal: 16,
492
+ paddingVertical: 12,
493
+ paddingBottom: Platform.OS === 'ios' ? 36 : 16,
494
+ backgroundColor: 'rgba(255,255,255,0.02)',
495
+ borderTopWidth: StyleSheet.hairlineWidth,
496
+ borderTopColor: 'rgba(255,255,255,0.06)',
497
+ },
498
+ input: {
499
+ flex: 1,
500
+ backgroundColor: 'rgba(255,255,255,0.06)',
501
+ borderWidth: 1,
502
+ borderColor: 'rgba(255,255,255,0.08)',
503
+ borderRadius: 24,
504
+ paddingHorizontal: 18,
505
+ paddingVertical: 11,
506
+ color: '#fff',
507
+ fontSize: 16,
508
+ },
509
+ sendBtn: {
510
+ width: 42,
511
+ height: 42,
512
+ borderRadius: 21,
513
+ justifyContent: 'center',
514
+ alignItems: 'center',
515
+ },
516
+ sendBtnActive: {
517
+ backgroundColor: '#7B68EE',
518
+ elevation: 3,
519
+ shadowColor: '#7B68EE',
520
+ shadowOffset: { width: 0, height: 2 },
521
+ shadowOpacity: 0.3,
522
+ shadowRadius: 4,
523
+ },
524
+ sendBtnInactive: {
525
+ backgroundColor: 'rgba(255,255,255,0.06)',
526
+ },
527
+ });