/** * Unified Chat Screen - Text Chat + Ultravox Voice AI * * Features: * - Text messaging with AI (keyboard input) * - High-quality Ultravox voice calls (WebRTC) * - Chat history with transcripts from voice calls * - Seamless switching between text and voice */ import React, { useState, useCallback, useRef, useEffect } from 'react'; import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity, KeyboardAvoidingView, Platform, Modal, ActivityIndicator, Keyboard, Animated, Easing, ScrollView, Share, } from 'react-native'; import * as Clipboard from 'expo-clipboard'; import { Ionicons, Feather } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as SecureStore from 'expo-secure-store'; import { useRouter } from 'expo-router'; import { useFocusEffect } from '@react-navigation/native'; import { useUltravox, UltravoxSessionStatus, } from 'ultravox-react-native'; import { AudioSession } from '@livekit/react-native'; import { api } from '@/services/api'; import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import type { Message, Beneficiary } from '@/types'; import { createCall, getSystemPrompt, VOICE_NAME, } from '@/services/ultravoxService'; const API_URL = 'https://eluxnetworks.net/function/well-api/api'; type VoiceCallState = 'idle' | 'connecting' | 'active' | 'ending'; // Log entry type interface LogEntry { time: string; type: 'info' | 'error' | 'status' | 'api'; message: string; } export default function ChatScreen() { const router = useRouter(); const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary(); // Chat state const [messages, setMessages] = useState([ { id: '1', role: 'assistant', content: 'Hello! I\'m Julia, your AI wellness assistant. You can type a message or tap the phone button to start a voice call.', timestamp: new Date(), }, ]); const [input, setInput] = useState(''); const [isSending, setIsSending] = useState(false); const flatListRef = useRef(null); // Voice call state (Ultravox) const [voiceCallState, setVoiceCallState] = useState('idle'); const voiceCallStateRef = useRef('idle'); // Ref to avoid useFocusEffect deps const [isMuted, setIsMuted] = useState(false); // Debug logs state const [logs, setLogs] = useState([]); const [showLogs, setShowLogs] = useState(true); const logsScrollRef = useRef(null); // Add log helper const addLog = useCallback((type: LogEntry['type'], message: string) => { const time = new Date().toLocaleTimeString('en-US', { hour12: false }); setLogs(prev => [...prev.slice(-50), { time, type, message }]); // Keep last 50 logs console.log(`[Chat ${type}] ${message}`); }, []); // Copy logs to clipboard const copyLogs = useCallback(async () => { const logsText = logs.map(l => `[${l.time}] [${l.type.toUpperCase()}] ${l.message}`).join('\n'); await Clipboard.setStringAsync(logsText); addLog('info', 'Logs copied to clipboard!'); }, [logs, addLog]); // Share logs const shareLogs = useCallback(async () => { const logsText = logs.map(l => `[${l.time}] [${l.type.toUpperCase()}] ${l.message}`).join('\n'); try { await Share.share({ message: logsText, title: 'WellNuo Voice Logs' }); } catch (err) { addLog('error', `Share failed: ${err}`); } }, [logs, addLog]); // Clear logs const clearLogs = useCallback(() => { setLogs([]); addLog('info', 'Logs cleared'); }, [addLog]); // Animations const pulseAnim = useRef(new Animated.Value(1)).current; const rotateAnim = useRef(new Animated.Value(0)).current; // Beneficiary picker const [showBeneficiaryPicker, setShowBeneficiaryPicker] = useState(false); const [beneficiaries, setBeneficiaries] = useState([]); const [loadingBeneficiaries, setLoadingBeneficiaries] = useState(false); // Tool implementations for Ultravox navigation const toolImplementations = { navigateToDashboard: () => { console.log('[Chat] Tool: navigateToDashboard'); router.push('/(tabs)/dashboard'); return 'Navigating to Dashboard'; }, navigateToBeneficiaries: () => { console.log('[Chat] Tool: navigateToBeneficiaries'); router.push('/(tabs)/beneficiaries'); return 'Navigating to Beneficiaries'; }, navigateToProfile: () => { console.log('[Chat] Tool: navigateToProfile'); router.push('/(tabs)/profile'); return 'Navigating to Profile'; }, }; // Ultravox hook for voice calls const { transcripts, joinCall, leaveCall, session } = useUltravox({ tools: toolImplementations, onStatusChange: (event) => { addLog('status', `Ultravox status: ${event.status}`); switch (event.status) { case UltravoxSessionStatus.IDLE: case UltravoxSessionStatus.DISCONNECTED: setVoiceCallState('idle'); break; case UltravoxSessionStatus.CONNECTING: setVoiceCallState('connecting'); break; case UltravoxSessionStatus.LISTENING: addLog('info', '🎤 LISTENING - microphone should be active'); setVoiceCallState('active'); break; case UltravoxSessionStatus.THINKING: addLog('info', '🤔 THINKING - processing audio'); setVoiceCallState('active'); break; case UltravoxSessionStatus.SPEAKING: addLog('info', '🔊 SPEAKING - audio output should play'); setVoiceCallState('active'); break; case UltravoxSessionStatus.DISCONNECTING: setVoiceCallState('ending'); break; } }, }); // Log on mount useEffect(() => { addLog('info', 'Chat screen mounted'); addLog('info', `Beneficiary: ${currentBeneficiary?.name || 'none'}`); }, []); // Track current streaming message ID for each speaker const streamingMessageIdRef = useRef<{ agent: string | null; user: string | null }>({ agent: null, user: null, }); const lastTranscriptIndexRef = useRef(-1); // Add voice transcripts to chat history - update existing message until final useEffect(() => { if (transcripts.length === 0) return; // Process only new transcripts for (let i = lastTranscriptIndexRef.current + 1; i < transcripts.length; i++) { const transcript = transcripts[i]; if (!transcript.text.trim()) continue; const role = transcript.speaker === 'agent' ? 'assistant' : 'user'; const speakerKey = transcript.speaker === 'agent' ? 'agent' : 'user'; const isFinal = transcript.isFinal; if (streamingMessageIdRef.current[speakerKey] && !isFinal) { // Update existing streaming message setMessages(prev => prev.map(m => m.id === streamingMessageIdRef.current[speakerKey] ? { ...m, content: transcript.text } : m )); } else if (!streamingMessageIdRef.current[speakerKey]) { // Create new message for this speaker const newId = `voice-${speakerKey}-${Date.now()}`; streamingMessageIdRef.current[speakerKey] = newId; const newMessage: Message = { id: newId, role, content: transcript.text, timestamp: new Date(), isVoice: true, }; setMessages(prev => [...prev, newMessage]); } // If final, clear the streaming ID so next utterance creates new message if (isFinal) { // Final update to ensure we have the complete text setMessages(prev => prev.map(m => m.id === streamingMessageIdRef.current[speakerKey] ? { ...m, content: transcript.text } : m )); streamingMessageIdRef.current[speakerKey] = null; } } lastTranscriptIndexRef.current = transcripts.length - 1; }, [transcripts]); // Pulse animation when voice call is active useEffect(() => { if (voiceCallState === 'active') { const pulse = Animated.loop( Animated.sequence([ Animated.timing(pulseAnim, { toValue: 1.15, duration: 1000, easing: Easing.inOut(Easing.ease), useNativeDriver: true, }), Animated.timing(pulseAnim, { toValue: 1, duration: 1000, easing: Easing.inOut(Easing.ease), useNativeDriver: true, }), ]) ); pulse.start(); return () => pulse.stop(); } else { pulseAnim.setValue(1); } }, [voiceCallState, pulseAnim]); // Rotate animation when connecting useEffect(() => { if (voiceCallState === 'connecting') { const rotate = Animated.loop( Animated.timing(rotateAnim, { toValue: 1, duration: 1500, easing: Easing.linear, useNativeDriver: true, }) ); rotate.start(); return () => rotate.stop(); } else { rotateAnim.setValue(0); } }, [voiceCallState, rotateAnim]); // Start voice call with Ultravox const startVoiceCall = useCallback(async () => { addLog('info', 'Starting voice call...'); setVoiceCallState('connecting'); Keyboard.dismiss(); // Add system message const systemMsg: Message = { id: `system-${Date.now()}`, role: 'assistant', content: '📞 Starting voice call...', timestamp: new Date(), isSystem: true, }; setMessages(prev => [...prev, systemMsg]); const systemPrompt = getSystemPrompt(); addLog('api', `System prompt length: ${systemPrompt.length} chars`); try { // Configure iOS audio session for voice calls if (Platform.OS === 'ios') { addLog('info', 'Configuring iOS audio session...'); await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', audioCategoryOptions: ['allowBluetooth', 'defaultToSpeaker', 'mixWithOthers'], audioMode: 'voiceChat', }); await AudioSession.startAudioSession(); addLog('info', 'iOS audio session configured'); } addLog('api', 'Calling createCall API...'); const result = await createCall({ systemPrompt, firstSpeaker: 'FIRST_SPEAKER_AGENT', }); if (!result.success) { addLog('error', `createCall failed: ${result.error}`); throw new Error(result.error); } addLog('api', `Call created! joinUrl: ${result.data.joinUrl?.substring(0, 50)}...`); addLog('info', 'Joining call via Ultravox...'); await joinCall(result.data.joinUrl); addLog('info', 'joinCall completed successfully'); // Log session info for audio debugging setTimeout(() => { if (session) { addLog('info', `Session active: ${!!session}`); addLog('info', `Session status: ${session.status}`); } }, 1000); // Update system message setMessages(prev => prev.map(m => m.id === systemMsg.id ? { ...m, content: '📞 Voice call connected. Julia is listening...' } : m )); } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); addLog('error', `Voice call failed: ${errorMsg}`); setVoiceCallState('idle'); // Update with error setMessages(prev => prev.map(m => m.id === systemMsg.id ? { ...m, content: `❌ Failed to connect: ${errorMsg}` } : m )); } }, [joinCall, addLog]); // End voice call const endVoiceCall = useCallback(async () => { setVoiceCallState('ending'); try { await leaveCall(); } catch (err) { console.error('[Chat] Error leaving call:', err); } // Stop iOS audio session if (Platform.OS === 'ios') { try { await AudioSession.stopAudioSession(); } catch (err) { console.error('[Chat] Error stopping audio session:', err); } } setVoiceCallState('idle'); // Add end message const endMsg: Message = { id: `system-end-${Date.now()}`, role: 'assistant', content: '📞 Voice call ended.', timestamp: new Date(), isSystem: true, }; setMessages(prev => [...prev, endMsg]); }, [leaveCall]); // Toggle mute const toggleMute = useCallback(() => { if (session) { const newMuted = !isMuted; if (newMuted) { session.muteMic(); } else { session.unmuteMic(); } setIsMuted(newMuted); } }, [session, isMuted]); // Sync voiceCallState with ref (to avoid useFocusEffect deps causing re-renders) useEffect(() => { voiceCallStateRef.current = voiceCallState; }, [voiceCallState]); // Store leaveCall in ref to avoid dependency issues const leaveCallRef = useRef(leaveCall); useEffect(() => { leaveCallRef.current = leaveCall; }, [leaveCall]); // End call when screen loses focus - NO dependencies to prevent callback recreation useFocusEffect( useCallback(() => { // Focus callback - nothing to do here return () => { // Cleanup on unfocus - use refs to get current values const currentState = voiceCallStateRef.current; if (currentState === 'active' || currentState === 'connecting') { console.log('[Chat] Screen unfocused, ending voice call'); leaveCallRef.current().catch(console.error); // Note: Don't setVoiceCallState here - let the status change effect handle it } }; }, []) // Empty deps - callback never recreates ); // Load beneficiaries const loadBeneficiaries = useCallback(async () => { setLoadingBeneficiaries(true); try { const response = await api.getAllBeneficiaries(); if (response.ok && response.data) { setBeneficiaries(response.data); return response.data; } return []; } catch (error) { console.error('Failed to load beneficiaries:', error); return []; } finally { setLoadingBeneficiaries(false); } }, []); // Auto-select first beneficiary useEffect(() => { const autoSelect = async () => { if (!currentBeneficiary) { const loaded = await loadBeneficiaries(); if (loaded.length > 0) { setCurrentBeneficiary(loaded[0]); } } }; autoSelect(); }, []); const openBeneficiaryPicker = useCallback(() => { setShowBeneficiaryPicker(true); loadBeneficiaries(); }, [loadBeneficiaries]); const selectBeneficiary = useCallback((beneficiary: Beneficiary) => { setCurrentBeneficiary(beneficiary); setShowBeneficiaryPicker(false); }, [setCurrentBeneficiary]); // Text chat - send message via API const sendTextMessage = useCallback(async () => { const trimmedInput = input.trim(); if (!trimmedInput || isSending) return; const userMessage: Message = { id: Date.now().toString(), role: 'user', content: trimmedInput, timestamp: new Date(), }; setMessages(prev => [...prev, userMessage]); setInput(''); setIsSending(true); Keyboard.dismiss(); try { const token = await SecureStore.getItemAsync('accessToken'); const userName = await SecureStore.getItemAsync('userName'); if (!token || !userName) { throw new Error('Please log in'); } // Get beneficiary context let beneficiary = currentBeneficiary; if (!beneficiary?.id) { const loaded = await loadBeneficiaries(); if (loaded.length > 0) { beneficiary = loaded[0]; setCurrentBeneficiary(beneficiary); } } const beneficiaryName = beneficiary?.name || 'the patient'; const deploymentId = beneficiary?.id?.toString() || ''; // Call API const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ function: 'voice_ask', clientId: '001', user_name: userName, token: token, question: `You are Julia, a caring assistant helping monitor ${beneficiaryName}'s wellbeing. Answer: ${trimmedInput}`, deployment_id: deploymentId, context: '', }).toString(), }); const data = await response.json(); if (data.ok && data.response?.body) { const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: 'assistant', content: data.response.body, timestamp: new Date(), }; setMessages(prev => [...prev, assistantMessage]); } else { throw new Error(data.status === '401 Unauthorized' ? 'Session expired' : 'Could not get response'); } } catch (error) { const errorMessage: Message = { id: (Date.now() + 1).toString(), role: 'assistant', content: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`, timestamp: new Date(), }; setMessages(prev => [...prev, errorMessage]); } finally { setIsSending(false); } }, [input, isSending, currentBeneficiary, loadBeneficiaries, setCurrentBeneficiary]); // Render message bubble const renderMessage = ({ item }: { item: Message }) => { const isUser = item.role === 'user'; const isSystem = (item as any).isSystem; const isVoice = (item as any).isVoice; if (isSystem) { return ( {item.content} ); } return ( {!isUser && ( J )} {isVoice && ( Voice )} {item.content} {item.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} ); }; // Voice call button with animations const renderVoiceCallButton = () => { const spin = rotateAnim.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'], }); if (voiceCallState === 'connecting') { return ( ); } if (voiceCallState === 'active') { return ( ); } if (voiceCallState === 'ending') { return ( ); } // Idle state return ( ); }; return ( {/* Header */} router.push('/(tabs)')}> J Julia AI {voiceCallState === 'active' ? `In call • ${VOICE_NAME}` : voiceCallState === 'connecting' ? 'Connecting...' : currentBeneficiary ? `About ${currentBeneficiary.name}` : 'Online'} {voiceCallState === 'active' && ( )} {/* Voice call indicator bar */} {voiceCallState === 'active' && ( Voice call active End )} {/* Beneficiary Picker Modal */} setShowBeneficiaryPicker(false)} > Select Beneficiary setShowBeneficiaryPicker(false)}> {loadingBeneficiaries ? ( ) : beneficiaries.length === 0 ? ( No beneficiaries found ) : ( item.id.toString()} renderItem={({ item }) => ( selectBeneficiary(item)} > {item.name.split(' ').map(n => n[0]).join('').slice(0, 2)} {item.name} {currentBeneficiary?.id === item.id && ( )} )} style={styles.beneficiaryList} /> )} {/* Messages */} item.id} renderItem={renderMessage} contentContainerStyle={styles.messagesList} showsVerticalScrollIndicator={false} onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })} /> {/* Input */} {/* Voice Call Button */} {renderVoiceCallButton()} {/* Debug Logs Panel */} {showLogs && ( Debug Logs ({logs.length}) setShowLogs(false)} style={styles.logButton}> logsScrollRef.current?.scrollToEnd({ animated: true })} > {logs.length === 0 ? ( No logs yet. Tap voice call button to start. ) : ( logs.map((log, index) => ( [{log.time}] [{log.type.toUpperCase()}] {log.message} )) )} )} {/* Show logs toggle when hidden */} {!showLogs && ( setShowLogs(true)}> Logs ({logs.length}) )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: AppColors.surface, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, backgroundColor: AppColors.background, borderBottomWidth: 1, borderBottomColor: AppColors.border, }, backButton: { padding: Spacing.xs, marginRight: Spacing.sm, }, headerInfo: { flex: 1, flexDirection: 'row', alignItems: 'center', }, headerAvatar: { width: 40, height: 40, borderRadius: BorderRadius.full, backgroundColor: AppColors.success, justifyContent: 'center', alignItems: 'center', marginRight: Spacing.sm, }, headerAvatarText: { fontSize: FontSizes.lg, fontWeight: '600', color: AppColors.white, }, headerTitle: { fontSize: FontSizes.lg, fontWeight: '600', color: AppColors.textPrimary, }, headerSubtitle: { fontSize: FontSizes.sm, color: AppColors.success, }, headerButtons: { flexDirection: 'row', gap: Spacing.xs, }, headerButton: { padding: Spacing.xs, }, voiceCallBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: AppColors.success, paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, }, voiceCallBarLeft: { flexDirection: 'row', alignItems: 'center', }, voiceCallBarDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: AppColors.white, marginRight: Spacing.sm, }, voiceCallBarText: { fontSize: FontSizes.sm, fontWeight: '500', color: AppColors.white, }, voiceCallBarEnd: { paddingHorizontal: Spacing.md, paddingVertical: Spacing.xs, backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: BorderRadius.md, }, voiceCallBarEndText: { fontSize: FontSizes.sm, fontWeight: '600', color: AppColors.white, }, chatContainer: { flex: 1, }, messagesList: { padding: Spacing.md, paddingBottom: Spacing.lg, }, messageContainer: { flexDirection: 'row', marginBottom: Spacing.md, alignItems: 'flex-end', }, userMessageContainer: { justifyContent: 'flex-end', }, assistantMessageContainer: { justifyContent: 'flex-start', }, systemMessageContainer: { alignItems: 'center', marginVertical: Spacing.sm, }, systemMessageText: { fontSize: FontSizes.sm, color: AppColors.textMuted, fontStyle: 'italic', }, avatarContainer: { width: 32, height: 32, borderRadius: BorderRadius.full, backgroundColor: AppColors.success, justifyContent: 'center', alignItems: 'center', marginRight: Spacing.xs, }, avatarText: { fontSize: FontSizes.sm, fontWeight: '600', color: AppColors.white, }, messageBubble: { maxWidth: '75%', padding: Spacing.sm + 4, borderRadius: BorderRadius.lg, }, userBubble: { backgroundColor: AppColors.primary, borderBottomRightRadius: BorderRadius.sm, }, assistantBubble: { backgroundColor: AppColors.background, borderBottomLeftRadius: BorderRadius.sm, }, voiceBadge: { flexDirection: 'row', alignItems: 'center', marginBottom: Spacing.xs, }, voiceBadgeText: { fontSize: FontSizes.xs, color: AppColors.primary, marginLeft: 4, }, messageText: { fontSize: FontSizes.base, lineHeight: 22, }, userMessageText: { color: AppColors.white, }, assistantMessageText: { color: AppColors.textPrimary, }, timestamp: { fontSize: FontSizes.xs, color: AppColors.textMuted, marginTop: Spacing.xs, alignSelf: 'flex-end', }, userTimestamp: { color: 'rgba(255,255,255,0.7)', }, inputContainer: { flexDirection: 'row', alignItems: 'flex-end', padding: Spacing.md, backgroundColor: AppColors.background, borderTopWidth: 1, borderTopColor: AppColors.border, }, voiceCallButton: { width: 44, height: 44, borderRadius: 22, justifyContent: 'center', alignItems: 'center', marginRight: Spacing.sm, }, voiceCallButtonInner: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', }, voiceCallIdle: { backgroundColor: AppColors.surface, borderWidth: 1, borderColor: AppColors.primary, }, voiceCallConnecting: { backgroundColor: AppColors.warning || '#FF9800', }, voiceCallActive: { backgroundColor: AppColors.success, }, voiceCallEnding: { backgroundColor: AppColors.textMuted, }, input: { flex: 1, backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, fontSize: FontSizes.base, color: AppColors.textPrimary, maxHeight: 100, marginRight: Spacing.sm, }, sendButton: { width: 44, height: 44, borderRadius: BorderRadius.full, backgroundColor: AppColors.primary, justifyContent: 'center', alignItems: 'center', }, sendButtonDisabled: { backgroundColor: AppColors.surface, }, // Modal styles modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'flex-end', }, modalContent: { backgroundColor: AppColors.background, borderTopLeftRadius: BorderRadius.xl, borderTopRightRadius: BorderRadius.xl, maxHeight: '70%', paddingBottom: Spacing.xl, }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: Spacing.md, borderBottomWidth: 1, borderBottomColor: AppColors.border, }, modalTitle: { fontSize: FontSizes.lg, fontWeight: '600', color: AppColors.textPrimary, }, modalLoading: { padding: Spacing.xl, alignItems: 'center', }, modalEmpty: { padding: Spacing.xl, alignItems: 'center', }, emptyText: { fontSize: FontSizes.base, color: AppColors.textSecondary, }, beneficiaryList: { paddingHorizontal: Spacing.md, }, beneficiaryItem: { flexDirection: 'row', alignItems: 'center', padding: Spacing.md, backgroundColor: AppColors.surface, borderRadius: BorderRadius.md, marginTop: Spacing.sm, }, beneficiaryItemSelected: { backgroundColor: AppColors.primaryLight || '#E3F2FD', borderWidth: 1, borderColor: AppColors.primary, }, beneficiaryAvatar: { width: 44, height: 44, borderRadius: BorderRadius.full, backgroundColor: AppColors.primary, justifyContent: 'center', alignItems: 'center', marginRight: Spacing.md, }, beneficiaryAvatarText: { fontSize: FontSizes.base, fontWeight: '600', color: AppColors.white, }, beneficiaryInfo: { flex: 1, }, beneficiaryName: { fontSize: FontSizes.base, fontWeight: '500', color: AppColors.textPrimary, }, // Debug Logs styles logsContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: '#1a1a2e', borderTopLeftRadius: BorderRadius.lg, borderTopRightRadius: BorderRadius.lg, maxHeight: 200, zIndex: 100, }, logsHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, borderBottomWidth: 1, borderBottomColor: '#2d2d44', }, logsTitle: { fontSize: FontSizes.sm, fontWeight: '600', color: '#fff', }, logsButtons: { flexDirection: 'row', gap: Spacing.sm, }, logButton: { padding: Spacing.xs, }, logsScrollView: { maxHeight: 150, paddingHorizontal: Spacing.sm, paddingVertical: Spacing.xs, }, logEmpty: { color: '#888', fontSize: FontSizes.sm, fontStyle: 'italic', padding: Spacing.md, textAlign: 'center', }, logLine: { fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontSize: 11, color: '#ccc', paddingVertical: 2, }, logError: { color: '#ff6b6b', }, logApi: { color: '#4ecdc4', }, logStatus: { color: '#ffe66d', }, showLogsButton: { position: 'absolute', bottom: 100, right: Spacing.md, backgroundColor: '#1a1a2e', flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, borderRadius: BorderRadius.full, gap: Spacing.xs, zIndex: 50, }, showLogsText: { color: '#fff', fontSize: FontSizes.sm, fontWeight: '500', }, });