@@ -29,6 +29,8 @@ export interface UseVoiceInputOptions {
2929 onTranscript : ( text : string , isFinal : boolean ) => void ;
3030 /** Called when an error occurs */
3131 onError ?: ( error : string ) => void ;
32+ /** Called to send the message (used by stopListeningAndSend) */
33+ onSend ?: ( ) => void ;
3234 /** Whether OpenAI API key is configured */
3335 openAIKeySet : boolean ;
3436}
@@ -46,33 +48,42 @@ export interface UseVoiceInputResult {
4648 startListening : ( ) => void ;
4749 /** Stop recording and transcribe */
4850 stopListening : ( ) => void ;
51+ /** Stop recording, transcribe, and send when done */
52+ stopListeningAndSend : ( ) => void ;
4953 /** Toggle recording state */
5054 toggleListening : ( ) => void ;
5155}
5256
5357export function useVoiceInput ( options : UseVoiceInputOptions ) : UseVoiceInputResult {
54- const { onTranscript, onError, openAIKeySet } = options ;
58+ const { onTranscript, onError, onSend , openAIKeySet } = options ;
5559
5660 const [ isListening , setIsListening ] = useState ( false ) ;
5761 const [ isTranscribing , setIsTranscribing ] = useState ( false ) ;
5862
5963 const mediaRecorderRef = useRef < MediaRecorder | null > ( null ) ;
6064 const audioChunksRef = useRef < Blob [ ] > ( [ ] ) ;
6165 const streamRef = useRef < MediaStream | null > ( null ) ;
66+ // Flag to auto-send after transcription completes
67+ const sendAfterTranscribeRef = useRef ( false ) ;
6268
6369 const isSupported = isMediaRecorderSupported ( ) ;
6470 const isMobile = isMobileDevice ( ) ;
6571
6672 // Store callbacks in refs to avoid recreating on every render
6773 const onTranscriptRef = useRef ( onTranscript ) ;
6874 const onErrorRef = useRef ( onError ) ;
75+ const onSendRef = useRef ( onSend ) ;
6976 useEffect ( ( ) => {
7077 onTranscriptRef . current = onTranscript ;
7178 onErrorRef . current = onError ;
72- } , [ onTranscript , onError ] ) ;
79+ onSendRef . current = onSend ;
80+ } , [ onTranscript , onError , onSend ] ) ;
7381
7482 const transcribeAudio = useCallback ( async ( audioBlob : Blob ) => {
7583 setIsTranscribing ( true ) ;
84+ const shouldSendAfter = sendAfterTranscribeRef . current ;
85+ sendAfterTranscribeRef . current = false ;
86+
7687 try {
7788 // Convert blob to base64
7889 const arrayBuffer = await audioBlob . arrayBuffer ( ) ;
@@ -86,6 +97,10 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
8697 if ( result . success ) {
8798 if ( result . data . trim ( ) ) {
8899 onTranscriptRef . current ( result . data , true ) ;
100+ // Auto-send after transcript is set (use setTimeout to let React update state)
101+ if ( shouldSendAfter ) {
102+ setTimeout ( ( ) => onSendRef . current ?.( ) , 0 ) ;
103+ }
89104 }
90105 } else {
91106 onErrorRef . current ?.( result . error ) ;
@@ -161,6 +176,11 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
161176 setIsListening ( false ) ;
162177 } , [ ] ) ;
163178
179+ const stopListeningAndSend = useCallback ( ( ) => {
180+ sendAfterTranscribeRef . current = true ;
181+ stopListening ( ) ;
182+ } , [ stopListening ] ) ;
183+
164184 const toggleListening = useCallback ( ( ) => {
165185 if ( isListening ) {
166186 stopListening ( ) ;
@@ -188,6 +208,7 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
188208 shouldShowUI : isSupported && ! isMobile && openAIKeySet ,
189209 startListening : ( ) => void startListening ( ) ,
190210 stopListening,
211+ stopListeningAndSend,
191212 toggleListening,
192213 } ;
193214}
0 commit comments