fix: STT recording lifecycle - stop on send, sync voiceActive with hook status, fix mobile re-record
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e727996a18
commit
aa982680fa
2 changed files with 33 additions and 11 deletions
|
|
@ -98,9 +98,9 @@ export function useVoiceStream(callbacks: VoiceStreamCallbacks): VoiceStreamApi
|
|||
_closeWs();
|
||||
_releaseDevices();
|
||||
setInterimText('');
|
||||
_setStatus('idle');
|
||||
_setStatusTracked('idle');
|
||||
stoppingRef.current = false;
|
||||
}, [_stopRecorder, _closeWs, _releaseDevices, _setStatus]);
|
||||
}, [_stopRecorder, _closeWs, _releaseDevices, _setStatusTracked]);
|
||||
|
||||
const _buildOpenPayload = useCallback(() => {
|
||||
const o = sttOpenOptsRef.current;
|
||||
|
|
@ -113,16 +113,25 @@ export function useVoiceStream(callbacks: VoiceStreamCallbacks): VoiceStreamApi
|
|||
};
|
||||
}, []);
|
||||
|
||||
const statusRef = useRef<VoiceStreamStatus>('idle');
|
||||
const _setStatusTracked = useCallback((next: VoiceStreamStatus) => {
|
||||
statusRef.current = next;
|
||||
_setStatus(next);
|
||||
}, [_setStatus]);
|
||||
|
||||
const start = useCallback(async (language?: string, sttOpenOptions?: SttStreamOpenOptions) => {
|
||||
if (status === 'listening' || status === 'connecting') return;
|
||||
if (statusRef.current === 'listening' || statusRef.current === 'connecting') return;
|
||||
stoppingRef.current = false;
|
||||
reconnectAttemptsRef.current = 0;
|
||||
languageRef.current = language || 'de-DE';
|
||||
sttOpenOptsRef.current = sttOpenOptions;
|
||||
_setStatus('connecting');
|
||||
_setStatusTracked('connecting');
|
||||
|
||||
try {
|
||||
if (!streamRef.current) {
|
||||
const existingStream = streamRef.current;
|
||||
const tracksAlive = existingStream?.getTracks().some(t => t.readyState === 'live');
|
||||
if (!existingStream || !tracksAlive) {
|
||||
if (existingStream) existingStream.getTracks().forEach(t => t.stop());
|
||||
streamRef.current = await navigator.mediaDevices.getUserMedia({
|
||||
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, channelCount: 1 },
|
||||
});
|
||||
|
|
@ -160,7 +169,7 @@ export function useVoiceStream(callbacks: VoiceStreamCallbacks): VoiceStreamApi
|
|||
};
|
||||
|
||||
recorder.start(_RECORDING_CHUNK_MS);
|
||||
_setStatus('listening');
|
||||
_setStatusTracked('listening');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
|
|
@ -191,23 +200,23 @@ export function useVoiceStream(callbacks: VoiceStreamCallbacks): VoiceStreamApi
|
|||
ws.onerror = () => {
|
||||
if (!stoppingRef.current) {
|
||||
cbRef.current.onError?.(new Error('WebSocket connection error'));
|
||||
_setStatus('error');
|
||||
_setStatusTracked('error');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (!stoppingRef.current) {
|
||||
_setStatus('idle');
|
||||
_setStatusTracked('idle');
|
||||
}
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
cbRef.current.onError?.(err);
|
||||
_setStatus('error');
|
||||
_setStatusTracked('error');
|
||||
_releaseDevices();
|
||||
throw err;
|
||||
}
|
||||
}, [status, _setStatus, _pickMimeType, _closeWs, _releaseDevices, _buildOpenPayload]);
|
||||
}, [_setStatusTracked, _pickMimeType, _closeWs, _releaseDevices, _buildOpenPayload]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -339,6 +339,13 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
|||
|
||||
const _handleSend = useCallback(() => {
|
||||
if ((!prompt.trim() && attachments.length === 0) || isProcessing) return;
|
||||
if (voiceActive) {
|
||||
voiceStream.stop();
|
||||
setVoiceActive(false);
|
||||
promptBeforeVoiceRef.current = '';
|
||||
finalizedTextRef.current = '';
|
||||
currentInterimRef.current = '';
|
||||
}
|
||||
const inlineFileIds = _extractFileRefs(prompt);
|
||||
const attachedFileIds = attachments.flatMap(a => a.type === 'file' ? [a.id] : a.fileIds);
|
||||
const allFileIds = [...new Set([...attachedFileIds, ...inlineFileIds])];
|
||||
|
|
@ -347,7 +354,7 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
|||
setPrompt('');
|
||||
setShowAutocomplete(false);
|
||||
setAttachments([]);
|
||||
}, [prompt, isProcessing, _extractFileRefs, attachments, attachedDataSourceIds, attachedFeatureDataSourceIds, neutralizeActive, onSend]);
|
||||
}, [prompt, isProcessing, voiceActive, voiceStream, _extractFileRefs, attachments, attachedDataSourceIds, attachedFeatureDataSourceIds, neutralizeActive, onSend]);
|
||||
|
||||
const _handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
|
|
@ -435,6 +442,12 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
|||
},
|
||||
onError: (error) => {
|
||||
console.warn('Workspace voice stream error', error);
|
||||
setVoiceActive(false);
|
||||
},
|
||||
onStatusChange: (nextStatus) => {
|
||||
if (nextStatus === 'idle' || nextStatus === 'error') {
|
||||
setVoiceActive(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue