Files
biblical-guide.com/components/chat/floating-chat.tsx
andupetcu dd5e1102eb Implement Azure OpenAI vector embeddings for Romanian Bible
- Add pgvector support with bible_passages table for vector search
- Create Python ingestion script for Azure OpenAI embed-3 embeddings
- Implement hybrid search combining vector similarity and full-text search
- Update AI chat to use vector search with Azure OpenAI gpt-4o
- Add floating chat component with Material UI design
- Import complete Romanian Bible (FIDELA) with 30K+ verses
- Add vector search library for semantic Bible search
- Create multi-language implementation plan for future expansion

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 15:18:00 +03:00

426 lines
13 KiB
TypeScript

'use client'
import {
Fab,
Drawer,
Box,
Typography,
TextField,
Button,
Paper,
Avatar,
Chip,
IconButton,
Divider,
List,
ListItem,
ListItemText,
useTheme,
Slide,
Grow,
Zoom,
} from '@mui/material'
import {
Chat,
Send,
Close,
SmartToy,
Person,
ContentCopy,
ThumbUp,
ThumbDown,
Minimize,
Launch,
} from '@mui/icons-material'
import { useState, useRef, useEffect } from 'react'
interface ChatMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: Date
}
export default function FloatingChat() {
const theme = useTheme()
const [isOpen, setIsOpen] = useState(false)
const [isMinimized, setIsMinimized] = useState(false)
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
role: 'assistant',
content: 'Bună ziua! Sunt asistentul tău AI pentru întrebări biblice. Cum te pot ajuta astăzi să înțelegi mai bine Scriptura?',
timestamp: new Date(),
}
])
const [inputMessage, setInputMessage] = useState('')
const [isLoading, setIsLoading] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
useEffect(() => {
scrollToBottom()
}, [messages])
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 response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: inputMessage,
history: messages.slice(-5),
}),
})
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 || 'Îmi pare rău, nu am putut procesa întrebarea ta. Te rog încearcă din nou.',
timestamp: new Date(),
}
setMessages(prev => [...prev, assistantMessage])
} catch (error) {
console.error('Error sending message:', error)
const errorMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: 'Îmi pare rău, a apărut o eroare. Te rog verifică conexiunea și încearcă din nou.',
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)
}
const suggestedQuestions = [
'Ce spune Biblia despre iubire?',
'Explică-mi parabola semănătorului',
'Care sunt fructele Duhului?',
'Ce înseamnă să fii născut din nou?',
'Cum pot să mă rog mai bine?',
]
const toggleChat = () => {
setIsOpen(!isOpen)
if (isMinimized) setIsMinimized(false)
}
const minimizeChat = () => {
setIsMinimized(!isMinimized)
}
const openFullChat = () => {
window.open('/chat', '_blank')
}
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, #2C5F6B 30%, #8B7355 90%)',
'&:hover': {
background: 'linear-gradient(45deg, #1e4148 30%, #6d5a43 90%)',
}
}}
>
<Chat />
</Fab>
</Zoom>
{/* Chat Overlay */}
<Slide direction="up" in={isOpen} mountOnExit>
<Paper
elevation={8}
sx={{
position: 'fixed',
bottom: 0,
right: 0,
width: { xs: '100vw', sm: '50vw', md: '40vw' },
height: isMinimized ? 'auto' : '100vh',
zIndex: 1200,
borderRadius: { 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, #2C5F6B 30%, #8B7355 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">
Chat AI Biblic
</Typography>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
Asistent pentru întrebări biblice
</Typography>
</Box>
</Box>
<Box>
<IconButton
size="small"
onClick={minimizeChat}
sx={{ color: 'white', mr: 0.5 }}
>
<Minimize />
</IconButton>
<IconButton
size="small"
onClick={openFullChat}
sx={{ color: 'white', mr: 0.5 }}
>
<Launch />
</IconButton>
<IconButton
size="small"
onClick={toggleChat}
sx={{ color: 'white' }}
>
<Close />
</IconButton>
</Box>
</Box>
{!isMinimized && (
<>
{/* Suggested Questions */}
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Întrebări sugerate:
</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,
}}
>
{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%',
}}
>
<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('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">
Scriu răspunsul...
</Typography>
</Paper>
</Box>
</Box>
)}
<div ref={messagesEndRef} />
</Box>
<Divider />
{/* Input */}
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<TextField
fullWidth
size="small"
multiline
maxRows={3}
placeholder="Scrie întrebarea ta despre Biblie..."
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, #2C5F6B 30%, #8B7355 90%)',
}}
>
<Send fontSize="small" />
</Button>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
Enter pentru a trimite Shift+Enter pentru linie nouă
</Typography>
</Box>
</>
)}
</Paper>
</Slide>
</>
)
}