Major updates: - Replace homepage with clean, minimalist Apple-style landing page - Focus on donation messaging and mission statement - Add comprehensive AI chat analysis documentation - Fix Azure OpenAI configuration with correct endpoints - Update embedding API to use text-embedding-ada-002 (1536 dims) Landing Page Features: - Hero section with tagline "Every Scripture. Every Language. Forever Free" - Mission statement emphasizing free access - Matthew 10:8 verse highlight - 6 feature cards (Global Library, Multilingual, Prayer Wall, AI Chat, Privacy, Offline) - Donation CTA sections with PayPal and card options - "Why It Matters" section with dark background - Clean footer with navigation links Technical Changes: - Updated .env.local with new Azure credentials - Fixed vector-search.ts to support separate embed API version - Integrated AuthModal into Bible reader and prayers page - Made prayer filters collapsible and mobile-responsive - Changed language picker to single-select Documentation Created: - AI_CHAT_FIX_PLAN.md - Comprehensive implementation plan - AI_CHAT_VERIFICATION_FINDINGS.md - Database analysis - AI_CHAT_ANALYSIS_SUMMARY.md - Executive summary - AI_CHAT_STATUS_UPDATE.md - Current status and next steps - logo.svg - App logo (MenuBook icon) Build: ✅ Successful (Next.js 15.5.3) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1033 lines
37 KiB
TypeScript
1033 lines
37 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'
|
|
|
|
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)
|
|
|
|
// 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('')
|
|
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 }}>
|
|
<Typography variant="body2">
|
|
{t('loading')}
|
|
</Typography>
|
|
</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"
|
|
/>
|
|
</>
|
|
)
|
|
}
|