update_260201-3
This commit is contained in:
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user