449 lines
14 KiB
TypeScript
Raw Normal View History

2025-02-04 13:39:50 -05:00
import React, { useEffect, useRef, useState, useMemo } from 'react';
2025-02-05 10:05:22 -05:00
import { Box, Typography, Paper, Avatar, IconButton, List, ListItem, ListItemIcon, ListItemText, useTheme } from '@mui/material';
2025-02-04 13:39:50 -05:00
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
2025-02-04 01:30:36 -05:00
import type { DocumentMetadata } from '../../../electron/types';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css';
2025-02-05 10:05:22 -05:00
import { Keyboard, Waves, Shield, DataObject, Folder, Lock } from '@mui/icons-material';
2025-02-04 01:30:36 -05:00
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',
},
}}>
2025-02-05 10:05:22 -05:00
{messages.length>0 ? (
<>
{messages.map((message) => (
2025-02-04 01:30:36 -05:00
<Box
key={message.id}
sx={{
display: 'flex',
flexDirection: message.isUser ? 'row-reverse' : 'row',
alignItems: 'flex-start',
gap: 2,
2025-02-04 13:39:50 -05:00
maxWidth: message.isUser ? '80%' : '100%',
alignSelf: message.isUser ? 'flex-end' : 'stretch',
2025-02-04 01:30:36 -05:00
}}
>
<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
}}
>
2025-02-05 10:05:22 -05:00
{message.isUser ? 'User' : 'Data Identification Manager'}
2025-02-04 01:30:36 -05:00
</Typography>
</Box>
<Box
component={Paper}
elevation={1}
sx={{
2025-02-05 10:05:22 -05:00
px: 2.5, // Keep horizontal padding
py: message.text.includes('<think>') ? 0 : 1.5, // Reduce vertical padding
pb: 1.5,
2025-02-04 01:30:36 -05:00
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': {
2025-02-05 10:05:22 -05:00
wordBreak: 'break-word',
minHeight: '1.5em',
lineHeight: 1.6,
'& pre': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: 0,
borderRadius: 1,
overflow: 'auto'
},
'& code': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
borderRadius: 1,
fontSize: '85%'
},
'& h1, & h2, & h3, & h4, & h5, & h6': {
marginTop: '0px', // Reduced from 24px
marginBottom: '0px', // Added margin bottom for headers
fontWeight: 600,
lineHeight: 1.25
},
'& p': {
marginTop: '0',
marginBottom: '0px' // Added margin bottom for paragraphs
},
'& a': {
color: (theme) => theme.palette.primary.main,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline'
2025-02-04 01:30:36 -05:00
}
2025-02-05 10:05:22 -05:00
},
'& 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 0px 0'
},
'& ul, & ol': {
paddingLeft: '2em',
marginTop: '0px', // Added margin top
marginBottom: '0px'
},
'& li': { // Added specific list item styling
marginBottom: '0px' // Add space between list items
},
'& li:last-child': { // Remove bottom margin from last list item
marginBottom: '0'
},
'& li > p': { // Adjust paragraph spacing within list items
marginBottom: '0px'
},
'& li > p:last-child': {
marginBottom: '0'
2025-02-04 01:30:36 -05:00
}
2025-02-05 10:05:22 -05:00
}
2025-02-04 01:30:36 -05:00
}}>
2025-02-05 10:05:22 -05:00
{message.text.includes('<think>') || message.text.length==0 ? (
2025-02-04 13:39:50 -05:00
message.text.split(/<think>|<\/think>/).map((segment, index) => {
if (index % 2 === 1) { // This is a thinking section
// Calculate thinking time - assume 1 character = 0.1s
const thinkingTime = (segment.length * 0.1).toFixed(1);
return (
<div key={index} className="thinking-section">
<div
className="thinking-header"
onClick={() => {
const content = document.getElementById(`thinking-content-${message.id}-${index}`);
const indicator = document.getElementById(`thinking-indicator-${message.id}-${index}`);
if (content && indicator) {
content.classList.toggle('open');
indicator.classList.toggle('open');
}
}}
>
<div className="thinking-header-left">
<ChevronRightIcon
id={`thinking-indicator-${message.id}-${index}`}
className="indicator"
/>
<span>Reasoning</span>
</div>
2025-02-05 10:05:22 -05:00
<span className="timestamp">(thought for {thinkingTime}ms)</span>
2025-02-04 13:39:50 -05:00
</div>
<div
id={`thinking-content-${message.id}-${index}`}
className="thinking-content"
2025-02-05 10:05:22 -05:00
>
<ReactMarkdown
key={index}
className="markdown-body"
rehypePlugins={[rehypeHighlight]}
2025-02-04 13:39:50 -05:00
>
{segment}
2025-02-05 10:05:22 -05:00
</ReactMarkdown>
2025-02-04 13:39:50 -05:00
</div>
</div>
);
} else if (segment.includes('Sorry, there was an error processing your request.')) {
return (
<div key={index} className="error-message">
{segment}
</div>
);
}
return (
<ReactMarkdown
key={index}
className="markdown-body"
rehypePlugins={[rehypeHighlight]}
>
{segment}
</ReactMarkdown>
);
})
) : (
<ReactMarkdown
className="markdown-body"
rehypePlugins={[rehypeHighlight]}
>
{message.text}
</ReactMarkdown>
)}
2025-02-04 01:30:36 -05:00
</Box>
{message.sources && message.sources.length > 0 && (
2025-02-04 13:39:50 -05:00
<Box sx={{ mt: 1, pt: 0, borderTop: '1px solid', borderColor: 'divider' }}>
2025-02-04 01:30:36 -05:00
<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>
)}
</Box>
</Box>
))}
2025-02-05 10:05:22 -05:00
</>
):(
<>
<WelcomeMessage/>
</>
)}
2025-02-04 01:30:36 -05:00
</Box>
);
}
2025-02-05 10:05:22 -05:00
const WelcomeMessage = () => {
const [text, setText] = useState('');
const fullText = 'Send a message...';
const [showCursor, setShowCursor] = useState(true);
const theme = useTheme();
useEffect(() => {
if (text.length < fullText.length) {
const timeout = setTimeout(() => {
setText(fullText.slice(0, text.length + 1));
}, 100);
return () => clearTimeout(timeout);
}
}, [text]);
useEffect(() => {
const cursorInterval = setInterval(() => {
setShowCursor(prev => !prev);
}, 530);
return () => clearInterval(cursorInterval);
}, []);
return (
<Paper
elevation={0}
sx={{
width: '100%',
maxWidth: '4xl',
mx: 'auto',
p: 4,
background: 'rgb(0,0,0,0)',
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3 }}>
{/* Animated Keyboard and Hands */}
<Box sx={{ position: 'relative' }}>
<Keyboard
style={{
width: 64,
height: 64,
color: theme.palette.primary.main,
animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -16,
left: '50%',
transform: 'translateX(-50%)'
}}
>
<Waves
style={{
width: 32,
height: 32,
color: theme.palette.primary.light,
animation: 'bounce 1s infinite'
}}
/>
</Box>
</Box>
{/* Typing Animation */}
<Typography
variant="h4"
sx={{
fontWeight: 600,
color: theme.palette.text.primary,
mt: 2
}}
>
{text}
<Box
component="span"
sx={{
ml: 0.5,
opacity: showCursor ? 1 : 0,
transition: 'opacity 0.3s'
}}
>
|
</Box>
</Typography>
{/* Example Questions */}
<List sx={{ width: '100%', mt: 4 }}>
<ListItem
component={Paper}
elevation={1}
sx={{
mb: 2,
borderRadius: 1,
transition: 'box-shadow 0.3s',
'&:hover': { boxShadow: 3 }
}}
>
<ListItemIcon>
<Folder style={{ color: theme.palette.primary.main }} />
</ListItemIcon>
<ListItemText
primary="Can you analyze my CSV file and create a visualization of the trends?"
sx={{ color: theme.palette.text.primary }}
/>
</ListItem>
<ListItem
component={Paper}
elevation={1}
sx={{
mb: 2,
borderRadius: 1,
transition: 'box-shadow 0.3s',
'&:hover': { boxShadow: 3 }
}}
>
<ListItemIcon>
<Shield style={{ color: theme.palette.success.main }} />
</ListItemIcon>
<ListItemText
primary="What security measures does Data443 implement for data protection?"
sx={{ color: theme.palette.text.primary }}
/>
</ListItem>
<ListItem
component={Paper}
elevation={1}
sx={{
mb: 2,
borderRadius: 1,
transition: 'box-shadow 0.3s',
'&:hover': { boxShadow: 3 }
}}
>
<ListItemIcon>
<Lock style={{ color: theme.palette.secondary.main }} />
</ListItemIcon>
<ListItemText
primary="How can Data443's products help with GDPR compliance?"
sx={{ color: theme.palette.text.primary }}
/>
</ListItem>
</List>
{/* Footer Note */}
<Typography
variant="body2"
sx={{
color: theme.palette.text.secondary,
textAlign: 'center',
mt: 3
}}
>
Ask me anything about your data, security needs, or Data443 products
</Typography>
</Box>
</Paper>
);
};