From 382fb93de2a11dbe8ac7b501c9de01abf993acd9 Mon Sep 17 00:00:00 2001 From: andupetcu <47487320+andupetcu@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:05:03 +0300 Subject: [PATCH] Complete Phase 3: Add conversation management capabilities to AI chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add conversation delete functionality with confirmation dialog - Add conversation rename functionality with input modal - Implement three-dot menu for each conversation item - Add proper authentication token key consistency - Fix JSX structure issues in floating chat component - Integrate full CRUD operations with conversation API 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- components/chat/floating-chat.tsx | 435 ++++++++++++++++++++++++++++-- 1 file changed, 412 insertions(+), 23 deletions(-) diff --git a/components/chat/floating-chat.tsx b/components/chat/floating-chat.tsx index 0d1a08c..dc786fb 100644 --- a/components/chat/floating-chat.tsx +++ b/components/chat/floating-chat.tsx @@ -18,6 +18,12 @@ import { Slide, Grow, Zoom, + Menu, + MenuItem, + Dialog, + DialogTitle, + DialogContent, + DialogActions, } from '@mui/material' import { Chat, @@ -31,8 +37,13 @@ import { Minimize, Launch, History, + Add, + Delete, + CircularProgress, + MoreVert, + Edit, } from '@mui/icons-material' -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, useCallback } from 'react' import { useTranslations, useLocale } from 'next-intl' import ReactMarkdown from 'react-markdown' @@ -43,6 +54,16 @@ interface ChatMessage { timestamp: Date } +interface Conversation { + id: string + title: string + language: string + messageCount: number + lastMessage: ChatMessage | null + lastMessageAt: string + createdAt: string +} + export default function FloatingChat() { const theme = useTheme() const t = useTranslations('chat') @@ -63,6 +84,18 @@ export default function FloatingChat() { ]) const [inputMessage, setInputMessage] = useState('') const [isLoading, setIsLoading] = useState(false) + + // Conversation management state + const [conversations, setConversations] = useState([]) + const [activeConversationId, setActiveConversationId] = useState(null) + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [authToken, setAuthToken] = useState(null) + const [isLoadingConversations, setIsLoadingConversations] = useState(false) + const [menuAnchorEl, setMenuAnchorEl] = useState(null) + const [selectedConversationId, setSelectedConversationId] = useState(null) + const [showRenameDialog, setShowRenameDialog] = useState(false) + const [showDeleteDialog, setShowDeleteDialog] = useState(false) + const [newTitle, setNewTitle] = useState('') const messagesEndRef = useRef(null) const scrollToBottom = () => { @@ -91,6 +124,204 @@ export default function FloatingChat() { return () => window.removeEventListener('floating-chat:open', handler as EventListener) }, []) + // Check authentication status + useEffect(() => { + checkAuthStatus() + }, []) + + // Load conversations when authenticated + useEffect(() => { + if (isAuthenticated && authToken) { + loadConversations() + } + }, [isAuthenticated, authToken, locale]) + + const checkAuthStatus = useCallback(async () => { + try { + const token = localStorage.getItem('authToken') + if (token) { + // Verify token with the server + const response = await fetch('/api/auth/me', { + headers: { + 'Authorization': `Bearer ${token}` + } + }) + + if (response.ok) { + setAuthToken(token) + setIsAuthenticated(true) + } else { + localStorage.removeItem('authToken') + setIsAuthenticated(false) + setAuthToken(null) + } + } + } catch (error) { + console.error('Auth check failed:', error) + setIsAuthenticated(false) + setAuthToken(null) + } + }, []) + + const loadConversations = useCallback(async () => { + if (!authToken) return + + setIsLoadingConversations(true) + try { + const response = await fetch(`/api/chat/conversations?language=${locale}&limit=20`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }) + + if (response.ok) { + const data = await response.json() + setConversations(data.conversations || []) + } else { + console.error('Failed to load conversations') + } + } catch (error) { + console.error('Error loading conversations:', error) + } finally { + setIsLoadingConversations(false) + } + }, [authToken, locale]) + + const loadConversation = useCallback(async (conversationId: string) => { + if (!authToken) return + + try { + const response = await fetch(`/api/chat/conversations/${conversationId}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }) + + if (response.ok) { + const data = await response.json() + const conversation = data.conversation + + // Convert messages to the expected format + const formattedMessages = conversation.messages.map((msg: any) => ({ + id: msg.id, + role: msg.role, + content: msg.content, + timestamp: new Date(msg.timestamp) + })) + + setMessages(formattedMessages) + setActiveConversationId(conversationId) + setShowHistory(false) // Close history panel + } + } catch (error) { + console.error('Error loading conversation:', error) + } + }, [authToken]) + + const createNewConversation = useCallback(() => { + // Reset to a new conversation + setMessages([ + { + id: '1', + role: 'assistant', + content: locale === 'ro' + ? 'Bună ziua! Sunt asistentul tău AI pentru întrebări biblice. Cum te pot ajuta astăzi să înțelegi mai bine Scriptura?' + : 'Hello! I am your AI assistant for biblical questions. How can I help you understand Scripture better today?', + timestamp: new Date(), + } + ]) + setActiveConversationId(null) + setShowHistory(false) + }, [locale]) + + const handleMenuOpen = useCallback((event: React.MouseEvent, conversationId: string) => { + event.stopPropagation() + setMenuAnchorEl(event.currentTarget) + setSelectedConversationId(conversationId) + }, []) + + const handleMenuClose = useCallback(() => { + setMenuAnchorEl(null) + setSelectedConversationId(null) + }, []) + + const handleRenameClick = useCallback(() => { + const conversation = conversations.find(c => c.id === selectedConversationId) + if (conversation) { + setNewTitle(conversation.title) + setShowRenameDialog(true) + } + handleMenuClose() + }, [conversations, selectedConversationId]) + + const handleDeleteClick = useCallback(() => { + setShowDeleteDialog(true) + handleMenuClose() + }, []) + + const handleRenameSubmit = useCallback(async () => { + if (!selectedConversationId || !newTitle.trim()) return + + try { + const token = localStorage.getItem('authToken') + if (!token) return + + const response = await fetch(`/api/chat/conversations/${selectedConversationId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ title: newTitle.trim() }) + }) + + if (response.ok) { + setConversations(prev => prev.map(conv => + conv.id === selectedConversationId + ? { ...conv, title: newTitle.trim() } + : conv + )) + } + } catch (error) { + console.error('Error renaming conversation:', error) + } + + setShowRenameDialog(false) + setSelectedConversationId(null) + setNewTitle('') + }, [selectedConversationId, newTitle]) + + const handleDeleteConfirm = useCallback(async () => { + if (!selectedConversationId) return + + try { + const token = localStorage.getItem('authToken') + if (!token) return + + const response = await fetch(`/api/chat/conversations/${selectedConversationId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}` + } + }) + + if (response.ok) { + setConversations(prev => prev.filter(conv => conv.id !== selectedConversationId)) + + // If we deleted the active conversation, clear the current chat + if (activeConversationId === selectedConversationId) { + setActiveConversationId(null) + setMessages([]) + } + } + } catch (error) { + console.error('Error deleting conversation:', error) + } + + setShowDeleteDialog(false) + setSelectedConversationId(null) + }, [selectedConversationId, activeConversationId]) + const handleSendMessage = async () => { if (!inputMessage.trim() || isLoading) return @@ -106,14 +337,22 @@ export default function FloatingChat() { setIsLoading(true) try { + const headers: any = { + 'Content-Type': 'application/json', + } + + // Add authentication if available + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}` + } + const response = await fetch('/api/chat', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers, body: JSON.stringify({ message: inputMessage, - history: messages.slice(-5), + conversationId: activeConversationId, + history: messages.slice(-5), // Fallback for anonymous users locale: locale, }), }) @@ -134,6 +373,15 @@ export default function FloatingChat() { } setMessages(prev => [...prev, assistantMessage]) + + // Update conversation state if we got a conversationId back + if (data.conversationId && isAuthenticated) { + if (!activeConversationId) { + setActiveConversationId(data.conversationId) + } + // Refresh conversations list to update last message + loadConversations() + } } catch (error) { console.error('Error sending message:', error) const errorMessage: ChatMessage = { @@ -287,29 +535,170 @@ export default function FloatingChat() { maxHeight: '300px', overflowY: 'auto' }}> - - Chat History - - - - 📝 Chat history will appear here + + + Chat History - - ✨ Sign in to save your conversations - - + + + + + setShowHistory(false)} + title="Close History" + > + + + + + {!isAuthenticated ? ( + + + ✨ Sign in to save your conversations + + + + ) : isLoadingConversations ? ( + + + + ) : conversations.length === 0 ? ( + + + 📝 No conversations yet + + + Start chatting to create your first conversation! + + + ) : ( + + {conversations.map((conversation) => ( + loadConversation(conversation.id)} + > + + + + {conversation.title} + + + {conversation.messageCount} messages • {new Date(conversation.lastMessageAt).toLocaleDateString()} + + {conversation.lastMessage && ( + + {conversation.lastMessage.content.substring(0, 60)}... + + )} + + handleMenuOpen(e, conversation.id)} + sx={{ ml: 1, flexShrink: 0 }} + > + + + + + ))} + + )} )} - <> + {/* Conversation Menu */} + + + + Rename + + + + Delete + + + + {/* Rename Dialog */} + setShowRenameDialog(false)} maxWidth="sm" fullWidth> + Rename Conversation + + setNewTitle(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleRenameSubmit() + } + }} + /> + + + + + + + + {/* Delete Confirmation Dialog */} + setShowDeleteDialog(false)}> + Delete Conversation + + + Are you sure you want to delete this conversation? This action cannot be undone. + + + + + + + + {/* Suggested Questions */}