refactor(app): replace voice hooks with voice runtime and audio engine architecture

This commit is contained in:
Mohamed Boudra
2026-03-13 20:53:55 +07:00
parent 730f34753a
commit 80a2f8052c
51 changed files with 4308 additions and 2380 deletions
+81 -10
View File
@@ -101,7 +101,6 @@ export interface MessageInputRef {
const MIN_INPUT_HEIGHT = 30
const MAX_INPUT_HEIGHT = 160
const IS_WEB = Platform.OS === 'web'
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__)
type WebTextInputKeyPressEvent = NativeSyntheticEvent<
TextInputKeyPressEventData & {
@@ -125,13 +124,10 @@ type TextAreaHandle = {
}
function logWebStickyBottom(
event: string,
details: Record<string, unknown>
_event: string,
_details: Record<string, unknown>
): void {
if (!IS_DEV || !IS_WEB) {
return
}
console.log('[WebStickyBottom]', event, details)
// Intentionally disabled: this path is too noisy during voice debugging.
}
function getDebugNow(): number | null {
@@ -220,6 +216,18 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
markScrollInvestigationRender(investigationComponentId)
const toast = useToast()
const voice = useVoiceOptional()
console.log("[MessageInput] render", {
voiceServerId: voiceServerId ?? null,
voiceAgentId: voiceAgentId ?? null,
hasVoice: Boolean(voice),
isVoiceMode: voice?.isVoiceMode ?? false,
isVoiceSwitching: voice?.isVoiceSwitching ?? false,
isMuted: voice?.isMuted ?? false,
disabled,
isSubmitLoading,
valueLength: value.length,
imageCount: images.length,
})
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT)
const rootRef = useRef<View | null>(null)
const inputWrapperRef = useRef<View | null>(null)
@@ -293,6 +301,19 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
)
)
useEffect(() => {
console.log("[MessageInput] mount", {
voiceServerId: voiceServerId ?? null,
voiceAgentId: voiceAgentId ?? null,
})
return () => {
console.log("[MessageInput] unmount", {
voiceServerId: voiceServerId ?? null,
voiceAgentId: voiceAgentId ?? null,
})
}
}, [voiceAgentId, voiceServerId])
useEffect(() => {
valueRef.current = value
}, [value])
@@ -407,6 +428,26 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
const showRealtimeOverlay = isRealtimeVoiceForCurrentAgent
const showOverlay = showDictationOverlay || showRealtimeOverlay
useEffect(() => {
console.log("[MessageInput] overlay_state", {
showDictationOverlay,
showRealtimeOverlay,
showOverlay,
isDictating,
isDictationProcessing,
dictationStatus,
isRealtimeVoiceForCurrentAgent,
})
}, [
dictationStatus,
isDictating,
isDictationProcessing,
isRealtimeVoiceForCurrentAgent,
showDictationOverlay,
showOverlay,
showRealtimeOverlay,
])
useEffect(() => {
if (isDictating || isDictationProcessing) {
return
@@ -443,6 +484,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
}))
const handleVoicePress = useCallback(async () => {
console.log("[MessageInput] handleVoicePress", {
isRealtimeVoiceForCurrentAgent,
isDictating,
hasVoice: Boolean(voice),
})
if (isRealtimeVoiceForCurrentAgent && voice) {
voice.toggleMute()
return
@@ -462,28 +508,39 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
])
const handleCancelRecording = useCallback(async () => {
console.log("[MessageInput] handleCancelRecording")
await cancelDictation()
}, [cancelDictation])
const handleAcceptRecording = useCallback(async () => {
console.log("[MessageInput] handleAcceptRecording")
sendAfterTranscriptRef.current = false
await confirmDictation()
}, [confirmDictation])
const handleAcceptAndSendRecording = useCallback(async () => {
console.log("[MessageInput] handleAcceptAndSendRecording")
sendAfterTranscriptRef.current = true
await confirmDictation()
}, [confirmDictation])
const handleRetryFailedRecording = useCallback(() => {
console.log("[MessageInput] handleRetryFailedRecording")
void retryFailedDictation()
}, [retryFailedDictation])
const handleDiscardFailedRecording = useCallback(() => {
console.log("[MessageInput] handleDiscardFailedRecording")
discardFailedDictation()
}, [discardFailedDictation])
const handleStopRealtimeVoice = useCallback(async () => {
console.log("[MessageInput] handleStopRealtimeVoice", {
hasVoice: Boolean(voice),
isRealtimeVoiceForCurrentAgent,
isAgentRunning,
voiceAgentId: voiceAgentId ?? null,
})
if (!voice || !isRealtimeVoiceForCurrentAgent) {
return
}
@@ -503,6 +560,18 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
}, [client, isAgentRunning, isRealtimeVoiceForCurrentAgent, voice, voiceAgentId])
const handleToggleRealtimeVoiceShortcut = useCallback(() => {
console.log("[MessageInput] handleToggleRealtimeVoiceShortcut", {
hasVoice: Boolean(voice),
voiceServerId: voiceServerId ?? null,
voiceAgentId: voiceAgentId ?? null,
isConnected,
disabled,
isVoiceSwitching: voice?.isVoiceSwitching ?? false,
isVoiceModeForAgent:
voice && voiceServerId && voiceAgentId
? voice.isVoiceModeForAgent(voiceServerId, voiceAgentId)
: false,
})
if (!voice || !voiceServerId || !voiceAgentId || !isConnected || disabled) {
return
}
@@ -524,6 +593,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
}, [disabled, handleStopRealtimeVoice, isConnected, toast, voice, voiceAgentId, voiceServerId])
const handleSendMessage = useCallback(() => {
console.log("[MessageInput] handleSendMessage", {
valueLength: value.length,
imageCount: images.length,
isAgentRunning,
})
const trimmed = value.trim()
if (!trimmed && images.length === 0) return
const payload = {
@@ -1070,10 +1144,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
/>
) : showRealtimeOverlay && voice ? (
<RealtimeVoiceOverlay
volume={voice.volume}
isMuted={voice.isMuted}
isDetecting={voice.isDetecting}
isSpeaking={voice.isSpeaking}
isSwitching={voice.isVoiceSwitching}
onToggleMute={voice.toggleMute}
onStop={() => {