/** * Chat Screen - Text Chat with Julia AI * * Clean text chat interface. * Voice calls are handled by separate voice-call.tsx screen. */ import React, { useState, useCallback, useRef, useEffect } from 'react'; import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity, KeyboardAvoidingView, Platform, Modal, ActivityIndicator, Keyboard, } from 'react-native'; import { Ionicons } 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 { api } from '@/services/api'; import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { useVoiceTranscript } from '@/contexts/VoiceTranscriptContext'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import type { Message, Beneficiary } from '@/types'; const API_URL = 'https://eluxnetworks.net/function/well-api/api'; export default function ChatScreen() { const router = useRouter(); const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary(); const { getTranscriptAsMessages, hasNewTranscript, markTranscriptAsShown } = useVoiceTranscript(); // 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(), }, ]); // Add voice call transcript to messages when returning from call useEffect(() => { if (hasNewTranscript) { const transcriptMessages = getTranscriptAsMessages(); if (transcriptMessages.length > 0) { // Add a separator message const separatorMessage: Message = { id: `voice-separator-${Date.now()}`, role: 'assistant', content: '--- Voice Call Transcript ---', timestamp: new Date(), isSystem: true, }; setMessages(prev => [...prev, separatorMessage, ...transcriptMessages]); markTranscriptAsShown(); // Scroll to bottom setTimeout(() => { flatListRef.current?.scrollToEnd({ animated: true }); }, 100); } } }, [hasNewTranscript, getTranscriptAsMessages, markTranscriptAsShown]); const [input, setInput] = useState(''); const [isSending, setIsSending] = useState(false); const flatListRef = useRef(null); // Beneficiary picker const [showBeneficiaryPicker, setShowBeneficiaryPicker] = useState(false); const [beneficiaries, setBeneficiaries] = useState([]); const [loadingBeneficiaries, setLoadingBeneficiaries] = useState(false); // 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]); // Start voice call - navigate to voice-call screen const startVoiceCall = useCallback(() => { router.push('/voice-call'); }, [router]); // 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 isVoice = item.isVoice; const isSystem = item.isSystem; // System messages (like "Voice Call Transcript" separator) if (isSystem) { return ( {item.content.replace(/---/g, '').trim()} ); } return ( {!isUser && ( J )} {isVoice && ( )} {item.content} {item.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} ); }; return ( {/* Header */} router.push('/(tabs)')}> J Julia AI {currentBeneficiary ? `About ${currentBeneficiary.name}` : 'Online'} {/* Voice Call Button */} {/* 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 in input area */} ); } 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', alignItems: 'center', gap: Spacing.sm, }, callButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: AppColors.success, justifyContent: 'center', alignItems: 'center', }, headerButton: { padding: Spacing.xs, }, 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', }, 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, }, 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, backgroundColor: AppColors.surface, borderWidth: 1, borderColor: AppColors.primary, justifyContent: 'center', alignItems: 'center', marginRight: Spacing.sm, }, 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, }, // Voice message styles voiceBubble: { borderWidth: 1, borderColor: 'rgba(59, 130, 246, 0.3)', }, voiceIndicator: { position: 'absolute', top: 6, right: 6, }, // System message styles systemMessageContainer: { flexDirection: 'row', alignItems: 'center', marginVertical: Spacing.md, paddingHorizontal: Spacing.md, }, systemMessageLine: { flex: 1, height: 1, backgroundColor: AppColors.border, }, systemMessageBadge: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.sm, paddingVertical: 4, backgroundColor: AppColors.surface, borderRadius: BorderRadius.sm, marginHorizontal: Spacing.sm, }, systemMessageText: { fontSize: FontSizes.xs, color: AppColors.textMuted, marginLeft: 4, }, });