import { Tabs } from 'expo-router'; import React, { useCallback, useEffect, useRef } from 'react'; import { Platform, View } 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); // 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'); interruptIfSpeaking(); } }, [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) { // Only process final results when NOT speaking (avoid processing interrupted speech) if (!isSpeaking && status !== 'speaking') { setTranscript(transcript); // Send to API when final result is received sendTranscript(transcript); } else { // Got final result while speaking - this is the interruption console.log('[TabLayout] Got final result while TTS playing - user interrupted'); } } else { setPartialTranscript(transcript); } }, [setTranscript, setPartialTranscript, sendTranscript, isSpeaking, status]); // Speech recognition with voice detection callback const { startListening, stopListening, isListening: sttIsListening, } = useSpeechRecognition({ 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]); // 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 */} ); }