207 lines
6.2 KiB
TypeScript
Raw Normal View History

2025-02-04 01:30:36 -05:00
import React, { useEffect, useRef } from 'react';
import { Box, Typography, Paper, Avatar } from '@mui/material';
import type { DocumentMetadata } from '../../../electron/types';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css';
interface ChatMessage {
id: string;
text: string;
isUser: boolean;
timestamp: string;
sources?: DocumentMetadata[];
avatar?: string;
}
interface MessageListProps {
messages: ChatMessage[];
}
export default function MessageList({ messages }: MessageListProps) {
const messageListRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (messageListRef.current) {
messageListRef.current.scrollTop = messageListRef.current.scrollHeight;
}
}, [messages]);
return (
<Box
ref={messageListRef}
sx={{
flex: 1,
overflow: 'auto',
p: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
scrollBehavior: 'smooth',
'&::-webkit-scrollbar': {
width: '8px',
background: 'transparent',
},
'&::-webkit-scrollbar-thumb': {
background: (theme) => theme.palette.divider,
borderRadius: '4px',
'&:hover': {
background: (theme) => theme.palette.action.hover,
},
},
'&::-webkit-scrollbar-track': {
background: 'transparent',
},
}}>
{messages.map((message) => (
<Box
key={message.id}
sx={{
display: 'flex',
flexDirection: message.isUser ? 'row-reverse' : 'row',
alignItems: 'flex-start',
gap: 2,
maxWidth: '80%',
alignSelf: message.isUser ? 'flex-end' : 'flex-start',
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
<Avatar
src={message.isUser ? '/profiles/user-profile.webp' : '/profiles/ai-profile.webp'}
alt={message.isUser ? 'User' : 'AI'}
variant="square"
sx={{
width: 40,
height: 40,
boxShadow: 1
}}
/>
<Typography
variant="caption"
sx={{
fontSize: '0.75rem',
color: 'text.secondary',
fontWeight: 500
}}
>
{message.isUser ? 'User' : 'Data Hound'}
</Typography>
</Box>
<Box
component={Paper}
elevation={1}
sx={{
p: 2,
flex: 1,
bgcolor: message.isUser ? 'primary.main' : 'background.paper',
color: message.isUser ? 'primary.contrastText' : 'text.primary',
transition: 'all 0.2s ease-in-out',
boxShadow: 1,
borderRadius: 2,
'&:hover': {
boxShadow: 2,
},
}}
>
<Box sx={{
'& .markdown-body': {
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
minHeight: '1.5em',
lineHeight: 1.6,
'& pre': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: 2,
borderRadius: 1,
overflow: 'auto'
},
'& code': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: '0.2em 0.4em',
borderRadius: 1,
fontSize: '85%'
},
'& h1, & h2, & h3, & h4, & h5, & h6': {
marginTop: '24px',
marginBottom: '16px',
fontWeight: 600,
lineHeight: 1.25
},
'& p': {
marginTop: '0',
marginBottom: '16px'
},
'& a': {
color: (theme) => theme.palette.primary.main,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline'
}
},
'& img': {
maxWidth: '100%',
height: 'auto'
},
'& blockquote': {
padding: '0 1em',
color: (theme) => theme.palette.text.secondary,
borderLeft: (theme) => `0.25em solid ${theme.palette.divider}`,
margin: '0 0 16px 0'
},
'& ul, & ol': {
paddingLeft: '2em',
marginBottom: '16px'
}
}
}}>
<ReactMarkdown
className="markdown-body"
rehypePlugins={[rehypeHighlight]}
>
{message.text}
</ReactMarkdown>
</Box>
{message.sources && message.sources.length > 0 && (
<Box sx={{ mt: 2, pt: 1, borderTop: '1px solid', borderColor: 'divider' }}>
<Typography
variant="caption"
color="text.secondary"
sx={{
display: 'block',
mb: 0.5,
fontWeight: 'medium'
}}
>
Sources:
</Typography>
{message.sources.map((source, index) => (
<Typography
key={index}
variant="caption"
component="div"
color="text.secondary"
sx={{
pl: 1,
borderLeft: '2px solid',
borderColor: 'divider'
}}
>
{source.path}
</Typography>
))}
</Box>
)}
<Typography
variant="caption"
color={message.isUser ? '#ffffff' : 'text.secondary'}
sx={{ display: 'block', mt: 0.5 }}
>
{new Date(message.timestamp).toLocaleTimeString()}
</Typography>
</Box>
</Box>
))}
</Box>
);
}