199 lines
7.1 KiB
TypeScript
199 lines
7.1 KiB
TypeScript
import React, { useState } 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?: () => void;
|
|
}
|
|
|
|
function ChatBubble(props: ChatBubbleProps) {
|
|
const { role, children, sx, className, title, onExpand, expandable }: ChatBubbleProps = props;
|
|
const [expanded, setExpanded] = useState<boolean>((props.expanded === undefined) ? true : props.expanded);
|
|
|
|
const theme = useTheme();
|
|
|
|
const defaultRadius = '16px';
|
|
const defaultStyle = {
|
|
padding: theme.spacing(1, 2),
|
|
fontSize: '0.875rem',
|
|
alignSelf: 'flex-start', // Left-aligned is used by default
|
|
maxWidth: '100%',
|
|
minWidth: '100%',
|
|
height: 'fit-content',
|
|
'& > *': {
|
|
color: 'inherit', // Children inherit 'color' from parent
|
|
overflow: 'hidden',
|
|
m: 0,
|
|
},
|
|
'& > :last-child': {
|
|
mb: 0,
|
|
m: 0,
|
|
p: 0,
|
|
}
|
|
}
|
|
|
|
const styles: any = {
|
|
'assistant': {
|
|
...defaultStyle,
|
|
backgroundColor: theme.palette.primary.main, // Midnight Blue (#1A2536)
|
|
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal (#4A7A7D)
|
|
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, // Rounded, flat bottom-left for assistant
|
|
color: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for text
|
|
},
|
|
'content': {
|
|
...defaultStyle,
|
|
backgroundColor: '#F5F2EA', // Light cream background for easy reading
|
|
border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre border
|
|
borderRadius: 0,
|
|
alignSelf: 'center', // Centered in the chat
|
|
color: theme.palette.text.primary, // Charcoal Black for maximum readability
|
|
padding: '8px 8px', // More generous padding for better text framing
|
|
marginBottom: '0px', // Space between content and conversation
|
|
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)', // Subtle elevation
|
|
fontSize: '0.9rem', // Slightly smaller than default
|
|
lineHeight: '1.3', // More compact line height
|
|
fontFamily: theme.typography.fontFamily, // Consistent font with your theme
|
|
},
|
|
'error': {
|
|
...defaultStyle,
|
|
backgroundColor: '#F8E7E7', // Soft light red background
|
|
border: `1px solid #D83A3A`, // Prominent red border
|
|
borderRadius: defaultRadius,
|
|
maxWidth: '90%',
|
|
minWidth: '90%',
|
|
alignSelf: 'center',
|
|
color: '#8B2525', // Deep red text for good contrast
|
|
padding: '10px 16px',
|
|
boxShadow: '0 1px 3px rgba(216, 58, 58, 0.15)', // Subtle shadow with red tint
|
|
},
|
|
'fact-check': 'qualifications',
|
|
'job-description': 'content',
|
|
'job-requirements': 'qualifications',
|
|
'info': {
|
|
...defaultStyle,
|
|
backgroundColor: '#BFD8D8', // Softened Dusty Teal
|
|
border: `1px solid ${theme.palette.secondary.main}`, // Dusty Teal
|
|
borderRadius: defaultRadius,
|
|
color: theme.palette.text.primary, // Charcoal Black (#2E2E2E) — much better contrast
|
|
opacity: 0.95,
|
|
},
|
|
'processing': "status",
|
|
'qualifications': {
|
|
...defaultStyle,
|
|
backgroundColor: theme.palette.primary.light, // Lighter shade, e.g., Soft Blue (#2A3B56)
|
|
border: `1px solid ${theme.palette.secondary.main}`, // Keep Dusty Teal (#4A7A7D) for contrast
|
|
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, // Unchanged
|
|
color: theme.palette.primary.contrastText, // Warm Gray (#D3CDBF) for readable text
|
|
},
|
|
'resume': 'content',
|
|
'searching': 'status',
|
|
'status': {
|
|
...defaultStyle,
|
|
backgroundColor: 'rgba(74, 122, 125, 0.15)', // Translucent dusty teal
|
|
border: `1px solid ${theme.palette.secondary.light}`, // Lighter dusty teal
|
|
borderRadius: '4px',
|
|
maxWidth: '75%',
|
|
minWidth: '75%',
|
|
alignSelf: 'center',
|
|
color: theme.palette.secondary.dark, // Darker dusty teal for text
|
|
fontWeight: 500, // Slightly bolder than normal
|
|
fontSize: '0.95rem', // Slightly smaller
|
|
padding: '8px 12px',
|
|
opacity: 0.9,
|
|
transition: 'opacity 0.3s ease-in-out', // Smooth fade effect for appearing/disappearing
|
|
},
|
|
'streaming': "assistant",
|
|
'system': {
|
|
...defaultStyle,
|
|
backgroundColor: '#EDEAE0', // Soft warm gray that plays nice with #D3CDBF
|
|
border: `1px dashed ${theme.palette.custom.highlight}`, // Golden Ochre
|
|
borderRadius: defaultRadius,
|
|
maxWidth: '90%',
|
|
minWidth: '90%',
|
|
alignSelf: 'center',
|
|
color: theme.palette.text.primary, // Charcoal Black
|
|
fontStyle: 'italic',
|
|
},
|
|
'thinking': "status",
|
|
'user': {
|
|
...defaultStyle,
|
|
backgroundColor: theme.palette.background.default, // Warm Gray (#D3CDBF)
|
|
border: `1px solid ${theme.palette.custom.highlight}`, // Golden Ochre (#D4A017)
|
|
borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`, // Rounded, flat bottom-right for user
|
|
alignSelf: 'flex-end', // Right-aligned for user
|
|
color: theme.palette.primary.main, // Midnight Blue (#1A2536) for text
|
|
},
|
|
};
|
|
for (const [key, value] of Object.entries(styles)) {
|
|
if (typeof (value) === "string") {
|
|
(styles as any)[key] = styles[value];
|
|
}
|
|
}
|
|
|
|
const icons: any = {
|
|
"error": <ErrorOutline color='error' />,
|
|
"info": <InfoOutline color='info' />,
|
|
"processing": <LocationSearchingIcon />,
|
|
// "streaming": <Stream />,
|
|
"searching": <Memory />,
|
|
"thinking": <Psychology />,
|
|
"tooling": <LocationSearchingIcon />,
|
|
};
|
|
|
|
if (expandable || (role === 'content' && title)) {
|
|
return (
|
|
<Accordion
|
|
expanded={expanded}
|
|
className={className}
|
|
onChange={() => { onExpand && onExpand(); setExpanded(!expanded); }}
|
|
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>
|
|
);
|
|
}
|
|
|
|
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
|
|
};
|
|
|