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>
|
|
|
|
);
|
|
|
|
};
|