import React, { useState, useCallback, useRef, useEffect } from 'react'; import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity, KeyboardAvoidingView, Platform, Alert, ActivityIndicator, Modal, ScrollView, Animated, } from 'react-native'; import { Ionicons, Feather } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as SecureStore from 'expo-secure-store'; import { Audio } from 'expo-av'; import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { api } from '@/services/api'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import type { Message, Beneficiary } from '@/types'; const OLD_API_URL = 'https://eluxnetworks.net/function/well-api/api'; interface VoiceAskResponse { ok: boolean; response: { Command: string; body: string; name?: string; reflected?: string; language?: string; time?: number; }; status: string; } export default function VoiceAIScreen() { const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary(); const [messages, setMessages] = useState([ { id: '1', role: 'assistant', content: 'Hello! I\'m your voice assistant for monitoring your loved ones. Select a beneficiary and ask me anything about their wellbeing. You can type or tap the microphone to speak.', timestamp: new Date(), }, ]); const [input, setInput] = useState(''); const [isSending, setIsSending] = useState(false); const [isRecording, setIsRecording] = useState(false); const [showBeneficiaryPicker, setShowBeneficiaryPicker] = useState(false); const [beneficiaries, setBeneficiaries] = useState([]); const flatListRef = useRef(null); const lastSendTimeRef = useRef(0); const recordingRef = useRef(null); const pulseAnim = useRef(new Animated.Value(1)).current; const SEND_COOLDOWN_MS = 1000; // Load beneficiaries on mount useEffect(() => { loadBeneficiaries(); setupAudio(); return () => { if (recordingRef.current) { recordingRef.current.stopAndUnloadAsync(); } }; }, []); const setupAudio = async () => { try { await Audio.requestPermissionsAsync(); await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true, }); } catch (error) { console.log('Audio setup error:', error); } }; const loadBeneficiaries = async () => { const response = await api.getAllBeneficiaries(); if (response.ok && response.data) { setBeneficiaries(response.data); } }; // Pulse animation for recording useEffect(() => { if (isRecording) { const pulse = Animated.loop( Animated.sequence([ Animated.timing(pulseAnim, { toValue: 1.3, duration: 500, useNativeDriver: true, }), Animated.timing(pulseAnim, { toValue: 1, duration: 500, useNativeDriver: true, }), ]) ); pulse.start(); return () => pulse.stop(); } }, [isRecording]); const sendToVoiceAsk = async (question: string): Promise => { const token = await SecureStore.getItemAsync('accessToken'); const userName = await SecureStore.getItemAsync('userName'); if (!token || !userName) { throw new Error('Please log in to use voice assistant'); } if (!currentBeneficiary?.id) { throw new Error('Please select a beneficiary first'); } const response = await fetch(OLD_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: question, deployment_id: currentBeneficiary.id.toString(), }).toString(), }); const data: VoiceAskResponse = await response.json(); if (data.ok && data.response?.body) { return data.response.body; } else if (data.status === '401 Unauthorized') { throw new Error('Session expired. Please log in again.'); } else { throw new Error('Could not get response from voice assistant'); } }; const startRecording = async () => { try { const { status } = await Audio.requestPermissionsAsync(); if (status !== 'granted') { Alert.alert('Permission Required', 'Please allow microphone access to use voice input.'); return; } await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true, }); const { recording } = await Audio.Recording.createAsync( Audio.RecordingOptionsPresets.HIGH_QUALITY ); recordingRef.current = recording; setIsRecording(true); } catch (error) { console.error('Failed to start recording:', error); Alert.alert('Error', 'Could not start recording. Please try again.'); } }; const stopRecording = async () => { if (!recordingRef.current) return; setIsRecording(false); try { await recordingRef.current.stopAndUnloadAsync(); const uri = recordingRef.current.getURI(); recordingRef.current = null; if (uri) { // For now, we'll show a message about speech-to-text // In production, this would send to a speech-to-text service Alert.alert( 'Voice Recorded', 'Speech-to-text conversion requires additional integration. For now, please type your question.', [{ text: 'OK' }] ); } } catch (error) { console.error('Failed to stop recording:', error); } }; const handleSend = useCallback(async () => { const trimmedInput = input.trim(); if (!trimmedInput || isSending) return; // Debounce const now = Date.now(); if (now - lastSendTimeRef.current < SEND_COOLDOWN_MS) return; lastSendTimeRef.current = now; // Require beneficiary selection if (!currentBeneficiary?.id) { Alert.alert( 'Select Beneficiary', 'Please select a beneficiary first to ask questions about their wellbeing.', [{ text: 'Select', onPress: () => setShowBeneficiaryPicker(true) }, { text: 'Cancel' }] ); return; } const userMessage: Message = { id: Date.now().toString(), role: 'user', content: trimmedInput, timestamp: new Date(), }; setMessages(prev => [...prev, userMessage]); setInput(''); setIsSending(true); try { const aiResponse = await sendToVoiceAsk(trimmedInput); const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: 'assistant', content: aiResponse, timestamp: new Date(), }; setMessages(prev => [...prev, assistantMessage]); } 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'}. Please try again.`, timestamp: new Date(), }; setMessages(prev => [...prev, errorMessage]); } finally { setIsSending(false); } }, [input, isSending, currentBeneficiary, messages]); const selectBeneficiary = (beneficiary: Beneficiary) => { setCurrentBeneficiary(beneficiary); setShowBeneficiaryPicker(false); // Add welcome message for selected beneficiary const welcomeMessage: Message = { id: Date.now().toString(), role: 'assistant', content: `Great! I'm now ready to answer questions about ${beneficiary.name}. ${beneficiary.wellness_descriptor ? `Current status: ${beneficiary.wellness_descriptor}.` : ''} Ask me anything!`, timestamp: new Date(), }; setMessages(prev => [...prev, welcomeMessage]); }; const renderMessage = ({ item }: { item: Message }) => { const isUser = item.role === 'user'; return ( {!isUser && ( )} {item.content} {item.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} ); }; return ( {/* Header */} Voice AI {isSending ? 'Thinking...' : currentBeneficiary ? `Monitoring ${currentBeneficiary.name}` : 'Select a beneficiary'} setShowBeneficiaryPicker(true)}> {currentBeneficiary?.name?.split(' ')[0] || 'Select'} {/* Messages */} item.id} renderItem={renderMessage} contentContainerStyle={styles.messagesList} showsVerticalScrollIndicator={false} onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })} /> {/* Recording indicator */} {isRecording && ( Listening... )} {/* Input */} {isSending ? ( ) : ( )} {/* Beneficiary Picker Modal */} Select Beneficiary setShowBeneficiaryPicker(false)}> {beneficiaries.length === 0 ? ( Loading beneficiaries... ) : ( beneficiaries.map(beneficiary => ( selectBeneficiary(beneficiary)} > {beneficiary.name} {beneficiary.wellness_descriptor || beneficiary.last_location || 'No data'} )) )} ); } 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, }, headerInfo: { flexDirection: 'row', alignItems: 'center', }, headerAvatar: { width: 40, height: 40, borderRadius: BorderRadius.full, backgroundColor: '#9B59B6', // Purple for Voice AI justifyContent: 'center', alignItems: 'center', marginRight: Spacing.sm, }, headerTitle: { fontSize: FontSizes.lg, fontWeight: '600', color: AppColors.textPrimary, }, headerSubtitle: { fontSize: FontSizes.sm, color: AppColors.success, }, beneficiaryButton: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.sm, paddingVertical: Spacing.xs, backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, borderWidth: 1, borderColor: AppColors.border, }, beneficiaryButtonText: { marginLeft: Spacing.xs, fontSize: FontSizes.sm, color: AppColors.primary, fontWeight: '500', }, 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: '#9B59B6', justifyContent: 'center', alignItems: 'center', marginRight: Spacing.xs, }, 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)', }, recordingIndicator: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: Spacing.sm, backgroundColor: 'rgba(155, 89, 182, 0.1)', }, recordingDot: { width: 12, height: 12, borderRadius: 6, backgroundColor: '#E74C3C', marginRight: Spacing.sm, }, recordingText: { fontSize: FontSizes.sm, color: '#9B59B6', fontWeight: '500', }, inputContainer: { flexDirection: 'row', alignItems: 'flex-end', padding: Spacing.md, backgroundColor: AppColors.background, borderTopWidth: 1, borderTopColor: AppColors.border, }, micButton: { width: 44, height: 44, borderRadius: BorderRadius.full, backgroundColor: AppColors.surface, justifyContent: 'center', alignItems: 'center', marginRight: Spacing.sm, borderWidth: 1, borderColor: AppColors.primary, }, micButtonActive: { backgroundColor: '#E74C3C', borderColor: '#E74C3C', }, 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: '#9B59B6', 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%', }, modalHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: Spacing.md, borderBottomWidth: 1, borderBottomColor: AppColors.border, }, modalTitle: { fontSize: FontSizes.lg, fontWeight: '600', color: AppColors.textPrimary, }, beneficiaryList: { padding: Spacing.md, }, beneficiaryItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: Spacing.md, backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, marginBottom: Spacing.sm, }, beneficiaryItemSelected: { backgroundColor: '#E8F0FE', borderWidth: 2, borderColor: AppColors.primary, }, beneficiaryInfo: { flex: 1, }, beneficiaryName: { fontSize: FontSizes.base, fontWeight: '600', color: AppColors.textPrimary, }, beneficiaryStatus: { fontSize: FontSizes.sm, color: AppColors.textMuted, marginTop: 2, }, statusDot: { width: 10, height: 10, borderRadius: 5, marginLeft: Spacing.sm, }, emptyState: { alignItems: 'center', padding: Spacing.xl, }, emptyStateText: { marginTop: Spacing.md, fontSize: FontSizes.base, color: AppColors.textMuted, }, });