207 lines
6.2 KiB
TypeScript
207 lines
6.2 KiB
TypeScript
|
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>
|
||
|
);
|
||
|
}
|