update_260201-3

This commit is contained in:
lusixing
2026-02-01 21:13:15 -08:00
parent 3ffcc60ee8
commit b5373c2d9a
11 changed files with 1327 additions and 396 deletions

View File

@@ -29,10 +29,14 @@ 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 { aiService, AIMessage } from '../services/ai.service';
import { assetsService } from '../services/assets.service';
import { useAuth } from '../context/AuthContext';
import { AI_CONFIG } from '../config';
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
@@ -78,6 +82,18 @@ export default function FlowScreen() {
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<ChatSession[]>([
// Sample history data
{
@@ -453,6 +469,107 @@ export default function FlowScreen() {
);
};
/**
* 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
await assetsService.createAsset({
title: `Chat Summary - ${new Date().toLocaleDateString()}`,
private_key_shard: shareServer,
content_inner_encrypted: encryptedSummary,
}, token);
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
// =============================================================================
@@ -596,6 +713,19 @@ export default function FlowScreen() {
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
</TouchableOpacity>
{/* Summary Button */}
<TouchableOpacity
style={[styles.historyButton, { marginRight: spacing.sm }]}
onPress={() => setShowSummaryConfirmModal(true)}
disabled={messages.length === 0 || isSummarizing}
>
<Ionicons
name="document-text-outline"
size={20}
color={messages.length === 0 || isSummarizing ? colors.flow.textSecondary : colors.flow.primary}
/>
</TouchableOpacity>
{/* History Button */}
<TouchableOpacity
style={styles.historyButton}
@@ -843,6 +973,212 @@ export default function FlowScreen() {
</View>
</TouchableWithoutFeedback>
</Modal>
{/* Summary Confirmation Modal */}
<Modal
visible={showSummaryConfirmModal}
transparent
animationType="fade"
onRequestClose={() => setShowSummaryConfirmModal(false)}
>
<TouchableWithoutFeedback onPress={() => setShowSummaryConfirmModal(false)}>
<View style={styles.modalOverlay}>
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
<View style={[styles.modalContent, { paddingBottom: spacing.xl }]}>
<View style={styles.modalHandle} />
<Text style={styles.modalTitle}>Generate Summary</Text>
<Text style={[styles.modalSubtitle, { marginVertical: spacing.base }]}>
Would you like to generate a summary for the current conversation?
</Text>
<View style={styles.modalActions}>
<TouchableOpacity
style={[styles.actionButton, styles.cancelButton]}
onPress={() => setShowSummaryConfirmModal(false)}
>
<Text style={styles.cancelButtonText}>No</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.confirmButton]}
onPress={handleGenerateSummary}
>
<LinearGradient
colors={[colors.nautical.teal, colors.nautical.seafoam]}
style={styles.actionButtonGradient}
>
<Text style={styles.confirmButtonText}>Yes, Generate</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
{/* Summary Result Modal */}
<Modal
visible={showSummaryResultModal}
transparent
animationType="slide"
onRequestClose={() => setShowSummaryResultModal(false)}
>
<TouchableWithoutFeedback onPress={() => setShowSummaryResultModal(false)}>
<View style={styles.modalOverlay}>
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
<View style={[styles.modalContent, { maxHeight: '70%' }]}>
<View style={styles.modalHandle} />
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Conversation Summary</Text>
<TouchableOpacity onPress={() => setShowSummaryResultModal(false)}>
<Ionicons name="close" size={24} color={colors.flow.textSecondary} />
</TouchableOpacity>
</View>
<ScrollView style={styles.summaryContainer} showsVerticalScrollIndicator={false}>
<View style={styles.summaryCard}>
<Text style={styles.summaryText}>{generatedSummary}</Text>
</View>
</ScrollView>
<View style={styles.summaryActions}>
<TouchableOpacity
style={[styles.actionButton, styles.saveToVaultButton]}
onPress={() => setShowVaultConfirmModal(true)}
disabled={isSavingToVault}
>
<LinearGradient
colors={[colors.nautical.teal, colors.nautical.seafoam]}
style={styles.actionButtonGradient}
>
{isSavingToVault ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<Ionicons name="shield-checkmark-outline" size={20} color="#fff" />
<Text style={styles.confirmButtonText}>Save to Vault</Text>
</>
)}
</LinearGradient>
</TouchableOpacity>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setShowSummaryResultModal(false)}
>
<Text style={styles.closeButtonText}>Done</Text>
</TouchableOpacity>
</View>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
{/* Save to Vault Confirmation Modal */}
<Modal
visible={showVaultConfirmModal}
transparent
animationType="fade"
onRequestClose={() => setShowVaultConfirmModal(false)}
>
<TouchableWithoutFeedback onPress={() => setShowVaultConfirmModal(false)}>
<View style={styles.modalOverlay}>
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
<View style={[styles.modalContent, { paddingBottom: spacing.xl }]}>
<View style={styles.modalHandle} />
<Text style={styles.modalTitle}>Save to Vault</Text>
<Text style={[styles.modalSubtitle, { marginVertical: spacing.base }]}>
Would you like to securely save this summary to your digital vault?
</Text>
<View style={styles.modalActions}>
<TouchableOpacity
style={[styles.actionButton, styles.cancelButton]}
onPress={() => setShowVaultConfirmModal(false)}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.confirmButton]}
onPress={handleSaveToVault}
>
<LinearGradient
colors={[colors.nautical.teal, colors.nautical.seafoam]}
style={styles.actionButtonGradient}
>
<Text style={styles.confirmButtonText}>Yes, Save</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
{/* Save Result Modal */}
<Modal
visible={showSaveResultModal}
transparent
animationType="fade"
onRequestClose={handleFinishSaveFlow}
>
<TouchableWithoutFeedback onPress={handleFinishSaveFlow}>
<View style={styles.modalOverlay}>
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
<View style={[styles.modalContent, { paddingBottom: spacing.xl, alignItems: 'center' }]}>
<View style={styles.modalHandle} />
<View style={[
styles.resultIconContainer,
saveResult.success ? styles.successIconBg : styles.errorIconBg
]}>
<Ionicons
name={saveResult.success ? "checkmark-circle" : "alert-circle"}
size={64}
color={saveResult.success ? colors.nautical.teal : colors.nautical.coral}
/>
</View>
<Text style={styles.modalTitle}>
{saveResult.success ? 'Success!' : 'Oops!'}
</Text>
<Text style={[styles.modalSubtitle, { marginVertical: spacing.base, textAlign: 'center' }]}>
{saveResult.message}
</Text>
<TouchableOpacity
style={[styles.actionButton, styles.confirmButton, { width: '100%' }]}
onPress={handleFinishSaveFlow}
>
<LinearGradient
colors={[colors.nautical.teal, colors.nautical.seafoam]}
style={styles.actionButtonGradient}
>
<Text style={styles.confirmButtonText}>Confirm</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
{/* Summary Loading Modal */}
<Modal
visible={isSummarizing}
transparent
animationType="fade"
>
<View style={styles.loadingOverlay}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.nautical.teal} />
<Text style={styles.loadingText}>Generating Summary...</Text>
</View>
</View>
</Modal>
</View>
);
}
@@ -1281,4 +1617,101 @@ const styles = StyleSheet.create({
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',
},
});