1285 lines
37 KiB
TypeScript
1285 lines
37 KiB
TypeScript
/**
|
|
* 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';
|
|
import { AI_CONFIG } from '../config';
|
|
import { storageService } from '../services/storage.service';
|
|
|
|
// =============================================================================
|
|
// 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 } = useAuth();
|
|
const scrollViewRef = useRef<ScrollView>(null);
|
|
|
|
// Current conversation state
|
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
const [newContent, setNewContent] = useState('');
|
|
const [isSending, setIsSending] = useState(false);
|
|
const [isRecording, setIsRecording] = useState(false);
|
|
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
|
|
|
// AI Role state
|
|
const [selectedRole, setSelectedRole] = useState(AI_CONFIG.ROLES[0]);
|
|
const [showRoleModal, setShowRoleModal] = useState(false);
|
|
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
|
|
|
// History modal state
|
|
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
|
const modalSlideAnim = useRef(new Animated.Value(0)).current;
|
|
|
|
const [chatHistory, setChatHistory] = useState<ChatSession[]>([
|
|
// 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) 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) {
|
|
console.error(`Failed to load messages for role ${selectedRole.id}:`, error);
|
|
setMessages([]);
|
|
}
|
|
};
|
|
|
|
loadRoleMessages();
|
|
}, [selectedRole.id, user]);
|
|
|
|
// Save current messages for the active role when they change
|
|
useEffect(() => {
|
|
if (user && 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) 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 with selected role's system prompt
|
|
const aiResponse = await aiService.sendMessage(userMessage, token, selectedRole.systemPrompt);
|
|
|
|
// 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) {
|
|
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))
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
// =============================================================================
|
|
// 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 (
|
|
<View
|
|
key={message.id}
|
|
style={[
|
|
styles.messageBubble,
|
|
isUser ? styles.userBubble : styles.aiBubble
|
|
]}
|
|
>
|
|
{!isUser && (
|
|
<View style={styles.aiAvatar}>
|
|
<Feather name="feather" size={16} color={colors.nautical.teal} />
|
|
</View>
|
|
)}
|
|
<View style={[
|
|
styles.messageContent,
|
|
isUser ? styles.userContent : styles.aiContent
|
|
]}>
|
|
{/* Show image if present */}
|
|
{message.imageUri && (
|
|
<Image
|
|
source={{ uri: message.imageUri }}
|
|
style={styles.messageImage}
|
|
resizeMode="cover"
|
|
/>
|
|
)}
|
|
<Text style={[
|
|
styles.messageText,
|
|
isUser ? styles.userText : styles.aiText
|
|
]}>
|
|
{message.content}
|
|
</Text>
|
|
<Text style={[
|
|
styles.messageTime,
|
|
isUser ? styles.userMessageTime : styles.aiMessageTime
|
|
]}>
|
|
{formatTime(message.createdAt)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Render empty state when no messages
|
|
*/
|
|
const renderEmptyState = () => (
|
|
<View style={styles.emptyState}>
|
|
<View style={styles.emptyIcon}>
|
|
<Feather name="feather" size={48} color={colors.nautical.seafoam} />
|
|
</View>
|
|
<Text style={styles.emptyTitle}>Chatting with {selectedRole.name}</Text>
|
|
<Text style={styles.emptySubtitle}>
|
|
{selectedRole.description}
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
/**
|
|
* Render history item in modal
|
|
*/
|
|
const renderHistoryItem = ({ item }: { item: ChatSession }) => (
|
|
<TouchableOpacity
|
|
style={styles.historyItem}
|
|
onPress={() => handleLoadHistory(item)}
|
|
onLongPress={() => handleDeleteHistory(item.id)}
|
|
>
|
|
<View style={styles.historyItemIcon}>
|
|
<Ionicons name="chatbubble-outline" size={20} color={colors.flow.primary} />
|
|
</View>
|
|
<View style={styles.historyItemContent}>
|
|
<Text style={styles.historyItemTitle} numberOfLines={1}>
|
|
{item.title}
|
|
</Text>
|
|
<Text style={styles.historyItemDate}>
|
|
{formatDate(item.updatedAt)} • {item.messages.length} messages
|
|
</Text>
|
|
</View>
|
|
<Ionicons name="chevron-forward" size={20} color={colors.flow.textSecondary} />
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
// =============================================================================
|
|
// Main Render
|
|
// =============================================================================
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<LinearGradient
|
|
colors={[colors.flow.backgroundGradientStart, colors.flow.backgroundGradientEnd]}
|
|
style={styles.gradient}
|
|
>
|
|
<SafeAreaView style={styles.safeArea}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<View style={styles.headerLeft}>
|
|
<View style={styles.iconCircle}>
|
|
<FontAwesome5 name="scroll" size={18} color={colors.flow.primary} />
|
|
</View>
|
|
<View>
|
|
<Text style={styles.headerTitle}>Flow</Text>
|
|
<Text style={styles.headerDate}>{dateStr}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Role Header Dropdown */}
|
|
<TouchableOpacity
|
|
style={styles.headerRoleButton}
|
|
onPress={() => setShowRoleModal(true)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Ionicons
|
|
name={selectedRole.icon as any}
|
|
size={16}
|
|
color={colors.nautical.teal}
|
|
/>
|
|
<Text style={styles.headerRoleText} numberOfLines={1}>
|
|
{selectedRole.name}
|
|
</Text>
|
|
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
|
|
</TouchableOpacity>
|
|
|
|
{/* History Button */}
|
|
<TouchableOpacity
|
|
style={styles.historyButton}
|
|
onPress={openHistoryModal}
|
|
>
|
|
<Ionicons name="time-outline" size={20} color={colors.flow.primary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Chat Messages */}
|
|
<ScrollView
|
|
ref={scrollViewRef}
|
|
style={styles.messagesContainer}
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={[
|
|
styles.messagesContent,
|
|
messages.length === 0 && styles.emptyContent
|
|
]}
|
|
>
|
|
{messages.length === 0 ? (
|
|
renderEmptyState()
|
|
) : (
|
|
messages.map((message, index) => renderMessage(message, index))
|
|
)}
|
|
|
|
{/* Loading indicator when sending */}
|
|
{isSending && (
|
|
<View style={[styles.messageBubble, styles.aiBubble]}>
|
|
<View style={styles.aiAvatar}>
|
|
<Feather name="feather" size={16} color={colors.nautical.teal} />
|
|
</View>
|
|
<View style={[styles.messageContent, styles.aiContent]}>
|
|
<ActivityIndicator size="small" color={colors.nautical.teal} />
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
<View style={{ height: 20 }} />
|
|
</ScrollView>
|
|
|
|
{/* Bottom Input Bar */}
|
|
<View style={styles.inputBarContainer}>
|
|
<View style={styles.inputBar}>
|
|
{/* Image attachment button */}
|
|
<TouchableOpacity
|
|
style={styles.inputBarButton}
|
|
onPress={handleAddImage}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Feather name="image" size={22} color={colors.flow.textSecondary} />
|
|
</TouchableOpacity>
|
|
|
|
{/* Text Input */}
|
|
<View style={styles.inputWrapper}>
|
|
<TextInput
|
|
style={styles.inputBarText}
|
|
placeholder="Message..."
|
|
placeholderTextColor={colors.flow.textSecondary}
|
|
value={newContent}
|
|
onChangeText={setNewContent}
|
|
multiline
|
|
maxLength={500}
|
|
/>
|
|
</View>
|
|
|
|
{/* Send or Voice button */}
|
|
{newContent.trim() || isSending ? (
|
|
<TouchableOpacity
|
|
style={[styles.sendButton, isSending && styles.sendButtonDisabled]}
|
|
onPress={handleSendMessage}
|
|
activeOpacity={0.8}
|
|
disabled={isSending}
|
|
>
|
|
<LinearGradient
|
|
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
|
style={styles.sendButtonGradient}
|
|
>
|
|
{isSending ? (
|
|
<ActivityIndicator size="small" color="#fff" />
|
|
) : (
|
|
<Ionicons name="arrow-up" size={20} color="#fff" />
|
|
)}
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
) : (
|
|
<TouchableOpacity
|
|
style={[styles.inputBarButton, isRecording && styles.recordingButton]}
|
|
onPress={handleVoiceRecord}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Feather
|
|
name="mic"
|
|
size={22}
|
|
color={isRecording ? '#fff' : colors.flow.textSecondary}
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</SafeAreaView>
|
|
</LinearGradient>
|
|
|
|
<Modal
|
|
visible={showHistoryModal}
|
|
animationType="none"
|
|
transparent
|
|
onRequestClose={closeHistoryModal}
|
|
>
|
|
<TouchableWithoutFeedback onPress={closeHistoryModal}>
|
|
<View style={styles.modalOverlay}>
|
|
<TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
|
|
<Animated.View
|
|
style={[
|
|
styles.modalContent,
|
|
{
|
|
transform: [{
|
|
translateY: modalSlideAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [600, 0],
|
|
})
|
|
}]
|
|
}
|
|
]}
|
|
>
|
|
<View style={styles.modalHandle} />
|
|
|
|
{/* Modal Header */}
|
|
<View style={styles.modalHeader}>
|
|
<Text style={styles.modalTitle}>Chat History</Text>
|
|
<TouchableOpacity
|
|
style={styles.newChatButton}
|
|
onPress={handleNewChat}
|
|
>
|
|
<Ionicons name="add" size={20} color="#fff" />
|
|
<Text style={styles.newChatText}>New Chat</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* History List */}
|
|
<FlatList
|
|
data={chatHistory}
|
|
renderItem={renderHistoryItem}
|
|
keyExtractor={item => item.id}
|
|
style={styles.historyList}
|
|
ListEmptyComponent={
|
|
<View style={styles.historyEmpty}>
|
|
<Ionicons name="chatbubbles-outline" size={48} color={colors.flow.textSecondary} />
|
|
<Text style={styles.historyEmptyText}>No chat history yet</Text>
|
|
</View>
|
|
}
|
|
/>
|
|
|
|
{/* Close Button */}
|
|
<TouchableOpacity
|
|
style={styles.closeButton}
|
|
onPress={closeHistoryModal}
|
|
>
|
|
<Text style={styles.closeButtonText}>Close</Text>
|
|
</TouchableOpacity>
|
|
</Animated.View>
|
|
</TouchableWithoutFeedback>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
</Modal>
|
|
|
|
{/* Role Selection Modal */}
|
|
<Modal
|
|
visible={showRoleModal}
|
|
animationType="fade"
|
|
transparent
|
|
onRequestClose={() => setShowRoleModal(false)}
|
|
>
|
|
<TouchableWithoutFeedback onPress={() => setShowRoleModal(false)}>
|
|
<View style={styles.modalOverlay}>
|
|
<TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
|
|
<View style={[styles.modalContent, styles.roleModalContent]}>
|
|
<View style={styles.modalHandle} />
|
|
<Text style={styles.modalTitle}>Choose AI Assistant</Text>
|
|
|
|
<ScrollView style={styles.roleList} showsVerticalScrollIndicator={false}>
|
|
{AI_CONFIG.ROLES.map((role) => (
|
|
<View key={role.id} style={styles.roleItemContainer}>
|
|
<View
|
|
style={[
|
|
styles.roleItem,
|
|
selectedRole.id === role.id && styles.roleItemActive
|
|
]}
|
|
>
|
|
<TouchableOpacity
|
|
style={styles.roleSelectionArea}
|
|
onPress={() => {
|
|
setSelectedRole(role as any);
|
|
setShowRoleModal(false);
|
|
}}
|
|
>
|
|
<View style={[
|
|
styles.roleItemIcon,
|
|
selectedRole.id === role.id && styles.roleItemIconActive
|
|
]}>
|
|
<Ionicons
|
|
name={role.icon as any}
|
|
size={20}
|
|
color={selectedRole.id === role.id ? '#fff' : colors.nautical.teal}
|
|
/>
|
|
</View>
|
|
<Text style={[
|
|
styles.roleItemName,
|
|
selectedRole.id === role.id && styles.roleItemNameActive
|
|
]}>
|
|
{role.name}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={styles.infoButton}
|
|
onPress={() => {
|
|
setExpandedRoleId(expandedRoleId === role.id ? null : role.id);
|
|
}}
|
|
>
|
|
<Ionicons
|
|
name={expandedRoleId === role.id ? "close-circle-outline" : "information-circle-outline"}
|
|
size={24}
|
|
color={expandedRoleId === role.id ? colors.nautical.coral : colors.flow.textSecondary}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{expandedRoleId === role.id && (
|
|
<View style={styles.roleDescription}>
|
|
<Text style={styles.roleDescriptionText}>{role.description}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
))}
|
|
</ScrollView>
|
|
|
|
<TouchableOpacity
|
|
style={styles.closeButton}
|
|
onPress={() => setShowRoleModal(false)}
|
|
>
|
|
<Text style={styles.closeButtonText}>Cancel</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
</Modal>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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',
|
|
},
|
|
});
|