import { Tabs } from 'expo-router'; import React, { useCallback, useEffect, useRef } from 'react'; import { Platform, View, AppState, AppStateStatus } from 'react-native'; import { Feather } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { HapticTab } from '@/components/haptic-tab'; import { VoiceFAB } from '@/components/VoiceFAB'; import { AppColors } from '@/constants/theme'; import { useColorScheme } from '@/hooks/use-color-scheme'; import { useVoiceCall } from '@/contexts/VoiceCallContext'; import { useVoice } from '@/contexts/VoiceContext'; import { useSpeechRecognition } from '@/hooks/useSpeechRecognition'; export default function TabLayout() { const colorScheme = useColorScheme(); const isDark = colorScheme === 'dark'; const insets = useSafeAreaInsets(); // VoiceFAB uses VoiceCallContext internally to hide when call is active useVoiceCall(); // Ensure context is available // Voice context for listening mode toggle and TTS interruption const { isListening, isSpeaking, status, startSession, stopSession, interruptIfSpeaking, setTranscript, setPartialTranscript, sendTranscript, } = useVoice(); // Track whether session is active (listening mode on, even during TTS) const sessionActiveRef = useRef(false); // Track if we need to restart STT after it ends during active session const shouldRestartSTTRef = useRef(false); // Track pending transcript from interruption (to send after TTS stops) const pendingInterruptTranscriptRef = useRef(null); // Callback for voice detection - interrupt TTS when user speaks const handleVoiceDetected = useCallback(() => { // Interrupt TTS when user starts speaking during 'speaking' state if (status === 'speaking' || isSpeaking) { console.log('[TabLayout] Voice detected during TTS playback - INTERRUPTING Julia'); const wasInterrupted = interruptIfSpeaking(); if (wasInterrupted) { console.log('[TabLayout] TTS interrupted successfully, now listening to user'); } } }, [status, isSpeaking, interruptIfSpeaking]); // Callback when STT ends - may need to restart if session is still active const handleSTTEnd = useCallback(() => { console.log('[TabLayout] STT ended, sessionActive:', sessionActiveRef.current); // If session is still active (user didn't stop it), we should restart STT // This ensures STT continues during and after TTS playback if (sessionActiveRef.current) { shouldRestartSTTRef.current = true; } }, []); // Callback for STT results const handleSpeechResult = useCallback((transcript: string, isFinal: boolean) => { if (isFinal) { // Check if we're still in speaking mode (user interrupted Julia) if (isSpeaking || status === 'speaking') { // Store the transcript to send after TTS fully stops console.log('[TabLayout] Got final result while TTS playing - storing for after interruption:', transcript); pendingInterruptTranscriptRef.current = transcript; } else { // Normal case: not speaking, send immediately setTranscript(transcript); sendTranscript(transcript); } } else { setPartialTranscript(transcript); } }, [setTranscript, setPartialTranscript, sendTranscript, isSpeaking, status]); // Speech recognition with voice detection callback const { startListening, stopListening, isListening: sttIsListening, } = useSpeechRecognition({ lang: 'ru-RU', continuous: true, interimResults: true, onVoiceDetected: handleVoiceDetected, onResult: handleSpeechResult, onEnd: handleSTTEnd, }); // Update session active ref when isListening changes useEffect(() => { sessionActiveRef.current = isListening; if (!isListening) { shouldRestartSTTRef.current = false; } }, [isListening]); // Start/stop STT when voice session starts/stops useEffect(() => { if (isListening) { console.log('[TabLayout] Starting STT for voice session'); startListening(); } else { console.log('[TabLayout] Stopping STT - session ended'); stopListening(); } }, [isListening, startListening, stopListening]); // Restart STT if it ended while session is still active // This ensures continuous listening even during/after TTS playback useEffect(() => { if (shouldRestartSTTRef.current && sessionActiveRef.current && !sttIsListening) { console.log('[TabLayout] Restarting STT - session still active'); shouldRestartSTTRef.current = false; // Small delay to ensure clean restart const timer = setTimeout(() => { if (sessionActiveRef.current) { startListening(); } }, 100); return () => clearTimeout(timer); } }, [sttIsListening, startListening]); // Track previous status to detect transition from speaking to listening const prevStatusRef = useRef('idle'); // Auto-restart STT when TTS finishes (status changes from 'speaking' to 'listening') // Also process any pending transcript from user interruption useEffect(() => { const prevStatus = prevStatusRef.current; prevStatusRef.current = status; // When transitioning from speaking to listening, handle pending interrupt transcript if (prevStatus === 'speaking' && status === 'listening' && sessionActiveRef.current) { console.log('[TabLayout] TTS finished/interrupted - checking for pending transcript'); // Process pending transcript from interruption if any const pendingTranscript = pendingInterruptTranscriptRef.current; if (pendingTranscript) { console.log('[TabLayout] Processing pending interrupt transcript:', pendingTranscript); pendingInterruptTranscriptRef.current = null; setTranscript(pendingTranscript); sendTranscript(pendingTranscript); } // Small delay to ensure TTS cleanup is complete, then restart STT const timer = setTimeout(() => { if (sessionActiveRef.current && !sttIsListening) { startListening(); } }, 200); return () => clearTimeout(timer); } }, [status, sttIsListening, startListening, setTranscript, sendTranscript]); // ============================================================================ // TAB NAVIGATION PERSISTENCE // Ensure voice session continues when user switches between tabs. // The session state is in VoiceContext (root level), but STT may stop due to: // 1. Native audio session changes // 2. Tab unmount/remount (though tabs layout doesn't unmount) // 3. AppState changes (background/foreground) // ============================================================================ // Monitor and recover STT state during tab navigation // If session is active but STT stopped unexpectedly, restart it // IMPORTANT: STT should run DURING TTS playback to detect user interruption! useEffect(() => { // Check every 500ms if STT needs to be restarted const intervalId = setInterval(() => { // Only act if session should be active (isListening from VoiceContext) // but STT is not actually listening // Note: We DO want STT running during 'speaking' to detect interruption! // Only skip during 'processing' (API call in progress) if ( sessionActiveRef.current && !sttIsListening && status !== 'processing' ) { console.log('[TabLayout] STT watchdog: restarting STT (session active but STT stopped, status:', status, ')'); startListening(); } }, 500); return () => clearInterval(intervalId); }, [sttIsListening, status, startListening]); // Handle app state changes (background/foreground) // When app comes back to foreground, restart STT if session was active useEffect(() => { const handleAppStateChange = (nextAppState: AppStateStatus) => { if (nextAppState === 'active' && sessionActiveRef.current) { // App came to foreground, give it a moment then check STT // STT should run even during 'speaking' to detect user interruption setTimeout(() => { if (sessionActiveRef.current && !sttIsListening && status !== 'processing') { console.log('[TabLayout] App foregrounded - restarting STT'); startListening(); } }, 300); } }; const subscription = AppState.addEventListener('change', handleAppStateChange); return () => subscription.remove(); }, [sttIsListening, status, startListening]); // Handle voice FAB press - toggle listening mode const handleVoiceFABPress = useCallback(() => { if (isListening) { stopSession(); } else { startSession(); } }, [isListening, startSession, stopSession]); // Calculate tab bar height based on safe area // On iOS with home indicator, insets.bottom is ~34px // On Android with gesture navigation or software buttons (Samsung/Pixel): // - insets.bottom should reflect the navigation bar height // - But some devices/modes may return 0, so we add a minimum for Android // Android minimum: 16px to ensure content doesn't touch system buttons const androidMinPadding = Platform.OS === 'android' ? 16 : 0; const bottomPadding = Math.max(insets.bottom, androidMinPadding, 10); const tabBarHeight = 60 + bottomPadding; // 60px for content + safe area padding return ( ( ), }} /> {/* Hide old dashboard - now index shows WebView dashboard */} {/* Chat with Julia AI */} ( ), }} /> ( ), }} /> {/* Hide explore tab */} {/* Audio Debug - hidden */} {/* Beneficiaries - hidden from tab bar but keeps tab bar visible */} {/* Voice FAB - toggle listening mode */} ); }