/** * 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 { AIRole } from '../types'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { aiService, AIMessage } from '../services/ai.service'; import { langGraphService } from '../services/langgraph.service'; import { HumanMessage, AIMessage as LangChainAIMessage, SystemMessage } from "@langchain/core/messages"; import { assetsService } from '../services/assets.service'; import { useAuth } from '../context/AuthContext'; import { AI_CONFIG, getVaultStorageKeys } from '../config'; import { storageService } from '../services/storage.service'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { SentinelVault } from '../utils/crypto_core'; import { Buffer } from 'buffer'; // ============================================================================= // 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, user, signOut, aiRoles, refreshAIRoles } = 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); // AI Role state - start with null to detect first load const [selectedRole, setSelectedRole] = useState(aiRoles[0] || null); const [showRoleModal, setShowRoleModal] = useState(false); const [expandedRoleId, setExpandedRoleId] = useState(null); // History modal state const [showHistoryModal, setShowHistoryModal] = useState(false); const modalSlideAnim = useRef(new Animated.Value(0)).current; // Summary state const [showSummaryConfirmModal, setShowSummaryConfirmModal] = useState(false); const [showSummaryResultModal, setShowSummaryResultModal] = useState(false); const [isSummarizing, setIsSummarizing] = useState(false); const [generatedSummary, setGeneratedSummary] = useState(''); // Save to Vault state const [showVaultConfirmModal, setShowVaultConfirmModal] = useState(false); const [showSaveResultModal, setShowSaveResultModal] = useState(false); const [saveResult, setSaveResult] = useState<{ success: boolean; message: string }>({ success: true, message: '' }); const [isSavingToVault, setIsSavingToVault] = useState(false); 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' }); // Load history on mount useEffect(() => { const loadHistory = async () => { if (!user) return; try { console.log('[FlowScreen] Loading chat history...'); const savedHistory = await storageService.getChatHistory(user.id); if (savedHistory && savedHistory.length > 0) { const formattedHistory = savedHistory.map((session: any) => ({ ...session, createdAt: new Date(session.createdAt), updatedAt: new Date(session.updatedAt), messages: session.messages.map((msg: any) => ({ ...msg, createdAt: new Date(msg.createdAt) })) })); setChatHistory(formattedHistory); console.log('[FlowScreen] Chat history loaded:', formattedHistory.length, 'sessions'); } else { console.log('[FlowScreen] No chat history found'); } } catch (error) { console.error('Failed to load history:', error); } }; loadHistory(); }, [user]); // Load messages whenever role changes useEffect(() => { const loadRoleMessages = async () => { if (!user || !selectedRole) return; try { const savedMessages = await storageService.getCurrentChat(selectedRole?.id || '', user.id); if (savedMessages) { const formattedMessages = savedMessages.map((msg: any) => ({ ...msg, createdAt: new Date(msg.createdAt) })); setMessages(formattedMessages); } else { setMessages([]); } } catch (error) { if (selectedRole) { console.error(`Failed to load messages for role ${selectedRole?.id}:`, error); } setMessages([]); } }; loadRoleMessages(); }, [selectedRole?.id, user]); // Ensure we have a valid selected role from the dynamic list useEffect(() => { if (aiRoles.length > 0) { if (!selectedRole) { // Initial load or first time roles become available setSelectedRole(aiRoles[0]); } else { // If roles refreshed, make sure current selectedRole is still valid or updated const updatedRole = aiRoles.find(r => r.id === selectedRole?.id); if (updatedRole) { setSelectedRole(updatedRole); } else { // Current role no longer exists in dynamic list, fallback to first setSelectedRole(aiRoles[0]); } } } else if (!selectedRole) { // Fallback if no dynamic roles yet setSelectedRole(AI_CONFIG.ROLES[0]); } }, [aiRoles]); // Save current messages for the active role when they change useEffect(() => { if (user && selectedRole && messages.length >= 0) { // Save even if empty to allow clearing storageService.saveCurrentChat(selectedRole?.id || '', messages, user.id); } if (messages.length > 0) { setTimeout(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); }, 100); } }, [messages, selectedRole?.id, user]); // Save history when it changes useEffect(() => { if (user) { storageService.saveChatHistory(chatHistory, user.id); } }, [chatHistory, user]); // 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 || !selectedRole) 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 { // 1. Convert current messages history to LangChain format const history: (HumanMessage | LangChainAIMessage | SystemMessage)[] = messages.map(msg => { if (msg.role === 'user') return new HumanMessage(msg.content); return new LangChainAIMessage(msg.content); }); // 2. Add system prompt const systemPrompt = new SystemMessage(selectedRole?.systemPrompt || ''); // 3. Add current new message const currentMsg = new HumanMessage(userMessage); // 4. Combine all messages for LangGraph processing const fullMessages = [systemPrompt, ...history, currentMsg]; // 5. Execute via LangGraph service (handles token limits and context) const aiResponse = await langGraphService.execute(fullMessages, 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 and storage for this role setMessages([]); if (user && selectedRole) { storageService.saveCurrentChat(selectedRole?.id || '', [], user.id); } 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)) }, ] ); }; /** * Handle generating summary for current conversation */ const handleGenerateSummary = async () => { if (messages.length === 0) { Alert.alert('No Messages', 'There are no messages to summarize.'); return; } if (!token) { Alert.alert('Login Required', 'Please login to generate a summary.'); return; } setShowSummaryConfirmModal(false); setIsSummarizing(true); try { // Convert messages to AIMessage format const aiMessages: AIMessage[] = messages.map(msg => ({ role: msg.role, content: msg.content, })); const summary = await aiService.summarizeChat(aiMessages, token); setGeneratedSummary(summary); setShowSummaryResultModal(true); } catch (error) { console.error('Failed to generate summary:', error); Alert.alert('Error', 'Failed to generate summary. Please try again later.'); } finally { setIsSummarizing(false); } }; /** * Handle saving the generated summary to the vault */ const handleSaveToVault = async () => { if (!generatedSummary || isSavingToVault) return; if (!token) { Alert.alert('Login Required', 'Please login to save to vault.'); return; } setShowVaultConfirmModal(false); setIsSavingToVault(true); try { // Retrieve vault keys if (!user) { Alert.alert('Error', 'User information not found. Please login again.'); return; } const vaultKeys = getVaultStorageKeys(user.id); const shareServer = await AsyncStorage.getItem(vaultKeys.SHARE_SERVER); const aesKeyHex = await AsyncStorage.getItem(vaultKeys.AES_KEY); if (!shareServer || !aesKeyHex) { Alert.alert( 'Vault Not Initialized', 'Your vault is not fully initialized. Please set it up in the Vault tab first.' ); return; } // Encrypt summary with AES key const vault = new SentinelVault(); const aesKey = Buffer.from(aesKeyHex, 'hex'); const encryptedSummary = vault.encryptData(aesKey, generatedSummary).toString('hex'); // Create asset in backend const createdAsset = await assetsService.createAsset({ title: `Chat Summary - ${new Date().toLocaleDateString()}`, private_key_shard: shareServer, content_inner_encrypted: encryptedSummary, }, token); // Backup plaintext content locally if (createdAsset && createdAsset.id && user?.id) { await storageService.saveAssetBackup(createdAsset.id, generatedSummary, user.id); } setSaveResult({ success: true, message: 'Summary encrypted and saved to your vault successfully.' }); setShowSaveResultModal(true); } catch (error) { console.error('Failed to save to vault:', error); setSaveResult({ success: false, message: 'Failed to save summary to vault. Please try again.' }); setShowSaveResultModal(true); } finally { setIsSavingToVault(false); } }; /** * Handle closing all summary related modals after successful save or manual close of result */ const handleFinishSaveFlow = () => { setShowSaveResultModal(false); if (saveResult.success) { setShowSummaryResultModal(false); setShowVaultConfirmModal(false); } }; // ============================================================================= // 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 = () => ( Chatting with {selectedRole?.name || 'AI'} {selectedRole?.description || 'Loading AI Assistant...'} ); /** * 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} {/* Role Header Dropdown */} setShowRoleModal(true)} activeOpacity={0.7} > {selectedRole && ( )} {selectedRole?.name || 'Loading...'} {/* Summary Button */} setShowSummaryConfirmModal(true)} disabled={messages.length === 0 || isSummarizing} > {/* 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 ? ( ) : ( )} ) : ( )} e.stopPropagation()}> {/* Modal Header */} Chat History New Chat {/* History List */} item.id} style={styles.historyList} ListEmptyComponent={ No chat history yet } /> {/* Close Button */} Close {/* Role Selection Modal */} setShowRoleModal(false)} > setShowRoleModal(false)}> e.stopPropagation()}> Choose AI Assistant {aiRoles.map((role) => ( { setSelectedRole(role); setShowRoleModal(false); }} > {role.name} { setExpandedRoleId(expandedRoleId === role.id ? null : role.id); }} > {expandedRoleId === role.id && ( {role.description} )} ))} setShowRoleModal(false)} > Cancel {/* Summary Confirmation Modal */} setShowSummaryConfirmModal(false)} > setShowSummaryConfirmModal(false)}> e.stopPropagation()}> Generate Summary Would you like to generate a summary for the current conversation? setShowSummaryConfirmModal(false)} > No Yes, Generate {/* Summary Result Modal */} setShowSummaryResultModal(false)} > setShowSummaryResultModal(false)}> e.stopPropagation()}> Conversation Summary setShowSummaryResultModal(false)}> {generatedSummary} setShowVaultConfirmModal(true)} disabled={isSavingToVault} > {isSavingToVault ? ( ) : ( <> Save to Vault )} setShowSummaryResultModal(false)} > Done {/* Save to Vault Confirmation Modal */} setShowVaultConfirmModal(false)} > setShowVaultConfirmModal(false)}> e.stopPropagation()}> Save to Vault Would you like to securely save this summary to your digital vault? setShowVaultConfirmModal(false)} > Cancel Yes, Save {/* Save Result Modal */} e.stopPropagation()}> {saveResult.success ? 'Success!' : 'Oops!'} {saveResult.message} Confirm {/* Summary Loading Modal */} Generating Summary... ); } // ============================================================================= // 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.sm, borderBottomWidth: 1, borderBottomColor: 'rgba(0,0,0,0.05)', }, headerLeft: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, flex: 1, }, headerRoleButton: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.flow.cardBackground, paddingHorizontal: spacing.sm, paddingVertical: 6, borderRadius: borderRadius.full, marginHorizontal: spacing.sm, borderWidth: 1, borderColor: colors.flow.cardBorder, maxWidth: '40%', }, headerRoleText: { fontSize: typography.fontSize.xs, fontWeight: '600', color: colors.flow.text, marginHorizontal: 4, }, 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', }, // Role selection styles roleDropdown: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.flow.cardBackground, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderRadius: borderRadius.lg, marginBottom: spacing.md, ...shadows.soft, borderWidth: 1, borderColor: colors.flow.cardBorder, }, roleIcon: { marginRight: spacing.sm, }, roleDropdownText: { fontSize: typography.fontSize.base, fontWeight: '600', color: colors.flow.text, marginRight: spacing.xs, }, roleModalContent: { paddingBottom: spacing.xl, }, roleList: { marginTop: spacing.sm, maxHeight: 400, }, roleItemContainer: { marginBottom: spacing.sm, }, roleItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderRadius: borderRadius.lg, backgroundColor: 'transparent', borderWidth: 1, borderColor: 'transparent', overflow: 'hidden', }, roleItemActive: { backgroundColor: colors.nautical.paleAqua, borderColor: colors.nautical.lightMint, }, roleSelectionArea: { flex: 1, flexDirection: 'row', alignItems: 'center', padding: spacing.md, }, roleItemIcon: { width: 36, height: 36, borderRadius: 18, backgroundColor: colors.flow.backgroundGradientStart, justifyContent: 'center', alignItems: 'center', marginRight: spacing.md, }, roleItemIconActive: { backgroundColor: colors.nautical.teal, }, roleItemName: { fontSize: typography.fontSize.base, fontWeight: '500', color: colors.flow.text, }, roleItemNameActive: { fontWeight: '700', color: colors.nautical.teal, }, infoButton: { padding: spacing.md, justifyContent: 'center', alignItems: 'center', }, roleDescription: { paddingHorizontal: spacing.md + 36 + spacing.md, // icon width + margins paddingBottom: spacing.sm, paddingTop: 0, }, roleDescriptionText: { fontSize: typography.fontSize.sm, color: colors.flow.textSecondary, fontStyle: 'italic', lineHeight: 18, }, // 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', }, // Summary Modal styles modalSubtitle: { fontSize: typography.fontSize.base, color: colors.flow.textSecondary, lineHeight: 22, }, modalActions: { flexDirection: 'row', gap: spacing.md, marginTop: spacing.base, }, actionButton: { flex: 1, height: 50, borderRadius: borderRadius.lg, justifyContent: 'center', alignItems: 'center', overflow: 'hidden', }, actionButtonGradient: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', }, cancelButton: { backgroundColor: colors.nautical.paleAqua, }, confirmButton: { // Gradient handled in child }, cancelButtonText: { fontSize: typography.fontSize.base, fontWeight: '600', color: colors.flow.textSecondary, }, confirmButtonText: { fontSize: typography.fontSize.base, fontWeight: '600', color: '#fff', }, summaryContainer: { marginVertical: spacing.md, }, summaryCard: { backgroundColor: colors.nautical.paleAqua + '40', // 25% opacity padding: spacing.md, borderRadius: borderRadius.lg, borderWidth: 1, borderColor: colors.nautical.lightMint, }, summaryText: { fontSize: typography.fontSize.base, color: colors.flow.text, lineHeight: 24, }, summaryActions: { marginTop: spacing.md, gap: spacing.sm, }, saveToVaultButton: { height: 54, }, resultIconContainer: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: spacing.md, }, successIconBg: { backgroundColor: colors.nautical.paleAqua, }, errorIconBg: { backgroundColor: 'rgba(231, 76, 60, 0.1)', // coral at 10% }, loadingOverlay: { flex: 1, backgroundColor: 'rgba(26, 58, 74, 0.6)', justifyContent: 'center', alignItems: 'center', }, loadingContainer: { backgroundColor: colors.flow.cardBackground, padding: spacing.xl, borderRadius: borderRadius.xl, alignItems: 'center', ...shadows.soft, gap: spacing.md, }, loadingText: { fontSize: typography.fontSize.base, color: colors.flow.text, fontWeight: '600', }, });