react-native-srschat 0.1.18 → 0.1.20

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 (91) hide show
  1. package/README.md +2 -0
  2. package/lib/commonjs/components/email.js +6 -5
  3. package/lib/commonjs/components/email.js.map +1 -1
  4. package/lib/commonjs/components/header.js +12 -20
  5. package/lib/commonjs/components/header.js.map +1 -1
  6. package/lib/commonjs/components/input.js +4 -2
  7. package/lib/commonjs/components/input.js.map +1 -1
  8. package/lib/commonjs/components/productCard.js +5 -1
  9. package/lib/commonjs/components/productCard.js.map +1 -1
  10. package/lib/commonjs/components/progressCircle.js +99 -0
  11. package/lib/commonjs/components/progressCircle.js.map +1 -0
  12. package/lib/commonjs/components/voice.js +96 -0
  13. package/lib/commonjs/components/voice.js.map +1 -0
  14. package/lib/commonjs/components/welcomeInput.js +3 -1
  15. package/lib/commonjs/components/welcomeInput.js.map +1 -1
  16. package/lib/commonjs/contexts/AppContext.js +6 -1
  17. package/lib/commonjs/contexts/AppContext.js.map +1 -1
  18. package/lib/commonjs/hooks/stream.js +9 -9
  19. package/lib/commonjs/hooks/stream.js.map +1 -1
  20. package/lib/commonjs/layout/disclaimer.js +1 -1
  21. package/lib/commonjs/layout/disclaimer.js.map +1 -1
  22. package/lib/commonjs/layout/layout.js +11 -26
  23. package/lib/commonjs/layout/layout.js.map +1 -1
  24. package/lib/commonjs/layout/welcome.js +4 -3
  25. package/lib/commonjs/layout/welcome.js.map +1 -1
  26. package/lib/commonjs/layout/window.js +85 -7
  27. package/lib/commonjs/layout/window.js.map +1 -1
  28. package/lib/commonjs/utils/audioRecorder.js +287 -0
  29. package/lib/commonjs/utils/audioRecorder.js.map +1 -0
  30. package/lib/commonjs/utils/textToSpeech.js +45 -0
  31. package/lib/commonjs/utils/textToSpeech.js.map +1 -0
  32. package/lib/module/components/email.js +6 -5
  33. package/lib/module/components/email.js.map +1 -1
  34. package/lib/module/components/header.js +12 -20
  35. package/lib/module/components/header.js.map +1 -1
  36. package/lib/module/components/input.js +4 -2
  37. package/lib/module/components/input.js.map +1 -1
  38. package/lib/module/components/productCard.js +4 -1
  39. package/lib/module/components/productCard.js.map +1 -1
  40. package/lib/module/components/progressCircle.js +90 -0
  41. package/lib/module/components/progressCircle.js.map +1 -0
  42. package/lib/module/components/voice.js +86 -0
  43. package/lib/module/components/voice.js.map +1 -0
  44. package/lib/module/components/welcomeInput.js +3 -1
  45. package/lib/module/components/welcomeInput.js.map +1 -1
  46. package/lib/module/contexts/AppContext.js +6 -1
  47. package/lib/module/contexts/AppContext.js.map +1 -1
  48. package/lib/module/hooks/stream.js +9 -9
  49. package/lib/module/hooks/stream.js.map +1 -1
  50. package/lib/module/layout/disclaimer.js +1 -1
  51. package/lib/module/layout/disclaimer.js.map +1 -1
  52. package/lib/module/layout/layout.js +11 -26
  53. package/lib/module/layout/layout.js.map +1 -1
  54. package/lib/module/layout/welcome.js +2 -3
  55. package/lib/module/layout/welcome.js.map +1 -1
  56. package/lib/module/layout/window.js +85 -7
  57. package/lib/module/layout/window.js.map +1 -1
  58. package/lib/module/utils/audioRecorder.js +275 -0
  59. package/lib/module/utils/audioRecorder.js.map +1 -0
  60. package/lib/module/utils/textToSpeech.js +34 -0
  61. package/lib/module/utils/textToSpeech.js.map +1 -0
  62. package/lib/typescript/components/input.d.ts.map +1 -1
  63. package/lib/typescript/components/productCard.d.ts.map +1 -1
  64. package/lib/typescript/components/progressCircle.d.ts +3 -0
  65. package/lib/typescript/components/progressCircle.d.ts.map +1 -0
  66. package/lib/typescript/components/voice.d.ts +3 -0
  67. package/lib/typescript/components/voice.d.ts.map +1 -0
  68. package/lib/typescript/components/welcomeInput.d.ts.map +1 -1
  69. package/lib/typescript/contexts/AppContext.d.ts.map +1 -1
  70. package/lib/typescript/layout/layout.d.ts.map +1 -1
  71. package/lib/typescript/layout/window.d.ts.map +1 -1
  72. package/lib/typescript/utils/audioRecorder.d.ts +7 -0
  73. package/lib/typescript/utils/audioRecorder.d.ts.map +1 -0
  74. package/lib/typescript/utils/textToSpeech.d.ts +2 -0
  75. package/lib/typescript/utils/textToSpeech.d.ts.map +1 -0
  76. package/package.json +8 -3
  77. package/src/components/email.js +7 -7
  78. package/src/components/header.js +12 -10
  79. package/src/components/input.js +5 -4
  80. package/src/components/productCard.js +3 -0
  81. package/src/components/progressCircle.js +86 -0
  82. package/src/components/voice.js +111 -0
  83. package/src/components/welcomeInput.js +8 -4
  84. package/src/contexts/AppContext.js +6 -1
  85. package/src/hooks/stream.js +2 -2
  86. package/src/layout/disclaimer.js +1 -1
  87. package/src/layout/layout.js +11 -15
  88. package/src/layout/welcome.js +1 -1
  89. package/src/layout/window.js +65 -7
  90. package/src/utils/audioRecorder.js +320 -0
  91. package/src/utils/textToSpeech.js +38 -0
@@ -11,10 +11,11 @@ import { useWebSocketMessage } from '../hooks/stream';
11
11
  import { ProductCard } from '../components/productCard'
12
12
  import Markdown from 'react-native-markdown-display';
13
13
  import { Feedback } from '../components/feedback';
14
+ import { ProgressCircle } from '../components/progressCircle';
14
15
 
15
16
  export const ChatWindow = ({ panHandlers }) => {
16
17
  const { handleSend, messages, input, setInput, ghostMessage, handleButtonClick,
17
- onProductCardClick, onAddToCartClick, uiConfig
18
+ onProductCardClick, onAddToCartClick, uiConfig, ghostCard, typingIndicator
18
19
  } = useContext(AppContext);
19
20
 
20
21
  const scrollViewRef = useRef(null);
@@ -47,7 +48,7 @@ export const ChatWindow = ({ panHandlers }) => {
47
48
 
48
49
  return (
49
50
  <View style={styles.container}>
50
- <View {...panHandlers}>
51
+ <View>
51
52
  <Header />
52
53
  </View>
53
54
 
@@ -72,7 +73,7 @@ export const ChatWindow = ({ panHandlers }) => {
72
73
  {msg.type !== "middle" && (
73
74
  <View style={[ styles.messageBubble, msg.type === "user" ? styles.userMessage : styles.aiMessage,]}>
74
75
  <Markdown style={{ body: { color: msg.type === "user" ? "#ffffff" : "#161616",fontSize: 16, lineHeight: 22 }}}>
75
- {msg.text}
76
+ {typeof msg.text === 'string' ? msg.text : String(msg.text || '')}
76
77
  </Markdown>
77
78
  {(msg.type == 'ai' && i != 0 && msg.message_id ) &&
78
79
  <Feedback message={msg} messageId={msg.message_id}/>
@@ -80,6 +81,15 @@ export const ChatWindow = ({ panHandlers }) => {
80
81
  </View>
81
82
  )}
82
83
 
84
+ {msg.type == "middle" && (
85
+ <View style={[styles.middleMessageBubble, styles.middleMessage]}>
86
+ <Ionicons name="sparkles-outline" size={20} style={{marginRight: 10}}/>
87
+ <Markdown style={{ body: { color: msg.type === "user" ? "#ffffff" : "#161616",fontSize: 16, lineHeight: 22 }}}>
88
+ {typeof msg.text === 'string' ? msg.text : String(msg.text || '')}
89
+ </Markdown>
90
+ </View>
91
+ )}
92
+
83
93
  {msg.products && msg.products.length > 0 &&
84
94
  msg.products.map((prod, index) => (
85
95
  <View key={index} style={styles.productCardWrapper}>
@@ -87,8 +97,6 @@ export const ChatWindow = ({ panHandlers }) => {
87
97
  </View>
88
98
  ))}
89
99
 
90
- {/* "https://player.vimeo.com/video/857307005?h=441fb14207&badge=0&autopause=0&player_id=0&app_id=58479"
91
- */}
92
100
  {msg.resource && msg.resource.length > 0 && msg.resource_type == "video" &&
93
101
  <TouchableOpacity style={styles.resourceButton} onPress={() => openLink(msg.resource)}>
94
102
  <Text style={styles.resourceText}>Watch Video</Text>
@@ -96,7 +104,8 @@ export const ChatWindow = ({ panHandlers }) => {
96
104
  </TouchableOpacity>
97
105
  }
98
106
 
99
- {msg.suggested_questions && Array.isArray(msg.questions) && msg.questions.map((question, index) => (
107
+ {msg.suggested_questions && Array.isArray(msg.suggested_questions) && msg.suggested_questions.length > 0 &&
108
+ msg.suggested_questions.map((question, index) => (
100
109
  <TouchableOpacity key={index} style={styles.suggestedQuestionButton}
101
110
  onPress={() => handleButtonClick(question)}>
102
111
  <Text style={styles.suggestedQuestionText}>{question}</Text>
@@ -110,6 +119,18 @@ export const ChatWindow = ({ panHandlers }) => {
110
119
  <Animated.View style={[styles.ghostBar, styles.ghostBarMedium, { opacity: fadeAnim }]} />
111
120
  </View>
112
121
  )}
122
+
123
+ {ghostCard && i === messages.length - 1 && (
124
+ <View style={styles.ghostCardContainer}>
125
+ <View style={styles.ghostSquare} />
126
+ <View style={styles.ghostBarsContainer}>
127
+ <Animated.View style={[styles.ghostBar, styles.ghostBarShort, { opacity: fadeAnim }]} />
128
+ <Animated.View style={[styles.ghostBar, styles.ghostBarLong, { opacity: fadeAnim }]} />
129
+ <Animated.View style={[styles.ghostBar, styles.ghostBarMedium, { opacity: fadeAnim }]} />
130
+ </View>
131
+ </View>
132
+ )}
133
+
113
134
  </View>
114
135
  ))}
115
136
  </KeyboardAwareScrollView>
@@ -124,7 +145,10 @@ export const ChatWindow = ({ panHandlers }) => {
124
145
  onAddToCartClick={onAddToCartClick}
125
146
  />
126
147
  }
127
- <ChatInput/>
148
+ {!typingIndicator ?
149
+ <ChatInput/> :
150
+ <ProgressCircle />
151
+ }
128
152
  </KeyboardAvoidingView>
129
153
  </View>
130
154
  );
@@ -199,6 +223,26 @@ const styles = StyleSheet.create({
199
223
  ghostBarLong: {
200
224
  width: "100%",
201
225
  },
226
+ ghostCardContainer: {
227
+ flexDirection: "row", // Arrange elements in a row
228
+ alignItems: "center", // Vertically align items
229
+ width: "100%",
230
+ backgroundColor: "#FFFFFF",
231
+ borderRadius: 10,
232
+ borderTopLeftRadius: 0,
233
+ padding: 14,
234
+ marginVertical: 5,
235
+ },
236
+ ghostSquare: {
237
+ width: "25%", // Takes up 25% of the container
238
+ aspectRatio: 1, // Makes it a square
239
+ backgroundColor: "#ebebeb", // Adjust color if needed
240
+ borderRadius: 8,
241
+ },
242
+ ghostBarsContainer: {
243
+ width: "75%", // Takes up 75% of the container
244
+ paddingLeft: 10, // Adds some spacing from the square
245
+ },
202
246
  resourceButton: {
203
247
  flexDirection: 'row',
204
248
  alignItems: 'center',
@@ -214,6 +258,20 @@ const styles = StyleSheet.create({
214
258
  fontSize: 16,
215
259
  marginRight: 8,
216
260
  },
261
+ middleMessageBubble:{
262
+ padding: 6,
263
+ paddingHorizontal: 16,
264
+ borderRadius: 12,
265
+ marginBottom: 5,
266
+ flexDirection: 'row',
267
+ alignItems: 'center'
268
+ },
269
+ middleMessage:{
270
+ color: '#161616',
271
+ alignSelf: 'flex-start',
272
+ backgroundColor: '#e0f4fc', //'#e0f4fc',
273
+ width: '100%',
274
+ }
217
275
  });
218
276
 
219
277
  {/* <Testing
@@ -0,0 +1,320 @@
1
+ // audioRecorder.js
2
+
3
+ import { Platform, PermissionsAndroid } from 'react-native';
4
+ import Voice from '@react-native-community/voice';
5
+ import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
6
+
7
+ let resultCallback = null;
8
+ let silenceTimer = null;
9
+ let isCurrentlyRecording = false;
10
+ let finalResult = '';
11
+ const SILENCE_DURATION = 1500; // 1.5 seconds of silence before stopping
12
+
13
+ // Initialize Voice handlers
14
+ export async function initVoice(onResult) {
15
+ try {
16
+ resultCallback = onResult;
17
+ finalResult = '';
18
+
19
+ // First check if speech recognition is available
20
+ const isAvailable = await Voice.isAvailable();
21
+ if (!isAvailable) {
22
+ console.error('Speech recognition is not available on this device');
23
+ return false;
24
+ }
25
+
26
+ // Set up all event listeners
27
+ Voice.onSpeechStart = (e) => {
28
+ console.log('onSpeechStart: ', e);
29
+ isCurrentlyRecording = true;
30
+ finalResult = '';
31
+
32
+ if (silenceTimer) {
33
+ clearTimeout(silenceTimer);
34
+ silenceTimer = null;
35
+ }
36
+ };
37
+
38
+ Voice.onSpeechRecognized = (e) => {
39
+ console.log('onSpeechRecognized: ', e);
40
+ if (e.isFinal) {
41
+ console.log('Speech recognition final');
42
+ handleFinalResult();
43
+ }
44
+ };
45
+
46
+ Voice.onSpeechEnd = async (e) => {
47
+ console.log('onSpeechEnd: ', e);
48
+
49
+ if (silenceTimer) {
50
+ clearTimeout(silenceTimer);
51
+ silenceTimer = null;
52
+ }
53
+
54
+ // Only handle final result if we're still recording
55
+ if (isCurrentlyRecording) {
56
+ await handleFinalResult();
57
+ }
58
+ };
59
+
60
+ Voice.onSpeechError = async (e) => {
61
+ console.error('onSpeechError: ', e);
62
+
63
+
64
+ if (silenceTimer) {
65
+ clearTimeout(silenceTimer);
66
+ silenceTimer = null;
67
+ }
68
+
69
+
70
+ await cleanupVoiceSession();
71
+ resultCallback(null, e.error?.message || 'Speech recognition error');
72
+ };
73
+
74
+ Voice.onSpeechResults = (e) => {
75
+ console.log('onSpeechResults: ', e);
76
+ if (e.value && e.value.length > 0) {
77
+ finalResult = e.value[0];
78
+ handleSilenceDetection();
79
+ }
80
+ };
81
+
82
+ Voice.onSpeechPartialResults = (e) => {
83
+ console.log('onSpeechPartialResults: ', e);
84
+
85
+ if (silenceTimer) {
86
+ clearTimeout(silenceTimer);
87
+ }
88
+
89
+ if (e.value && e.value.length > 0) {
90
+ finalResult = e.value[0];
91
+ handleSilenceDetection();
92
+ }
93
+ };
94
+
95
+ if (Platform.OS === 'android') {
96
+ Voice.onSpeechVolumeChanged = (e) => {
97
+ console.log('onSpeechVolumeChanged: ', e);
98
+ };
99
+ }
100
+
101
+ return true;
102
+ } catch (error) {
103
+ console.error('Error initializing Voice:', error);
104
+ return false;
105
+ }
106
+ }
107
+
108
+ const handleSilenceDetection = () => {
109
+ if (silenceTimer) {
110
+ clearTimeout(silenceTimer);
111
+ }
112
+
113
+ silenceTimer = setTimeout(async () => {
114
+ if (isCurrentlyRecording) {
115
+ await handleFinalResult();
116
+ }
117
+ }, SILENCE_DURATION);
118
+ };
119
+
120
+ const handleFinalResult = async () => {
121
+ if (!isCurrentlyRecording) return;
122
+
123
+ if (finalResult) {
124
+ resultCallback(finalResult);
125
+ }
126
+
127
+ // Stop recording first
128
+ await stopRecording();
129
+
130
+ // Then clean up the session
131
+ await cleanupVoiceSession();
132
+ };
133
+
134
+ const cleanupVoiceSession = async () => {
135
+ isCurrentlyRecording = false;
136
+
137
+ if (silenceTimer) {
138
+ clearTimeout(silenceTimer);
139
+ silenceTimer = null;
140
+ }
141
+
142
+ try {
143
+ // First try to stop if still recognizing
144
+ const isRecognizing = await Voice.isRecognizing();
145
+ if (isRecognizing) {
146
+ try {
147
+ await Voice.stop();
148
+ await new Promise(resolve => setTimeout(resolve, 100));
149
+ } catch (e) {
150
+ console.error('Error stopping in cleanup:', e);
151
+ }
152
+ }
153
+
154
+ // Then force destroy
155
+ await Voice.destroy();
156
+ await new Promise(resolve => setTimeout(resolve, 300));
157
+
158
+ // Double check and force destroy again if needed
159
+ const stillRecognizing = await Voice.isRecognizing();
160
+ if (stillRecognizing) {
161
+ await Voice.destroy();
162
+ await new Promise(resolve => setTimeout(resolve, 300));
163
+ }
164
+ } catch (error) {
165
+ console.error('Error in cleanupVoiceSession:', error);
166
+ // Final attempt to destroy on error
167
+ try {
168
+ await Voice.destroy();
169
+ } catch (e) {
170
+ console.error('Final destroy attempt failed:', e);
171
+ }
172
+ }
173
+
174
+ finalResult = '';
175
+ };
176
+
177
+ export async function startRecording() {
178
+ try {
179
+ // Ensure cleanup of any existing session
180
+ await cleanupVoiceSession();
181
+
182
+ const hasPermission = await requestAudioPermission();
183
+ if (!hasPermission) {
184
+ console.error('No permission to record audio');
185
+ return false;
186
+ }
187
+
188
+ await Voice.start('en-US');
189
+ isCurrentlyRecording = true;
190
+ return true;
191
+ } catch (error) {
192
+ console.error('Error starting voice recognition:', error);
193
+ await cleanupVoiceSession();
194
+ return false;
195
+ }
196
+ }
197
+
198
+ export async function stopRecording() {
199
+ try {
200
+ if (!isCurrentlyRecording) return;
201
+
202
+ // Set this first to prevent race conditions
203
+ isCurrentlyRecording = false;
204
+
205
+ if (silenceTimer) {
206
+ clearTimeout(silenceTimer);
207
+ silenceTimer = null;
208
+ }
209
+
210
+ // First try to stop
211
+ try {
212
+ await Voice.stop();
213
+ // Wait a bit for stop to complete
214
+ await new Promise(resolve => setTimeout(resolve, 100));
215
+ } catch (error) {
216
+ console.error('Error stopping Voice:', error);
217
+ }
218
+
219
+ // Then force destroy
220
+ try {
221
+ await Voice.destroy();
222
+ await new Promise(resolve => setTimeout(resolve, 300));
223
+ } catch (error) {
224
+ console.error('Error destroying Voice:', error);
225
+ }
226
+
227
+ // Final cleanup
228
+ await cleanupVoiceSession();
229
+ } catch (error) {
230
+ console.error('Error in stopRecording:', error);
231
+ // Force cleanup on error
232
+ await cleanupVoiceSession();
233
+ }
234
+ }
235
+
236
+ export async function cancelRecording() {
237
+ try {
238
+ await Voice.cancel();
239
+ await cleanupVoiceSession();
240
+ } catch (error) {
241
+ console.error('Error canceling voice recognition:', error);
242
+ await cleanupVoiceSession();
243
+ }
244
+
245
+ }
246
+
247
+ export async function requestAudioPermission() {
248
+ if (Platform.OS === 'android') {
249
+ return await requestAndroidPermission();
250
+ } else if (Platform.OS === 'ios') {
251
+ return await requestIOSPermission();
252
+ }
253
+
254
+ return false;
255
+ }
256
+
257
+ async function requestAndroidPermission() {
258
+ try {
259
+ // Check available speech recognition services on Android
260
+ const services = await Voice.getSpeechRecognitionServices();
261
+ if (!services || services.length === 0) {
262
+ console.error('No speech recognition services available');
263
+ return false;
264
+ }
265
+
266
+ const granted = await PermissionsAndroid.request(
267
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
268
+ {
269
+ title: 'Microphone Permission',
270
+
271
+ message: 'This app needs access to your microphone for voice recognition.',
272
+
273
+ buttonPositive: 'OK',
274
+ buttonNegative: 'Cancel',
275
+ }
276
+ );
277
+
278
+ return granted === PermissionsAndroid.RESULTS.GRANTED;
279
+ } catch (error) {
280
+ console.error('Error requesting Android permission:', error);
281
+
282
+ return false;
283
+ }
284
+ }
285
+
286
+
287
+ async function requestIOSPermission() {
288
+ try {
289
+ // Request microphone permission
290
+ const micPermission = await request(PERMISSIONS.IOS.MICROPHONE);
291
+ if (micPermission !== RESULTS.GRANTED) {
292
+ console.log('Microphone permission denied');
293
+ return false;
294
+ }
295
+
296
+ // Request speech recognition permission
297
+ const speechPermission = await request(PERMISSIONS.IOS.SPEECH_RECOGNITION);
298
+ if (speechPermission !== RESULTS.GRANTED) {
299
+ console.log('Speech recognition permission denied');
300
+ return false;
301
+ }
302
+
303
+ return true;
304
+ } catch (error) {
305
+ console.error('Error requesting iOS permissions:', error);
306
+ return false;
307
+ }
308
+ }
309
+
310
+ export function cleanup() {
311
+ Voice.destroy().then(() => {
312
+ Voice.removeAllListeners();
313
+ cleanupVoiceSession();
314
+ }).catch(error => {
315
+ console.error('Error in cleanup:', error);
316
+ // Try one more time
317
+ Voice.destroy().catch(e => console.error('Final cleanup attempt failed:', e));
318
+ });
319
+ }
320
+
@@ -0,0 +1,38 @@
1
+ // textToSpeech.js
2
+ import React,{ useState, useContext} from 'react';
3
+ import axios from 'axios';
4
+ import Sound from 'react-native-sound';
5
+ import { AppContext } from '../contexts/AppContext';
6
+
7
+ export const TextToSpeech = async (inputText) => {
8
+ const { data } = useContext(AppContext)
9
+ try {
10
+ const response = await axios.post(
11
+ 'https://api.openai.com/v1/audio/speech',
12
+ {
13
+ model: 'tts-1',
14
+ voice: 'alloy',
15
+ input: inputText,
16
+ },
17
+ {
18
+ headers: {
19
+ Authorization: `Bearer ${data.openai_key}`,
20
+ 'Content-Type': 'application/json',
21
+ },
22
+ responseType: 'arraybuffer',
23
+ }
24
+ );
25
+
26
+ const audioFile = `data:audio/mp3;base64,${Buffer.from(response.data).toString('base64')}`;
27
+
28
+ const sound = new Sound(audioFile, null, (error) => {
29
+ if (error) {
30
+ console.error('Error playing sound:', error);
31
+ } else {
32
+ sound.play();
33
+ }
34
+ });
35
+ } catch (error) {
36
+ console.error('Error generating TTS:', error);
37
+ }
38
+ };