react-native-srschat 0.1.26 → 0.1.28
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 +144 -0
- package/lib/commonjs/components/productCard.js +6 -1
- package/lib/commonjs/components/productCard.js.map +1 -1
- package/lib/commonjs/components/voice.js +88 -17
- package/lib/commonjs/components/voice.js.map +1 -1
- package/lib/commonjs/hooks/Stream.js +4 -3
- package/lib/commonjs/hooks/Stream.js.map +1 -1
- package/lib/commonjs/utils/audioRecorder.js +58 -7
- package/lib/commonjs/utils/audioRecorder.js.map +1 -1
- package/lib/module/components/productCard.js +6 -1
- package/lib/module/components/productCard.js.map +1 -1
- package/lib/module/components/voice.js +90 -19
- package/lib/module/components/voice.js.map +1 -1
- package/lib/module/hooks/Stream.js +4 -3
- package/lib/module/hooks/Stream.js.map +1 -1
- package/lib/module/utils/audioRecorder.js +56 -7
- package/lib/module/utils/audioRecorder.js.map +1 -1
- package/lib/typescript/components/productCard.d.ts.map +1 -1
- package/lib/typescript/components/voice.d.ts.map +1 -1
- package/lib/typescript/hooks/Stream.d.ts.map +1 -1
- package/lib/typescript/utils/audioRecorder.d.ts +2 -0
- package/lib/typescript/utils/audioRecorder.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/productCard.js +6 -1
- package/src/components/voice.js +107 -23
- package/src/hooks/Stream.js +5 -3
- package/src/utils/audioRecorder.js +60 -13
package/src/components/voice.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// VoiceButton.js
|
|
2
2
|
|
|
3
3
|
import React, { useState, useContext, useEffect } from 'react';
|
|
4
|
-
import { TouchableOpacity, ActivityIndicator, StyleSheet, Alert } from 'react-native';
|
|
4
|
+
import { TouchableOpacity, ActivityIndicator, StyleSheet, Alert, Linking, Platform } from 'react-native';
|
|
5
5
|
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
6
|
+
import useAsyncStorage from '../hooks/useAsyncStorage';
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
startRecording,
|
|
@@ -10,60 +11,129 @@ import {
|
|
|
10
11
|
cancelRecording,
|
|
11
12
|
requestAudioPermission,
|
|
12
13
|
cleanup,
|
|
13
|
-
initVoice
|
|
14
|
+
initVoice,
|
|
15
|
+
resetStoredPermission,
|
|
16
|
+
setPermissionStatusHandlers
|
|
14
17
|
} from '../utils/audioRecorder';
|
|
15
18
|
import { AppContext } from '../contexts/AppContext';
|
|
16
19
|
|
|
20
|
+
const PERMISSION_STORAGE_KEY = '@voice_permission_status';
|
|
21
|
+
|
|
17
22
|
export const VoiceButton = () => {
|
|
18
23
|
const { handleVoiceSend } = useContext(AppContext);
|
|
19
24
|
const [isListening, setIsListening] = useState(false);
|
|
20
25
|
const [loading, setLoading] = useState(false);
|
|
26
|
+
const [permissionChecked, setPermissionChecked] = useState(false);
|
|
27
|
+
const [hasPermission, setHasPermission] = useState(null);
|
|
28
|
+
|
|
29
|
+
// Use your custom AsyncStorage hook
|
|
30
|
+
const [permissionStatus, setPermissionStatus] = useAsyncStorage(PERMISSION_STORAGE_KEY, null);
|
|
21
31
|
|
|
22
32
|
useEffect(() => {
|
|
33
|
+
// Register our permission handlers
|
|
34
|
+
setPermissionStatusHandlers(() => permissionStatus, setPermissionStatus);
|
|
35
|
+
|
|
23
36
|
const setupVoice = async () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
try {
|
|
38
|
+
// Check stored permission first
|
|
39
|
+
if (permissionStatus === 'denied') {
|
|
40
|
+
// We already know permission was denied, don't show alert again
|
|
41
|
+
setHasPermission(false);
|
|
42
|
+
setPermissionChecked(true);
|
|
29
43
|
return;
|
|
30
44
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
|
|
46
|
+
// Only request permission if not already denied
|
|
47
|
+
const permissionResult = await requestAudioPermission();
|
|
48
|
+
setHasPermission(permissionResult);
|
|
49
|
+
|
|
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);
|
|
56
|
+
}
|
|
57
|
+
setIsListening(false);
|
|
58
|
+
setLoading(false);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (result) {
|
|
62
|
+
handleVoiceSend(null, result);
|
|
63
|
+
}
|
|
64
|
+
// Always reset states when the recognition ends
|
|
65
|
+
setIsListening(false);
|
|
66
|
+
setLoading(false);
|
|
67
|
+
});
|
|
37
68
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
69
|
+
if (!initialized) {
|
|
70
|
+
// Only show this alert once per session
|
|
71
|
+
if (!permissionChecked) {
|
|
72
|
+
Alert.alert(
|
|
73
|
+
'Error',
|
|
74
|
+
'Speech recognition is not available on this device'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error in setupVoice:', error);
|
|
81
|
+
} finally {
|
|
82
|
+
setPermissionChecked(true);
|
|
43
83
|
}
|
|
44
84
|
};
|
|
45
85
|
|
|
46
|
-
|
|
86
|
+
if (!permissionChecked) {
|
|
87
|
+
setupVoice();
|
|
88
|
+
}
|
|
47
89
|
|
|
48
90
|
return () => {
|
|
49
91
|
cleanup();
|
|
50
92
|
};
|
|
51
|
-
}, []);
|
|
93
|
+
}, [permissionStatus, permissionChecked]);
|
|
52
94
|
|
|
53
95
|
const toggleRecording = async () => {
|
|
54
96
|
try {
|
|
55
97
|
if (!isListening) {
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
Alert.alert(
|
|
98
|
+
// If we already know we don't have permission, show a better message
|
|
99
|
+
if (hasPermission === false) {
|
|
100
|
+
Alert.alert(
|
|
101
|
+
'Permission Required',
|
|
102
|
+
'Voice recognition requires microphone permission. Would you like to update your settings?',
|
|
103
|
+
[
|
|
104
|
+
{
|
|
105
|
+
text: 'Cancel',
|
|
106
|
+
style: 'cancel'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
text: 'Settings',
|
|
110
|
+
onPress: () => {
|
|
111
|
+
// Reset stored permission so we can check again
|
|
112
|
+
setPermissionStatus(null);
|
|
113
|
+
// Open device settings
|
|
114
|
+
openAppSettings();
|
|
115
|
+
// Reset our permission check
|
|
116
|
+
setPermissionChecked(false);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
);
|
|
59
121
|
return;
|
|
60
122
|
}
|
|
61
123
|
|
|
62
124
|
setLoading(true);
|
|
125
|
+
const checkPermission = await requestAudioPermission();
|
|
126
|
+
if (!checkPermission) {
|
|
127
|
+
setHasPermission(false);
|
|
128
|
+
setLoading(false);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
63
132
|
const started = await startRecording();
|
|
64
133
|
if (started) {
|
|
65
134
|
setIsListening(true);
|
|
66
135
|
} else {
|
|
136
|
+
// Only show error if not permission related
|
|
67
137
|
Alert.alert('Error', 'Failed to start voice recognition');
|
|
68
138
|
}
|
|
69
139
|
} else {
|
|
@@ -78,7 +148,21 @@ export const VoiceButton = () => {
|
|
|
78
148
|
await cleanup();
|
|
79
149
|
} finally {
|
|
80
150
|
setLoading(false);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
81
153
|
|
|
154
|
+
const openAppSettings = async () => {
|
|
155
|
+
try {
|
|
156
|
+
if (Platform.OS === 'ios') {
|
|
157
|
+
// For iOS
|
|
158
|
+
await Linking.openURL('app-settings://');
|
|
159
|
+
} else {
|
|
160
|
+
// For Android
|
|
161
|
+
await Linking.openSettings();
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Cannot open settings', error);
|
|
165
|
+
Alert.alert('Error', 'Unable to open settings. Please open settings manually.');
|
|
82
166
|
}
|
|
83
167
|
};
|
|
84
168
|
|
package/src/hooks/Stream.js
CHANGED
|
@@ -76,8 +76,9 @@ export function useWebSocketMessage() {
|
|
|
76
76
|
|
|
77
77
|
ws.onmessage = (event) => {
|
|
78
78
|
const response = JSON.parse(event.data);
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
|
|
80
|
+
if (response.type !== "chunk") {
|
|
81
|
+
console.log(JSON.stringify(response, null, 2));
|
|
81
82
|
}
|
|
82
83
|
switch (response.type) {
|
|
83
84
|
case 'middle_message':
|
|
@@ -179,7 +180,8 @@ export function useWebSocketMessage() {
|
|
|
179
180
|
product_description: prod.product_details.product_description || "",
|
|
180
181
|
product_name: prod.product_details.product_name || "",
|
|
181
182
|
type: prod.product_details.type || "",
|
|
182
|
-
vertical: prod.product_details.vertical || ""
|
|
183
|
+
vertical: prod.product_details.vertical || "",
|
|
184
|
+
sku: prod.product_details.sku || ""
|
|
183
185
|
} : {}
|
|
184
186
|
}));
|
|
185
187
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Platform, PermissionsAndroid } from 'react-native';
|
|
4
4
|
import Voice from '@react-native-community/voice';
|
|
5
5
|
import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
|
|
6
|
+
import useAsyncStorage from '../hooks/useAsyncStorage';
|
|
6
7
|
|
|
7
8
|
let resultCallback = null;
|
|
8
9
|
let silenceTimer = null;
|
|
@@ -10,6 +11,19 @@ let isCurrentlyRecording = false;
|
|
|
10
11
|
let finalResult = '';
|
|
11
12
|
const SILENCE_DURATION = 1500; // 1.5 seconds of silence before stopping
|
|
12
13
|
|
|
14
|
+
// Add this constant for AsyncStorage key
|
|
15
|
+
const PERMISSION_STORAGE_KEY = '@voice_permission_status';
|
|
16
|
+
|
|
17
|
+
// Create a function that can be called to get permission status
|
|
18
|
+
// This needs to be outside the React component lifecycle
|
|
19
|
+
let permissionStatusGetter = null;
|
|
20
|
+
let permissionStatusSetter = null;
|
|
21
|
+
|
|
22
|
+
export function setPermissionStatusHandlers(getter, setter) {
|
|
23
|
+
permissionStatusGetter = getter;
|
|
24
|
+
permissionStatusSetter = setter;
|
|
25
|
+
}
|
|
26
|
+
|
|
13
27
|
// Initialize Voice handlers
|
|
14
28
|
export async function initVoice(onResult) {
|
|
15
29
|
try {
|
|
@@ -60,15 +74,25 @@ export async function initVoice(onResult) {
|
|
|
60
74
|
Voice.onSpeechError = async (e) => {
|
|
61
75
|
console.error('onSpeechError: ', e);
|
|
62
76
|
|
|
63
|
-
|
|
64
77
|
if (silenceTimer) {
|
|
65
78
|
clearTimeout(silenceTimer);
|
|
66
79
|
silenceTimer = null;
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
// Check for "No speech detected" error
|
|
83
|
+
const isNoSpeechError = e.error?.code === "recognition_fail" &&
|
|
84
|
+
e.error?.message?.includes("No speech detected");
|
|
69
85
|
|
|
70
86
|
await cleanupVoiceSession();
|
|
71
|
-
|
|
87
|
+
|
|
88
|
+
// Only send error to callback if it's not a "No speech detected" error
|
|
89
|
+
if (!isNoSpeechError) {
|
|
90
|
+
resultCallback(null, e.error?.message || 'Speech recognition error');
|
|
91
|
+
} else {
|
|
92
|
+
console.log('No speech detected, ignoring error');
|
|
93
|
+
// Optionally, call the callback with null parameters or a special indicator
|
|
94
|
+
resultCallback(null, null); // This won't trigger an error alert in the component
|
|
95
|
+
}
|
|
72
96
|
};
|
|
73
97
|
|
|
74
98
|
Voice.onSpeechResults = (e) => {
|
|
@@ -245,13 +269,32 @@ export async function cancelRecording() {
|
|
|
245
269
|
}
|
|
246
270
|
|
|
247
271
|
export async function requestAudioPermission() {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
272
|
+
try {
|
|
273
|
+
// Get stored permission if available
|
|
274
|
+
const storedPermission = permissionStatusGetter ? permissionStatusGetter() : null;
|
|
275
|
+
|
|
276
|
+
if (storedPermission === 'denied') {
|
|
277
|
+
console.log('Permission previously denied by user');
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let permissionResult = false;
|
|
282
|
+
if (Platform.OS === 'android') {
|
|
283
|
+
permissionResult = await requestAndroidPermission();
|
|
284
|
+
} else if (Platform.OS === 'ios') {
|
|
285
|
+
permissionResult = await requestIOSPermission();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Store the result
|
|
289
|
+
if (permissionStatusSetter) {
|
|
290
|
+
permissionStatusSetter(permissionResult ? 'granted' : 'denied');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return permissionResult;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error('Error checking stored permission:', error);
|
|
296
|
+
return false;
|
|
252
297
|
}
|
|
253
|
-
|
|
254
|
-
return false;
|
|
255
298
|
}
|
|
256
299
|
|
|
257
300
|
async function requestAndroidPermission() {
|
|
@@ -267,23 +310,19 @@ async function requestAndroidPermission() {
|
|
|
267
310
|
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
268
311
|
{
|
|
269
312
|
title: 'Microphone Permission',
|
|
270
|
-
|
|
271
313
|
message: 'This app needs access to your microphone for voice recognition.',
|
|
272
|
-
|
|
273
314
|
buttonPositive: 'OK',
|
|
274
315
|
buttonNegative: 'Cancel',
|
|
275
316
|
}
|
|
276
317
|
);
|
|
277
|
-
|
|
318
|
+
|
|
278
319
|
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
279
320
|
} catch (error) {
|
|
280
321
|
console.error('Error requesting Android permission:', error);
|
|
281
|
-
|
|
282
322
|
return false;
|
|
283
323
|
}
|
|
284
324
|
}
|
|
285
325
|
|
|
286
|
-
|
|
287
326
|
async function requestIOSPermission() {
|
|
288
327
|
try {
|
|
289
328
|
// Request microphone permission
|
|
@@ -307,6 +346,14 @@ async function requestIOSPermission() {
|
|
|
307
346
|
}
|
|
308
347
|
}
|
|
309
348
|
|
|
349
|
+
export function resetStoredPermission() {
|
|
350
|
+
if (permissionStatusSetter) {
|
|
351
|
+
permissionStatusSetter(null);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
310
357
|
export function cleanup() {
|
|
311
358
|
Voice.destroy().then(() => {
|
|
312
359
|
Voice.removeAllListeners();
|