Remove standalone /[locale]/chat page; enhance FloatingChat with fullscreen toggle via Launch icon; add global event to open chat (optionally fullscreen); wire home buttons/cards to open modal instead of routing.
This commit is contained in:
@@ -1,333 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import {
|
|
||||||
Container,
|
|
||||||
Grid,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Typography,
|
|
||||||
Box,
|
|
||||||
TextField,
|
|
||||||
Button,
|
|
||||||
Paper,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
Avatar,
|
|
||||||
Chip,
|
|
||||||
IconButton,
|
|
||||||
Divider,
|
|
||||||
useTheme,
|
|
||||||
CircularProgress,
|
|
||||||
Skeleton,
|
|
||||||
} from '@mui/material'
|
|
||||||
import {
|
|
||||||
Chat,
|
|
||||||
Send,
|
|
||||||
Person,
|
|
||||||
SmartToy,
|
|
||||||
ContentCopy,
|
|
||||||
ThumbUp,
|
|
||||||
ThumbDown,
|
|
||||||
Refresh,
|
|
||||||
} from '@mui/icons-material'
|
|
||||||
import { useState, useRef, useEffect } from 'react'
|
|
||||||
import { useTranslations, useLocale } from 'next-intl'
|
|
||||||
|
|
||||||
interface ChatMessage {
|
|
||||||
id: string
|
|
||||||
role: 'user' | 'assistant'
|
|
||||||
content: string
|
|
||||||
timestamp: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ChatPage() {
|
|
||||||
const theme = useTheme()
|
|
||||||
const t = useTranslations('chat')
|
|
||||||
const locale = useLocale()
|
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: t('subtitle'),
|
|
||||||
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,
|
|
||||||
locale,
|
|
||||||
history: messages.slice(-5).map(m => ({
|
|
||||||
id: m.id,
|
|
||||||
role: m.role,
|
|
||||||
content: m.content,
|
|
||||||
timestamp: m.timestamp.toISOString(),
|
|
||||||
})),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
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 || t('error') || 'Error',
|
|
||||||
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: t('error') || 'Error',
|
|
||||||
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?',
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
|
|
||||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
|
||||||
{/* Header */}
|
|
||||||
<Box sx={{ mb: 4, textAlign: 'center' }}>
|
|
||||||
<Typography variant="h3" component="h1" gutterBottom>
|
|
||||||
<Chat sx={{ fontSize: 40, mr: 2, verticalAlign: 'middle' }} />
|
|
||||||
{t('title')}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
{t('subtitle')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Grid container spacing={4}>
|
|
||||||
{/* Suggested Questions Sidebar */}
|
|
||||||
<Grid item xs={12} md={3}>
|
|
||||||
<Card>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
{t('suggestions.title')}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
||||||
{t('suggestions.title')}
|
|
||||||
</Typography>
|
|
||||||
{ (t.raw('suggestions.questions') as string[]).map((question, index) => (
|
|
||||||
<Chip
|
|
||||||
key={index}
|
|
||||||
label={question}
|
|
||||||
onClick={() => setInputMessage(question)}
|
|
||||||
sx={{
|
|
||||||
mb: 1,
|
|
||||||
mr: 1,
|
|
||||||
cursor: 'pointer',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: 'primary.light',
|
|
||||||
color: 'white',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Divider sx={{ my: 2 }} />
|
|
||||||
|
|
||||||
{/* Tips section can be localized later via messages */}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Main Chat Area */}
|
|
||||||
<Grid item xs={12} md={9}>
|
|
||||||
<Card sx={{ height: '70vh', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
{/* Chat Messages */}
|
|
||||||
<Box sx={{ flexGrow: 1, overflow: 'auto', p: 2 }}>
|
|
||||||
{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: '80%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
sx={{
|
|
||||||
bgcolor: message.role === 'user' ? 'primary.main' : 'secondary.main',
|
|
||||||
mx: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{message.role === 'user' ? <Person /> : <SmartToy />}
|
|
||||||
</Avatar>
|
|
||||||
|
|
||||||
<Paper
|
|
||||||
elevation={1}
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
bgcolor: message.role === 'user' ? 'primary.light' : 'background.paper',
|
|
||||||
color: message.role === 'user' ? 'white' : 'text.primary',
|
|
||||||
borderRadius: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" sx={{ whiteSpace: 'pre-wrap' }}>
|
|
||||||
{message.content}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{message.role === 'assistant' && (
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, mt: 1, justifyContent: 'flex-end' }}>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() => copyToClipboard(message.content)}
|
|
||||||
title="Copiază răspunsul"
|
|
||||||
>
|
|
||||||
<ContentCopy fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton size="small" title="Răspuns util">
|
|
||||||
<ThumbUp fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton size="small" title="Răspuns neutil">
|
|
||||||
<ThumbDown fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
display: 'block',
|
|
||||||
textAlign: 'right',
|
|
||||||
mt: 1,
|
|
||||||
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' }}>
|
|
||||||
<Avatar sx={{ bgcolor: 'secondary.main', mx: 1 }}>
|
|
||||||
<SmartToy />
|
|
||||||
</Avatar>
|
|
||||||
<Paper elevation={1} sx={{ p: 2, borderRadius: 2 }}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<CircularProgress size={16} />
|
|
||||||
<Typography variant="body1">
|
|
||||||
{t('loading')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Message Input */}
|
|
||||||
<Box sx={{ p: 2, borderTop: 1, borderColor: 'divider' }}>
|
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
multiline
|
|
||||||
maxRows={4}
|
|
||||||
placeholder={t('placeholder')}
|
|
||||||
value={inputMessage}
|
|
||||||
onChange={(e) => setInputMessage(e.target.value)}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
disabled={isLoading}
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleSendMessage}
|
|
||||||
disabled={!inputMessage.trim() || isLoading}
|
|
||||||
sx={{ minWidth: 'auto', px: 2 }}
|
|
||||||
>
|
|
||||||
<Send />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
|
||||||
{t('enterToSend')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,7 @@ export default function Home() {
|
|||||||
title: t('features.chat.title'),
|
title: t('features.chat.title'),
|
||||||
description: t('features.chat.description'),
|
description: t('features.chat.description'),
|
||||||
icon: <Chat sx={{ fontSize: 40, color: 'secondary.main' }} />,
|
icon: <Chat sx={{ fontSize: 40, color: 'secondary.main' }} />,
|
||||||
path: '/chat',
|
path: '/__open-chat__',
|
||||||
color: theme.palette.secondary.main,
|
color: theme.palette.secondary.main,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -106,7 +106,9 @@ export default function Home() {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
startIcon={<Chat />}
|
startIcon={<Chat />}
|
||||||
onClick={() => router.push(`/${locale}/chat`)}
|
onClick={() => {
|
||||||
|
window.dispatchEvent(new CustomEvent('floating-chat:open', { detail: { fullscreen: true } }))
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('hero.cta.askAI')}
|
{t('hero.cta.askAI')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -152,7 +154,13 @@ export default function Home() {
|
|||||||
boxShadow: 4,
|
boxShadow: 4,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onClick={() => router.push(`/${locale}${feature.path}`)}
|
onClick={() => {
|
||||||
|
if (feature.path === '/__open-chat__') {
|
||||||
|
window.dispatchEvent(new CustomEvent('floating-chat:open', { detail: { fullscreen: true } }))
|
||||||
|
} else {
|
||||||
|
router.push(`/${locale}${feature.path}`)
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{
|
<CardContent sx={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function FloatingChat() {
|
|||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [isMinimized, setIsMinimized] = useState(false)
|
const [isMinimized, setIsMinimized] = useState(false)
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -69,6 +70,18 @@ export default function FloatingChat() {
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
|
// Allow external triggers to open the chat (optionally fullscreen)
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: Event) => {
|
||||||
|
const detail = (e as CustomEvent).detail || {}
|
||||||
|
setIsOpen(true)
|
||||||
|
setIsMinimized(false)
|
||||||
|
if (typeof detail.fullscreen === 'boolean') setIsFullscreen(detail.fullscreen)
|
||||||
|
}
|
||||||
|
window.addEventListener('floating-chat:open', handler as EventListener)
|
||||||
|
return () => window.removeEventListener('floating-chat:open', handler as EventListener)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async () => {
|
||||||
if (!inputMessage.trim() || isLoading) return
|
if (!inputMessage.trim() || isLoading) return
|
||||||
|
|
||||||
@@ -151,9 +164,7 @@ export default function FloatingChat() {
|
|||||||
setIsMinimized(!isMinimized)
|
setIsMinimized(!isMinimized)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openFullChat = () => {
|
const toggleFullscreen = () => setIsFullscreen(prev => !prev)
|
||||||
window.open('/chat', '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -185,10 +196,12 @@ export default function FloatingChat() {
|
|||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
width: { xs: '100vw', sm: '50vw', md: '40vw' },
|
top: isFullscreen ? 0 : 'auto',
|
||||||
|
left: isFullscreen ? 0 : 'auto',
|
||||||
|
width: isFullscreen ? '100vw' : { xs: '100vw', sm: '50vw', md: '40vw' },
|
||||||
height: isMinimized ? 'auto' : '100vh',
|
height: isMinimized ? 'auto' : '100vh',
|
||||||
zIndex: 1200,
|
zIndex: 1200,
|
||||||
borderRadius: { xs: 0, sm: '12px 0 0 0' },
|
borderRadius: isFullscreen ? 0 : { xs: 0, sm: '12px 0 0 0' },
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -229,7 +242,7 @@ export default function FloatingChat() {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={openFullChat}
|
onClick={toggleFullscreen}
|
||||||
sx={{ color: 'white', mr: 0.5 }}
|
sx={{ color: 'white', mr: 0.5 }}
|
||||||
>
|
>
|
||||||
<Launch />
|
<Launch />
|
||||||
@@ -354,7 +367,7 @@ export default function FloatingChat() {
|
|||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{message.timestamp.toLocaleTimeString('ro-RO', {
|
{message.timestamp.toLocaleTimeString(locale === 'en' ? 'en-US' : 'ro-RO', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user