import { useState, useEffect, useCallback } from "react"; import { Modal, View, Text, Pressable, ScrollView, Alert, KeyboardAvoidingView, Platform, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { MessageSquare, X, Plus, Trash2 } from "lucide-react-native"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { StyleSheet } from "react-native-unistyles"; import type { DaemonClientV2 } from "@server/client/daemon-client-v2"; const STORAGE_KEY = "@paseo:voice-conversation-id"; interface Conversation { id: string; lastUpdated: string; messageCount: number; } interface ConversationSelectorProps { currentVoiceConversationId: string | null; onSelectVoiceConversation: (voiceConversationId: string | null) => void; client: DaemonClientV2 | null; } export function ConversationSelector({ currentVoiceConversationId, onSelectVoiceConversation, client, }: ConversationSelectorProps) { const [conversations, setConversations] = useState([]); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const fetchConversations = useCallback(async () => { setIsLoading(true); if (!client) { setIsLoading(false); Alert.alert("Error", "Daemon unavailable."); return; } try { console.log("[ConversationSelector] Fetching conversations"); const response = await client.listVoiceConversations(); setConversations(response.conversations); } catch (error) { console.error( "[ConversationSelector] Failed to list conversations:", error ); Alert.alert("Error", "Failed to load conversations."); } finally { setIsLoading(false); } }, [client]); // Fetch conversations when modal opens useEffect(() => { if (isOpen) { void fetchConversations(); } }, [fetchConversations, isOpen]); const handleDeleteConversation = useCallback( (id: string) => { Alert.alert( "Delete Conversation", "Are you sure you want to delete this conversation?", [ { text: "Cancel", style: "cancel" }, { text: "Delete", style: "destructive", onPress: () => { if (!client) { Alert.alert("Error", "Daemon unavailable."); return; } console.log("[ConversationSelector] Deleting conversation:", id); void (async () => { try { const response = await client.deleteVoiceConversation(id); if (response.success) { await fetchConversations(); if (response.voiceConversationId === currentVoiceConversationId) { await handleNewConversation(); } } else { Alert.alert( "Error", `Failed to delete conversation: ${response.error}` ); } } catch (error) { console.error( "[ConversationSelector] Failed to delete conversation:", error ); Alert.alert("Error", "Failed to delete conversation."); } })(); }, }, ] ); }, [currentVoiceConversationId, fetchConversations, handleNewConversation, client] ); const handleClearAll = useCallback(() => { Alert.alert( "Clear All Conversations", "Are you sure you want to delete all conversations?", [ { text: "Cancel", style: "cancel" }, { text: "Clear All", style: "destructive", onPress: () => { if (!client) { Alert.alert("Error", "Daemon unavailable."); return; } console.log("[ConversationSelector] Clearing all conversations"); void (async () => { setIsLoading(true); try { for (const conv of conversations) { await client.deleteVoiceConversation(conv.id); } setConversations([]); await handleNewConversation(); setIsOpen(false); } catch (error) { console.error( "[ConversationSelector] Failed to clear conversations:", error ); Alert.alert("Error", "Failed to clear conversations."); } finally { setIsLoading(false); } })(); }, }, ] ); }, [conversations, handleNewConversation, client]); async function handleSelectConversation(id: string) { try { // Save to AsyncStorage for persistence await AsyncStorage.setItem(STORAGE_KEY, id); onSelectVoiceConversation(id); setIsOpen(false); } catch (error) { console.error( "[ConversationSelector] Failed to save conversation ID:", error ); } } async function handleNewConversation() { try { // Clear saved conversation ID await AsyncStorage.removeItem(STORAGE_KEY); onSelectVoiceConversation(null); setIsOpen(false); } catch (error) { console.error( "[ConversationSelector] Failed to clear conversation ID:", error ); } } function formatDate(dateString: string): string { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } return ( setIsOpen(true)} style={styles.triggerButton}> setIsOpen(false)} > setIsOpen(false)} /> {/* Header */} Conversations{" "} {conversations.length > 0 && `(${conversations.length})`} setIsOpen(false)}> {/* New Conversation Button */} New Conversation {/* Conversations List */} {isLoading && ( Loading... )} {!isLoading && conversations.length === 0 && ( No saved conversations )} {!isLoading && conversations.map((conversation) => ( handleSelectConversation(conversation.id) } > {conversation.messageCount} messages {formatDate(conversation.lastUpdated)} handleDeleteConversation(conversation.id) } style={styles.deleteButton} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > ))} {!isLoading && conversations.length > 0 && ( Clear All )} ); } const styles = StyleSheet.create((theme) => ({ triggerButton: { backgroundColor: theme.colors.surface2, padding: theme.spacing[3], borderRadius: theme.borderRadius.lg, }, modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.5)", }, modalBackdrop: { flex: 1, }, modalContent: { backgroundColor: theme.colors.surface2, borderTopLeftRadius: theme.spacing[6], borderTopRightRadius: theme.spacing[6], height: "80%", }, modalInner: { flex: 1, }, header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", padding: theme.spacing[6], borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, }, headerTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.lg, fontWeight: theme.fontWeight.semibold, }, newButtonContainer: { padding: theme.spacing[4], borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, }, newButton: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: theme.spacing[2], backgroundColor: theme.colors.surface2, paddingVertical: theme.spacing[3], borderRadius: theme.borderRadius.lg, }, newButtonText: { color: theme.colors.foreground, fontWeight: theme.fontWeight.semibold, }, scrollView: { flex: 1, }, scrollContent: { paddingBottom: theme.spacing[4] * 5, }, loadingContainer: { padding: theme.spacing[8], alignItems: "center", }, loadingText: { color: theme.colors.foregroundMuted, }, emptyContainer: { padding: theme.spacing[8], alignItems: "center", }, emptyText: { color: theme.colors.foregroundMuted, }, conversationItem: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", padding: theme.spacing[4], borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, backgroundColor: "transparent", }, conversationItemActive: { backgroundColor: theme.colors.surface2, }, conversationContent: { flex: 1, }, conversationTitle: { color: theme.colors.foreground, fontWeight: theme.fontWeight.semibold, fontSize: theme.fontSize.base, }, conversationDate: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, marginTop: theme.spacing[1], }, deleteButton: { padding: theme.spacing[2], }, clearAllContainer: { padding: theme.spacing[4], }, clearAllButton: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: theme.spacing[2], backgroundColor: "rgba(127, 29, 29, 0.2)", paddingVertical: theme.spacing[3], borderRadius: theme.borderRadius.lg, borderWidth: theme.borderWidth[1], borderColor: theme.colors.palette.red[900], }, clearAllText: { color: theme.colors.destructive, fontWeight: theme.fontWeight.semibold, }, }));