228 lines
6.3 KiB
TypeScript
228 lines
6.3 KiB
TypeScript
import React from 'react';
|
|
import { Box } from '@mui/material';
|
|
import { useTheme } from '@mui/material/styles';
|
|
import { SxProps, Theme } from '@mui/material';
|
|
import Accordion from '@mui/material/Accordion';
|
|
import AccordionSummary from '@mui/material/AccordionSummary';
|
|
import AccordionDetails from '@mui/material/AccordionDetails';
|
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
import LocationSearchingIcon from '@mui/icons-material/LocationSearching';
|
|
|
|
import { MessageRoles } from './Message';
|
|
import { ErrorOutline, InfoOutline, Memory, Psychology, /* Stream, */ } from '@mui/icons-material';
|
|
|
|
interface ChatBubbleProps {
|
|
role: MessageRoles,
|
|
isInfo?: boolean;
|
|
children: React.ReactNode;
|
|
sx?: SxProps<Theme>;
|
|
className?: string;
|
|
title?: string;
|
|
expanded?: boolean;
|
|
expandable?: boolean;
|
|
onExpand?: (open: boolean) => void;
|
|
}
|
|
|
|
function ChatBubble(props: ChatBubbleProps) {
|
|
const { role, children, sx, className, title, onExpand, expandable, expanded } = props;
|
|
|
|
const theme = useTheme();
|
|
|
|
const defaultRadius = '16px';
|
|
const defaultStyle = {
|
|
padding: theme.spacing(1, 2),
|
|
fontSize: '0.875rem',
|
|
alignSelf: 'flex-start',
|
|
maxWidth: '100%',
|
|
minWidth: '100%',
|
|
height: 'fit-content',
|
|
'& > *': {
|
|
color: 'inherit',
|
|
overflow: 'hidden',
|
|
m: 0,
|
|
},
|
|
'& > :last-child': {
|
|
mb: 0,
|
|
m: 0,
|
|
p: 0,
|
|
},
|
|
};
|
|
|
|
const styles: any = {
|
|
assistant: {
|
|
...defaultStyle,
|
|
backgroundColor: theme.palette.primary.main,
|
|
border: `1px solid ${theme.palette.secondary.main}`,
|
|
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`,
|
|
color: theme.palette.primary.contrastText,
|
|
},
|
|
content: {
|
|
...defaultStyle,
|
|
backgroundColor: '#F5F2EA',
|
|
border: `1px solid ${theme.palette.custom.highlight}`,
|
|
borderRadius: 0,
|
|
alignSelf: 'center',
|
|
color: theme.palette.text.primary,
|
|
padding: '8px 8px',
|
|
marginBottom: '0px',
|
|
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)',
|
|
fontSize: '0.9rem',
|
|
lineHeight: '1.3',
|
|
fontFamily: theme.typography.fontFamily,
|
|
},
|
|
error: {
|
|
...defaultStyle,
|
|
backgroundColor: '#F8E7E7',
|
|
border: `1px solid #D83A3A`,
|
|
borderRadius: defaultRadius,
|
|
maxWidth: '90%',
|
|
minWidth: '90%',
|
|
alignSelf: 'center',
|
|
color: '#8B2525',
|
|
padding: '10px 16px',
|
|
boxShadow: '0 1px 3px rgba(216, 58, 58, 0.15)',
|
|
},
|
|
'fact-check': 'qualifications',
|
|
'job-description': 'content',
|
|
'job-requirements': 'qualifications',
|
|
info: {
|
|
...defaultStyle,
|
|
backgroundColor: '#BFD8D8',
|
|
border: `1px solid ${theme.palette.secondary.main}`,
|
|
borderRadius: defaultRadius,
|
|
color: theme.palette.text.primary,
|
|
opacity: 0.95,
|
|
},
|
|
processing: 'status',
|
|
qualifications: {
|
|
...defaultStyle,
|
|
backgroundColor: theme.palette.primary.light,
|
|
border: `1px solid ${theme.palette.secondary.main}`,
|
|
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`,
|
|
color: theme.palette.primary.contrastText,
|
|
},
|
|
resume: 'content',
|
|
searching: 'status',
|
|
status: {
|
|
...defaultStyle,
|
|
backgroundColor: 'rgba(74, 122, 125, 0.15)',
|
|
border: `1px solid ${theme.palette.secondary.light}`,
|
|
borderRadius: '4px',
|
|
maxWidth: '75%',
|
|
minWidth: '75%',
|
|
alignSelf: 'center',
|
|
color: theme.palette.secondary.dark,
|
|
fontWeight: 500,
|
|
fontSize: '0.95rem',
|
|
padding: '8px 12px',
|
|
opacity: 0.9,
|
|
transition: 'opacity 0.3s ease-in-out',
|
|
},
|
|
streaming: 'assistant',
|
|
system: {
|
|
...defaultStyle,
|
|
backgroundColor: '#EDEAE0',
|
|
border: `1px dashed ${theme.palette.custom.highlight}`,
|
|
borderRadius: defaultRadius,
|
|
maxWidth: '90%',
|
|
minWidth: '90%',
|
|
alignSelf: 'center',
|
|
color: theme.palette.text.primary,
|
|
fontStyle: 'italic',
|
|
},
|
|
thinking: 'status',
|
|
user: {
|
|
...defaultStyle,
|
|
backgroundColor: theme.palette.background.default,
|
|
border: `1px solid ${theme.palette.custom.highlight}`,
|
|
borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`,
|
|
alignSelf: 'flex-end',
|
|
color: theme.palette.primary.main,
|
|
},
|
|
};
|
|
|
|
// Resolve string references in styles
|
|
for (const [key, value] of Object.entries(styles)) {
|
|
if (typeof value === 'string') {
|
|
styles[key] = styles[value];
|
|
}
|
|
}
|
|
|
|
const icons: any = {
|
|
error: <ErrorOutline color="error" />,
|
|
info: <InfoOutline color="info" />,
|
|
processing: <LocationSearchingIcon />,
|
|
searching: <Memory />,
|
|
thinking: <Psychology />,
|
|
tooling: <LocationSearchingIcon />,
|
|
};
|
|
|
|
// Render Accordion for expandable content
|
|
if (expandable || title) {
|
|
// Determine if Accordion is controlled
|
|
const isControlled = typeof expanded === 'boolean' && typeof onExpand === 'function';
|
|
return (
|
|
<Accordion
|
|
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled
|
|
defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion
|
|
className={className}
|
|
onChange={(_event, newExpanded) => {
|
|
if (isControlled && onExpand) {
|
|
onExpand(newExpanded); // Call onExpand with new state
|
|
}
|
|
}}
|
|
sx={{ ...styles[role], ...sx }}
|
|
>
|
|
<AccordionSummary
|
|
expandIcon={<ExpandMoreIcon />}
|
|
slotProps={{
|
|
content: {
|
|
sx: {
|
|
fontWeight: 'bold',
|
|
fontSize: '1.1rem',
|
|
m: 0,
|
|
p: 0,
|
|
display: 'flex',
|
|
justifyItems: 'center',
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
{title || ''}
|
|
</AccordionSummary>
|
|
<AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}>
|
|
{children}
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
);
|
|
}
|
|
|
|
// Render non-expandable content
|
|
return (
|
|
<Box
|
|
className={className}
|
|
sx={{
|
|
...(role in styles ? styles[role] : styles['status']),
|
|
gap: 1,
|
|
display: 'flex',
|
|
...sx,
|
|
flexDirection: 'row',
|
|
}}
|
|
>
|
|
{icons[role] !== undefined && icons[role]}
|
|
<Box sx={{ p: 0, m: 0, gap: 0, display: 'flex', flexGrow: 1, flexDirection: 'column' }}>
|
|
{children}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export type {
|
|
ChatBubbleProps
|
|
};
|
|
|
|
export {
|
|
ChatBubble
|
|
};
|
|
|