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 && (
|
||||
<>
|
||||
{/* Chat History Panel */}
|
||||
{showHistory && (
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'grey.50',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">
|
||||
Chat History
|
||||
</Typography>
|
||||
<Box>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={createNewConversation}
|
||||
title="New Conversation"
|
||||
sx={{ mr: 0.5 }}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setShowHistory(false)}
|
||||
title="Close History"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
{/* Main Content Area - Side by Side Layout */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Chat History Panel - Left Side */}
|
||||
{showHistory && (
|
||||
<Box sx={{
|
||||
width: '300px',
|
||||
borderRight: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'grey.50',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">
|
||||
Chat History
|
||||
</Typography>
|
||||
<Box>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={createNewConversation}
|
||||
title="New Conversation"
|
||||
sx={{ mr: 0.5 }}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setShowHistory(false)}
|
||||
title="Close History"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 2 }}>
|
||||
|
||||
{!isAuthenticated ? (
|
||||
<Box sx={{ textAlign: 'center', py: 3 }}>
|
||||
@@ -638,8 +648,242 @@ export default function FloatingChat() {
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* Conversation Menu */}
|
||||
<Menu
|
||||
@@ -709,230 +953,6 @@ export default function FloatingChat() {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user