'use client' import { Fab, Drawer, Box, Typography, TextField, Button, Paper, Avatar, Chip, IconButton, Divider, List, ListItem, ListItemText, useTheme, Slide, Grow, Zoom, Menu, MenuItem, Dialog, DialogTitle, DialogContent, DialogActions, } from '@mui/material' import { Chat, Send, Close, SmartToy, Person, ContentCopy, ThumbUp, ThumbDown, Minimize, Launch, History, Add, Delete, MoreVert, Edit, } from '@mui/icons-material' import { useState, useRef, useEffect, useCallback } from 'react' import { useTranslations, useLocale } from 'next-intl' import ReactMarkdown from 'react-markdown' import { AuthModal } from '@/components/auth/auth-modal' interface ChatMessage { id: string role: 'user' | 'assistant' content: string 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') const locale = useLocale() const [isOpen, setIsOpen] = useState(false) const [isMinimized, setIsMinimized] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false) const [showHistory, setShowHistory] = useState(false) const [messages, setMessages] = useState([ { 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(), } ]) 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 [authModalOpen, setAuthModalOpen] = useState(false) const messagesEndRef = useRef(null) const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) } useEffect(() => { scrollToBottom() }, [messages]) // Allow external triggers to open the chat (optionally fullscreen and with initial message) useEffect(() => { const handler = (e: Event) => { const detail = (e as CustomEvent).detail || {} setIsOpen(true) setIsMinimized(false) if (typeof detail.fullscreen === 'boolean') setIsFullscreen(detail.fullscreen) if (typeof detail.initialMessage === 'string') { // Use setTimeout to ensure the input is set after the component is fully rendered setTimeout(() => { setInputMessage(detail.initialMessage) }, 100) } } window.addEventListener('floating-chat:open', handler as EventListener) return () => window.removeEventListener('floating-chat:open', handler as EventListener) }, []) // Listen for auth sign-in required events useEffect(() => { const handler = () => { setAuthModalOpen(true) } window.addEventListener('auth:sign-in-required', handler as EventListener) return () => window.removeEventListener('auth:sign-in-required', 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) } } else { setIsAuthenticated(false) setAuthToken(null) } } catch (error) { console.error('Chat - 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 const userMessage: ChatMessage = { id: Date.now().toString(), role: 'user', content: inputMessage, timestamp: new Date(), } setMessages(prev => [...prev, userMessage]) setInputMessage('') setIsLoading(true) try { const headers: any = { 'Content-Type': 'application/json', } // Add authentication if available console.log('Chat - authToken value:', authToken ? 'present' : 'null') if (authToken) { headers['Authorization'] = `Bearer ${authToken}` console.log('Chat - Authorization header added') } else { console.log('Chat - No authToken, skipping Authorization header') } const response = await fetch('/api/chat', { method: 'POST', headers, body: JSON.stringify({ message: inputMessage, ...(activeConversationId && { conversationId: activeConversationId }), history: messages.slice(-5), // Fallback for anonymous users locale: locale, }), }) if (!response.ok) { throw new Error('Failed to get response') } const data = await response.json() const assistantMessage: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: data.response || (locale === 'ro' ? 'Îmi pare rău, nu am putut procesa întrebarea ta. Te rog încearcă din nou.' : 'Sorry, I could not process your question. Please try again.'), timestamp: new Date(), } 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 = { id: (Date.now() + 1).toString(), role: 'assistant', content: locale === 'ro' ? 'Îmi pare rău, a apărut o eroare. Te rog verifică conexiunea și încearcă din nou.' : 'Sorry, an error occurred. Please check your connection and try again.', timestamp: new Date(), } setMessages(prev => [...prev, errorMessage]) } finally { setIsLoading(false) } } const handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault() handleSendMessage() } } const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text) } // Use t.raw() to get the actual array from translations const suggestedQuestions = t.raw('suggestions.questions') as string[] const toggleChat = () => { setIsOpen(!isOpen) if (isMinimized) setIsMinimized(false) } const minimizeChat = () => { setIsMinimized(!isMinimized) } const toggleFullscreen = () => setIsFullscreen(prev => !prev) const handleAuthSuccess = () => { setAuthModalOpen(false) // Re-check auth status to update the UI checkAuthStatus() } return ( <> {/* Floating Action Button */} {/* Chat Overlay */} {/* Header */} {t('title')} {t('subtitle')} setShowHistory(!showHistory)} sx={{ color: 'white', mr: 0.5 }} title="Chat History" > {!isMinimized && ( <> {/* Main Content Area - Side by Side Layout */} {/* Chat History Panel - Left Side */} {showHistory && ( Chat History setShowHistory(false)} title="Close History" > {!isAuthenticated ? ( ✨ {locale === 'ro' ? 'Conectează-te pentru a salva conversațiile' : '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 }} > ))} )} )} {/* Main Chat Area - Right Side */} {/* Suggested Questions */} {t('suggestions.title')} {suggestedQuestions.slice(0, 3).map((question, index) => ( setInputMessage(question)} sx={{ fontSize: '0.75rem', cursor: 'pointer', '&:hover': { bgcolor: 'primary.light', color: 'white', }, }} /> ))} {/* Messages */} {!isAuthenticated ? ( {locale === 'ro' ? 'Bun venit la AI Chat Biblic!' : 'Welcome to Biblical AI Chat!'} {locale === 'ro' ? 'Pentru a accesa chat-ul AI și a salva conversațiile tale, te rugăm să îți creezi un cont sau să te conectezi.' : 'To access the AI chat and save your conversations, please create an account or sign in.' } ) : ( <> {messages.map((message) => ( {message.role === 'user' ? : } {message.role === 'assistant' ? ( ( {children} ), h3: ({ children }) => ( {children} ), strong: ({ children }) => ( {children} ), ul: ({ children }) => ( {children} ), li: ({ children }) => ( {children} ), }} > {message.content} ) : ( {message.content} )} {message.role === 'assistant' && ( copyToClipboard(message.content)} > )} {message.timestamp.toLocaleTimeString(locale === 'en' ? 'en-US' : 'ro-RO', { hour: '2-digit', minute: '2-digit', })} ))} {isLoading && ( {t('loading')} )}
)} {/* Input */} {isAuthenticated && ( setInputMessage(e.target.value)} onKeyPress={handleKeyPress} disabled={isLoading} variant="outlined" sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2, } }} /> {t('enterToSend')} )} {/* 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. )} {/* Auth Modal */} setAuthModalOpen(false)} onSuccess={handleAuthSuccess} message={locale === 'ro' ? 'Vă rugăm să vă autentificați pentru a accesa chat-ul AI și a salva conversațiile.' : 'Please sign in to access the AI chat and save your conversations.'} defaultTab="login" /> ) }