@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.
- package/README.md +279 -0
- package/lib/commonjs/components/LinguChat.js +283 -0
- package/lib/commonjs/components/LinguChat.js.map +1 -0
- package/lib/commonjs/components/LinguWebView.js +203 -0
- package/lib/commonjs/components/LinguWebView.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +91 -0
- package/lib/commonjs/components/MessageBubble.js.map +1 -0
- package/lib/commonjs/components/NativeChatUI.js +192 -0
- package/lib/commonjs/components/NativeChatUI.js.map +1 -0
- package/lib/commonjs/components/TypingIndicator.js +92 -0
- package/lib/commonjs/components/TypingIndicator.js.map +1 -0
- package/lib/commonjs/context/LinguProvider.js +47 -0
- package/lib/commonjs/context/LinguProvider.js.map +1 -0
- package/lib/commonjs/context/index.js +17 -0
- package/lib/commonjs/context/index.js.map +1 -0
- package/lib/commonjs/hooks/useLinguChat.js +295 -0
- package/lib/commonjs/hooks/useLinguChat.js.map +1 -0
- package/lib/commonjs/index.js +129 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/services/LinguAPI.js +196 -0
- package/lib/commonjs/services/LinguAPI.js.map +1 -0
- package/lib/commonjs/services/SessionManager.js +131 -0
- package/lib/commonjs/services/SessionManager.js.map +1 -0
- package/lib/commonjs/services/StorageManager.js +116 -0
- package/lib/commonjs/services/StorageManager.js.map +1 -0
- package/lib/commonjs/theme/ThemeProvider.js +49 -0
- package/lib/commonjs/theme/ThemeProvider.js.map +1 -0
- package/lib/commonjs/theme/index.js +28 -0
- package/lib/commonjs/theme/index.js.map +1 -0
- package/lib/commonjs/theme/theme.js +150 -0
- package/lib/commonjs/theme/theme.js.map +1 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/commonjs/utils/animations.js +118 -0
- package/lib/commonjs/utils/animations.js.map +1 -0
- package/lib/commonjs/utils/formatters.js +89 -0
- package/lib/commonjs/utils/formatters.js.map +1 -0
- package/lib/commonjs/utils/index.js +28 -0
- package/lib/commonjs/utils/index.js.map +1 -0
- package/lib/module/components/LinguChat.js +277 -0
- package/lib/module/components/LinguChat.js.map +1 -0
- package/lib/module/components/LinguWebView.js +195 -0
- package/lib/module/components/LinguWebView.js.map +1 -0
- package/lib/module/components/MessageBubble.js +83 -0
- package/lib/module/components/MessageBubble.js.map +1 -0
- package/lib/module/components/NativeChatUI.js +184 -0
- package/lib/module/components/NativeChatUI.js.map +1 -0
- package/lib/module/components/TypingIndicator.js +84 -0
- package/lib/module/components/TypingIndicator.js.map +1 -0
- package/lib/module/context/LinguProvider.js +38 -0
- package/lib/module/context/LinguProvider.js.map +1 -0
- package/lib/module/context/index.js +6 -0
- package/lib/module/context/index.js.map +1 -0
- package/lib/module/hooks/useLinguChat.js +288 -0
- package/lib/module/hooks/useLinguChat.js.map +1 -0
- package/lib/module/index.js +30 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/services/LinguAPI.js +190 -0
- package/lib/module/services/LinguAPI.js.map +1 -0
- package/lib/module/services/SessionManager.js +124 -0
- package/lib/module/services/SessionManager.js.map +1 -0
- package/lib/module/services/StorageManager.js +108 -0
- package/lib/module/services/StorageManager.js.map +1 -0
- package/lib/module/theme/ThemeProvider.js +40 -0
- package/lib/module/theme/ThemeProvider.js.map +1 -0
- package/lib/module/theme/index.js +7 -0
- package/lib/module/theme/index.js.map +1 -0
- package/lib/module/theme/theme.js +142 -0
- package/lib/module/theme/theme.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/utils/animations.js +106 -0
- package/lib/module/utils/animations.js.map +1 -0
- package/lib/module/utils/formatters.js +78 -0
- package/lib/module/utils/formatters.js.map +1 -0
- package/lib/module/utils/index.js +7 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/typescript/components/LinguChat.d.ts +18 -0
- package/lib/typescript/components/LinguChat.d.ts.map +1 -0
- package/lib/typescript/components/LinguWebView.d.ts +16 -0
- package/lib/typescript/components/LinguWebView.d.ts.map +1 -0
- package/lib/typescript/components/MessageBubble.d.ts +13 -0
- package/lib/typescript/components/MessageBubble.d.ts.map +1 -0
- package/lib/typescript/components/NativeChatUI.d.ts +16 -0
- package/lib/typescript/components/NativeChatUI.d.ts.map +1 -0
- package/lib/typescript/components/TypingIndicator.d.ts +7 -0
- package/lib/typescript/components/TypingIndicator.d.ts.map +1 -0
- package/lib/typescript/context/LinguProvider.d.ts +32 -0
- package/lib/typescript/context/LinguProvider.d.ts.map +1 -0
- package/lib/typescript/context/index.d.ts +5 -0
- package/lib/typescript/context/index.d.ts.map +1 -0
- package/lib/typescript/hooks/useLinguChat.d.ts +18 -0
- package/lib/typescript/hooks/useLinguChat.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +19 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/services/LinguAPI.d.ts +39 -0
- package/lib/typescript/services/LinguAPI.d.ts.map +1 -0
- package/lib/typescript/services/SessionManager.d.ts +46 -0
- package/lib/typescript/services/SessionManager.d.ts.map +1 -0
- package/lib/typescript/services/StorageManager.d.ts +36 -0
- package/lib/typescript/services/StorageManager.d.ts.map +1 -0
- package/lib/typescript/theme/ThemeProvider.d.ts +23 -0
- package/lib/typescript/theme/ThemeProvider.d.ts.map +1 -0
- package/lib/typescript/theme/index.d.ts +6 -0
- package/lib/typescript/theme/index.d.ts.map +1 -0
- package/lib/typescript/theme/theme.d.ts +130 -0
- package/lib/typescript/theme/theme.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts +50 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/lib/typescript/utils/animations.d.ts +34 -0
- package/lib/typescript/utils/animations.d.ts.map +1 -0
- package/lib/typescript/utils/formatters.d.ts +25 -0
- package/lib/typescript/utils/formatters.d.ts.map +1 -0
- package/lib/typescript/utils/index.d.ts +6 -0
- package/lib/typescript/utils/index.d.ts.map +1 -0
- package/package.json +98 -0
- package/src/components/LinguChat.tsx +309 -0
- package/src/components/LinguWebView.tsx +216 -0
- package/src/components/MessageBubble.tsx +120 -0
- package/src/components/NativeChatUI.tsx +247 -0
- package/src/components/TypingIndicator.tsx +84 -0
- package/src/context/LinguProvider.tsx +53 -0
- package/src/context/index.ts +5 -0
- package/src/hooks/useLinguChat.ts +314 -0
- package/src/index.ts +43 -0
- package/src/services/LinguAPI.ts +214 -0
- package/src/services/SessionManager.ts +131 -0
- package/src/services/StorageManager.ts +111 -0
- package/src/theme/ThemeProvider.tsx +58 -0
- package/src/theme/index.ts +6 -0
- package/src/theme/theme.ts +254 -0
- package/src/types/index.ts +59 -0
- package/src/utils/animations.ts +141 -0
- package/src/utils/formatters.ts +80 -0
- package/src/utils/index.ts +6 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Chat UI Component
|
|
3
|
+
* Pure React Native implementation without WebView
|
|
4
|
+
* Now with theme support and enhanced styling
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
8
|
+
import {
|
|
9
|
+
View,
|
|
10
|
+
Text,
|
|
11
|
+
TextInput,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
FlatList,
|
|
14
|
+
StyleSheet,
|
|
15
|
+
KeyboardAvoidingView,
|
|
16
|
+
Platform,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
import { useTheme } from '../theme';
|
|
19
|
+
import { MessageBubble } from './MessageBubble';
|
|
20
|
+
import { TypingIndicator } from './TypingIndicator';
|
|
21
|
+
import type { Message, WidgetConfig } from '../types';
|
|
22
|
+
|
|
23
|
+
const LOG_PREFIX = '[NativeChatUI]';
|
|
24
|
+
|
|
25
|
+
interface NativeChatUIProps {
|
|
26
|
+
config: WidgetConfig | null;
|
|
27
|
+
messages: Message[];
|
|
28
|
+
isLoading: boolean;
|
|
29
|
+
onSendMessage: (message: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const NativeChatUI: React.FC<NativeChatUIProps> = ({
|
|
33
|
+
config,
|
|
34
|
+
messages,
|
|
35
|
+
isLoading,
|
|
36
|
+
onSendMessage,
|
|
37
|
+
}) => {
|
|
38
|
+
const { theme } = useTheme();
|
|
39
|
+
const [inputText, setInputText] = useState('');
|
|
40
|
+
const flatListRef = useRef<FlatList>(null);
|
|
41
|
+
|
|
42
|
+
console.log(`${LOG_PREFIX} Rendering with ${messages.length} messages`);
|
|
43
|
+
|
|
44
|
+
// Auto-scroll to bottom when new messages arrive
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (messages.length > 0 && flatListRef.current) {
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
flatListRef.current?.scrollToEnd({ animated: true });
|
|
49
|
+
}, 100);
|
|
50
|
+
}
|
|
51
|
+
}, [messages]);
|
|
52
|
+
|
|
53
|
+
const handleSend = () => {
|
|
54
|
+
if (inputText.trim()) {
|
|
55
|
+
console.log(`${LOG_PREFIX} Sending message:`, inputText);
|
|
56
|
+
onSendMessage(inputText.trim());
|
|
57
|
+
setInputText('');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const renderMessage = ({ item }: { item: Message }) => {
|
|
62
|
+
return <MessageBubble message={item} showTimestamp={false} />;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const renderEmpty = () => (
|
|
66
|
+
<View style={styles.emptyContainer}>
|
|
67
|
+
<Text
|
|
68
|
+
style={[
|
|
69
|
+
styles.greetingText,
|
|
70
|
+
{
|
|
71
|
+
color: theme.colors.gray500,
|
|
72
|
+
fontSize: theme.typography.fontSizes.base,
|
|
73
|
+
},
|
|
74
|
+
]}
|
|
75
|
+
>
|
|
76
|
+
{config?.greetingMessage || 'Hello! How can I help you today?'}
|
|
77
|
+
</Text>
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const renderFooter = () => {
|
|
82
|
+
if (isLoading) {
|
|
83
|
+
return <TypingIndicator />;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const charCount = inputText.length;
|
|
89
|
+
const maxChars = 5000;
|
|
90
|
+
const showCharCount = charCount > maxChars * 0.8;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<KeyboardAvoidingView
|
|
94
|
+
style={[styles.container, { backgroundColor: theme.colors.background }]}
|
|
95
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
96
|
+
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
|
|
97
|
+
>
|
|
98
|
+
{/* Messages List */}
|
|
99
|
+
<FlatList
|
|
100
|
+
ref={flatListRef}
|
|
101
|
+
data={messages}
|
|
102
|
+
renderItem={renderMessage}
|
|
103
|
+
keyExtractor={(item) => item.id}
|
|
104
|
+
contentContainerStyle={[
|
|
105
|
+
styles.messagesList,
|
|
106
|
+
{ padding: theme.spacing.lg },
|
|
107
|
+
]}
|
|
108
|
+
ListEmptyComponent={renderEmpty}
|
|
109
|
+
ListFooterComponent={renderFooter}
|
|
110
|
+
showsVerticalScrollIndicator={false}
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
{/* Input Area */}
|
|
114
|
+
<View
|
|
115
|
+
style={[
|
|
116
|
+
styles.inputContainer,
|
|
117
|
+
{
|
|
118
|
+
backgroundColor: theme.colors.background,
|
|
119
|
+
borderTopColor: theme.colors.inputBorder,
|
|
120
|
+
padding: theme.spacing.lg,
|
|
121
|
+
},
|
|
122
|
+
]}
|
|
123
|
+
>
|
|
124
|
+
<TextInput
|
|
125
|
+
style={[
|
|
126
|
+
styles.input,
|
|
127
|
+
{
|
|
128
|
+
backgroundColor: theme.colors.inputBackground,
|
|
129
|
+
borderColor: theme.colors.inputBorder,
|
|
130
|
+
color: theme.colors.inputText,
|
|
131
|
+
fontSize: theme.typography.fontSizes.base,
|
|
132
|
+
borderRadius: theme.borderRadius.xl,
|
|
133
|
+
paddingHorizontal: theme.spacing.lg,
|
|
134
|
+
paddingVertical: theme.spacing.sm + 2,
|
|
135
|
+
marginRight: theme.spacing.sm,
|
|
136
|
+
},
|
|
137
|
+
]}
|
|
138
|
+
value={inputText}
|
|
139
|
+
onChangeText={setInputText}
|
|
140
|
+
placeholder="Type a message..."
|
|
141
|
+
placeholderTextColor={theme.colors.gray400}
|
|
142
|
+
multiline
|
|
143
|
+
maxLength={maxChars}
|
|
144
|
+
onSubmitEditing={handleSend}
|
|
145
|
+
returnKeyType="send"
|
|
146
|
+
blurOnSubmit={false}
|
|
147
|
+
/>
|
|
148
|
+
<TouchableOpacity
|
|
149
|
+
style={[
|
|
150
|
+
styles.sendButton,
|
|
151
|
+
{
|
|
152
|
+
backgroundColor: theme.colors.primary,
|
|
153
|
+
borderRadius: theme.borderRadius.xl,
|
|
154
|
+
paddingHorizontal: theme.spacing.lg,
|
|
155
|
+
paddingVertical: theme.spacing.sm + 2,
|
|
156
|
+
},
|
|
157
|
+
!inputText.trim() && styles.sendButtonDisabled,
|
|
158
|
+
]}
|
|
159
|
+
onPress={handleSend}
|
|
160
|
+
disabled={!inputText.trim() || isLoading}
|
|
161
|
+
>
|
|
162
|
+
<Text
|
|
163
|
+
style={[
|
|
164
|
+
styles.sendButtonText,
|
|
165
|
+
{
|
|
166
|
+
fontSize: theme.typography.fontSizes.base,
|
|
167
|
+
fontWeight: theme.typography.fontWeights.semibold,
|
|
168
|
+
},
|
|
169
|
+
]}
|
|
170
|
+
>
|
|
171
|
+
Send
|
|
172
|
+
</Text>
|
|
173
|
+
</TouchableOpacity>
|
|
174
|
+
</View>
|
|
175
|
+
|
|
176
|
+
{/* Character count */}
|
|
177
|
+
{showCharCount && (
|
|
178
|
+
<View
|
|
179
|
+
style={[
|
|
180
|
+
styles.charCountContainer,
|
|
181
|
+
{ paddingHorizontal: theme.spacing.lg },
|
|
182
|
+
]}
|
|
183
|
+
>
|
|
184
|
+
<Text
|
|
185
|
+
style={[
|
|
186
|
+
styles.charCount,
|
|
187
|
+
{
|
|
188
|
+
color: charCount >= maxChars ? theme.colors.errorText : theme.colors.gray500,
|
|
189
|
+
fontSize: theme.typography.fontSizes.xs,
|
|
190
|
+
},
|
|
191
|
+
]}
|
|
192
|
+
>
|
|
193
|
+
{charCount}/{maxChars}
|
|
194
|
+
</Text>
|
|
195
|
+
</View>
|
|
196
|
+
)}
|
|
197
|
+
</KeyboardAvoidingView>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const styles = StyleSheet.create({
|
|
202
|
+
container: {
|
|
203
|
+
flex: 1,
|
|
204
|
+
},
|
|
205
|
+
messagesList: {
|
|
206
|
+
flexGrow: 1,
|
|
207
|
+
},
|
|
208
|
+
emptyContainer: {
|
|
209
|
+
flex: 1,
|
|
210
|
+
justifyContent: 'center',
|
|
211
|
+
alignItems: 'center',
|
|
212
|
+
padding: 32,
|
|
213
|
+
},
|
|
214
|
+
greetingText: {
|
|
215
|
+
textAlign: 'center',
|
|
216
|
+
},
|
|
217
|
+
inputContainer: {
|
|
218
|
+
flexDirection: 'row',
|
|
219
|
+
borderTopWidth: 1,
|
|
220
|
+
alignItems: 'flex-end',
|
|
221
|
+
},
|
|
222
|
+
input: {
|
|
223
|
+
flex: 1,
|
|
224
|
+
minHeight: 40,
|
|
225
|
+
maxHeight: 100,
|
|
226
|
+
borderWidth: 1,
|
|
227
|
+
},
|
|
228
|
+
sendButton: {
|
|
229
|
+
justifyContent: 'center',
|
|
230
|
+
alignItems: 'center',
|
|
231
|
+
minWidth: 70,
|
|
232
|
+
},
|
|
233
|
+
sendButtonDisabled: {
|
|
234
|
+
opacity: 0.5,
|
|
235
|
+
},
|
|
236
|
+
sendButtonText: {
|
|
237
|
+
color: '#ffffff',
|
|
238
|
+
},
|
|
239
|
+
charCountContainer: {
|
|
240
|
+
position: 'absolute',
|
|
241
|
+
bottom: 70,
|
|
242
|
+
right: 0,
|
|
243
|
+
},
|
|
244
|
+
charCount: {
|
|
245
|
+
textAlign: 'right',
|
|
246
|
+
},
|
|
247
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typing Indicator Component
|
|
3
|
+
* Animated dots to show bot is typing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useRef } from 'react';
|
|
7
|
+
import { View, Animated, StyleSheet } from 'react-native';
|
|
8
|
+
import { useTheme } from '../theme';
|
|
9
|
+
import { typingDotAnimation } from '../utils/animations';
|
|
10
|
+
|
|
11
|
+
export const TypingIndicator: React.FC = () => {
|
|
12
|
+
const { theme } = useTheme();
|
|
13
|
+
|
|
14
|
+
const dot1 = useRef(new Animated.Value(0)).current;
|
|
15
|
+
const dot2 = useRef(new Animated.Value(0)).current;
|
|
16
|
+
const dot3 = useRef(new Animated.Value(0)).current;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
// Start animations with staggered delays
|
|
20
|
+
const anim1 = typingDotAnimation(dot1, 0);
|
|
21
|
+
const anim2 = typingDotAnimation(dot2, 150);
|
|
22
|
+
const anim3 = typingDotAnimation(dot3, 300);
|
|
23
|
+
|
|
24
|
+
anim1.start();
|
|
25
|
+
anim2.start();
|
|
26
|
+
anim3.start();
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
anim1.stop();
|
|
30
|
+
anim2.stop();
|
|
31
|
+
anim3.stop();
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View style={[styles.container, { backgroundColor: theme.colors.botBubble }]}>
|
|
37
|
+
<View style={styles.dotsContainer}>
|
|
38
|
+
<Animated.View
|
|
39
|
+
style={[
|
|
40
|
+
styles.dot,
|
|
41
|
+
{ backgroundColor: theme.colors.gray400 },
|
|
42
|
+
{ transform: [{ translateY: dot1 }] },
|
|
43
|
+
]}
|
|
44
|
+
/>
|
|
45
|
+
<Animated.View
|
|
46
|
+
style={[
|
|
47
|
+
styles.dot,
|
|
48
|
+
{ backgroundColor: theme.colors.gray400 },
|
|
49
|
+
{ transform: [{ translateY: dot2 }] },
|
|
50
|
+
]}
|
|
51
|
+
/>
|
|
52
|
+
<Animated.View
|
|
53
|
+
style={[
|
|
54
|
+
styles.dot,
|
|
55
|
+
{ backgroundColor: theme.colors.gray400 },
|
|
56
|
+
{ transform: [{ translateY: dot3 }] },
|
|
57
|
+
]}
|
|
58
|
+
/>
|
|
59
|
+
</View>
|
|
60
|
+
</View>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const styles = StyleSheet.create({
|
|
65
|
+
container: {
|
|
66
|
+
maxWidth: '75%',
|
|
67
|
+
padding: 12,
|
|
68
|
+
borderRadius: 16,
|
|
69
|
+
borderBottomLeftRadius: 4,
|
|
70
|
+
marginBottom: 12,
|
|
71
|
+
},
|
|
72
|
+
dotsContainer: {
|
|
73
|
+
flexDirection: 'row',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
justifyContent: 'center',
|
|
76
|
+
height: 20,
|
|
77
|
+
},
|
|
78
|
+
dot: {
|
|
79
|
+
width: 8,
|
|
80
|
+
height: 8,
|
|
81
|
+
borderRadius: 4,
|
|
82
|
+
marginHorizontal: 3,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lingu SDK Context Provider
|
|
3
|
+
* Provides standalone state management for the SDK
|
|
4
|
+
* Completely isolated from host application state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { createContext, useContext, ReactNode } from 'react';
|
|
8
|
+
import { useLinguChat } from '../hooks/useLinguChat';
|
|
9
|
+
import { ThemeProvider } from '../theme';
|
|
10
|
+
import type { Message, WidgetConfig, LinguConfig } from '../types';
|
|
11
|
+
|
|
12
|
+
interface LinguContextValue {
|
|
13
|
+
messages: Message[];
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
config: WidgetConfig | null;
|
|
16
|
+
sendMessage: (text: string) => Promise<void>;
|
|
17
|
+
startSession: () => Promise<void>;
|
|
18
|
+
endSession: () => Promise<void>;
|
|
19
|
+
isSessionActive: boolean;
|
|
20
|
+
clearHistory: () => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LinguContext = createContext<LinguContextValue | undefined>(undefined);
|
|
24
|
+
|
|
25
|
+
interface LinguProviderProps extends LinguConfig {
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* LinguProvider - Wraps the SDK with standalone state management
|
|
31
|
+
* This ensures the SDK state is completely isolated from the host app
|
|
32
|
+
*/
|
|
33
|
+
export const LinguProvider: React.FC<LinguProviderProps> = ({ children, ...linguConfig }) => {
|
|
34
|
+
const chatState = useLinguChat(linguConfig);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<LinguContext.Provider value={chatState}>
|
|
38
|
+
<ThemeProvider config={chatState.config}>{children}</ThemeProvider>
|
|
39
|
+
</LinguContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hook to access Lingu SDK context
|
|
45
|
+
* Must be used within a LinguProvider
|
|
46
|
+
*/
|
|
47
|
+
export const useLinguContext = (): LinguContextValue => {
|
|
48
|
+
const context = useContext(LinguContext);
|
|
49
|
+
if (context === undefined) {
|
|
50
|
+
throw new Error('useLinguContext must be used within a LinguProvider');
|
|
51
|
+
}
|
|
52
|
+
return context;
|
|
53
|
+
};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom hook for managing Lingu chat state and operations
|
|
3
|
+
* Now with message persistence and comprehensive logging
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
7
|
+
import { LinguAPI } from '../services/LinguAPI';
|
|
8
|
+
import { SessionManager } from '../services/SessionManager';
|
|
9
|
+
import { StorageManager } from '../services/StorageManager';
|
|
10
|
+
import type { Message, WidgetConfig, LinguConfig } from '../types';
|
|
11
|
+
|
|
12
|
+
const LOG_PREFIX = '[useLinguChat]';
|
|
13
|
+
|
|
14
|
+
interface UseLinguChatReturn {
|
|
15
|
+
messages: Message[];
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
config: WidgetConfig | null;
|
|
18
|
+
sendMessage: (text: string) => Promise<void>;
|
|
19
|
+
startSession: () => Promise<void>;
|
|
20
|
+
endSession: () => Promise<void>;
|
|
21
|
+
isSessionActive: boolean;
|
|
22
|
+
clearHistory: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useLinguChat = (linguConfig: LinguConfig): UseLinguChatReturn => {
|
|
26
|
+
console.log(`${LOG_PREFIX} Initializing hook with config:`, {
|
|
27
|
+
apiKey: linguConfig.apiKey?.substring(0, 20) + '...',
|
|
28
|
+
baseURL: linguConfig.baseURL,
|
|
29
|
+
autoOpen: linguConfig.autoOpen,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
33
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
34
|
+
const [config, setConfig] = useState<WidgetConfig | null>(null);
|
|
35
|
+
const [isSessionActive, setIsSessionActive] = useState(false);
|
|
36
|
+
|
|
37
|
+
const apiRef = useRef<LinguAPI | null>(null);
|
|
38
|
+
const sessionManagerRef = useRef<SessionManager | null>(null);
|
|
39
|
+
const currentSessionIdRef = useRef<string | null>(null);
|
|
40
|
+
|
|
41
|
+
// Initialize API client and session manager
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
console.log(`${LOG_PREFIX} Setting up API client and session manager`);
|
|
44
|
+
|
|
45
|
+
const baseURL = linguConfig.baseURL || 'https://api.uselingu.app/api';
|
|
46
|
+
console.log(`${LOG_PREFIX} API Base URL:`, baseURL);
|
|
47
|
+
|
|
48
|
+
const api = new LinguAPI(linguConfig.apiKey, baseURL);
|
|
49
|
+
apiRef.current = api;
|
|
50
|
+
|
|
51
|
+
const sessionManager = new SessionManager(api, {
|
|
52
|
+
onSessionStart: async (sessionId) => {
|
|
53
|
+
console.log(`${LOG_PREFIX} ✅ Session started:`, sessionId);
|
|
54
|
+
currentSessionIdRef.current = sessionId;
|
|
55
|
+
setIsSessionActive(true);
|
|
56
|
+
|
|
57
|
+
// Save session ID
|
|
58
|
+
await StorageManager.saveCurrentSessionId(sessionId);
|
|
59
|
+
console.log(`${LOG_PREFIX} Session ID saved to storage`);
|
|
60
|
+
|
|
61
|
+
// Try to load previous messages for this session
|
|
62
|
+
const savedMessages = await StorageManager.loadMessages(sessionId);
|
|
63
|
+
if (savedMessages.length > 0) {
|
|
64
|
+
console.log(`${LOG_PREFIX} Loaded ${savedMessages.length} messages from storage`);
|
|
65
|
+
setMessages(savedMessages);
|
|
66
|
+
} else {
|
|
67
|
+
console.log(`${LOG_PREFIX} No previous messages found for this session`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
linguConfig.onSessionStart?.(sessionId);
|
|
71
|
+
},
|
|
72
|
+
onSessionEnd: async (sessionId) => {
|
|
73
|
+
console.log(`${LOG_PREFIX} Session ending:`, sessionId);
|
|
74
|
+
|
|
75
|
+
// Save messages before ending
|
|
76
|
+
if (messages.length > 0) {
|
|
77
|
+
console.log(`${LOG_PREFIX} Saving ${messages.length} messages before session end`);
|
|
78
|
+
await StorageManager.saveMessages(sessionId, messages);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
currentSessionIdRef.current = null;
|
|
82
|
+
setIsSessionActive(false);
|
|
83
|
+
console.log(`${LOG_PREFIX} ✅ Session ended successfully`);
|
|
84
|
+
linguConfig.onSessionEnd?.(sessionId);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
sessionManagerRef.current = sessionManager;
|
|
88
|
+
|
|
89
|
+
// Load widget configuration
|
|
90
|
+
console.log(`${LOG_PREFIX} Fetching widget configuration...`);
|
|
91
|
+
api
|
|
92
|
+
.getConfig()
|
|
93
|
+
.then((fetchedConfig) => {
|
|
94
|
+
console.log(`${LOG_PREFIX} ✅ Widget config loaded:`, {
|
|
95
|
+
theme: fetchedConfig.theme,
|
|
96
|
+
primaryColor: fetchedConfig.primaryColor,
|
|
97
|
+
brandName: fetchedConfig.brandName,
|
|
98
|
+
greetingMessage: fetchedConfig.greetingMessage?.substring(0, 50),
|
|
99
|
+
});
|
|
100
|
+
setConfig(fetchedConfig);
|
|
101
|
+
})
|
|
102
|
+
.catch((error) => {
|
|
103
|
+
console.error(`${LOG_PREFIX} ❌ Failed to load config:`, error);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Try to restore previous session
|
|
107
|
+
console.log(`${LOG_PREFIX} Attempting to restore previous session...`);
|
|
108
|
+
restorePreviousSession();
|
|
109
|
+
|
|
110
|
+
// Cleanup on unmount
|
|
111
|
+
return () => {
|
|
112
|
+
console.log(`${LOG_PREFIX} Component unmounting, cleaning up...`);
|
|
113
|
+
// Save messages before cleanup
|
|
114
|
+
if (currentSessionIdRef.current && messages.length > 0) {
|
|
115
|
+
console.log(`${LOG_PREFIX} Saving messages on unmount`);
|
|
116
|
+
StorageManager.saveMessages(currentSessionIdRef.current, messages);
|
|
117
|
+
}
|
|
118
|
+
sessionManagerRef.current?.cleanup();
|
|
119
|
+
console.log(`${LOG_PREFIX} Cleanup complete`);
|
|
120
|
+
};
|
|
121
|
+
}, [linguConfig.apiKey, linguConfig.baseURL]);
|
|
122
|
+
|
|
123
|
+
// Save messages whenever they change
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (currentSessionIdRef.current && messages.length > 0) {
|
|
126
|
+
console.log(`${LOG_PREFIX} Auto-saving ${messages.length} messages`);
|
|
127
|
+
StorageManager.saveMessages(currentSessionIdRef.current, messages);
|
|
128
|
+
}
|
|
129
|
+
}, [messages]);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Try to restore previous session
|
|
133
|
+
*/
|
|
134
|
+
const restorePreviousSession = async () => {
|
|
135
|
+
try {
|
|
136
|
+
const previousSessionId = await StorageManager.getCurrentSessionId();
|
|
137
|
+
if (previousSessionId) {
|
|
138
|
+
console.log(`${LOG_PREFIX} Found previous session:`, previousSessionId);
|
|
139
|
+
const savedMessages = await StorageManager.loadMessages(previousSessionId);
|
|
140
|
+
if (savedMessages.length > 0) {
|
|
141
|
+
console.log(`${LOG_PREFIX} ✅ Restored ${savedMessages.length} messages from previous session`);
|
|
142
|
+
setMessages(savedMessages);
|
|
143
|
+
currentSessionIdRef.current = previousSessionId;
|
|
144
|
+
} else {
|
|
145
|
+
console.log(`${LOG_PREFIX} Previous session had no messages`);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
console.log(`${LOG_PREFIX} No previous session found`);
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`${LOG_PREFIX} ❌ Failed to restore previous session:`, error);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Start a new chat session
|
|
157
|
+
*/
|
|
158
|
+
const startSession = useCallback(async () => {
|
|
159
|
+
if (!sessionManagerRef.current) {
|
|
160
|
+
console.error(`${LOG_PREFIX} ❌ Session manager not initialized`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
console.log(`${LOG_PREFIX} Starting new session...`);
|
|
166
|
+
await sessionManagerRef.current.start();
|
|
167
|
+
|
|
168
|
+
// Add greeting message if configured and no previous messages
|
|
169
|
+
if (config?.greetingMessage && messages.length === 0) {
|
|
170
|
+
console.log(`${LOG_PREFIX} Adding greeting message`);
|
|
171
|
+
const greetingMsg: Message = {
|
|
172
|
+
id: `greeting-${Date.now()}`,
|
|
173
|
+
text: config.greetingMessage,
|
|
174
|
+
sender: 'bot',
|
|
175
|
+
timestamp: new Date(),
|
|
176
|
+
};
|
|
177
|
+
setMessages([greetingMsg]);
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error(`${LOG_PREFIX} ❌ Failed to start session:`, error);
|
|
181
|
+
}
|
|
182
|
+
}, [config, messages.length]);
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* End the current chat session
|
|
186
|
+
*/
|
|
187
|
+
const endSession = useCallback(async () => {
|
|
188
|
+
if (!sessionManagerRef.current) {
|
|
189
|
+
console.error(`${LOG_PREFIX} ❌ Session manager not initialized`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
console.log(`${LOG_PREFIX} Ending session...`);
|
|
195
|
+
await sessionManagerRef.current.end();
|
|
196
|
+
// Don't clear messages - they're saved in storage
|
|
197
|
+
console.log(`${LOG_PREFIX} ✅ Session ended, messages preserved`);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error(`${LOG_PREFIX} ❌ Failed to end session:`, error);
|
|
200
|
+
}
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Clear chat history
|
|
205
|
+
*/
|
|
206
|
+
const clearHistory = useCallback(async () => {
|
|
207
|
+
console.log(`${LOG_PREFIX} Clearing chat history...`);
|
|
208
|
+
if (currentSessionIdRef.current) {
|
|
209
|
+
await StorageManager.clearMessages(currentSessionIdRef.current);
|
|
210
|
+
console.log(`${LOG_PREFIX} ✅ Storage cleared for session:`, currentSessionIdRef.current);
|
|
211
|
+
}
|
|
212
|
+
setMessages([]);
|
|
213
|
+
console.log(`${LOG_PREFIX} ✅ Messages cleared from state`);
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Send a message to the AI
|
|
218
|
+
*/
|
|
219
|
+
const sendMessage = useCallback(
|
|
220
|
+
async (text: string) => {
|
|
221
|
+
if (!apiRef.current || !sessionManagerRef.current) {
|
|
222
|
+
console.error(`${LOG_PREFIX} ❌ API or session manager not initialized`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const sessionId = sessionManagerRef.current.getSessionId();
|
|
227
|
+
if (!sessionId) {
|
|
228
|
+
console.error(`${LOG_PREFIX} ❌ No active session`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`${LOG_PREFIX} Sending message:`, {
|
|
233
|
+
sessionId,
|
|
234
|
+
messageLength: text.length,
|
|
235
|
+
preview: text.substring(0, 50),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Add user message
|
|
239
|
+
const userMessage: Message = {
|
|
240
|
+
id: `user-${Date.now()}`,
|
|
241
|
+
text,
|
|
242
|
+
sender: 'user',
|
|
243
|
+
timestamp: new Date(),
|
|
244
|
+
};
|
|
245
|
+
setMessages((prev) => {
|
|
246
|
+
console.log(`${LOG_PREFIX} Adding user message to state (total: ${prev.length + 1})`);
|
|
247
|
+
return [...prev, userMessage];
|
|
248
|
+
});
|
|
249
|
+
linguConfig.onMessageSent?.(text);
|
|
250
|
+
|
|
251
|
+
// Send to API
|
|
252
|
+
setIsLoading(true);
|
|
253
|
+
console.log(`${LOG_PREFIX} Waiting for AI response...`);
|
|
254
|
+
try {
|
|
255
|
+
const startTime = Date.now();
|
|
256
|
+
const response = await apiRef.current.sendMessage(text, sessionId);
|
|
257
|
+
const duration = Date.now() - startTime;
|
|
258
|
+
|
|
259
|
+
console.log(`${LOG_PREFIX} ✅ Received AI response in ${duration}ms:`, {
|
|
260
|
+
responseLength: response.length,
|
|
261
|
+
preview: response.substring(0, 50),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Add bot response
|
|
265
|
+
const botMessage: Message = {
|
|
266
|
+
id: `bot-${Date.now()}`,
|
|
267
|
+
text: response,
|
|
268
|
+
sender: 'bot',
|
|
269
|
+
timestamp: new Date(),
|
|
270
|
+
};
|
|
271
|
+
setMessages((prev) => {
|
|
272
|
+
console.log(`${LOG_PREFIX} Adding bot message to state (total: ${prev.length + 1})`);
|
|
273
|
+
return [...prev, botMessage];
|
|
274
|
+
});
|
|
275
|
+
linguConfig.onMessageReceived?.(response);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(`${LOG_PREFIX} ❌ Failed to send message:`, error);
|
|
278
|
+
|
|
279
|
+
// Add error message
|
|
280
|
+
const errorMessage: Message = {
|
|
281
|
+
id: `error-${Date.now()}`,
|
|
282
|
+
text: 'Sorry, I encountered an error. Please try again.',
|
|
283
|
+
sender: 'bot',
|
|
284
|
+
timestamp: new Date(),
|
|
285
|
+
};
|
|
286
|
+
setMessages((prev) => [...prev, errorMessage]);
|
|
287
|
+
console.log(`${LOG_PREFIX} Added error message to state`);
|
|
288
|
+
} finally {
|
|
289
|
+
setIsLoading(false);
|
|
290
|
+
console.log(`${LOG_PREFIX} Message send complete`);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
[linguConfig]
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
console.log(`${LOG_PREFIX} Current state:`, {
|
|
297
|
+
messagesCount: messages.length,
|
|
298
|
+
isLoading,
|
|
299
|
+
hasConfig: !!config,
|
|
300
|
+
isSessionActive,
|
|
301
|
+
currentSessionId: currentSessionIdRef.current,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
messages,
|
|
306
|
+
isLoading,
|
|
307
|
+
config,
|
|
308
|
+
sendMessage,
|
|
309
|
+
startSession,
|
|
310
|
+
endSession,
|
|
311
|
+
isSessionActive,
|
|
312
|
+
clearHistory,
|
|
313
|
+
};
|
|
314
|
+
};
|