Restructure chat interface to display history side-by-side with live chat
- Change chat layout from vertical stacking to horizontal side-by-side layout - History panel (300px width) displays on left with conversation list - Main chat area takes remaining space on right with messages and input - Improves UX by making chat history visible alongside active conversation - Fix JSX structure and nesting for proper component rendering 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -536,38 +536,48 @@ export default function FloatingChat() {
|
|||||||
|
|
||||||
{!isMinimized && (
|
{!isMinimized && (
|
||||||
<>
|
<>
|
||||||
{/* Chat History Panel */}
|
{/* Main Content Area - Side by Side Layout */}
|
||||||
{showHistory && (
|
<Box sx={{
|
||||||
<Box sx={{
|
display: 'flex',
|
||||||
p: 2,
|
flexGrow: 1,
|
||||||
borderBottom: 1,
|
overflow: 'hidden'
|
||||||
borderColor: 'divider',
|
}}>
|
||||||
bgcolor: 'grey.50',
|
{/* Chat History Panel - Left Side */}
|
||||||
maxHeight: '300px',
|
{showHistory && (
|
||||||
overflowY: 'auto'
|
<Box sx={{
|
||||||
}}>
|
width: '300px',
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
borderRight: 1,
|
||||||
<Typography variant="h6">
|
borderColor: 'divider',
|
||||||
Chat History
|
bgcolor: 'grey.50',
|
||||||
</Typography>
|
display: 'flex',
|
||||||
<Box>
|
flexDirection: 'column',
|
||||||
<IconButton
|
}}>
|
||||||
size="small"
|
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||||
onClick={createNewConversation}
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
title="New Conversation"
|
<Typography variant="h6">
|
||||||
sx={{ mr: 0.5 }}
|
Chat History
|
||||||
>
|
</Typography>
|
||||||
<Add />
|
<Box>
|
||||||
</IconButton>
|
<IconButton
|
||||||
<IconButton
|
size="small"
|
||||||
size="small"
|
onClick={createNewConversation}
|
||||||
onClick={() => setShowHistory(false)}
|
title="New Conversation"
|
||||||
title="Close History"
|
sx={{ mr: 0.5 }}
|
||||||
>
|
>
|
||||||
<Close />
|
<Add />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => setShowHistory(false)}
|
||||||
|
title="Close History"
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
|
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 2 }}>
|
||||||
|
|
||||||
{!isAuthenticated ? (
|
{!isAuthenticated ? (
|
||||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||||
@@ -638,8 +648,242 @@ export default function FloatingChat() {
|
|||||||
))}
|
))}
|
||||||
</Box>
|
</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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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 */}
|
||||||
|
<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>
|
||||||
)}
|
</Box>
|
||||||
|
|
||||||
{/* Conversation Menu */}
|
{/* Conversation Menu */}
|
||||||
<Menu
|
<Menu
|
||||||
@@ -709,230 +953,6 @@ export default function FloatingChat() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* 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,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{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 */}
|
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
Reference in New Issue
Block a user