/** * FlowScreen - AI Chat Interface * * Main chat screen for AI conversations with history management. * Features: * - Current conversation displayed in main window * - Chat history accessible from top-right button * - ChatGPT-style input bar at bottom */ import React, { useState, useRef, useEffect } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, TouchableWithoutFeedback, Modal, TextInput, SafeAreaView, ActivityIndicator, Alert, FlatList, Animated, Image, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { aiService } from '../services/ai.service'; import { useAuth } from '../context/AuthContext'; // ============================================================================= // Type Definitions // ============================================================================= interface ChatMessage { id: string; role: 'user' | 'assistant'; content: string; imageUri?: string; createdAt: Date; } interface ChatSession { id: string; title: string; messages: ChatMessage[]; createdAt: Date; updatedAt: Date; } // ============================================================================= // Component // ============================================================================= export default function FlowScreen() { const { token, signOut } = useAuth(); const scrollViewRef = useRef(null); // Current conversation state const [messages, setMessages] = useState([]); const [newContent, setNewContent] = useState(''); const [isSending, setIsSending] = useState(false); const [isRecording, setIsRecording] = useState(false); const [selectedImage, setSelectedImage] = useState(null); // History modal state const [showHistoryModal, setShowHistoryModal] = useState(false); const modalSlideAnim = useRef(new Animated.Value(0)).current; const [chatHistory, setChatHistory] = useState([ // Sample history data { id: '1', title: 'Morning reflection', messages: [], createdAt: new Date('2024-01-18T10:30:00'), updatedAt: new Date('2024-01-18T10:45:00'), }, { id: '2', title: 'Project brainstorm', messages: [], createdAt: new Date('2024-01-17T14:00:00'), updatedAt: new Date('2024-01-17T15:30:00'), }, { id: '3', title: 'Evening thoughts', messages: [], createdAt: new Date('2024-01-16T20:00:00'), updatedAt: new Date('2024-01-16T20:30:00'), }, ]); // Header date display const today = new Date(); const dateStr = today.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }); // Auto-scroll to bottom when new messages arrive useEffect(() => { if (messages.length > 0) { setTimeout(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); }, 100); } }, [messages]); // Modal animation control const openHistoryModal = () => { setShowHistoryModal(true); Animated.spring(modalSlideAnim, { toValue: 1, useNativeDriver: true, tension: 65, friction: 11, }).start(); }; const closeHistoryModal = () => { Animated.timing(modalSlideAnim, { toValue: 0, duration: 200, useNativeDriver: true, }).start(() => { setShowHistoryModal(false); }); }; // ============================================================================= // Event Handlers // ============================================================================= /** * Handle sending a message to AI */ const handleSendMessage = async () => { if (!newContent.trim() || isSending) return; // Check authentication if (!token) { Alert.alert( 'Login Required', 'Please login to send messages', [{ text: 'OK', onPress: () => signOut() }] ); return; } const userMessage = newContent.trim(); setIsSending(true); setNewContent(''); // Add user message immediately const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', content: userMessage, createdAt: new Date(), }; setMessages(prev => [...prev, userMsg]); try { // Call AI proxy const aiResponse = await aiService.sendMessage(userMessage, token); // Add AI response const aiMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: aiResponse, createdAt: new Date(), }; setMessages(prev => [...prev, aiMsg]); } catch (error) { console.error('AI request failed:', error); const errorMessage = error instanceof Error ? error.message : String(error); // Handle authentication errors (401, credentials, unauthorized) const isAuthError = errorMessage.includes('401') || errorMessage.includes('credentials') || errorMessage.includes('Unauthorized') || errorMessage.includes('Not authenticated') || errorMessage.includes('validate'); if (isAuthError) { signOut(); Alert.alert( 'Session Expired', 'Your login session has expired. Please login again.', [{ text: 'OK' }] ); return; } // Show error as AI message const errorMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: `Error: ${errorMessage}`, createdAt: new Date(), }; setMessages(prev => [...prev, errorMsg]); } finally { setIsSending(false); } }; /** * Handle voice recording toggle */ const handleVoiceRecord = () => { setIsRecording(!isRecording); // TODO: Implement voice recording functionality }; /** * Handle image attachment - pick image and analyze with AI */ const handleAddImage = async () => { // Request permission const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { Alert.alert('Permission Required', 'Please grant permission to access photos'); return; } // Pick image const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, quality: 0.8, base64: true, }); if (!result.canceled && result.assets[0]) { const imageAsset = result.assets[0]; setSelectedImage(imageAsset.uri); // Check authentication if (!token) { Alert.alert( 'Login Required', 'Please login to analyze images', [{ text: 'OK', onPress: () => signOut() }] ); return; } setIsSending(true); // Add user message with image const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', content: 'Analyze this image', imageUri: imageAsset.uri, createdAt: new Date(), }; setMessages(prev => [...prev, userMsg]); try { // Call AI with image (using base64) const aiResponse = await aiService.sendMessageWithImage( 'Please describe and analyze this image in detail.', imageAsset.base64 || '', token ); const aiMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: aiResponse, createdAt: new Date(), }; setMessages(prev => [...prev, aiMsg]); } catch (error) { console.error('AI image analysis failed:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Handle authentication errors const isAuthError = errorMessage.includes('401') || errorMessage.includes('Unauthorized') || errorMessage.includes('credentials') || errorMessage.includes('validate'); if (isAuthError) { signOut(); Alert.alert( 'Session Expired', 'Your login session has expired. Please login again.', [{ text: 'OK' }] ); return; } const errorMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: `⚠️ Error analyzing image: ${errorMessage}`, createdAt: new Date(), }; setMessages(prev => [...prev, errorMsg]); } finally { setIsSending(false); setSelectedImage(null); } } }; /** * Start a new conversation */ const handleNewChat = () => { // Save current conversation to history if it has messages if (messages.length > 0) { const newSession: ChatSession = { id: Date.now().toString(), title: messages[0]?.content.substring(0, 30) + '...' || 'New conversation', messages: [...messages], createdAt: messages[0]?.createdAt || new Date(), updatedAt: new Date(), }; setChatHistory(prev => [newSession, ...prev]); } // Clear current messages setMessages([]); closeHistoryModal(); }; /** * Load a conversation from history */ const handleLoadHistory = (session: ChatSession) => { // Save current conversation first if it has messages if (messages.length > 0) { const currentSession: ChatSession = { id: Date.now().toString(), title: messages[0]?.content.substring(0, 30) + '...' || 'Conversation', messages: [...messages], createdAt: messages[0]?.createdAt || new Date(), updatedAt: new Date(), }; setChatHistory(prev => [currentSession, ...prev.filter(s => s.id !== session.id)]); } // Load selected conversation setMessages(session.messages); closeHistoryModal(); }; /** * Delete a conversation from history */ const handleDeleteHistory = (sessionId: string) => { Alert.alert( 'Delete Conversation', 'Are you sure you want to delete this conversation?', [ { text: 'Cancel', style: 'cancel' }, { text: 'Delete', style: 'destructive', onPress: () => setChatHistory(prev => prev.filter(s => s.id !== sessionId)) }, ] ); }; // ============================================================================= // Helper Functions // ============================================================================= const formatTime = (date: Date) => { return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); }; const formatDate = (date: Date) => { return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }; // ============================================================================= // Render Functions // ============================================================================= /** * Render a single chat message bubble */ const renderMessage = (message: ChatMessage, index: number) => { const isUser = message.role === 'user'; return ( {!isUser && ( )} {/* Show image if present */} {message.imageUri && ( )} {message.content} {formatTime(message.createdAt)} ); }; /** * Render empty state when no messages */ const renderEmptyState = () => ( Start a conversation Ask me anything or share your thoughts ); /** * Render history item in modal */ const renderHistoryItem = ({ item }: { item: ChatSession }) => ( handleLoadHistory(item)} onLongPress={() => handleDeleteHistory(item.id)} > {item.title} {formatDate(item.updatedAt)} • {item.messages.length} messages ); // ============================================================================= // Main Render // ============================================================================= return ( {/* Header */} Flow {dateStr} {/* History Button */} {/* Chat Messages */} {messages.length === 0 ? ( renderEmptyState() ) : ( messages.map((message, index) => renderMessage(message, index)) )} {/* Loading indicator when sending */} {isSending && ( )} {/* Bottom Input Bar */} {/* Image attachment button */} {/* Text Input */} {/* Send or Voice button */} {newContent.trim() || isSending ? ( {isSending ? ( ) : ( )} ) : ( )} {/* History Modal - Background appears instantly, content slides up */} e.stopPropagation()}> {/* Modal Header */} Chat History New Chat {/* History List */} item.id} style={styles.historyList} ListEmptyComponent={ No chat history yet } /> {/* Close Button */} Close ); } // ============================================================================= // Styles // ============================================================================= const styles = StyleSheet.create({ // Container styles container: { flex: 1 }, gradient: { flex: 1 }, safeArea: { flex: 1 }, // Header styles header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: spacing.base, paddingTop: spacing.sm, paddingBottom: spacing.md, }, headerLeft: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, }, iconCircle: { width: 44, height: 44, borderRadius: 14, backgroundColor: colors.flow.cardBackground, justifyContent: 'center', alignItems: 'center', ...shadows.soft, }, headerTitle: { fontSize: typography.fontSize.xl, fontWeight: '700', color: colors.flow.text, }, headerDate: { fontSize: typography.fontSize.sm, color: colors.flow.textSecondary, }, historyButton: { width: 44, height: 44, borderRadius: 14, backgroundColor: colors.flow.cardBackground, justifyContent: 'center', alignItems: 'center', ...shadows.soft, }, // Messages container styles messagesContainer: { flex: 1 }, messagesContent: { padding: spacing.base, paddingTop: 0 }, emptyContent: { flex: 1, justifyContent: 'center', }, // Empty state styles emptyState: { alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.xxl, }, emptyIcon: { width: 100, height: 100, borderRadius: 50, backgroundColor: colors.flow.cardBackground, justifyContent: 'center', alignItems: 'center', marginBottom: spacing.lg, ...shadows.soft, }, emptyTitle: { fontSize: typography.fontSize.lg, fontWeight: '600', color: colors.flow.text, marginBottom: spacing.sm, }, emptySubtitle: { fontSize: typography.fontSize.base, color: colors.flow.textSecondary, textAlign: 'center', }, // Message bubble styles messageBubble: { flexDirection: 'row', marginBottom: spacing.md, alignItems: 'flex-end', }, userBubble: { justifyContent: 'flex-end', }, aiBubble: { justifyContent: 'flex-start', }, aiAvatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: colors.flow.cardBackground, justifyContent: 'center', alignItems: 'center', marginRight: spacing.sm, ...shadows.soft, }, messageContent: { maxWidth: '75%', borderRadius: borderRadius.xl, padding: spacing.md, ...shadows.soft, }, userContent: { backgroundColor: colors.nautical.teal, borderBottomRightRadius: 4, marginLeft: 'auto', }, aiContent: { backgroundColor: colors.flow.cardBackground, borderBottomLeftRadius: 4, }, messageImage: { width: '100%', height: 200, borderRadius: borderRadius.lg, marginBottom: spacing.sm, }, messageText: { fontSize: typography.fontSize.base, lineHeight: typography.fontSize.base * 1.5, }, userText: { color: '#fff', }, aiText: { color: colors.flow.text, }, messageTime: { fontSize: typography.fontSize.xs, marginTop: spacing.xs, textAlign: 'right', }, userMessageTime: { color: 'rgba(255, 255, 255, 0.7)', }, aiMessageTime: { color: colors.flow.textSecondary, }, // Input bar styles inputBarContainer: { paddingHorizontal: spacing.base, paddingBottom: spacing.lg, paddingTop: spacing.sm, backgroundColor: 'transparent', }, inputBar: { flexDirection: 'row', alignItems: 'flex-end', backgroundColor: colors.flow.cardBackground, borderRadius: borderRadius.xl, paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, ...shadows.soft, gap: spacing.xs, }, inputBarButton: { width: 40, height: 40, borderRadius: borderRadius.lg, justifyContent: 'center', alignItems: 'center', }, recordingButton: { backgroundColor: colors.nautical.coral, }, inputWrapper: { flex: 1, position: 'relative', justifyContent: 'center', }, inputBarText: { fontSize: typography.fontSize.base, color: colors.flow.text, paddingHorizontal: spacing.sm, paddingVertical: spacing.sm, maxHeight: 100, minHeight: 40, }, sendButton: { width: 40, height: 40, borderRadius: borderRadius.lg, overflow: 'hidden', }, sendButtonDisabled: { opacity: 0.7, }, sendButtonGradient: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', }, // Modal styles modalOverlay: { flex: 1, backgroundColor: 'rgba(26, 58, 74, 0.4)', justifyContent: 'flex-end', }, modalContent: { backgroundColor: colors.flow.cardBackground, borderTopLeftRadius: borderRadius.xxl, borderTopRightRadius: borderRadius.xxl, padding: spacing.lg, paddingBottom: spacing.xxl, maxHeight: '80%', }, modalHandle: { width: 36, height: 4, backgroundColor: colors.flow.cardBorder, borderRadius: 2, alignSelf: 'center', marginBottom: spacing.md, }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: spacing.lg, }, modalTitle: { fontSize: typography.fontSize.lg, fontWeight: '700', color: colors.flow.text, }, newChatButton: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.nautical.teal, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderRadius: borderRadius.lg, gap: spacing.xs, }, newChatText: { fontSize: typography.fontSize.sm, fontWeight: '600', color: '#fff', }, // History list styles historyList: { flex: 1, }, historyItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: spacing.md, borderBottomWidth: 1, borderBottomColor: colors.flow.cardBorder, }, historyItemIcon: { width: 40, height: 40, borderRadius: borderRadius.lg, backgroundColor: colors.nautical.lightMint, justifyContent: 'center', alignItems: 'center', marginRight: spacing.md, }, historyItemContent: { flex: 1, }, historyItemTitle: { fontSize: typography.fontSize.base, fontWeight: '600', color: colors.flow.text, marginBottom: 2, }, historyItemDate: { fontSize: typography.fontSize.sm, color: colors.flow.textSecondary, }, historyEmpty: { alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.xxl, }, historyEmptyText: { fontSize: typography.fontSize.base, color: colors.flow.textSecondary, marginTop: spacing.md, }, closeButton: { paddingVertical: spacing.md, borderRadius: borderRadius.lg, backgroundColor: colors.nautical.paleAqua, alignItems: 'center', marginTop: spacing.md, }, closeButtonText: { fontSize: typography.fontSize.base, color: colors.flow.textSecondary, fontWeight: '600', }, });