import { useEffect, useRef, useState } from 'react'; import { spokenReplyText } from '../app-voice-utils.js'; import { useRealtimeVoiceDialog } from './useRealtimeVoiceDialog.js'; import { useRecordedVoiceDialog } from './useRecordedVoiceDialog.js'; export function useVoiceDialogController({ status, selectedProject, selectedProjectRef, messages, runningById, rememberRelayOperationLock, onVoiceSubmit, submitCodexMessage, onStopMessageSpeech }) { const awaitingTurnRef = useRef(null); const lastSpokenRef = useRef(''); const autoListenRef = useRef(false); const openRef = useRef(false); const stateRef = useRef('idle'); const realtimeRef = useRef(false); const ideaBufferRef = useRef([]); const handoffDraftRef = useRef(''); const clearRecordedAudioRef = useRef(() => {}); const [open, setOpen] = useState(false); const [state, setState] = useState('idle'); const [error, setError] = useState(''); const [transcript, setTranscript] = useState(''); const [assistantText, setAssistantText] = useState(''); const [handoffDraft, setHandoffDraft] = useState(''); const setMode = (next) => { stateRef.current = next; setState(next); }; const setHandoffDraftValue = (next) => { const value = String(next || ''); handoffDraftRef.current = value; setHandoffDraft(value); }; const setErrorBriefly = (message) => { setError(message); setMode('error'); }; const common = { status, selectedProject, selectedProjectRef, awaitingTurnRef, autoListenRef, openRef, stateRef, realtimeRef, ideaBufferRef, handoffDraftRef, rememberRelayOperationLock, onVoiceSubmit, setMode, setError, setErrorBriefly, setTranscript, setAssistantText, setHandoffDraft: setHandoffDraftValue, clearRecordedAudio: (...args) => clearRecordedAudioRef.current(...args) }; const realtime = useRealtimeVoiceDialog(common); const recorded = useRecordedVoiceDialog(common, realtime); clearRecordedAudioRef.current = recorded.clearAudio; const continueHandoffCollection = () => { setHandoffDraftValue(''); setError(''); setAssistantText(''); realtime.resumeAssistantAudio(); setMode('listening'); }; const cancelHandoffConfirmation = () => { setHandoffDraftValue(''); setError(''); realtime.resumeAssistantAudio(); setMode('listening'); }; const submitHandoffToCodex = async () => { const message = handoffDraftRef.current.trim(); if (!message) { return; } if (!selectedProjectRef.current && !selectedProject) { setError('请先选择项目'); setMode('handoff'); return; } try { setError(''); setMode('sending'); await submitCodexMessage({ message }); ideaBufferRef.current = []; setHandoffDraftValue(''); closeDialog(); } catch (submitError) { setError(submitError.message || '发送给 Codex 失败'); setMode('handoff'); } }; const openDialog = () => { onStopMessageSpeech?.(); recorded.unlockAudio(); openRef.current = true; realtimeRef.current = Boolean(status.voiceRealtime?.configured); autoListenRef.current = !realtimeRef.current; awaitingTurnRef.current = null; ideaBufferRef.current = []; setHandoffDraftValue(''); setOpen(true); setError(''); setTranscript(''); setAssistantText(''); setMode('idle'); window.setTimeout(() => { if (!openRef.current) { return; } if (realtimeRef.current) { realtime.startRealtime(); } else { recorded.startRecording(); } }, 80); }; const closeDialog = () => { autoListenRef.current = false; openRef.current = false; awaitingTurnRef.current = null; ideaBufferRef.current = []; setHandoffDraftValue(''); realtime.stopRealtime(); recorded.closeRecorded(); setOpen(false); setError(''); setTranscript(''); setAssistantText(''); setMode('idle'); }; useEffect(() => () => closeDialog(), []); useEffect(() => { const awaiting = awaitingTurnRef.current; if (!open || !awaiting?.turnId || stateRef.current !== 'waiting') { return; } if (runningById[awaiting.turnId]) { return; } const reversed = [...messages].reverse(); let reply = reversed.find( (message) => message.role === 'assistant' && message.turnId === awaiting.turnId && String(message.content || '').trim() ); if (!reply) { let userIndex = -1; for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if ( message.role === 'user' && (message.turnId === awaiting.turnId || String(message.content || '').trim() === awaiting.message) ) { userIndex = index; break; } } if (userIndex >= 0) { reply = [...messages.slice(userIndex + 1)].reverse().find( (message) => message.role === 'assistant' && String(message.content || '').trim() ); } } const speechText = spokenReplyText(reply?.content); if (!reply || !speechText) { return; } const speechKey = `${awaiting.turnId}:${reply.id}:${speechText.length}`; if (lastSpokenRef.current === speechKey) { return; } lastSpokenRef.current = speechKey; awaitingTurnRef.current = null; recorded.playReply(reply); }, [messages, open, recorded, runningById]); return { open, state, error, transcript, assistantText, handoffDraft, setHandoffDraft: setHandoffDraftValue, submitHandoffToCodex, continueHandoffCollection, cancelHandoffConfirmation, startRecording: recorded.startRecording, stopRecording: recorded.stopRecording, openDialog, closeDialog }; }