@uselingu/react-native-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +279 -0
  2. package/lib/commonjs/components/LinguChat.js +283 -0
  3. package/lib/commonjs/components/LinguChat.js.map +1 -0
  4. package/lib/commonjs/components/LinguWebView.js +203 -0
  5. package/lib/commonjs/components/LinguWebView.js.map +1 -0
  6. package/lib/commonjs/components/MessageBubble.js +91 -0
  7. package/lib/commonjs/components/MessageBubble.js.map +1 -0
  8. package/lib/commonjs/components/NativeChatUI.js +192 -0
  9. package/lib/commonjs/components/NativeChatUI.js.map +1 -0
  10. package/lib/commonjs/components/TypingIndicator.js +92 -0
  11. package/lib/commonjs/components/TypingIndicator.js.map +1 -0
  12. package/lib/commonjs/context/LinguProvider.js +47 -0
  13. package/lib/commonjs/context/LinguProvider.js.map +1 -0
  14. package/lib/commonjs/context/index.js +17 -0
  15. package/lib/commonjs/context/index.js.map +1 -0
  16. package/lib/commonjs/hooks/useLinguChat.js +295 -0
  17. package/lib/commonjs/hooks/useLinguChat.js.map +1 -0
  18. package/lib/commonjs/index.js +129 -0
  19. package/lib/commonjs/index.js.map +1 -0
  20. package/lib/commonjs/services/LinguAPI.js +196 -0
  21. package/lib/commonjs/services/LinguAPI.js.map +1 -0
  22. package/lib/commonjs/services/SessionManager.js +131 -0
  23. package/lib/commonjs/services/SessionManager.js.map +1 -0
  24. package/lib/commonjs/services/StorageManager.js +116 -0
  25. package/lib/commonjs/services/StorageManager.js.map +1 -0
  26. package/lib/commonjs/theme/ThemeProvider.js +49 -0
  27. package/lib/commonjs/theme/ThemeProvider.js.map +1 -0
  28. package/lib/commonjs/theme/index.js +28 -0
  29. package/lib/commonjs/theme/index.js.map +1 -0
  30. package/lib/commonjs/theme/theme.js +150 -0
  31. package/lib/commonjs/theme/theme.js.map +1 -0
  32. package/lib/commonjs/types/index.js +2 -0
  33. package/lib/commonjs/types/index.js.map +1 -0
  34. package/lib/commonjs/utils/animations.js +118 -0
  35. package/lib/commonjs/utils/animations.js.map +1 -0
  36. package/lib/commonjs/utils/formatters.js +89 -0
  37. package/lib/commonjs/utils/formatters.js.map +1 -0
  38. package/lib/commonjs/utils/index.js +28 -0
  39. package/lib/commonjs/utils/index.js.map +1 -0
  40. package/lib/module/components/LinguChat.js +277 -0
  41. package/lib/module/components/LinguChat.js.map +1 -0
  42. package/lib/module/components/LinguWebView.js +195 -0
  43. package/lib/module/components/LinguWebView.js.map +1 -0
  44. package/lib/module/components/MessageBubble.js +83 -0
  45. package/lib/module/components/MessageBubble.js.map +1 -0
  46. package/lib/module/components/NativeChatUI.js +184 -0
  47. package/lib/module/components/NativeChatUI.js.map +1 -0
  48. package/lib/module/components/TypingIndicator.js +84 -0
  49. package/lib/module/components/TypingIndicator.js.map +1 -0
  50. package/lib/module/context/LinguProvider.js +38 -0
  51. package/lib/module/context/LinguProvider.js.map +1 -0
  52. package/lib/module/context/index.js +6 -0
  53. package/lib/module/context/index.js.map +1 -0
  54. package/lib/module/hooks/useLinguChat.js +288 -0
  55. package/lib/module/hooks/useLinguChat.js.map +1 -0
  56. package/lib/module/index.js +30 -0
  57. package/lib/module/index.js.map +1 -0
  58. package/lib/module/services/LinguAPI.js +190 -0
  59. package/lib/module/services/LinguAPI.js.map +1 -0
  60. package/lib/module/services/SessionManager.js +124 -0
  61. package/lib/module/services/SessionManager.js.map +1 -0
  62. package/lib/module/services/StorageManager.js +108 -0
  63. package/lib/module/services/StorageManager.js.map +1 -0
  64. package/lib/module/theme/ThemeProvider.js +40 -0
  65. package/lib/module/theme/ThemeProvider.js.map +1 -0
  66. package/lib/module/theme/index.js +7 -0
  67. package/lib/module/theme/index.js.map +1 -0
  68. package/lib/module/theme/theme.js +142 -0
  69. package/lib/module/theme/theme.js.map +1 -0
  70. package/lib/module/types/index.js +2 -0
  71. package/lib/module/types/index.js.map +1 -0
  72. package/lib/module/utils/animations.js +106 -0
  73. package/lib/module/utils/animations.js.map +1 -0
  74. package/lib/module/utils/formatters.js +78 -0
  75. package/lib/module/utils/formatters.js.map +1 -0
  76. package/lib/module/utils/index.js +7 -0
  77. package/lib/module/utils/index.js.map +1 -0
  78. package/lib/typescript/components/LinguChat.d.ts +18 -0
  79. package/lib/typescript/components/LinguChat.d.ts.map +1 -0
  80. package/lib/typescript/components/LinguWebView.d.ts +16 -0
  81. package/lib/typescript/components/LinguWebView.d.ts.map +1 -0
  82. package/lib/typescript/components/MessageBubble.d.ts +13 -0
  83. package/lib/typescript/components/MessageBubble.d.ts.map +1 -0
  84. package/lib/typescript/components/NativeChatUI.d.ts +16 -0
  85. package/lib/typescript/components/NativeChatUI.d.ts.map +1 -0
  86. package/lib/typescript/components/TypingIndicator.d.ts +7 -0
  87. package/lib/typescript/components/TypingIndicator.d.ts.map +1 -0
  88. package/lib/typescript/context/LinguProvider.d.ts +32 -0
  89. package/lib/typescript/context/LinguProvider.d.ts.map +1 -0
  90. package/lib/typescript/context/index.d.ts +5 -0
  91. package/lib/typescript/context/index.d.ts.map +1 -0
  92. package/lib/typescript/hooks/useLinguChat.d.ts +18 -0
  93. package/lib/typescript/hooks/useLinguChat.d.ts.map +1 -0
  94. package/lib/typescript/index.d.ts +19 -0
  95. package/lib/typescript/index.d.ts.map +1 -0
  96. package/lib/typescript/services/LinguAPI.d.ts +39 -0
  97. package/lib/typescript/services/LinguAPI.d.ts.map +1 -0
  98. package/lib/typescript/services/SessionManager.d.ts +46 -0
  99. package/lib/typescript/services/SessionManager.d.ts.map +1 -0
  100. package/lib/typescript/services/StorageManager.d.ts +36 -0
  101. package/lib/typescript/services/StorageManager.d.ts.map +1 -0
  102. package/lib/typescript/theme/ThemeProvider.d.ts +23 -0
  103. package/lib/typescript/theme/ThemeProvider.d.ts.map +1 -0
  104. package/lib/typescript/theme/index.d.ts +6 -0
  105. package/lib/typescript/theme/index.d.ts.map +1 -0
  106. package/lib/typescript/theme/theme.d.ts +130 -0
  107. package/lib/typescript/theme/theme.d.ts.map +1 -0
  108. package/lib/typescript/types/index.d.ts +50 -0
  109. package/lib/typescript/types/index.d.ts.map +1 -0
  110. package/lib/typescript/utils/animations.d.ts +34 -0
  111. package/lib/typescript/utils/animations.d.ts.map +1 -0
  112. package/lib/typescript/utils/formatters.d.ts +25 -0
  113. package/lib/typescript/utils/formatters.d.ts.map +1 -0
  114. package/lib/typescript/utils/index.d.ts +6 -0
  115. package/lib/typescript/utils/index.d.ts.map +1 -0
  116. package/package.json +98 -0
  117. package/src/components/LinguChat.tsx +309 -0
  118. package/src/components/LinguWebView.tsx +216 -0
  119. package/src/components/MessageBubble.tsx +120 -0
  120. package/src/components/NativeChatUI.tsx +247 -0
  121. package/src/components/TypingIndicator.tsx +84 -0
  122. package/src/context/LinguProvider.tsx +53 -0
  123. package/src/context/index.ts +5 -0
  124. package/src/hooks/useLinguChat.ts +314 -0
  125. package/src/index.ts +43 -0
  126. package/src/services/LinguAPI.ts +214 -0
  127. package/src/services/SessionManager.ts +131 -0
  128. package/src/services/StorageManager.ts +111 -0
  129. package/src/theme/ThemeProvider.tsx +58 -0
  130. package/src/theme/index.ts +6 -0
  131. package/src/theme/theme.ts +254 -0
  132. package/src/types/index.ts +59 -0
  133. package/src/utils/animations.ts +141 -0
  134. package/src/utils/formatters.ts +80 -0
  135. package/src/utils/index.ts +6 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * LinguChat Component
3
+ * Full-page chat interface with back navigation
4
+ * Now with theme support and improved styling
5
+ */
6
+
7
+ import React, { useState, useEffect, useRef } from 'react';
8
+ import {
9
+ View,
10
+ TouchableOpacity,
11
+ StyleSheet,
12
+ Animated,
13
+ SafeAreaView,
14
+ Text,
15
+ } from 'react-native';
16
+ import { NativeChatUI } from './NativeChatUI';
17
+ import { LinguProvider, useLinguContext } from '../context';
18
+ import { useTheme } from '../theme';
19
+ import { scaleIn, pulse } from '../utils/animations';
20
+ import type { LinguConfig, ChatPosition } from '../types';
21
+
22
+ export interface LinguChatProps extends LinguConfig {
23
+ onBackPress?: () => void;
24
+ showBackButton?: boolean;
25
+ position?: ChatPosition; // UI preference for FAB position
26
+ }
27
+
28
+ interface LinguChatInnerProps {
29
+ onBackPress?: () => void;
30
+ showBackButton?: boolean;
31
+ autoOpen?: boolean;
32
+ position?: ChatPosition;
33
+ }
34
+
35
+ /**
36
+ * Internal component that uses context
37
+ */
38
+ const LinguChatInner: React.FC<LinguChatInnerProps> = (props) => {
39
+ const { theme } = useTheme();
40
+ const [isOpen, setIsOpen] = useState(props.autoOpen || false);
41
+ const [hasUnread, setHasUnread] = useState(false);
42
+ const scaleAnim = useRef(new Animated.Value(0)).current;
43
+ const badgePulse = useRef(new Animated.Value(1)).current;
44
+
45
+ const {
46
+ messages,
47
+ isLoading,
48
+ config,
49
+ sendMessage,
50
+ startSession,
51
+ endSession,
52
+ isSessionActive,
53
+ } = useLinguContext();
54
+
55
+ // Entrance animation for FAB
56
+ useEffect(() => {
57
+ scaleIn(scaleAnim).start();
58
+ }, []);
59
+
60
+ // Pulse animation for unread badge
61
+ useEffect(() => {
62
+ if (hasUnread) {
63
+ pulse(badgePulse).start();
64
+ } else {
65
+ badgePulse.setValue(1);
66
+ }
67
+ }, [hasUnread]);
68
+
69
+ // Auto-open if configured
70
+ useEffect(() => {
71
+ if (props.autoOpen && !isSessionActive) {
72
+ handleOpen();
73
+ }
74
+ }, [props.autoOpen]);
75
+
76
+ // Mark unread when new message arrives and chat is closed
77
+ useEffect(() => {
78
+ if (!isOpen && messages.length > 0) {
79
+ const lastMessage = messages[messages.length - 1];
80
+ if (lastMessage.sender === 'bot') {
81
+ setHasUnread(true);
82
+ }
83
+ }
84
+ }, [messages, isOpen]);
85
+
86
+ const handleOpen = async () => {
87
+ setIsOpen(true);
88
+ setHasUnread(false);
89
+
90
+ if (!isSessionActive) {
91
+ await startSession();
92
+ }
93
+ };
94
+
95
+ const handleClose = async () => {
96
+ setIsOpen(false);
97
+ await endSession();
98
+ props.onBackPress?.();
99
+ };
100
+
101
+ const handleSendMessage = async (text: string) => {
102
+ await sendMessage(text);
103
+ };
104
+
105
+ const getPositionStyle = (position: ChatPosition = 'bottom-right') => {
106
+ const base = { position: 'absolute' as const };
107
+ switch (position) {
108
+ case 'bottom-right':
109
+ return { ...base, bottom: 20, right: 20 };
110
+ case 'bottom-left':
111
+ return { ...base, bottom: 20, left: 20 };
112
+ case 'top-right':
113
+ return { ...base, top: 20, right: 20 };
114
+ case 'top-left':
115
+ return { ...base, top: 20, left: 20 };
116
+ default:
117
+ return { ...base, bottom: 20, right: 20 };
118
+ }
119
+ };
120
+
121
+ const primaryColor = theme.colors.primary;
122
+ const position = props.position || config?.widgetPosition || 'bottom-right';
123
+ const showBackButton = props.showBackButton ?? true;
124
+
125
+ // Full-page chat view
126
+ if (isOpen) {
127
+ return (
128
+ <View style={styles.fullScreenOverlay}>
129
+ <SafeAreaView
130
+ style={[
131
+ styles.fullPageContainer,
132
+ { backgroundColor: theme.colors.background },
133
+ ]}
134
+ >
135
+ {/* Header with back button */}
136
+ {showBackButton && (
137
+ <View
138
+ style={[
139
+ styles.header,
140
+ {
141
+ backgroundColor: primaryColor,
142
+ paddingHorizontal: theme.spacing.lg,
143
+ paddingVertical: theme.spacing.md,
144
+ },
145
+ theme.shadows.md,
146
+ ]}
147
+ >
148
+ <TouchableOpacity
149
+ style={styles.backButton}
150
+ onPress={handleClose}
151
+ activeOpacity={0.7}
152
+ >
153
+ <Text style={styles.backButtonText}>←</Text>
154
+ </TouchableOpacity>
155
+ <Text
156
+ style={[
157
+ styles.headerTitle,
158
+ {
159
+ fontSize: theme.typography.fontSizes.lg,
160
+ fontWeight: theme.typography.fontWeights.semibold,
161
+ },
162
+ ]}
163
+ >
164
+ {config?.brandName || 'Lingu Chat'}
165
+ </Text>
166
+ <View style={styles.headerSpacer} />
167
+ </View>
168
+ )}
169
+
170
+ {/* Native Chat UI */}
171
+ <NativeChatUI
172
+ config={config}
173
+ messages={messages}
174
+ isLoading={isLoading}
175
+ onSendMessage={handleSendMessage}
176
+ />
177
+ </SafeAreaView>
178
+ </View>
179
+ );
180
+ }
181
+
182
+ // Floating action button
183
+ return (
184
+ <View style={[styles.fabContainer, getPositionStyle(position)]} pointerEvents="box-none">
185
+ <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
186
+ <TouchableOpacity
187
+ style={[
188
+ styles.fab,
189
+ {
190
+ backgroundColor: primaryColor,
191
+ width: 60,
192
+ height: 60,
193
+ borderRadius: 30,
194
+ },
195
+ theme.shadows.lg,
196
+ ]}
197
+ onPress={handleOpen}
198
+ activeOpacity={0.8}
199
+ >
200
+ {hasUnread && (
201
+ <Animated.View
202
+ style={[
203
+ styles.badge,
204
+ {
205
+ backgroundColor: theme.colors.badge,
206
+ transform: [{ scale: badgePulse }],
207
+ },
208
+ ]}
209
+ />
210
+ )}
211
+ <View style={styles.icon}>
212
+ {/* Chat icon */}
213
+ <View style={styles.iconBubble} />
214
+ </View>
215
+ </TouchableOpacity>
216
+ </Animated.View>
217
+ </View>
218
+ );
219
+ };
220
+
221
+ /**
222
+ * LinguChat - Main exported component
223
+ * Wraps everything in LinguProvider for standalone state management
224
+ */
225
+ export const LinguChat: React.FC<LinguChatProps> = ({
226
+ onBackPress,
227
+ showBackButton,
228
+ autoOpen,
229
+ position,
230
+ ...linguConfig
231
+ }) => {
232
+ return (
233
+ <LinguProvider {...linguConfig} autoOpen={autoOpen}>
234
+ <LinguChatInner
235
+ onBackPress={onBackPress}
236
+ showBackButton={showBackButton}
237
+ autoOpen={autoOpen}
238
+ position={position}
239
+ />
240
+ </LinguProvider>
241
+ );
242
+ };
243
+
244
+ const styles = StyleSheet.create({
245
+ fullScreenOverlay: {
246
+ position: 'absolute',
247
+ top: 0,
248
+ left: 0,
249
+ right: 0,
250
+ bottom: 0,
251
+ zIndex: 10000,
252
+ },
253
+ fullPageContainer: {
254
+ flex: 1,
255
+ },
256
+ header: {
257
+ flexDirection: 'row',
258
+ alignItems: 'center',
259
+ justifyContent: 'space-between',
260
+ },
261
+ backButton: {
262
+ width: 40,
263
+ height: 40,
264
+ justifyContent: 'center',
265
+ alignItems: 'center',
266
+ },
267
+ backButtonText: {
268
+ fontSize: 28,
269
+ color: '#fff',
270
+ fontWeight: '600',
271
+ },
272
+ headerTitle: {
273
+ color: '#fff',
274
+ flex: 1,
275
+ textAlign: 'center',
276
+ },
277
+ headerSpacer: {
278
+ width: 40,
279
+ },
280
+ fabContainer: {
281
+ zIndex: 9999,
282
+ },
283
+ fab: {
284
+ justifyContent: 'center',
285
+ alignItems: 'center',
286
+ },
287
+ badge: {
288
+ position: 'absolute',
289
+ top: 8,
290
+ right: 8,
291
+ width: 12,
292
+ height: 12,
293
+ borderRadius: 6,
294
+ borderWidth: 2,
295
+ borderColor: '#fff',
296
+ },
297
+ icon: {
298
+ width: 30,
299
+ height: 30,
300
+ justifyContent: 'center',
301
+ alignItems: 'center',
302
+ },
303
+ iconBubble: {
304
+ width: 24,
305
+ height: 20,
306
+ backgroundColor: '#fff',
307
+ borderRadius: 12,
308
+ },
309
+ });
@@ -0,0 +1,216 @@
1
+ /**
2
+ * LinguWebView Component
3
+ * Wraps the existing web widget in a WebView with native bridge
4
+ * Now with comprehensive logging and message display
5
+ */
6
+
7
+ import React, { useRef, useEffect } from 'react';
8
+ import { WebView, WebViewMessageEvent } from 'react-native-webview';
9
+ import { StyleSheet } from 'react-native';
10
+ import type { Message, WidgetConfig } from '../types';
11
+
12
+ const LOG_PREFIX = '[LinguWebView]';
13
+
14
+ interface LinguWebViewProps {
15
+ config: WidgetConfig | null;
16
+ messages: Message[];
17
+ onSendMessage: (message: string) => void;
18
+ onClose: () => void;
19
+ }
20
+
21
+ export const LinguWebView: React.FC<LinguWebViewProps> = ({
22
+ config,
23
+ messages,
24
+ onSendMessage,
25
+ onClose,
26
+ }) => {
27
+ const webViewRef = useRef<WebView>(null);
28
+
29
+ console.log(`${LOG_PREFIX} Rendering with ${messages.length} messages`);
30
+
31
+ // Inject messages into WebView when they change
32
+ useEffect(() => {
33
+ if (webViewRef.current && messages.length > 0) {
34
+ console.log(`${LOG_PREFIX} Injecting ${messages.length} messages into WebView`);
35
+ const script = `
36
+ (function() {
37
+ const messagesContainer = document.getElementById('messages');
38
+ if (messagesContainer) {
39
+ messagesContainer.innerHTML = ${JSON.stringify(
40
+ messages
41
+ .map(
42
+ (msg) => `
43
+ <div style="margin-bottom: 12px; display: flex; justify-content: ${msg.sender === 'user' ? 'flex-end' : 'flex-start'
44
+ };">
45
+ <div style="
46
+ max-width: 70%;
47
+ padding: 12px;
48
+ border-radius: 12px;
49
+ background: ${msg.sender === 'user' ? '#4ade80' : '#f3f4f6'};
50
+ color: ${msg.sender === 'user' ? 'white' : 'black'};
51
+ ">
52
+ ${msg.text}
53
+ </div>
54
+ </div>
55
+ `
56
+ )
57
+ .join('')
58
+ )};
59
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
60
+ console.log('Messages updated in WebView');
61
+ }
62
+ })();
63
+ true;
64
+ `;
65
+ webViewRef.current.injectJavaScript(script);
66
+ }
67
+ }, [messages]);
68
+
69
+ // Handle messages from WebView
70
+ const handleMessage = (event: WebViewMessageEvent) => {
71
+ try {
72
+ const data = JSON.parse(event.nativeEvent.data);
73
+ console.log(`${LOG_PREFIX} Received message from WebView:`, data.type);
74
+
75
+ switch (data.type) {
76
+ case 'SEND_MESSAGE':
77
+ console.log(`${LOG_PREFIX} User sent message:`, data.payload.message);
78
+ onSendMessage(data.payload.message);
79
+ break;
80
+ case 'CLOSE_CHAT':
81
+ console.log(`${LOG_PREFIX} User closed chat`);
82
+ onClose();
83
+ break;
84
+ case 'WEBVIEW_LOG':
85
+ console.log(`${LOG_PREFIX} WebView log:`, data.payload);
86
+ break;
87
+ default:
88
+ console.log(`${LOG_PREFIX} Unknown message type:`, data.type);
89
+ }
90
+ } catch (error) {
91
+ console.error(`${LOG_PREFIX} Failed to parse message:`, error);
92
+ }
93
+ };
94
+
95
+ // Generate HTML for the widget
96
+ const generateHTML = () => {
97
+ const configJSON = JSON.stringify(config || {});
98
+ console.log(`${LOG_PREFIX} Generating HTML with config:`, config?.brandName);
99
+
100
+ return `
101
+ <!DOCTYPE html>
102
+ <html>
103
+ <head>
104
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
105
+ <style>
106
+ * {
107
+ margin: 0;
108
+ padding: 0;
109
+ box-sizing: border-box;
110
+ }
111
+ body {
112
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
113
+ overflow: hidden;
114
+ }
115
+ #widget-container {
116
+ width: 100vw;
117
+ height: 100vh;
118
+ display: flex;
119
+ flex-direction: column;
120
+ }
121
+ #messages {
122
+ flex: 1;
123
+ overflow-y: auto;
124
+ padding: 16px;
125
+ background: white;
126
+ }
127
+ #input-container {
128
+ padding: 16px;
129
+ border-top: 1px solid #e5e7eb;
130
+ background: white;
131
+ }
132
+ #message-input {
133
+ width: 100%;
134
+ padding: 12px;
135
+ border: 1px solid #d1d5db;
136
+ border-radius: 8px;
137
+ font-size: 16px;
138
+ }
139
+ </style>
140
+ </head>
141
+ <body>
142
+ <div id="widget-container">
143
+ <div id="messages">
144
+ <div style="text-align: center; color: #9ca3af; padding: 20px;">
145
+ ${config?.greetingMessage || 'Start chatting...'}
146
+ </div>
147
+ </div>
148
+ <div id="input-container">
149
+ <input
150
+ type="text"
151
+ id="message-input"
152
+ placeholder="Type a message..."
153
+ />
154
+ </div>
155
+ </div>
156
+
157
+ <script>
158
+ console.log('WebView initialized');
159
+
160
+ // Configuration from native
161
+ window.linguConfig = ${configJSON};
162
+
163
+ // Bridge to React Native
164
+ window.sendToNative = function(type, payload) {
165
+ console.log('Sending to native:', type, payload);
166
+ window.ReactNativeWebView.postMessage(JSON.stringify({
167
+ type: type,
168
+ payload: payload
169
+ }));
170
+ };
171
+
172
+ // Log to native
173
+ window.logToNative = function(message) {
174
+ window.sendToNative('WEBVIEW_LOG', message);
175
+ };
176
+
177
+ // Handle input
178
+ const input = document.getElementById('message-input');
179
+ input.addEventListener('keypress', function(e) {
180
+ if (e.key === 'Enter' && input.value.trim()) {
181
+ console.log('User pressed enter:', input.value);
182
+ window.sendToNative('SEND_MESSAGE', { message: input.value });
183
+ input.value = '';
184
+ }
185
+ });
186
+
187
+ console.log('Event listeners attached');
188
+ window.logToNative('WebView ready');
189
+ </script>
190
+ </body>
191
+ </html>
192
+ `;
193
+ };
194
+
195
+ return (
196
+ <WebView
197
+ ref={webViewRef}
198
+ source={{ html: generateHTML() }}
199
+ style={styles.webview}
200
+ onMessage={handleMessage}
201
+ javaScriptEnabled
202
+ domStorageEnabled
203
+ startInLoadingState
204
+ scalesPageToFit
205
+ onLoad={() => console.log(`${LOG_PREFIX} WebView loaded`)}
206
+ onError={(error) => console.error(`${LOG_PREFIX} WebView error:`, error)}
207
+ />
208
+ );
209
+ };
210
+
211
+ const styles = StyleSheet.create({
212
+ webview: {
213
+ flex: 1,
214
+ backgroundColor: 'white',
215
+ },
216
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Message Bubble Component
3
+ * Displays a single message with animations and styling
4
+ */
5
+
6
+ import React, { useEffect, useRef } from 'react';
7
+ import { View, Text, StyleSheet, Animated } from 'react-native';
8
+ import { useTheme } from '../theme';
9
+ import { formatRelativeTime } from '../utils/formatters';
10
+ import { messageEntrance } from '../utils/animations';
11
+ import type { Message } from '../types';
12
+
13
+ interface MessageBubbleProps {
14
+ message: Message;
15
+ showTimestamp?: boolean;
16
+ }
17
+
18
+ export const MessageBubble: React.FC<MessageBubbleProps> = ({
19
+ message,
20
+ showTimestamp = false,
21
+ }) => {
22
+ const { theme } = useTheme();
23
+ const isUser = message.sender === 'user';
24
+
25
+ // Animation values
26
+ const translateY = useRef(new Animated.Value(20)).current;
27
+ const opacity = useRef(new Animated.Value(0)).current;
28
+
29
+ useEffect(() => {
30
+ // Animate message entrance
31
+ messageEntrance(translateY, opacity, theme.animations.normal).start();
32
+ }, []);
33
+
34
+ const bubbleStyle = isUser
35
+ ? {
36
+ backgroundColor: theme.colors.userBubble,
37
+ }
38
+ : {
39
+ backgroundColor: theme.colors.botBubble,
40
+ ...theme.shadows.sm,
41
+ };
42
+
43
+ const textStyle = {
44
+ color: isUser ? theme.colors.userText : theme.colors.botText,
45
+ };
46
+
47
+ return (
48
+ <Animated.View
49
+ style={[
50
+ styles.container,
51
+ isUser ? styles.userContainer : styles.botContainer,
52
+ {
53
+ opacity,
54
+ transform: [{ translateY }],
55
+ },
56
+ ]}
57
+ >
58
+ <View
59
+ style={[
60
+ styles.bubble,
61
+ bubbleStyle,
62
+ {
63
+ borderRadius: theme.borderRadius.lg,
64
+ borderBottomRightRadius: isUser ? theme.borderRadius.sm : theme.borderRadius.lg,
65
+ borderBottomLeftRadius: isUser ? theme.borderRadius.lg : theme.borderRadius.sm,
66
+ paddingHorizontal: theme.spacing.md,
67
+ paddingVertical: theme.spacing.sm + 2,
68
+ },
69
+ ]}
70
+ >
71
+ <Text
72
+ style={[
73
+ styles.text,
74
+ textStyle,
75
+ {
76
+ fontSize: theme.typography.fontSizes.base,
77
+ lineHeight: theme.typography.fontSizes.base * theme.typography.lineHeights.normal,
78
+ },
79
+ ]}
80
+ >
81
+ {message.text}
82
+ </Text>
83
+ {showTimestamp && (
84
+ <Text
85
+ style={[
86
+ styles.timestamp,
87
+ {
88
+ color: isUser ? theme.colors.gray100 : theme.colors.gray500,
89
+ fontSize: theme.typography.fontSizes.xs,
90
+ marginTop: theme.spacing.xs,
91
+ },
92
+ ]}
93
+ >
94
+ {formatRelativeTime(message.timestamp)}
95
+ </Text>
96
+ )}
97
+ </View>
98
+ </Animated.View>
99
+ );
100
+ };
101
+
102
+ const styles = StyleSheet.create({
103
+ container: {
104
+ marginBottom: 12,
105
+ flexDirection: 'row',
106
+ },
107
+ userContainer: {
108
+ justifyContent: 'flex-end',
109
+ },
110
+ botContainer: {
111
+ justifyContent: 'flex-start',
112
+ },
113
+ bubble: {
114
+ maxWidth: '75%',
115
+ },
116
+ text: {},
117
+ timestamp: {
118
+ fontSize: 11,
119
+ },
120
+ });