Files
biblical-guide.com/components/chat/floating-chat.tsx
Andrei a01377b21a feat: implement AI chat with vector search and random loading messages
Major Features:
-  AI chat with Azure OpenAI GPT-4o integration
-  Vector search across Bible versions (ASV English, RVA 1909 Spanish)
-  Multi-language support with automatic English fallback
-  Bible version citations in responses [ASV] [RVA 1909]
-  Random Bible-themed loading messages (5 variants)
-  Safe build script with memory guardrails
-  8GB swap memory for build safety
-  Stripe donation integration (multiple payment methods)

AI Chat Improvements:
- Implement vector search with 1536-dim embeddings (Azure text-embedding-ada-002)
- Search all Bible versions in user's language, fallback to English
- Cite Bible versions properly in AI responses
- Add 5 random loading messages: "Searching the Scriptures...", etc.
- Fix Ollama conflict (disabled to use Azure OpenAI exclusively)
- Optimize hybrid search queries for actual table schema

Build & Infrastructure:
- Create safe-build.sh script with memory monitoring (prevents server crashes)
- Add 8GB swap memory for emergency relief
- Document build process in BUILD_GUIDE.md
- Set Node.js memory limits (4GB max during builds)

Database:
- Clean up 115 old vector tables with wrong dimensions
- Keep only 2 tables with correct 1536-dim embeddings
- Add Stripe schema for donations and subscriptions

Documentation:
- AI_CHAT_FINAL_STATUS.md - Complete implementation status
- AI_CHAT_IMPLEMENTATION_COMPLETE.md - Technical details
- BUILD_GUIDE.md - Safe building guide with guardrails
- CHAT_LOADING_MESSAGES.md - Loading messages implementation
- STRIPE_IMPLEMENTATION_COMPLETE.md - Stripe integration docs
- STRIPE_SETUP_GUIDE.md - Stripe configuration guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 19:37:24 +00:00

1086 lines
40 KiB
TypeScript

'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'
// Random Bible-related loading messages
const LOADING_MESSAGES = [
"Searching the Scriptures...",
"Seeking wisdom from God's Word...",
"Consulting the Holy Scriptures...",
"Finding relevant Bible verses...",
"Exploring God's eternal truth..."
]
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<ChatMessage[]>([
{
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)
const [loadingMessage, setLoadingMessage] = useState('')
// Conversation management state
const [conversations, setConversations] = useState<Conversation[]>([])
const [activeConversationId, setActiveConversationId] = useState<string | null>(null)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [authToken, setAuthToken] = useState<string | null>(null)
const [isLoadingConversations, setIsLoadingConversations] = useState(false)
const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null)
const [selectedConversationId, setSelectedConversationId] = useState<string | null>(null)
const [showRenameDialog, setShowRenameDialog] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [newTitle, setNewTitle] = useState('')
const [authModalOpen, setAuthModalOpen] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(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<HTMLElement>, 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('')
// Pick a random loading message
const randomMessage = LOADING_MESSAGES[Math.floor(Math.random() * LOADING_MESSAGES.length)]
setLoadingMessage(randomMessage)
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 */}
<Zoom in={!isOpen} unmountOnExit>
<Fab
color="primary"
onClick={toggleChat}
sx={{
position: 'fixed',
bottom: 24,
right: 24,
zIndex: 1000,
background: 'linear-gradient(45deg, #009688 30%, #00796B 90%)',
'&:hover': {
background: 'linear-gradient(45deg, #00695C 30%, #004D40 90%)',
}
}}
>
<Chat />
</Fab>
</Zoom>
{/* Chat Overlay */}
<Slide direction="up" in={isOpen} unmountOnExit>
<Paper
elevation={8}
data-floating-chat="true"
sx={{
position: 'fixed',
bottom: 0,
right: 0,
top: isFullscreen ? 0 : 'auto',
left: isFullscreen ? 0 : 'auto',
width: isFullscreen ? '100vw' : { xs: '100vw', sm: '50vw', md: '40vw' },
height: isMinimized ? 'auto' : '100vh',
zIndex: 1200,
borderRadius: isFullscreen ? 0 : { xs: 0, sm: '12px 0 0 0' },
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(to bottom, #f8f9fa, #ffffff)',
}}
>
{/* Header */}
<Box
sx={{
p: 2,
background: 'linear-gradient(45deg, #009688 30%, #00796B 90%)',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Avatar sx={{ bgcolor: 'rgba(255,255,255,0.2)' }}>
<SmartToy />
</Avatar>
<Box>
<Typography variant="subtitle1" fontWeight="bold">
{t('title')}
</Typography>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
{t('subtitle')}
</Typography>
</Box>
</Box>
<Box>
<IconButton
size="small"
onClick={() => setShowHistory(!showHistory)}
sx={{ color: 'white', mr: 0.5 }}
title="Chat History"
>
<History />
</IconButton>
<IconButton
size="small"
onClick={minimizeChat}
sx={{ color: 'white', mr: 0.5 }}
>
<Minimize />
</IconButton>
<IconButton
size="small"
onClick={toggleFullscreen}
sx={{ color: 'white', mr: 0.5 }}
>
<Launch />
</IconButton>
<IconButton
size="small"
onClick={toggleChat}
sx={{ color: 'white' }}
>
<Close />
</IconButton>
</Box>
</Box>
{!isMinimized && (
<>
{/* Main Content Area - Side by Side Layout */}
<Box sx={{
display: 'flex',
flexGrow: 1,
overflow: 'hidden'
}}>
{/* Chat History Panel - Left Side */}
{showHistory && (
<Box sx={{
width: '300px',
borderRight: 1,
borderColor: 'divider',
bgcolor: 'grey.50',
display: 'flex',
flexDirection: 'column',
}}>
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">
Chat History
</Typography>
<Box>
<IconButton
size="small"
onClick={createNewConversation}
title="New Conversation"
sx={{ mr: 0.5 }}
>
<Add />
</IconButton>
<IconButton
size="small"
onClick={() => setShowHistory(false)}
title="Close History"
>
<Close />
</IconButton>
</Box>
</Box>
</Box>
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 2 }}>
{!isAuthenticated ? (
<Box sx={{ textAlign: 'center', py: 3 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{locale === 'ro' ? 'Conectează-te pentru a salva conversațiile' : 'Sign in to save your conversations'}
</Typography>
<Button
variant="outlined"
size="small"
onClick={() => {
window.dispatchEvent(new CustomEvent('auth:sign-in-required'))
}}
>
{locale === 'ro' ? 'Conectează-te' : 'Sign In'}
</Button>
</Box>
) : isLoadingConversations ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 3 }}>
</Box>
) : conversations.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 3 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
📝 No conversations yet
</Typography>
<Typography variant="body2" color="text.secondary">
Start chatting to create your first conversation!
</Typography>
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{conversations.map((conversation) => (
<Paper
key={conversation.id}
sx={{
p: 2,
cursor: 'pointer',
border: activeConversationId === conversation.id ? '2px solid' : '1px solid',
borderColor: activeConversationId === conversation.id ? 'primary.main' : 'divider',
'&:hover': { bgcolor: 'action.hover' },
position: 'relative'
}}
onClick={() => loadConversation(conversation.id)}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ mb: 0.5, fontWeight: 'medium' }}>
{conversation.title}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
{conversation.messageCount} messages {new Date(conversation.lastMessageAt).toLocaleDateString()}
</Typography>
{conversation.lastMessage && (
<Typography variant="body2" color="text.secondary" sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
}}>
{conversation.lastMessage.content.substring(0, 60)}...
</Typography>
)}
</Box>
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, conversation.id)}
sx={{ ml: 1, flexShrink: 0 }}
>
<MoreVert fontSize="small" />
</IconButton>
</Box>
</Paper>
))}
</Box>
)}
</Box>
</Box>
)}
{/* Main Chat Area - Right Side */}
<Box sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}>
{/* Suggested Questions */}
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{t('suggestions.title')}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{suggestedQuestions.slice(0, 3).map((question, index) => (
<Chip
key={index}
label={question}
size="small"
variant="outlined"
onClick={() => setInputMessage(question)}
sx={{
fontSize: '0.75rem',
cursor: 'pointer',
'&:hover': {
bgcolor: 'primary.light',
color: 'white',
},
}}
/>
))}
</Box>
</Box>
{/* Messages */}
<Box
sx={{
flexGrow: 1,
overflow: 'auto',
p: 1,
}}
>
{!isAuthenticated ? (
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
textAlign: 'center',
p: 3
}}>
<SmartToy sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" gutterBottom>
{locale === 'ro' ? 'Bun venit la AI Chat Biblic!' : 'Welcome to Biblical AI Chat!'}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3, maxWidth: 400 }}>
{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.'
}
</Typography>
<Button
variant="contained"
size="large"
onClick={() => {
window.dispatchEvent(new CustomEvent('auth:sign-in-required'))
}}
sx={{
background: 'linear-gradient(45deg, #009688 30%, #00796B 90%)',
px: 4,
py: 1.5
}}
>
{locale === 'ro' ? 'Creează Cont / Conectează-te' : 'Create Account / Sign In'}
</Button>
</Box>
) : (
<>
{messages.map((message) => (
<Box
key={message.id}
sx={{
display: 'flex',
justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start',
mb: 2,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: message.role === 'user' ? 'row-reverse' : 'row',
alignItems: 'flex-start',
maxWidth: '85%',
gap: 1,
}}
>
<Avatar
sx={{
width: 32,
height: 32,
bgcolor: message.role === 'user' ? 'primary.main' : 'secondary.main',
}}
>
{message.role === 'user' ? <Person fontSize="small" /> : <SmartToy fontSize="small" />}
</Avatar>
<Paper
elevation={1}
sx={{
p: 1.5,
bgcolor: message.role === 'user' ? 'primary.light' : 'background.paper',
color: message.role === 'user' ? 'white' : 'text.primary',
borderRadius: 2,
maxWidth: '100%',
}}
>
{message.role === 'assistant' ? (
<ReactMarkdown
components={{
p: ({ children }) => (
<Typography
variant="body2"
sx={{ mb: 1, lineHeight: 1.4 }}
>
{children}
</Typography>
),
h3: ({ children }) => (
<Typography
variant="h6"
sx={{ fontWeight: 'bold', mt: 2, mb: 1 }}
>
{children}
</Typography>
),
strong: ({ children }) => (
<Typography
component="span"
sx={{ fontWeight: 'bold' }}
>
{children}
</Typography>
),
ul: ({ children }) => (
<Box component="ul" sx={{ pl: 2, mb: 1 }}>
{children}
</Box>
),
li: ({ children }) => (
<Typography
component="li"
variant="body2"
sx={{ mb: 0.5 }}
>
{children}
</Typography>
),
}}
>
{message.content}
</ReactMarkdown>
) : (
<Typography
variant="body2"
sx={{
whiteSpace: 'pre-wrap',
lineHeight: 1.4,
}}
>
{message.content}
</Typography>
)}
{message.role === 'assistant' && (
<Box sx={{ display: 'flex', gap: 0.5, mt: 1, justifyContent: 'flex-end' }}>
<IconButton
size="small"
onClick={() => copyToClipboard(message.content)}
>
<ContentCopy fontSize="small" />
</IconButton>
<IconButton size="small">
<ThumbUp fontSize="small" />
</IconButton>
<IconButton size="small">
<ThumbDown fontSize="small" />
</IconButton>
</Box>
)}
<Typography
variant="caption"
sx={{
display: 'block',
textAlign: 'right',
mt: 0.5,
opacity: 0.7,
}}
>
{message.timestamp.toLocaleTimeString(locale === 'en' ? 'en-US' : 'ro-RO', {
hour: '2-digit',
minute: '2-digit',
})}
</Typography>
</Paper>
</Box>
</Box>
))}
{isLoading && (
<Box sx={{ display: 'flex', justifyContent: 'flex-start', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: 'secondary.main' }}>
<SmartToy fontSize="small" />
</Avatar>
<Paper elevation={1} sx={{ p: 1.5, borderRadius: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{ display: 'flex', gap: 0.5 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: 'primary.main',
animation: 'bounce 1.4s infinite ease-in-out both',
'@keyframes bounce': {
'0%, 80%, 100%': { transform: 'scale(0)' },
'40%': { transform: 'scale(1)' }
}
}} />
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: 'primary.main',
animation: 'bounce 1.4s infinite ease-in-out both',
animationDelay: '-0.32s',
'@keyframes bounce': {
'0%, 80%, 100%': { transform: 'scale(0)' },
'40%': { transform: 'scale(1)' }
}
}} />
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: 'primary.main',
animation: 'bounce 1.4s infinite ease-in-out both',
animationDelay: '-0.16s',
'@keyframes bounce': {
'0%, 80%, 100%': { transform: 'scale(0)' },
'40%': { transform: 'scale(1)' }
}
}} />
</Box>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'text.secondary' }}>
{loadingMessage}
</Typography>
</Box>
</Paper>
</Box>
</Box>
)}
<div ref={messagesEndRef} />
</>
)}
</Box>
<Divider />
{/* Input */}
{isAuthenticated && (
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<TextField
fullWidth
size="small"
multiline
maxRows={3}
placeholder={t('placeholder')}
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={handleKeyPress}
disabled={isLoading}
variant="outlined"
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
}
}}
/>
<Button
variant="contained"
onClick={handleSendMessage}
disabled={!inputMessage.trim() || isLoading}
sx={{
minWidth: 'auto',
px: 2,
borderRadius: 2,
background: 'linear-gradient(45deg, #009688 30%, #00796B 90%)',
}}
>
<Send fontSize="small" />
</Button>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
{t('enterToSend')}
</Typography>
</Box>
)}
</Box>
</Box>
{/* Conversation Menu */}
<Menu
anchorEl={menuAnchorEl}
open={Boolean(menuAnchorEl)}
onClose={handleMenuClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem onClick={handleRenameClick}>
<Edit fontSize="small" sx={{ mr: 1 }} />
Rename
</MenuItem>
<MenuItem onClick={handleDeleteClick} sx={{ color: 'error.main' }}>
<Delete fontSize="small" sx={{ mr: 1 }} />
Delete
</MenuItem>
</Menu>
{/* Rename Dialog */}
<Dialog open={showRenameDialog} onClose={() => setShowRenameDialog(false)} maxWidth="sm" fullWidth>
<DialogTitle>Rename Conversation</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Conversation Title"
fullWidth
variant="outlined"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleRenameSubmit()
}
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowRenameDialog(false)}>Cancel</Button>
<Button
onClick={handleRenameSubmit}
variant="contained"
disabled={!newTitle.trim()}
>
Rename
</Button>
</DialogActions>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteDialog} onClose={() => setShowDeleteDialog(false)}>
<DialogTitle>Delete Conversation</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete this conversation? This action cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowDeleteDialog(false)}>Cancel</Button>
<Button
onClick={handleDeleteConfirm}
variant="contained"
color="error"
>
Delete
</Button>
</DialogActions>
</Dialog>
</>
)}
</Paper>
</Slide>
{/* Auth Modal */}
<AuthModal
open={authModalOpen}
onClose={() => 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"
/>
</>
)
}