@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.
- package/README.md +11 -0
- package/lib/module/components/AIAgent.js +513 -36
- package/lib/module/components/AIAgent.js.map +1 -1
- package/lib/module/components/AgentChatBar.js +320 -13
- package/lib/module/components/AgentChatBar.js.map +1 -1
- package/lib/module/config/endpoints.js +22 -0
- package/lib/module/config/endpoints.js.map +1 -0
- package/lib/module/core/systemPrompt.js +126 -100
- package/lib/module/core/systemPrompt.js.map +1 -1
- package/lib/module/services/AudioInputService.js +9 -0
- package/lib/module/services/AudioInputService.js.map +1 -1
- package/lib/module/services/flags/FlagService.js +1 -1
- package/lib/module/services/flags/FlagService.js.map +1 -1
- package/lib/module/services/telemetry/TelemetryService.js +39 -13
- package/lib/module/services/telemetry/TelemetryService.js.map +1 -1
- package/lib/module/services/telemetry/device.js +80 -10
- package/lib/module/services/telemetry/device.js.map +1 -1
- package/lib/module/support/EscalationSocket.js +46 -7
- package/lib/module/support/EscalationSocket.js.map +1 -1
- package/lib/module/support/SupportChatModal.js +516 -0
- package/lib/module/support/SupportChatModal.js.map +1 -0
- package/lib/module/support/TicketStore.js +93 -0
- package/lib/module/support/TicketStore.js.map +1 -0
- package/lib/module/support/escalateTool.js +39 -13
- package/lib/module/support/escalateTool.js.map +1 -1
- package/lib/module/support/index.js.map +1 -1
- package/lib/typescript/src/components/AIAgent.d.ts +24 -1
- package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
- package/lib/typescript/src/components/AgentChatBar.d.ts +24 -2
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
- package/lib/typescript/src/config/endpoints.d.ts +18 -0
- package/lib/typescript/src/config/endpoints.d.ts.map +1 -0
- package/lib/typescript/src/core/systemPrompt.d.ts +4 -13
- package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +1 -1
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useAction.d.ts +2 -2
- package/lib/typescript/src/hooks/useAction.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/services/AudioInputService.d.ts.map +1 -1
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +1 -1
- package/lib/typescript/src/services/telemetry/device.d.ts +15 -4
- package/lib/typescript/src/services/telemetry/device.d.ts.map +1 -1
- package/lib/typescript/src/support/EscalationSocket.d.ts +7 -1
- package/lib/typescript/src/support/EscalationSocket.d.ts.map +1 -1
- package/lib/typescript/src/support/SupportChatModal.d.ts +19 -0
- package/lib/typescript/src/support/SupportChatModal.d.ts.map +1 -0
- package/lib/typescript/src/support/TicketStore.d.ts +34 -0
- package/lib/typescript/src/support/TicketStore.d.ts.map +1 -0
- package/lib/typescript/src/support/escalateTool.d.ts +15 -1
- package/lib/typescript/src/support/escalateTool.d.ts.map +1 -1
- package/lib/typescript/src/support/index.d.ts +1 -1
- package/lib/typescript/src/support/index.d.ts.map +1 -1
- package/lib/typescript/src/support/types.d.ts +15 -0
- package/lib/typescript/src/support/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/AIAgent.tsx +507 -36
- package/src/components/AgentChatBar.tsx +355 -9
- package/src/config/endpoints.ts +22 -0
- package/src/core/systemPrompt.ts +126 -100
- package/src/core/types.ts +1 -1
- package/src/hooks/useAction.ts +2 -2
- package/src/index.ts +1 -0
- package/src/services/AudioInputService.ts +9 -0
- package/src/services/flags/FlagService.ts +1 -1
- package/src/services/telemetry/TelemetryService.ts +40 -13
- package/src/services/telemetry/device.ts +88 -11
- package/src/support/EscalationSocket.ts +47 -8
- package/src/support/SupportChatModal.tsx +527 -0
- package/src/support/TicketStore.ts +100 -0
- package/src/support/escalateTool.ts +47 -13
- package/src/support/index.ts +1 -0
- 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
|
|
78
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
+
});
|