From 4523b0a16d79877aa68679acb7c17b0d0874ed39 Mon Sep 17 00:00:00 2001 From: Aayushgoyal00 Date: Wed, 12 Nov 2025 03:13:50 +0530 Subject: [PATCH] feat: add conversation title editing functionality - Introduced state management for editing conversation titles. - Implemented UI for editing titles directly within the chat history. - Added backend support for updating conversation titles in the database. - Enhanced conversation display logic to reflect updated titles. - Improved user experience with immediate local state updates upon title changes. --- apps/client/app/(main)/chat-history.tsx | 209 ++++++++++++++++-- .../features/chat/services/conversations.ts | 40 ++++ apps/client/hooks/useChatHistoryData.ts | 2 +- 3 files changed, 234 insertions(+), 17 deletions(-) diff --git a/apps/client/app/(main)/chat-history.tsx b/apps/client/app/(main)/chat-history.tsx index 56ff88b5..bc374b72 100644 --- a/apps/client/app/(main)/chat-history.tsx +++ b/apps/client/app/(main)/chat-history.tsx @@ -71,6 +71,9 @@ export default function ChatHistoryScreen() { const [isRefreshing, setIsRefreshing] = useState(false); const [currentConversationId, setCurrentConversationId] = useState(null); const [isCreatingChat, setIsCreatingChat] = useState(false); + const [editingConversationId, setEditingConversationId] = useState(null); + const [editingTitle, setEditingTitle] = useState(''); + const lastTapRef = useRef<{ [key: string]: number }>({}); // Determine if we're on mobile (small screen) or desktop/tablet const [windowWidth, setWindowWidth] = useState(Dimensions.get('window').width); @@ -287,10 +290,17 @@ export default function ChatHistoryScreen() { // Helper function to get display title for a conversation const getConversationDisplayTitle = ( - metadata?: { summary_title?: string }, - fallback: string = 'New conversation' + conversation: ConversationWithPreview ): string => { - return metadata?.summary_title || fallback; + // Use the title field directly if it's been customized + if (conversation.title && conversation.title !== 'mallory-global') { + return conversation.title; + } + // Check if there's an AI-generated summary in metadata + if (conversation.metadata?.summary_title) { + return conversation.metadata.summary_title; + } + return 'New conversation'; }; // Load current conversation ID for active indicator @@ -445,6 +455,59 @@ export default function ChatHistoryScreen() { ); }; + // Handle conversation rename + const handleRenameConversation = async (conversationId: string, newTitle: string) => { + try { + console.log('📝 Renaming conversation:', conversationId, 'to:', newTitle); + + // Find the conversation to ensure it exists + const conversation = conversations.find(c => c.id === conversationId); + if (!conversation) return; + + // Update the conversation title directly + const { error } = await supabase + .from('conversations') + .update({ + title: newTitle.trim(), + updated_at: new Date().toISOString() + }) + .eq('id', conversationId) + .eq('user_id', user?.id); + + if (error) { + console.error('❌ Failed to rename conversation:', error); + return; + } + + console.log('✅ Conversation renamed successfully'); + + // Update local state immediately for better UX (realtime will also update for consistency) + handleConversationUpdate({ + ...conversation, + title: newTitle.trim(), + updated_at: new Date().toISOString() + }); + + // Clear editing state + setEditingConversationId(null); + setEditingTitle(''); + } catch (error) { + console.error('Error renaming conversation:', error); + } + }; + + // Start editing a conversation title + const startEditingTitle = (conversationId: string, currentTitle: string) => { + setEditingConversationId(conversationId); + setEditingTitle(currentTitle); + }; + + // Cancel editing + const cancelEditingTitle = () => { + setEditingConversationId(null); + setEditingTitle(''); + }; + // Handle new chat creation const handleNewChat = async () => { // Prevent multiple rapid clicks @@ -489,24 +552,110 @@ export default function ChatHistoryScreen() { // Render conversation item const renderConversationItem = ({ item }: { item: ConversationWithPreview }) => { const isActive = currentConversationId === item.id; - const displayTitle = getConversationDisplayTitle(item.metadata, 'New conversation'); + const isEditing = editingConversationId === item.id; + const displayTitle = getConversationDisplayTitle(item); const dateLabel = formatDate(item.updated_at); return ( - handleConversationTap(item.id)} - activeOpacity={0.7} - > - - {isActive && } - - {displayTitle} - - {dateLabel} + {isEditing ? ( + // Edit mode + + + {isActive && } + { + if (editingTitle.trim()) { + handleRenameConversation(item.id, editingTitle); + } else { + cancelEditingTitle(); + } + }} + onBlur={() => { + // Small delay to prevent blur when clicking save button + setTimeout(() => { + if (editingConversationId === item.id) { + if (editingTitle.trim() && editingTitle !== displayTitle) { + handleRenameConversation(item.id, editingTitle); + } else { + cancelEditingTitle(); + } + } + }, 200); + }} + placeholder="Enter conversation title..." + placeholderTextColor="#999" + selectionColor="rgba(0, 0, 0, 0.3)" + returnKeyType="done" + /> + { + if (editingTitle.trim()) { + handleRenameConversation(item.id, editingTitle); + } + }} + style={styles.editButton} + > + + + + + + - + ) : ( + // Normal mode + handleConversationTap(item.id)} + onLongPress={() => startEditingTitle(item.id, displayTitle)} + activeOpacity={0.7} + > + + {isActive && } + { + // Check for double tap + const now = Date.now(); + const lastTap = lastTapRef.current[item.id]; + if (lastTap && (now - lastTap) < 300) { + e.stopPropagation(); + startEditingTitle(item.id, displayTitle); + delete lastTapRef.current[item.id]; + } else { + lastTapRef.current[item.id] = now; + handleConversationTap(item.id); + } + }} + > + {displayTitle} + + { + e.stopPropagation(); + startEditingTitle(item.id, displayTitle); + }} + style={styles.editIconButton} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + > + + + {dateLabel} + + + )} ); }; @@ -836,4 +985,32 @@ const styles = StyleSheet.create({ color: '#000000', fontFamily: 'Satoshi', }, + conversationEditContent: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + }, + editInput: { + flex: 1, + fontSize: 16, + fontWeight: '400', + color: '#000000', + fontFamily: 'Satoshi', + backgroundColor: '#FFFFFF', + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 6, + marginRight: 8, + borderWidth: 1, + borderColor: '#E67B25', + }, + editButton: { + padding: 6, + marginHorizontal: 4, + }, + editIconButton: { + padding: 4, + marginHorizontal: 8, + opacity: 0.6, + }, }); diff --git a/apps/client/features/chat/services/conversations.ts b/apps/client/features/chat/services/conversations.ts index 4f38ecb3..8e4f9bdb 100644 --- a/apps/client/features/chat/services/conversations.ts +++ b/apps/client/features/chat/services/conversations.ts @@ -290,6 +290,46 @@ export async function createOnboardingConversation(userId?: string): Promise { + try { + console.log('📝 Updating conversation title:', { conversationId, newTitle }); + + // Get userId if not provided + let authUserId = userId; + if (!authUserId) { + const { data: { user } } = await supabase.auth.getUser(); + authUserId = user?.id; + } + + if (!authUserId) { + console.error('No user ID available for updating conversation title'); + return false; + } + + // Update the title field directly + const { error: updateError } = await supabase + .from('conversations') + .update({ + title: newTitle.trim(), + updated_at: new Date().toISOString() + }) + .eq('id', conversationId) + .eq('user_id', authUserId); + + if (updateError) { + console.error('Error updating conversation title:', updateError); + return false; + } + + console.log('✅ Conversation title updated successfully'); + return true; + } catch (error) { + console.error('Error in updateConversationTitle:', error); + return false; + } +} + // Get current conversation or load most recent from history (only auto-create if no history exists) export async function getCurrentOrCreateConversation( userId?: string, diff --git a/apps/client/hooks/useChatHistoryData.ts b/apps/client/hooks/useChatHistoryData.ts index 2f328aab..d8364957 100644 --- a/apps/client/hooks/useChatHistoryData.ts +++ b/apps/client/hooks/useChatHistoryData.ts @@ -210,7 +210,7 @@ export function useChatHistoryData(userId?: string) { setConversations(prev => { const updated = prev.map(conv => conv.id === newRecord.id - ? { ...conv, updated_at: newRecord.updated_at, metadata: newRecord.metadata } + ? { ...conv, ...newRecord } // Merge all updated fields including title : conv ).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());