multi_prompt_local_storage

This commit is contained in:
lusixing
2026-01-31 11:11:25 -08:00
parent fb1377eb4b
commit ed1f6fc49d
7 changed files with 727 additions and 156 deletions

View File

@@ -31,6 +31,8 @@ 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
@@ -57,16 +59,21 @@ interface ChatSession {
// =============================================================================
export default function FlowScreen() {
const { token, signOut } = useAuth();
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;
@@ -95,23 +102,87 @@ export default function FlowScreen() {
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'
const dateStr = today.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
});
// Auto-scroll to bottom when new messages arrive
// 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]);
}, [messages, selectedRole.id, user]);
// Save history when it changes
useEffect(() => {
if (user) {
storageService.saveChatHistory(chatHistory, user.id);
}
}, [chatHistory, user]);
// Modal animation control
const openHistoryModal = () => {
@@ -143,7 +214,7 @@ export default function FlowScreen() {
*/
const handleSendMessage = async () => {
if (!newContent.trim() || isSending) return;
// Check authentication
if (!token) {
Alert.alert(
@@ -153,11 +224,11 @@ export default function FlowScreen() {
);
return;
}
const userMessage = newContent.trim();
setIsSending(true);
setNewContent('');
// Add user message immediately
const userMsg: ChatMessage = {
id: Date.now().toString(),
@@ -166,11 +237,11 @@ export default function FlowScreen() {
createdAt: new Date(),
};
setMessages(prev => [...prev, userMsg]);
try {
// Call AI proxy
const aiResponse = await aiService.sendMessage(userMessage, token);
// 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(),
@@ -179,20 +250,20 @@ export default function FlowScreen() {
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') ||
const isAuthError =
errorMessage.includes('401') ||
errorMessage.includes('credentials') ||
errorMessage.includes('Unauthorized') ||
errorMessage.includes('Not authenticated') ||
errorMessage.includes('validate');
if (isAuthError) {
signOut();
Alert.alert(
@@ -202,7 +273,7 @@ export default function FlowScreen() {
);
return;
}
// Show error as AI message
const errorMsg: ChatMessage = {
id: (Date.now() + 1).toString(),
@@ -246,7 +317,7 @@ export default function FlowScreen() {
if (!result.canceled && result.assets[0]) {
const imageAsset = result.assets[0];
setSelectedImage(imageAsset.uri);
// Check authentication
if (!token) {
Alert.alert(
@@ -289,9 +360,9 @@ export default function FlowScreen() {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Handle authentication errors
const isAuthError =
errorMessage.includes('401') ||
errorMessage.includes('Unauthorized') ||
const isAuthError =
errorMessage.includes('401') ||
errorMessage.includes('Unauthorized') ||
errorMessage.includes('credentials') ||
errorMessage.includes('validate');
@@ -334,9 +405,12 @@ export default function FlowScreen() {
};
setChatHistory(prev => [newSession, ...prev]);
}
// Clear current messages
// Clear current messages and storage for this role
setMessages([]);
if (user) {
storageService.saveCurrentChat(selectedRole.id, [], user.id);
}
closeHistoryModal();
};
@@ -355,7 +429,7 @@ export default function FlowScreen() {
};
setChatHistory(prev => [currentSession, ...prev.filter(s => s.id !== session.id)]);
}
// Load selected conversation
setMessages(session.messages);
closeHistoryModal();
@@ -370,8 +444,8 @@ export default function FlowScreen() {
'Are you sure you want to delete this conversation?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
{
text: 'Delete',
style: 'destructive',
onPress: () => setChatHistory(prev => prev.filter(s => s.id !== sessionId))
},
@@ -400,10 +474,10 @@ export default function FlowScreen() {
*/
const renderMessage = (message: ChatMessage, index: number) => {
const isUser = message.role === 'user';
return (
<View
key={message.id}
<View
key={message.id}
style={[
styles.messageBubble,
isUser ? styles.userBubble : styles.aiBubble
@@ -420,8 +494,8 @@ export default function FlowScreen() {
]}>
{/* Show image if present */}
{message.imageUri && (
<Image
source={{ uri: message.imageUri }}
<Image
source={{ uri: message.imageUri }}
style={styles.messageImage}
resizeMode="cover"
/>
@@ -451,9 +525,9 @@ export default function FlowScreen() {
<View style={styles.emptyIcon}>
<Feather name="feather" size={48} color={colors.nautical.seafoam} />
</View>
<Text style={styles.emptyTitle}>Start a conversation</Text>
<Text style={styles.emptyTitle}>Chatting with {selectedRole.name}</Text>
<Text style={styles.emptySubtitle}>
Ask me anything or share your thoughts
{selectedRole.description}
</Text>
</View>
);
@@ -462,7 +536,7 @@ export default function FlowScreen() {
* Render history item in modal
*/
const renderHistoryItem = ({ item }: { item: ChatSession }) => (
<TouchableOpacity
<TouchableOpacity
style={styles.historyItem}
onPress={() => handleLoadHistory(item)}
onLongPress={() => handleDeleteHistory(item.id)}
@@ -504,9 +578,26 @@ export default function FlowScreen() {
<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
<TouchableOpacity
style={styles.historyButton}
onPress={openHistoryModal}
>
@@ -515,7 +606,7 @@ export default function FlowScreen() {
</View>
{/* Chat Messages */}
<ScrollView
<ScrollView
ref={scrollViewRef}
style={styles.messagesContainer}
showsVerticalScrollIndicator={false}
@@ -529,7 +620,7 @@ export default function FlowScreen() {
) : (
messages.map((message, index) => renderMessage(message, index))
)}
{/* Loading indicator when sending */}
{isSending && (
<View style={[styles.messageBubble, styles.aiBubble]}>
@@ -541,7 +632,7 @@ export default function FlowScreen() {
</View>
</View>
)}
<View style={{ height: 20 }} />
</ScrollView>
@@ -549,7 +640,7 @@ export default function FlowScreen() {
<View style={styles.inputBarContainer}>
<View style={styles.inputBar}>
{/* Image attachment button */}
<TouchableOpacity
<TouchableOpacity
style={styles.inputBarButton}
onPress={handleAddImage}
activeOpacity={0.7}
@@ -572,7 +663,7 @@ export default function FlowScreen() {
{/* Send or Voice button */}
{newContent.trim() || isSending ? (
<TouchableOpacity
<TouchableOpacity
style={[styles.sendButton, isSending && styles.sendButtonDisabled]}
onPress={handleSendMessage}
activeOpacity={0.8}
@@ -590,15 +681,15 @@ export default function FlowScreen() {
</LinearGradient>
</TouchableOpacity>
) : (
<TouchableOpacity
<TouchableOpacity
style={[styles.inputBarButton, isRecording && styles.recordingButton]}
onPress={handleVoiceRecord}
activeOpacity={0.7}
>
<Feather
name="mic"
size={22}
color={isRecording ? '#fff' : colors.flow.textSecondary}
<Feather
name="mic"
size={22}
color={isRecording ? '#fff' : colors.flow.textSecondary}
/>
</TouchableOpacity>
)}
@@ -607,17 +698,16 @@ export default function FlowScreen() {
</SafeAreaView>
</LinearGradient>
{/* History Modal - Background appears instantly, content slides up */}
<Modal
visible={showHistoryModal}
<Modal
visible={showHistoryModal}
animationType="none"
transparent
transparent
onRequestClose={closeHistoryModal}
>
<TouchableWithoutFeedback onPress={closeHistoryModal}>
<View style={styles.modalOverlay}>
<TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
<Animated.View
<Animated.View
style={[
styles.modalContent,
{
@@ -630,46 +720,129 @@ export default function FlowScreen() {
}
]}
>
<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 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>
}
/>
{/* Close Button */}
<TouchableOpacity
style={styles.closeButton}
onPress={closeHistoryModal}
>
<Text style={styles.closeButtonText}>Close</Text>
</TouchableOpacity>
{/* 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>
);
}
@@ -680,16 +853,16 @@ export default function FlowScreen() {
const styles = StyleSheet.create({
// Container styles
container: {
flex: 1
container: {
flex: 1
},
gradient: {
flex: 1
gradient: {
flex: 1
},
safeArea: {
flex: 1
safeArea: {
flex: 1
},
// Header styles
header: {
flexDirection: 'row',
@@ -697,12 +870,33 @@ const styles = StyleSheet.create({
alignItems: 'center',
paddingHorizontal: spacing.base,
paddingTop: spacing.sm,
paddingBottom: spacing.md,
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,
@@ -731,20 +925,20 @@ const styles = StyleSheet.create({
alignItems: 'center',
...shadows.soft,
},
// Messages container styles
messagesContainer: {
flex: 1
messagesContainer: {
flex: 1
},
messagesContent: {
padding: spacing.base,
paddingTop: 0
messagesContent: {
padding: spacing.base,
paddingTop: 0
},
emptyContent: {
flex: 1,
justifyContent: 'center',
},
// Empty state styles
emptyState: {
alignItems: 'center',
@@ -772,7 +966,97 @@ const styles = StyleSheet.create({
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',
@@ -837,7 +1121,7 @@ const styles = StyleSheet.create({
aiMessageTime: {
color: colors.flow.textSecondary,
},
// Input bar styles
inputBarContainer: {
paddingHorizontal: spacing.base,
@@ -893,7 +1177,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
// Modal styles
modalOverlay: {
flex: 1,
@@ -941,7 +1225,7 @@ const styles = StyleSheet.create({
fontWeight: '600',
color: '#fff',
},
// History list styles
historyList: {
flex: 1,