react-native-srschat 0.1.59 → 0.1.60

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.
@@ -19,7 +19,7 @@ import { AppContext } from '../contexts/AppContext';
19
19
 
20
20
  const PERMISSION_STORAGE_KEY = '@voice_permission_status';
21
21
 
22
- export const VoiceButton = () => {
22
+ export const VoiceButton = ({ setInput }) => {
23
23
  const { handleVoiceSend } = useContext(AppContext);
24
24
  const [isListening, setIsListening] = useState(false);
25
25
  const [loading, setLoading] = useState(false);
@@ -48,23 +48,42 @@ export const VoiceButton = () => {
48
48
  setHasPermission(permissionResult);
49
49
 
50
50
  if (permissionResult) {
51
- const initialized = await initVoice((result, error) => {
52
- if (error) {
53
- // Don't show alert for permission errors since we handle that elsewhere
54
- if (!error.includes('permission')) {
55
- Alert.alert('Error', error);
51
+ const initialized = await initVoice(
52
+ // Final result callback - just update input, don't send
53
+ (result, error) => {
54
+ console.log('Voice final result:', result, 'Error:', error);
55
+ if (error) {
56
+ // Don't show alert for permission errors since we handle that elsewhere
57
+ if (!error.includes('permission')) {
58
+ Alert.alert('Error', error);
59
+ }
60
+ setIsListening(false);
61
+ setLoading(false);
62
+ return;
63
+ }
64
+ if (result) {
65
+ if (setInput) {
66
+ // Just update the input field, don't send
67
+ setInput(result);
68
+ } else {
69
+ console.warn('VoiceButton: setInput prop is not provided');
70
+ }
56
71
  }
72
+ // Always reset states when the recognition ends
57
73
  setIsListening(false);
58
74
  setLoading(false);
59
- return;
60
- }
61
- if (result) {
62
- handleVoiceSend(null, result);
75
+ },
76
+ // Partial result callback for live transcription
77
+ (partialResult) => {
78
+ if (partialResult) {
79
+ if (setInput) {
80
+ setInput(partialResult);
81
+ } else {
82
+ console.warn('VoiceButton: setInput prop is not provided for partial results');
83
+ }
84
+ }
63
85
  }
64
- // Always reset states when the recognition ends
65
- setIsListening(false);
66
- setLoading(false);
67
- });
86
+ );
68
87
 
69
88
  if (!initialized) {
70
89
  // Only show this alert once per session
@@ -17,6 +17,7 @@ import { VoiceButton } from './voice';
17
17
  export const WelcomeInput = ({ onProductCardClick, onAddToCartClick }) => {
18
18
 
19
19
  const { data, handleSend, input, setInput, showModal, theme } = useContext(AppContext);
20
+ const inputRef = useRef(null);
20
21
 
21
22
  const handleKeyPress = ({ nativeEvent }) => {
22
23
  if (nativeEvent.key === 'return' && !nativeEvent.shiftKey) {
@@ -35,6 +36,7 @@ export const WelcomeInput = ({ onProductCardClick, onAddToCartClick }) => {
35
36
  return (
36
37
  <View style={styles.inputContainer}>
37
38
  <TextInput
39
+ ref={inputRef}
38
40
  style={styles.input}
39
41
  value={input}
40
42
  onChangeText={setInput}
@@ -45,9 +47,32 @@ export const WelcomeInput = ({ onProductCardClick, onAddToCartClick }) => {
45
47
  enablesReturnKeyAutomatically={true}
46
48
  onKeyPress={handleKeyPress}
47
49
  onSubmitEditing={onSubmitEditing}
50
+ selection={undefined}
48
51
  // blurOnSubmit={false}
49
52
  />
50
- <VoiceButton/>
53
+ <VoiceButton setInput={(text) => {
54
+ setInput(text);
55
+ // Auto-scroll to end
56
+ if (inputRef.current) {
57
+ // For iOS and Android, blur and refocus to ensure scroll
58
+ inputRef.current.blur();
59
+ setTimeout(() => {
60
+ if (inputRef.current) {
61
+ inputRef.current.focus();
62
+ // Set selection to end - Android sometimes needs a slight delay
63
+ if (Platform.OS === 'android') {
64
+ setTimeout(() => {
65
+ if (inputRef.current) {
66
+ inputRef.current.setSelection(text.length, text.length);
67
+ }
68
+ }, 10);
69
+ } else {
70
+ inputRef.current.setSelection(text.length, text.length);
71
+ }
72
+ }
73
+ }, 50);
74
+ }
75
+ }}/>
51
76
  <TouchableOpacity
52
77
  style={styles.sendButton}
53
78
  onPress={() => handleSend(input)}
@@ -6,10 +6,13 @@ import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
6
6
  import useAsyncStorage from '../hooks/useAsyncStorage';
7
7
 
8
8
  let resultCallback = null;
9
+ let partialResultCallback = null;
9
10
  let silenceTimer = null;
10
11
  let isCurrentlyRecording = false;
11
12
  let finalResult = '';
13
+ let lastPartialResultTime = 0;
12
14
  const SILENCE_DURATION = 1500; // 1.5 seconds of silence before stopping
15
+ const PARTIAL_RESULT_THROTTLE = 100; // Throttle partial results to every 100ms
13
16
 
14
17
  // Add this constant for AsyncStorage key
15
18
  const PERMISSION_STORAGE_KEY = '@voice_permission_status';
@@ -25,9 +28,10 @@ export function setPermissionStatusHandlers(getter, setter) {
25
28
  }
26
29
 
27
30
  // Initialize Voice handlers
28
- export async function initVoice(onResult) {
31
+ export async function initVoice(onResult, onPartialResult = null) {
29
32
  try {
30
33
  resultCallback = onResult;
34
+ partialResultCallback = onPartialResult;
31
35
  finalResult = '';
32
36
 
33
37
  // Check if Voice module is available
@@ -42,6 +46,18 @@ export async function initVoice(onResult) {
42
46
  console.error('Speech recognition is not available on this device');
43
47
  return false;
44
48
  }
49
+
50
+ // Check if another recognition session is active
51
+ try {
52
+ const isRecognizing = await Voice.isRecognizing();
53
+ if (isRecognizing) {
54
+ console.log('Another recognition session is active, cleaning up...');
55
+ await Voice.destroy();
56
+ await new Promise(resolve => setTimeout(resolve, 300));
57
+ }
58
+ } catch (e) {
59
+ // Ignore errors checking recognition state
60
+ }
45
61
 
46
62
  // Set up all event listeners
47
63
  Voice.onSpeechStart = (e) => {
@@ -53,6 +69,9 @@ export async function initVoice(onResult) {
53
69
  clearTimeout(silenceTimer);
54
70
  silenceTimer = null;
55
71
  }
72
+
73
+ // Start initial silence detection
74
+ handleSilenceDetection();
56
75
  };
57
76
 
58
77
  Voice.onSpeechRecognized = (e) => {
@@ -71,40 +90,54 @@ export async function initVoice(onResult) {
71
90
  silenceTimer = null;
72
91
  }
73
92
 
74
- // Only handle final result if we're still recording
75
- if (isCurrentlyRecording) {
76
- await handleFinalResult();
93
+ // On iOS, onSpeechEnd can fire multiple times, so check if we're still recording
94
+ if (isCurrentlyRecording && finalResult) {
95
+ // Don't call handleFinalResult here on iOS as it causes the error
96
+ // Instead, just mark that speech ended and let silence detection handle it
97
+ if (Platform.OS === 'ios') {
98
+ console.log('iOS: Speech ended, waiting for silence detection to handle cleanup');
99
+ } else {
100
+ await handleFinalResult();
101
+ }
77
102
  }
78
103
  };
79
104
 
80
105
  Voice.onSpeechError = async (e) => {
81
- // console.error('onSpeechError: ', e);
106
+ console.error('onSpeechError: ', e);
82
107
 
83
108
  if (silenceTimer) {
84
109
  clearTimeout(silenceTimer);
85
110
  silenceTimer = null;
86
111
  }
87
112
 
88
- // Check for "No speech detected" error
113
+ // Check for specific error types
89
114
  const isNoSpeechError = e.error?.code === "recognition_fail" &&
90
115
  e.error?.message?.includes("No speech detected");
91
116
 
117
+ // iOS error 1101 is often transient and related to service availability
118
+ const isTransientIOSError = Platform.OS === 'ios' &&
119
+ (e.error?.code === "1101" ||
120
+ e.error?.message?.includes("kAFAssistantErrorDomain"));
121
+
92
122
  await cleanupVoiceSession();
93
123
 
94
- // Only send error to callback if it's not a "No speech detected" error
95
- // if (!isNoSpeechError) {
96
- // resultCallback(null, e.error?.message || 'Speech recognition error');
97
- // } else {
98
- // console.log('No speech detected, ignoring error');
99
- // // Optionally, call the callback with null parameters or a special indicator
100
- // resultCallback(null, null); // This won't trigger an error alert in the component
101
- // }
124
+ // Only send error to callback for non-transient errors
125
+ if (!isNoSpeechError && !isTransientIOSError && resultCallback) {
126
+ resultCallback(null, e.error?.message || 'Speech recognition error');
127
+ } else if (isTransientIOSError) {
128
+ console.log('Transient iOS speech recognition error, attempting to recover');
129
+ // Send the final result if we have one
130
+ if (finalResult && resultCallback) {
131
+ resultCallback(finalResult);
132
+ }
133
+ }
102
134
  };
103
135
 
104
136
  Voice.onSpeechResults = (e) => {
105
137
  console.log('onSpeechResults: ', e);
106
138
  if (e.value && e.value.length > 0) {
107
139
  finalResult = e.value[0];
140
+ // Also trigger silence detection for final results
108
141
  handleSilenceDetection();
109
142
  }
110
143
  };
@@ -118,6 +151,14 @@ export async function initVoice(onResult) {
118
151
 
119
152
  if (e.value && e.value.length > 0) {
120
153
  finalResult = e.value[0];
154
+
155
+ // Throttle partial results to prevent overwhelming the UI and speech service
156
+ const now = Date.now();
157
+ if (partialResultCallback && (now - lastPartialResultTime) > PARTIAL_RESULT_THROTTLE) {
158
+ partialResultCallback(e.value[0]);
159
+ lastPartialResultTime = now;
160
+ }
161
+
121
162
  handleSilenceDetection();
122
163
  }
123
164
  };
@@ -141,7 +182,8 @@ const handleSilenceDetection = () => {
141
182
  }
142
183
 
143
184
  silenceTimer = setTimeout(async () => {
144
- if (isCurrentlyRecording) {
185
+ console.log('Silence detected, stopping recording...');
186
+ if (isCurrentlyRecording && finalResult) {
145
187
  await handleFinalResult();
146
188
  }
147
189
  }, SILENCE_DURATION);
@@ -150,15 +192,27 @@ const handleSilenceDetection = () => {
150
192
  const handleFinalResult = async () => {
151
193
  if (!isCurrentlyRecording) return;
152
194
 
153
- if (finalResult) {
195
+ // Set flag immediately to prevent race conditions
196
+ isCurrentlyRecording = false;
197
+
198
+ // Clear silence timer first
199
+ if (silenceTimer) {
200
+ clearTimeout(silenceTimer);
201
+ silenceTimer = null;
202
+ }
203
+
204
+ // Send result if we have one
205
+ if (finalResult && resultCallback) {
154
206
  resultCallback(finalResult);
155
207
  }
156
208
 
157
- // Stop recording first
158
- await stopRecording();
209
+ // Give iOS time to finish processing before cleanup
210
+ if (Platform.OS === 'ios') {
211
+ await new Promise(resolve => setTimeout(resolve, 100));
212
+ }
159
213
 
160
- // Then clean up the session
161
- await cleanupVoiceSession();
214
+ // Stop recording with proper sequence
215
+ await stopRecording();
162
216
  };
163
217
 
164
218
  const cleanupVoiceSession = async () => {
@@ -229,6 +283,9 @@ export async function startRecording() {
229
283
  return false;
230
284
  }
231
285
 
286
+ // Reset throttle timer
287
+ lastPartialResultTime = 0;
288
+
232
289
  await Voice.start('en-US');
233
290
  isCurrentlyRecording = true;
234
291
  return true;
@@ -241,7 +298,21 @@ export async function startRecording() {
241
298
 
242
299
  export async function stopRecording() {
243
300
  try {
244
- if (!isCurrentlyRecording || !Voice) return;
301
+ if (!Voice) return;
302
+
303
+ // Already stopped
304
+ if (!isCurrentlyRecording) {
305
+ // Still try to clean up any lingering session
306
+ try {
307
+ const isRecognizing = await Voice.isRecognizing();
308
+ if (isRecognizing) {
309
+ await Voice.stop();
310
+ }
311
+ } catch (e) {
312
+ // Ignore errors checking recognition state
313
+ }
314
+ return;
315
+ }
245
316
 
246
317
  // Set this first to prevent race conditions
247
318
  isCurrentlyRecording = false;
@@ -251,21 +322,46 @@ export async function stopRecording() {
251
322
  silenceTimer = null;
252
323
  }
253
324
 
254
- // First try to stop
255
- try {
256
- await Voice.stop();
257
- // Wait a bit for stop to complete
258
- await new Promise(resolve => setTimeout(resolve, 100));
259
- } catch (error) {
260
- console.error('Error stopping Voice:', error);
261
- }
325
+ // For iOS, use a more careful approach
326
+ if (Platform.OS === 'ios') {
327
+ try {
328
+ // First cancel any ongoing recognition
329
+ await Voice.cancel();
330
+ await new Promise(resolve => setTimeout(resolve, 50));
331
+ } catch (error) {
332
+ // Ignore cancel errors
333
+ }
334
+
335
+ try {
336
+ // Then stop
337
+ await Voice.stop();
338
+ await new Promise(resolve => setTimeout(resolve, 100));
339
+ } catch (error) {
340
+ console.log('Error stopping Voice (expected on iOS):', error);
341
+ }
342
+
343
+ try {
344
+ // Finally destroy
345
+ await Voice.destroy();
346
+ await new Promise(resolve => setTimeout(resolve, 200));
347
+ } catch (error) {
348
+ console.log('Error destroying Voice (expected on iOS):', error);
349
+ }
350
+ } else {
351
+ // Android can handle the normal sequence
352
+ try {
353
+ await Voice.stop();
354
+ await new Promise(resolve => setTimeout(resolve, 100));
355
+ } catch (error) {
356
+ console.error('Error stopping Voice:', error);
357
+ }
262
358
 
263
- // Then force destroy
264
- try {
265
- await Voice.destroy();
266
- await new Promise(resolve => setTimeout(resolve, 300));
267
- } catch (error) {
268
- console.error('Error destroying Voice:', error);
359
+ try {
360
+ await Voice.destroy();
361
+ await new Promise(resolve => setTimeout(resolve, 300));
362
+ } catch (error) {
363
+ console.error('Error destroying Voice:', error);
364
+ }
269
365
  }
270
366
 
271
367
  // Final cleanup