diff --git a/contexts/VoiceContext.tsx b/contexts/VoiceContext.tsx index 397b398..1b4fe26 100644 --- a/contexts/VoiceContext.tsx +++ b/contexts/VoiceContext.tsx @@ -234,19 +234,21 @@ export function VoiceProvider({ children }: { children: ReactNode }) { */ const sendTranscript = useCallback( async (text: string): Promise => { + const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]'; const trimmedText = text.trim(); + if (!trimmedText) { - console.log('[VoiceContext] Empty transcript, skipping API call'); + console.log(`${platformPrefix} [VoiceContext] Empty transcript, skipping API call`); return null; } // Don't send if session was stopped if (sessionStoppedRef.current) { - console.log('[VoiceContext] Session stopped, skipping API call'); + console.log(`${platformPrefix} [VoiceContext] âš ī¸ Session stopped, skipping API call`); return null; } - console.log(`[VoiceContext] Sending transcript to API (${voiceApiType}):`, trimmedText); + console.log(`${platformPrefix} [VoiceContext] 📤 Sending transcript to API (${voiceApiType}): "${trimmedText}"`); setStatus('processing'); setError(null); @@ -261,23 +263,28 @@ export function VoiceProvider({ children }: { children: ReactNode }) { abortControllerRef.current = abortController; try { + const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]'; + // Get API token + console.log(`${platformPrefix} [VoiceContext] 🔑 Getting API token...`); const token = await getWellNuoToken(); + console.log(`${platformPrefix} [VoiceContext] ✅ Token obtained`); // Check if aborted if (abortController.signal.aborted || sessionStoppedRef.current) { - console.log('[VoiceContext] Request aborted before API call'); + console.log(`${platformPrefix} [VoiceContext] âš ī¸ Request aborted before API call`); return null; } // Normalize question const normalizedQuestion = normalizeQuestion(trimmedText); + console.log(`${platformPrefix} [VoiceContext] 📝 Normalized question: "${normalizedQuestion}"`); // Get deployment ID const deploymentId = deploymentIdRef.current || '21'; // Log which API type we're using - console.log('[VoiceContext] Using API type:', voiceApiType); + console.log(`${platformPrefix} [VoiceContext] 📡 Using API type: ${voiceApiType}, deployment: ${deploymentId}`); // Build request params const requestParams: Record = { @@ -295,6 +302,7 @@ export function VoiceProvider({ children }: { children: ReactNode }) { // Currently single deployment mode only } + console.log(`${platformPrefix} [VoiceContext] 🌐 Sending API request...`); const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -302,33 +310,37 @@ export function VoiceProvider({ children }: { children: ReactNode }) { signal: abortController.signal, }); + console.log(`${platformPrefix} [VoiceContext] đŸ“Ĩ API response received, parsing...`); const data = await response.json(); // Check if session was stopped while waiting for response if (sessionStoppedRef.current) { - console.log('[VoiceContext] Session stopped during API call, discarding response'); + console.log(`${platformPrefix} [VoiceContext] âš ī¸ Session stopped during API call, discarding response`); return null; } if (data.ok && data.response?.body) { const responseText = data.response.body; - console.log('[VoiceContext] API response:', responseText.slice(0, 100) + '...'); + console.log(`${platformPrefix} [VoiceContext] ✅ API SUCCESS: "${responseText.slice(0, 100)}..."`); setLastResponse(responseText); // Add Julia's response to transcript for chat display addTranscriptEntry('assistant', responseText); + console.log(`${platformPrefix} [VoiceContext] 🔊 Starting TTS for response...`); // Speak the response (will be skipped if session stopped) await speak(responseText); + console.log(`${platformPrefix} [VoiceContext] ✅ TTS completed`); return responseText; } else { // Token might be expired - retry with new token if (data.status === '401 Unauthorized') { - console.log('[VoiceContext] Token expired, retrying with new token...'); + console.log(`${platformPrefix} [VoiceContext] âš ī¸ 401 Unauthorized - Token expired, retrying...`); apiTokenRef.current = null; // Get new token and retry request + console.log(`${platformPrefix} [VoiceContext] 🔑 Getting new token for retry...`); const newToken = await getWellNuoToken(); const retryRequestParams: Record = { @@ -351,27 +363,31 @@ export function VoiceProvider({ children }: { children: ReactNode }) { if (retryData.ok && retryData.response?.body) { const responseText = retryData.response.body; - console.log('[VoiceContext] Retry succeeded:', responseText.slice(0, 100) + '...'); + console.log(`${platformPrefix} [VoiceContext] ✅ Retry SUCCEEDED: "${responseText.slice(0, 100)}..."`); setLastResponse(responseText); addTranscriptEntry('assistant', responseText); await speak(responseText); return responseText; } else { + console.error(`${platformPrefix} [VoiceContext] ❌ Retry FAILED:`, retryData.message); throw new Error(retryData.message || 'Could not get response after retry'); } } + console.error(`${platformPrefix} [VoiceContext] ❌ API error:`, data.message || data.status); throw new Error(data.message || 'Could not get response'); } } catch (err) { + const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]'; + // Ignore abort errors if (err instanceof Error && err.name === 'AbortError') { - console.log('[VoiceContext] API request aborted'); + console.log(`${platformPrefix} [VoiceContext] âš ī¸ API request aborted`); return null; } // Handle API errors gracefully with voice feedback const errorMsg = err instanceof Error ? err.message : 'Unknown error'; - console.warn('[VoiceContext] API error:', errorMsg); + console.error(`${platformPrefix} [VoiceContext] ❌ API ERROR:`, errorMsg); // Create user-friendly error message for TTS const spokenError = `Sorry, I encountered an error: ${errorMsg}. Please try again.`; @@ -397,59 +413,80 @@ export function VoiceProvider({ children }: { children: ReactNode }) { * Call this from the STT hook when voice activity is detected */ const interruptIfSpeaking = useCallback(() => { + const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]'; + if (isSpeaking) { - console.log('[VoiceContext] User interrupted - stopping TTS'); + console.log(`${platformPrefix} [VoiceContext] âš ī¸ User INTERRUPTED - stopping TTS`); Speech.stop(); setIsSpeaking(false); setStatus('listening'); + console.log(`${platformPrefix} [VoiceContext] → TTS stopped, status=listening`); return true; + } else { + console.log(`${platformPrefix} [VoiceContext] interruptIfSpeaking called but NOT speaking`); + return false; } - return false; }, [isSpeaking]); /** * Speak text using TTS */ const speak = useCallback(async (text: string): Promise => { - if (!text.trim()) return; + const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]'; - // Don't speak if session was stopped - if (sessionStoppedRef.current) { - console.log('[VoiceContext] Session stopped, skipping TTS'); + if (!text.trim()) { + console.log(`${platformPrefix} [VoiceContext] Empty text, skipping TTS`); return; } - console.log('[VoiceContext] Speaking:', text.slice(0, 50) + '...'); + // Don't speak if session was stopped + if (sessionStoppedRef.current) { + console.log(`${platformPrefix} [VoiceContext] âš ī¸ Session stopped, skipping TTS`); + return; + } + + console.log(`${platformPrefix} [VoiceContext] 🔊 Starting TTS: "${text.slice(0, 50)}..."`); setStatus('speaking'); setIsSpeaking(true); return new Promise((resolve) => { Speech.speak(text, { language: 'en-US', - rate: 0.9, - pitch: 1.0, + rate: 1.1, // Faster, more natural (was 0.9) + pitch: 1.15, // Slightly higher, less robotic (was 1.0) + // iOS Premium voice (Siri-quality, female) + // Android will use default high-quality voice + voice: Platform.OS === 'ios' ? 'com.apple.voice.premium.en-US.Samantha' : undefined, onStart: () => { - console.log('[VoiceContext] TTS started'); + console.log(`${platformPrefix} [VoiceContext] â–ļī¸ TTS playback STARTED`); }, onDone: () => { - console.log('[VoiceContext] TTS completed'); + console.log(`${platformPrefix} [VoiceContext] ✅ TTS playback COMPLETED`); + // On iOS: Delay turning off green indicator to match STT restart delay (300ms) // On Android: Turn off immediately (audio focus conflict with STT) if (Platform.OS === 'ios') { + console.log('[iOS] [VoiceContext] âąī¸ Delaying isSpeaking=false by 300ms (match STT restart)'); setTimeout(() => { + console.log('[iOS] [VoiceContext] → isSpeaking = false (after 300ms delay)'); setIsSpeaking(false); }, 300); } else { + console.log('[Android] [VoiceContext] → isSpeaking = false (immediate - audio focus release)'); setIsSpeaking(false); } + // Return to listening state after speaking (if session wasn't stopped) if (!sessionStoppedRef.current) { + console.log(`${platformPrefix} [VoiceContext] → status = listening (ready for next input)`); setStatus('listening'); + } else { + console.log(`${platformPrefix} [VoiceContext] âš ī¸ Session stopped, NOT returning to listening`); } resolve(); }, onError: (error) => { - console.warn('[VoiceContext] TTS error:', error); + console.error(`${platformPrefix} [VoiceContext] ❌ TTS ERROR:`, error); // On error, turn off indicator immediately (no delay) setIsSpeaking(false); if (!sessionStoppedRef.current) { @@ -458,12 +495,15 @@ export function VoiceProvider({ children }: { children: ReactNode }) { resolve(); }, onStopped: () => { - console.log('[VoiceContext] TTS stopped (interrupted)'); + console.log(`${platformPrefix} [VoiceContext] âšī¸ TTS STOPPED (interrupted by user)`); // When interrupted by user, turn off indicator immediately setIsSpeaking(false); // Don't set status to listening if session was stopped by user if (!sessionStoppedRef.current) { + console.log(`${platformPrefix} [VoiceContext] → status = listening (after interruption)`); setStatus('listening'); + } else { + console.log(`${platformPrefix} [VoiceContext] âš ī¸ Session stopped, NOT returning to listening`); } resolve(); }, @@ -483,34 +523,46 @@ export function VoiceProvider({ children }: { children: ReactNode }) { * Start voice session */ const startSession = useCallback(() => { - console.log('[VoiceContext] Starting voice session'); + const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]'; + console.log(`${platformPrefix} [VoiceContext] 🎤 STARTING voice session`); sessionStoppedRef.current = false; setStatus('listening'); setIsListening(true); setError(null); setTranscript(''); setPartialTranscript(''); + console.log(`${platformPrefix} [VoiceContext] → Session initialized, status=listening`); }, []); /** * Stop voice session */ const stopSession = useCallback(() => { - console.log('[VoiceContext] Stopping voice session'); + const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]'; + console.log(`${platformPrefix} [VoiceContext] 🛑 STOPPING voice session`); + // Mark session as stopped FIRST to prevent any pending callbacks sessionStoppedRef.current = true; + console.log(`${platformPrefix} [VoiceContext] → sessionStopped flag set to TRUE`); + // Abort any in-flight API requests if (abortControllerRef.current) { + console.log(`${platformPrefix} [VoiceContext] → Aborting in-flight API request`); abortControllerRef.current.abort(); abortControllerRef.current = null; } + // Stop TTS + console.log(`${platformPrefix} [VoiceContext] → Stopping TTS`); Speech.stop(); + // Reset all state + console.log(`${platformPrefix} [VoiceContext] → Resetting all state to idle`); setStatus('idle'); setIsListening(false); setIsSpeaking(false); setError(null); + console.log(`${platformPrefix} [VoiceContext] ✅ Voice session stopped`); }, []); // Computed values