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:
andupetcu
2025-09-20 19:03:40 +03:00
parent 3f2407b704
commit 8b26d72c1c
3 changed files with 33 additions and 345 deletions

View File

@@ -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>
)
}

View File

@@ -39,7 +39,7 @@ export default function Home() {
title: t('features.chat.title'),
description: t('features.chat.description'),
icon: <Chat sx={{ fontSize: 40, color: 'secondary.main' }} />,
path: '/chat',
path: '/__open-chat__',
color: theme.palette.secondary.main,
},
{
@@ -106,7 +106,9 @@ export default function Home() {
},
}}
startIcon={<Chat />}
onClick={() => router.push(`/${locale}/chat`)}
onClick={() => {
window.dispatchEvent(new CustomEvent('floating-chat:open', { detail: { fullscreen: true } }))
}}
>
{t('hero.cta.askAI')}
</Button>
@@ -152,7 +154,13 @@ export default function Home() {
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={{
flexGrow: 1,
@@ -238,4 +246,4 @@ export default function Home() {
</Container>
</Box>
)
}
}