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.
Files changed (65) hide show
  1. package/README.md +1 -0
  2. package/lib/commonjs/components/email.js +1 -1
  3. package/lib/commonjs/components/email.js.map +1 -1
  4. package/lib/commonjs/components/header.js +1 -0
  5. package/lib/commonjs/components/header.js.map +1 -1
  6. package/lib/commonjs/components/input.js +2 -7
  7. package/lib/commonjs/components/input.js.map +1 -1
  8. package/lib/commonjs/components/voice.js +47 -38
  9. package/lib/commonjs/components/voice.js.map +1 -1
  10. package/lib/commonjs/components/welcomeInput.js +4 -9
  11. package/lib/commonjs/components/welcomeInput.js.map +1 -1
  12. package/lib/commonjs/contexts/AppContext.js +6 -1
  13. package/lib/commonjs/contexts/AppContext.js.map +1 -1
  14. package/lib/commonjs/layout/disclaimer.js +1 -1
  15. package/lib/commonjs/layout/disclaimer.js.map +1 -1
  16. package/lib/commonjs/layout/layout.js +8 -23
  17. package/lib/commonjs/layout/layout.js.map +1 -1
  18. package/lib/commonjs/layout/welcome.js +4 -3
  19. package/lib/commonjs/layout/welcome.js.map +1 -1
  20. package/lib/commonjs/layout/window.js +4 -4
  21. package/lib/commonjs/layout/window.js.map +1 -1
  22. package/lib/commonjs/utils/audioRecorder.js +234 -44
  23. package/lib/commonjs/utils/audioRecorder.js.map +1 -1
  24. package/lib/module/components/email.js +1 -1
  25. package/lib/module/components/email.js.map +1 -1
  26. package/lib/module/components/header.js +1 -0
  27. package/lib/module/components/header.js.map +1 -1
  28. package/lib/module/components/input.js +2 -7
  29. package/lib/module/components/input.js.map +1 -1
  30. package/lib/module/components/voice.js +51 -41
  31. package/lib/module/components/voice.js.map +1 -1
  32. package/lib/module/components/welcomeInput.js +4 -9
  33. package/lib/module/components/welcomeInput.js.map +1 -1
  34. package/lib/module/contexts/AppContext.js +6 -1
  35. package/lib/module/contexts/AppContext.js.map +1 -1
  36. package/lib/module/layout/disclaimer.js +1 -1
  37. package/lib/module/layout/disclaimer.js.map +1 -1
  38. package/lib/module/layout/layout.js +8 -23
  39. package/lib/module/layout/layout.js.map +1 -1
  40. package/lib/module/layout/welcome.js +2 -3
  41. package/lib/module/layout/welcome.js.map +1 -1
  42. package/lib/module/layout/window.js +4 -4
  43. package/lib/module/layout/window.js.map +1 -1
  44. package/lib/module/utils/audioRecorder.js +232 -45
  45. package/lib/module/utils/audioRecorder.js.map +1 -1
  46. package/lib/typescript/components/input.d.ts.map +1 -1
  47. package/lib/typescript/components/voice.d.ts.map +1 -1
  48. package/lib/typescript/components/welcomeInput.d.ts.map +1 -1
  49. package/lib/typescript/contexts/AppContext.d.ts.map +1 -1
  50. package/lib/typescript/layout/layout.d.ts.map +1 -1
  51. package/lib/typescript/layout/window.d.ts.map +1 -1
  52. package/lib/typescript/utils/audioRecorder.d.ts +6 -3
  53. package/lib/typescript/utils/audioRecorder.d.ts.map +1 -1
  54. package/package.json +5 -1
  55. package/src/components/email.js +1 -1
  56. package/src/components/header.js +1 -0
  57. package/src/components/input.js +2 -3
  58. package/src/components/voice.js +77 -50
  59. package/src/components/welcomeInput.js +6 -4
  60. package/src/contexts/AppContext.js +6 -1
  61. package/src/layout/disclaimer.js +1 -1
  62. package/src/layout/layout.js +8 -12
  63. package/src/layout/welcome.js +1 -1
  64. package/src/layout/window.js +5 -4
  65. package/src/utils/audioRecorder.js +266 -45
@@ -1,75 +1,102 @@
1
1
  // VoiceButton.js
2
- import React, { useState, useContext } from 'react';
3
- import { TouchableOpacity, ActivityIndicator, View, StyleSheet } from 'react-native';
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 { startRecording, stopRecording, requestAudioPermission } from '../utils/audioRecorder';
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 { data, handleVoiceSend } = useContext(AppContext);
18
+ const { handleVoiceSend } = useContext(AppContext);
12
19
  const [isListening, setIsListening] = useState(false);
13
20
  const [loading, setLoading] = useState(false);
14
21
 
15
- const toggleRecording = async () => {
16
- const hasPermission = await requestAudioPermission();
17
-
18
- if (!hasPermission) {
19
- console.error('Permission denied');
20
- return;
21
- }
22
-
23
- if (!isListening) {
24
- startRecording(handleStopRecording);
25
- setIsListening(true);
26
- } else {
27
- handleStopRecording();
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
- const handleStopRecording = async () => {
32
- setLoading(true);
33
- const audioPath = await stopRecording();
34
- const transcription = await transcribeAudio(audioPath);
35
- setLoading(false);
36
- setIsListening(false);
37
- handleVoiceSend(audioPath, transcription);
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
- const transcribeAudio = async (audioPath) => {
48
+ return () => {
49
+ cleanup();
50
+ };
51
+ }, []);
52
+
53
+ const toggleRecording = async () => {
41
54
  try {
42
- const formData = new FormData();
43
- formData.append('file', {
44
- uri: audioPath,
45
- type: 'audio/wav',
46
- name: 'audio.wav',
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
- const response = await axios.post(
51
- 'https://api.openai.com/v1/audio/transcriptions',
52
- formData,
53
- {
54
- headers: {
55
- Authorization: `Bearer ${data.openai_key}`,
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
- return response.data.text;
69
+ } else {
70
+ setLoading(true);
71
+ setIsListening(false);
72
+ await stopRecording();
73
+ }
61
74
  } catch (error) {
62
- console.error('Error transcribing audio:', error);
63
- return '';
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 style={styles.button} onPress={toggleRecording} disabled={loading}>
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 name={isListening ? 'stop-circle' : 'mic-outline'} size={24} color="#8E8E93" />
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
- const { handleSend, input, setInput, showModal, theme, data } = useContext(AppContext);
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
- <TouchableOpacity style={styles.inputButton}>
31
- <Ionicons name="mic-outline" size={24} color="#8E8E93" />
32
- </TouchableOpacity>
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}
@@ -28,7 +28,7 @@ export const Disclaimer = ({ panHandlers }) => {
28
28
 
29
29
  return (
30
30
  <View style={styles.container}>
31
- <View {...panHandlers}>
31
+ <View>
32
32
  <Header />
33
33
  </View>
34
34
  <ScrollView contentContainerStyle={styles.scrollContainer}>
@@ -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
  });
@@ -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} {...panHandlers}>
27
+ <View style={styles.topHeader}>
28
28
  <Image
29
29
  source={require('../assets/heritage.png')}
30
30
  style={[styles.logo, { tintColor: "white" }]}
@@ -48,7 +48,7 @@ export const ChatWindow = ({ panHandlers }) => {
48
48
 
49
49
  return (
50
50
  <View style={styles.container}>
51
- <View {...panHandlers}>
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.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) => (
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
- const audioRecorderPlayer = new AudioRecorderPlayer();
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 rmsValues = [];
10
- const SILENCE_RMS_THRESHOLD = 0.02;
11
- const SILENCE_LENGTH_MS = 2000;
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
- audioRecorderPlayer = new AudioRecorderPlayer();
13
+ // Initialize Voice handlers
14
+ export async function initVoice(onResult) {
15
+ try {
16
+ resultCallback = onResult;
17
+ finalResult = '';
16
18
 
17
- rmsValues = [];
18
- audioRecorderPlayer.startRecorder();
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
- audioRecorderPlayer.addRecordBackListener((e) => {
21
- const rms = Math.sqrt(
22
- e.currentMetering.reduce((sum, value) => sum + Math.pow(value / 100, 2), 0) / e.currentMetering.length
23
- );
24
- rmsValues.push(rms);
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
- // Detect silence
27
- if (rms > SILENCE_RMS_THRESHOLD) {
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
- } else if (!silenceTimer) {
33
- silenceTimer = setTimeout(() => {
34
- handleStopRecording();
35
- }, SILENCE_LENGTH_MS);
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
- console.log('Recording started...');
100
+
101
+ return true;
102
+ } catch (error) {
103
+ console.error('Error initializing Voice:', error);
104
+ return false;
105
+ }
39
106
  }
40
107
 
41
- export async function stopRecording() {
42
- if (!audioRecorderPlayer) {
43
- console.error('AudioRecorderPlayer instance is null');
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
- const result = await audioRecorderPlayer.stopRecorder();
50
- console.log('Recording stopped:', result);
51
- return result;
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
- message: 'This app needs access to your microphone to record audio.',
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 microphone permission:', error);
280
+ console.error('Error requesting Android permission:', error);
281
+
78
282
  return false;
79
283
  }
80
284
  }
81
285
 
82
- // ✅ iOS: Request Microphone Permission
286
+
83
287
  async function requestIOSPermission() {
84
288
  try {
85
- const { AVAudioSession } = NativeModules;
86
- if (AVAudioSession) {
87
- await AVAudioSession.requestRecordPermission((granted) => {
88
- console.log('iOS Microphone Permission:', granted);
89
- return granted;
90
- });
91
- } else {
92
- console.warn('AVAudioSession not available');
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 microphone permission on iOS:', error);
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
+