react-native-srschat 0.1.19 → 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.
- package/README.md +1 -0
- package/lib/commonjs/components/email.js +1 -1
- package/lib/commonjs/components/email.js.map +1 -1
- package/lib/commonjs/components/header.js +1 -0
- package/lib/commonjs/components/header.js.map +1 -1
- package/lib/commonjs/components/input.js +2 -7
- package/lib/commonjs/components/input.js.map +1 -1
- package/lib/commonjs/components/voice.js +47 -38
- package/lib/commonjs/components/voice.js.map +1 -1
- package/lib/commonjs/components/welcomeInput.js +4 -9
- package/lib/commonjs/components/welcomeInput.js.map +1 -1
- package/lib/commonjs/contexts/AppContext.js +6 -1
- package/lib/commonjs/contexts/AppContext.js.map +1 -1
- package/lib/commonjs/layout/disclaimer.js +1 -1
- package/lib/commonjs/layout/disclaimer.js.map +1 -1
- package/lib/commonjs/layout/layout.js +8 -23
- package/lib/commonjs/layout/layout.js.map +1 -1
- package/lib/commonjs/layout/welcome.js +4 -3
- package/lib/commonjs/layout/welcome.js.map +1 -1
- package/lib/commonjs/layout/window.js +4 -4
- package/lib/commonjs/layout/window.js.map +1 -1
- package/lib/commonjs/utils/audioRecorder.js +234 -44
- package/lib/commonjs/utils/audioRecorder.js.map +1 -1
- package/lib/module/components/email.js +1 -1
- package/lib/module/components/email.js.map +1 -1
- package/lib/module/components/header.js +1 -0
- package/lib/module/components/header.js.map +1 -1
- package/lib/module/components/input.js +2 -7
- package/lib/module/components/input.js.map +1 -1
- package/lib/module/components/voice.js +51 -41
- package/lib/module/components/voice.js.map +1 -1
- package/lib/module/components/welcomeInput.js +4 -9
- package/lib/module/components/welcomeInput.js.map +1 -1
- package/lib/module/contexts/AppContext.js +6 -1
- package/lib/module/contexts/AppContext.js.map +1 -1
- package/lib/module/layout/disclaimer.js +1 -1
- package/lib/module/layout/disclaimer.js.map +1 -1
- package/lib/module/layout/layout.js +8 -23
- package/lib/module/layout/layout.js.map +1 -1
- package/lib/module/layout/welcome.js +2 -3
- package/lib/module/layout/welcome.js.map +1 -1
- package/lib/module/layout/window.js +4 -4
- package/lib/module/layout/window.js.map +1 -1
- package/lib/module/utils/audioRecorder.js +232 -45
- package/lib/module/utils/audioRecorder.js.map +1 -1
- package/lib/typescript/components/input.d.ts.map +1 -1
- package/lib/typescript/components/voice.d.ts.map +1 -1
- package/lib/typescript/components/welcomeInput.d.ts.map +1 -1
- package/lib/typescript/contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/layout/layout.d.ts.map +1 -1
- package/lib/typescript/layout/window.d.ts.map +1 -1
- package/lib/typescript/utils/audioRecorder.d.ts +6 -3
- package/lib/typescript/utils/audioRecorder.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/email.js +1 -1
- package/src/components/header.js +1 -0
- package/src/components/input.js +2 -3
- package/src/components/voice.js +77 -50
- package/src/components/welcomeInput.js +6 -4
- package/src/contexts/AppContext.js +6 -1
- package/src/layout/disclaimer.js +1 -1
- package/src/layout/layout.js +8 -12
- package/src/layout/welcome.js +1 -1
- package/src/layout/window.js +5 -4
- package/src/utils/audioRecorder.js +266 -45
package/src/components/voice.js
CHANGED
|
@@ -1,75 +1,102 @@
|
|
|
1
1
|
// VoiceButton.js
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
|
|
3
|
+
import React, { useState, useContext, useEffect } from 'react';
|
|
4
|
+
import { TouchableOpacity, ActivityIndicator, StyleSheet, Alert } from 'react-native';
|
|
4
5
|
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
5
|
-
import axios from 'axios';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
startRecording,
|
|
9
|
+
stopRecording,
|
|
10
|
+
cancelRecording,
|
|
11
|
+
requestAudioPermission,
|
|
12
|
+
cleanup,
|
|
13
|
+
initVoice
|
|
14
|
+
} from '../utils/audioRecorder';
|
|
8
15
|
import { AppContext } from '../contexts/AppContext';
|
|
9
16
|
|
|
10
17
|
export const VoiceButton = () => {
|
|
11
|
-
const {
|
|
18
|
+
const { handleVoiceSend } = useContext(AppContext);
|
|
12
19
|
const [isListening, setIsListening] = useState(false);
|
|
13
20
|
const [loading, setLoading] = useState(false);
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const setupVoice = async () => {
|
|
24
|
+
const initialized = await initVoice((result, error) => {
|
|
25
|
+
if (error) {
|
|
26
|
+
Alert.alert('Error', error);
|
|
27
|
+
setIsListening(false);
|
|
28
|
+
setLoading(false);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (result) {
|
|
32
|
+
handleVoiceSend(null, result);
|
|
33
|
+
setIsListening(false);
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if (!initialized) {
|
|
39
|
+
Alert.alert(
|
|
40
|
+
'Error',
|
|
41
|
+
'Speech recognition is not available on this device'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
setupVoice();
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
return () => {
|
|
49
|
+
cleanup();
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const toggleRecording = async () => {
|
|
41
54
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
formData.append('model', 'whisper-1');
|
|
55
|
+
if (!isListening) {
|
|
56
|
+
const hasPermission = await requestAudioPermission();
|
|
57
|
+
if (!hasPermission) {
|
|
58
|
+
Alert.alert('Permission Denied', 'Microphone permission is required for voice recognition');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'Content-Type': 'multipart/form-data',
|
|
57
|
-
},
|
|
62
|
+
setLoading(true);
|
|
63
|
+
const started = await startRecording();
|
|
64
|
+
if (started) {
|
|
65
|
+
setIsListening(true);
|
|
66
|
+
} else {
|
|
67
|
+
Alert.alert('Error', 'Failed to start voice recognition');
|
|
58
68
|
}
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
} else {
|
|
70
|
+
setLoading(true);
|
|
71
|
+
setIsListening(false);
|
|
72
|
+
await stopRecording();
|
|
73
|
+
}
|
|
61
74
|
} catch (error) {
|
|
62
|
-
console.error('Error
|
|
63
|
-
|
|
75
|
+
console.error('Error in toggleRecording:', error);
|
|
76
|
+
Alert.alert('Error', 'An error occurred while managing voice recognition');
|
|
77
|
+
setIsListening(false);
|
|
78
|
+
await cleanup();
|
|
79
|
+
} finally {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
|
|
64
82
|
}
|
|
65
83
|
};
|
|
66
84
|
|
|
67
85
|
return (
|
|
68
|
-
<TouchableOpacity
|
|
86
|
+
<TouchableOpacity
|
|
87
|
+
style={styles.button}
|
|
88
|
+
onPress={toggleRecording}
|
|
89
|
+
disabled={loading}
|
|
90
|
+
>
|
|
69
91
|
{loading ? (
|
|
70
92
|
<ActivityIndicator size="small" color="#8E8E93" />
|
|
71
93
|
) : (
|
|
72
|
-
<Ionicons
|
|
94
|
+
<Ionicons
|
|
95
|
+
name={isListening ? 'stop-circle' : 'mic-outline'}
|
|
96
|
+
size={24}
|
|
97
|
+
color="#8E8E93"
|
|
98
|
+
/>
|
|
99
|
+
|
|
73
100
|
)}
|
|
74
101
|
</TouchableOpacity>
|
|
75
102
|
);
|
|
@@ -12,9 +12,11 @@ import {
|
|
|
12
12
|
import { Header } from '../components/header';
|
|
13
13
|
import { AppContext } from '../contexts/AppContext';
|
|
14
14
|
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
15
|
+
import { VoiceButton } from './voice';
|
|
15
16
|
|
|
16
17
|
export const WelcomeInput = ({ onProductCardClick, onAddToCartClick }) => {
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
const { data, handleSend, input, setInput, showModal, theme } = useContext(AppContext);
|
|
18
20
|
|
|
19
21
|
return (
|
|
20
22
|
<View style={styles.inputContainer}>
|
|
@@ -27,9 +29,9 @@ export const WelcomeInput = ({ onProductCardClick, onAddToCartClick }) => {
|
|
|
27
29
|
multiline
|
|
28
30
|
/>
|
|
29
31
|
{data.openai_key &&
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
<VoiceButton/>
|
|
34
|
+
|
|
33
35
|
}
|
|
34
36
|
<TouchableOpacity
|
|
35
37
|
style={styles.sendButton}
|
|
@@ -250,6 +250,11 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
|
|
|
250
250
|
};
|
|
251
251
|
});
|
|
252
252
|
};
|
|
253
|
+
|
|
254
|
+
function handleVoiceSend(audio, transcription) {
|
|
255
|
+
//setReadAloud(true)
|
|
256
|
+
handleSend(transcription)
|
|
257
|
+
}
|
|
253
258
|
|
|
254
259
|
return (
|
|
255
260
|
<AppContext.Provider
|
|
@@ -259,7 +264,7 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
|
|
|
259
264
|
startStreaming, setStartStreaming, maintenance, setMaintenance, feedback, setFeedback, handleFeedback, feedbackOpen, setFeedbackOpen,
|
|
260
265
|
writeFeedback, setWriteFeedback, writeAnswer, setWriteAnswer, BASE_URL, lastMessageId, setLastMessageId,
|
|
261
266
|
onProductCardClick, onAddToCartClick, data, sessionId, setSessionId, handleWrittenFeedback, switchFeedbackOpen, confirmDisclaimer,
|
|
262
|
-
formatChatHistory, uiConfig
|
|
267
|
+
formatChatHistory, uiConfig, handleVoiceSend
|
|
263
268
|
}}
|
|
264
269
|
>
|
|
265
270
|
{children}
|
package/src/layout/disclaimer.js
CHANGED
package/src/layout/layout.js
CHANGED
|
@@ -22,7 +22,10 @@ export const Layout = () => {
|
|
|
22
22
|
setShowModal("Icon");
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
/* Currently removed this from the child components.
|
|
27
|
+
If activating drag to close, put {...panHandlers} in the view around the
|
|
28
|
+
header on each page (welcome, window, disclaimer) */
|
|
26
29
|
const panResponder = PanResponder.create({
|
|
27
30
|
onStartShouldSetPanResponder: () => true,
|
|
28
31
|
onMoveShouldSetPanResponder: () => true,
|
|
@@ -80,7 +83,7 @@ export const Layout = () => {
|
|
|
80
83
|
)}
|
|
81
84
|
{showModal === "ChatWindow" && (
|
|
82
85
|
<>
|
|
83
|
-
<Pressable style={styles.outsideTouchable} onPress={() => handleClick()} />
|
|
86
|
+
{/* <Pressable style={styles.outsideTouchable} onPress={() => handleClick()} /> */}
|
|
84
87
|
<Animated.View style={[styles.container, { transform: [{ translateY: panY }] }]}>
|
|
85
88
|
<ChatWindow panHandlers={panResponder.panHandlers} />
|
|
86
89
|
</Animated.View>
|
|
@@ -88,7 +91,7 @@ export const Layout = () => {
|
|
|
88
91
|
)}
|
|
89
92
|
{showModal === "Welcome" && (
|
|
90
93
|
<>
|
|
91
|
-
<Pressable style={styles.outsideTouchable} onPress={() => handleClick()} />
|
|
94
|
+
{/* <Pressable style={styles.outsideTouchable} onPress={() => handleClick()} /> */}
|
|
92
95
|
<Animated.View style={[styles.container, { transform: [{ translateY: panY }] }]}>
|
|
93
96
|
<Welcome panHandlers={panResponder.panHandlers} />
|
|
94
97
|
</Animated.View>
|
|
@@ -96,7 +99,7 @@ export const Layout = () => {
|
|
|
96
99
|
)}
|
|
97
100
|
{showModal === "Form" &&
|
|
98
101
|
<>
|
|
99
|
-
<Pressable style={styles.outsideTouchable} onPress={() => handleClick()} />
|
|
102
|
+
{/* <Pressable style={styles.outsideTouchable} onPress={() => handleClick()} /> */}
|
|
100
103
|
<Animated.View style={[styles.container, { transform: [{ translateY: panY }] }]}>
|
|
101
104
|
<Disclaimer panHandlers={panResponder.panHandlers} />
|
|
102
105
|
</Animated.View>
|
|
@@ -104,7 +107,7 @@ export const Layout = () => {
|
|
|
104
107
|
}
|
|
105
108
|
{showModal === "Email" &&
|
|
106
109
|
<>
|
|
107
|
-
<Pressable style={styles.outsideTouchable} onPress={() => handleClick()} />
|
|
110
|
+
{/* <Pressable style={styles.outsideTouchable} onPress={() => handleClick()} /> */}
|
|
108
111
|
<Animated.View style={[styles.container, { transform: [{ translateY: panY }] }]}>
|
|
109
112
|
<EmailForm panHandlers={panResponder.panHandlers} />
|
|
110
113
|
</Animated.View>
|
|
@@ -144,11 +147,4 @@ const styles = StyleSheet.create({
|
|
|
144
147
|
outsideTouchable: {
|
|
145
148
|
flex: 1,
|
|
146
149
|
},
|
|
147
|
-
dragHandle: {
|
|
148
|
-
height: 20,
|
|
149
|
-
width: '100%',
|
|
150
|
-
backgroundColor: '#DDD',
|
|
151
|
-
borderTopLeftRadius: 16,
|
|
152
|
-
borderTopRightRadius: 16,
|
|
153
|
-
}
|
|
154
150
|
});
|
package/src/layout/welcome.js
CHANGED
|
@@ -24,7 +24,7 @@ export const Welcome = ({ panHandlers }) => {
|
|
|
24
24
|
|
|
25
25
|
{/* Top section */}
|
|
26
26
|
<View style={styles.topContainer}>
|
|
27
|
-
<View style={styles.topHeader}
|
|
27
|
+
<View style={styles.topHeader}>
|
|
28
28
|
<Image
|
|
29
29
|
source={require('../assets/heritage.png')}
|
|
30
30
|
style={[styles.logo, { tintColor: "white" }]}
|
package/src/layout/window.js
CHANGED
|
@@ -48,7 +48,7 @@ export const ChatWindow = ({ panHandlers }) => {
|
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
50
|
<View style={styles.container}>
|
|
51
|
-
<View
|
|
51
|
+
<View>
|
|
52
52
|
<Header />
|
|
53
53
|
</View>
|
|
54
54
|
|
|
@@ -73,7 +73,7 @@ export const ChatWindow = ({ panHandlers }) => {
|
|
|
73
73
|
{msg.type !== "middle" && (
|
|
74
74
|
<View style={[ styles.messageBubble, msg.type === "user" ? styles.userMessage : styles.aiMessage,]}>
|
|
75
75
|
<Markdown style={{ body: { color: msg.type === "user" ? "#ffffff" : "#161616",fontSize: 16, lineHeight: 22 }}}>
|
|
76
|
-
{msg.text}
|
|
76
|
+
{typeof msg.text === 'string' ? msg.text : String(msg.text || '')}
|
|
77
77
|
</Markdown>
|
|
78
78
|
{(msg.type == 'ai' && i != 0 && msg.message_id ) &&
|
|
79
79
|
<Feedback message={msg} messageId={msg.message_id}/>
|
|
@@ -85,7 +85,7 @@ export const ChatWindow = ({ panHandlers }) => {
|
|
|
85
85
|
<View style={[styles.middleMessageBubble, styles.middleMessage]}>
|
|
86
86
|
<Ionicons name="sparkles-outline" size={20} style={{marginRight: 10}}/>
|
|
87
87
|
<Markdown style={{ body: { color: msg.type === "user" ? "#ffffff" : "#161616",fontSize: 16, lineHeight: 22 }}}>
|
|
88
|
-
{msg.text}
|
|
88
|
+
{typeof msg.text === 'string' ? msg.text : String(msg.text || '')}
|
|
89
89
|
</Markdown>
|
|
90
90
|
</View>
|
|
91
91
|
)}
|
|
@@ -104,7 +104,8 @@ export const ChatWindow = ({ panHandlers }) => {
|
|
|
104
104
|
</TouchableOpacity>
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
{msg.suggested_questions && Array.isArray(msg.
|
|
107
|
+
{msg.suggested_questions && Array.isArray(msg.suggested_questions) && msg.suggested_questions.length > 0 &&
|
|
108
|
+
msg.suggested_questions.map((question, index) => (
|
|
108
109
|
<TouchableOpacity key={index} style={styles.suggestedQuestionButton}
|
|
109
110
|
onPress={() => handleButtonClick(question)}>
|
|
110
111
|
<Text style={styles.suggestedQuestionText}>{question}</Text>
|
|
@@ -1,54 +1,247 @@
|
|
|
1
1
|
// audioRecorder.js
|
|
2
|
-
import AudioRecorderPlayer from 'react-native-audio-recorder-player';
|
|
3
|
-
import { Platform, PermissionsAndroid } from 'react-native';
|
|
4
|
-
import { NativeModules } from 'react-native';
|
|
5
2
|
|
|
6
|
-
|
|
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';
|
|
7
6
|
|
|
7
|
+
let resultCallback = null;
|
|
8
8
|
let silenceTimer = null;
|
|
9
|
-
let
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
export async function startRecording(handleStopRecording) {
|
|
9
|
+
let isCurrentlyRecording = false;
|
|
10
|
+
let finalResult = '';
|
|
11
|
+
const SILENCE_DURATION = 1500; // 1.5 seconds of silence before stopping
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
// Initialize Voice handlers
|
|
14
|
+
export async function initVoice(onResult) {
|
|
15
|
+
try {
|
|
16
|
+
resultCallback = onResult;
|
|
17
|
+
finalResult = '';
|
|
16
18
|
|
|
17
|
-
|
|
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
|
+
}
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
};
|
|
25
45
|
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
Voice.onSpeechEnd = async (e) => {
|
|
47
|
+
console.log('onSpeechEnd: ', e);
|
|
48
|
+
|
|
28
49
|
if (silenceTimer) {
|
|
29
50
|
clearTimeout(silenceTimer);
|
|
30
51
|
silenceTimer = null;
|
|
31
52
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
};
|
|
36
99
|
}
|
|
37
|
-
|
|
38
|
-
|
|
100
|
+
|
|
101
|
+
return true;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error initializing Voice:', error);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
39
106
|
}
|
|
40
107
|
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
return null;
|
|
108
|
+
const handleSilenceDetection = () => {
|
|
109
|
+
if (silenceTimer) {
|
|
110
|
+
clearTimeout(silenceTimer);
|
|
45
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
|
+
|
|
46
137
|
if (silenceTimer) {
|
|
47
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();
|
|
48
233
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
|
|
52
245
|
}
|
|
53
246
|
|
|
54
247
|
export async function requestAudioPermission() {
|
|
@@ -57,16 +250,26 @@ export async function requestAudioPermission() {
|
|
|
57
250
|
} else if (Platform.OS === 'ios') {
|
|
58
251
|
return await requestIOSPermission();
|
|
59
252
|
}
|
|
253
|
+
|
|
254
|
+
return false;
|
|
60
255
|
}
|
|
61
256
|
|
|
62
|
-
// ✅ Android: Request Microphone Permission
|
|
63
257
|
async function requestAndroidPermission() {
|
|
64
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
|
+
|
|
65
266
|
const granted = await PermissionsAndroid.request(
|
|
66
267
|
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
67
268
|
{
|
|
68
269
|
title: 'Microphone Permission',
|
|
69
|
-
|
|
270
|
+
|
|
271
|
+
message: 'This app needs access to your microphone for voice recognition.',
|
|
272
|
+
|
|
70
273
|
buttonPositive: 'OK',
|
|
71
274
|
buttonNegative: 'Cancel',
|
|
72
275
|
}
|
|
@@ -74,26 +277,44 @@ async function requestAndroidPermission() {
|
|
|
74
277
|
|
|
75
278
|
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
76
279
|
} catch (error) {
|
|
77
|
-
console.error('Error requesting
|
|
280
|
+
console.error('Error requesting Android permission:', error);
|
|
281
|
+
|
|
78
282
|
return false;
|
|
79
283
|
}
|
|
80
284
|
}
|
|
81
285
|
|
|
82
|
-
|
|
286
|
+
|
|
83
287
|
async function requestIOSPermission() {
|
|
84
288
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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');
|
|
93
300
|
return false;
|
|
94
301
|
}
|
|
302
|
+
|
|
303
|
+
return true;
|
|
95
304
|
} catch (error) {
|
|
96
|
-
console.error('Error requesting
|
|
305
|
+
console.error('Error requesting iOS permissions:', error);
|
|
97
306
|
return false;
|
|
98
307
|
}
|
|
99
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
|
+
|