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>
This commit is contained in:
426
components/chat/floating-chat.tsx
Normal file
426
components/chat/floating-chat.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
'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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user