Spaces:
Sleeping
Sleeping
| import React from 'react'; | |
| import { | |
| Box, | |
| Typography, | |
| Card, | |
| CardContent, | |
| Chip, | |
| List, | |
| ListItem, | |
| ListItemText, | |
| Link, | |
| Table, | |
| TableBody, | |
| TableCell, | |
| TableContainer, | |
| TableHead, | |
| TableRow, | |
| IconButton, | |
| Collapse, | |
| } from '@mui/material'; | |
| import { | |
| ExpandMore, | |
| ExpandLess, | |
| Code as CodeIcon, | |
| Link as LinkIcon, | |
| TableChart as TableIcon, | |
| } from '@mui/icons-material'; | |
| type SectionType = | |
| | 'header' | |
| | 'subheader' | |
| | 'text' | |
| | 'list' | |
| | 'code' | |
| | 'table' | |
| | 'link' | |
| | 'metric' | |
| | 'highlight'; | |
| interface Section { | |
| type: SectionType; | |
| content?: string; | |
| level?: number; | |
| items?: string[]; | |
| language?: string; | |
| headers?: string[]; | |
| rows?: string[][]; | |
| url?: string; | |
| title?: string; | |
| } | |
| interface ResponseRendererProps { | |
| content: string; | |
| isBotMessage?: boolean; | |
| } | |
| const ResponseRenderer: React.FC<ResponseRendererProps> = ({ content, isBotMessage = true }) => { | |
| const [expandedSections, setExpandedSections] = React.useState<{ [key: string]: boolean }>({}); | |
| const toggleSection = (sectionId: string) => { | |
| setExpandedSections((prev) => ({ | |
| ...prev, | |
| [sectionId]: !prev[sectionId], | |
| })); | |
| }; | |
| // Parse different content types | |
| const parseContent = (text: string): Section[] => { | |
| const sections: Section[] = []; | |
| const lines = text.split('\n'); | |
| for (let i = 0; i < lines.length; i++) { | |
| const line = lines[i].trim(); | |
| if (!line) { | |
| sections.push({ type: 'text', content: '' }); | |
| continue; | |
| } | |
| if (line.startsWith('# ')) { | |
| sections.push({ type: 'header', content: line.substring(2), level: 1 }); | |
| } else if (line.startsWith('## ')) { | |
| sections.push({ type: 'subheader', content: line.substring(3), level: 2 }); | |
| } else if (line.startsWith('### ')) { | |
| sections.push({ type: 'subheader', content: line.substring(4), level: 3 }); | |
| } else if (line.startsWith('**') && line.endsWith('**')) { | |
| sections.push({ type: 'highlight', content: line.substring(2, line.length - 2) }); | |
| } else if (line.startsWith('• ') || line.startsWith('- ') || line.startsWith('* ')) { | |
| const listItems = [line.substring(2)]; | |
| let j = i + 1; | |
| while ( | |
| j < lines.length && | |
| (lines[j].trim().startsWith('• ') || | |
| lines[j].trim().startsWith('- ') || | |
| lines[j].trim().startsWith('* ')) | |
| ) { | |
| listItems.push(lines[j].trim().substring(2)); | |
| j++; | |
| } | |
| i = j - 1; | |
| sections.push({ type: 'list', content: '', items: listItems }); | |
| } else if (line.startsWith('```')) { | |
| const language = line.substring(3).trim(); | |
| let codeContent = ''; | |
| let j = i + 1; | |
| while (j < lines.length && !lines[j].trim().startsWith('```')) { | |
| codeContent += lines[j] + '\n'; | |
| j++; | |
| } | |
| i = j; | |
| sections.push({ type: 'code', content: codeContent.trim(), language }); | |
| } else if (line.includes('|') && lines[i + 1]?.includes('|') && lines[i + 1]?.includes('-')) { | |
| const headers = line | |
| .split('|') | |
| .map((h) => h.trim()) | |
| .filter((h) => h); | |
| let j = i + 2; | |
| const rows: string[][] = []; | |
| while (j < lines.length && lines[j].trim().includes('|')) { | |
| const row = lines[j] | |
| .split('|') | |
| .map((cell) => cell.trim()) | |
| .filter((cell) => cell); | |
| if (row.length > 0) rows.push(row); | |
| j++; | |
| } | |
| i = j - 1; | |
| sections.push({ type: 'table', content: '', headers, rows }); | |
| } else if (line.match(/https?:\/\/[^\s]+/)) { | |
| const urlMatch = line.match(/(https?:\/\/[^\s]+)/); | |
| if (urlMatch) { | |
| const url = urlMatch[1]; | |
| const title = line.replace(url, '').trim(); | |
| sections.push({ type: 'link', content: title, url }); | |
| } else { | |
| sections.push({ type: 'text', content: line }); | |
| } | |
| } else if (line.includes('•') && line.includes(':')) { | |
| sections.push({ type: 'metric', content: line }); | |
| } else { | |
| sections.push({ type: 'text', content: line }); | |
| } | |
| } | |
| return sections; | |
| }; | |
| const sections = parseContent(content); | |
| const renderSection = (section: Section, index: number) => { | |
| const sectionId = `section-${index}`; | |
| switch (section.type) { | |
| case 'header': | |
| return ( | |
| <Box key={index} sx={{ mb: 3 }}> | |
| <Typography | |
| variant="h4" | |
| component="h1" | |
| sx={{ | |
| color: 'primary.main', | |
| fontWeight: 'bold', | |
| mb: 2, | |
| borderBottom: '2px solid', | |
| borderColor: 'primary.light', | |
| pb: 1, | |
| }} | |
| > | |
| {section.content} | |
| </Typography> | |
| </Box> | |
| ); | |
| case 'subheader': | |
| return ( | |
| <Box key={index} sx={{ mb: 2 }}> | |
| <Typography | |
| variant={section.level === 2 ? 'h5' : 'h6'} | |
| component="h2" | |
| sx={{ | |
| color: 'secondary.main', | |
| fontWeight: '600', | |
| mb: 1.5, | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 1, | |
| }} | |
| > | |
| <Box | |
| sx={{ | |
| width: section.level === 2 ? 4 : 3, | |
| height: section.level === 2 ? 4 : 3, | |
| bgcolor: 'primary.main', | |
| borderRadius: '50%', | |
| }} | |
| /> | |
| {section.content} | |
| </Typography> | |
| </Box> | |
| ); | |
| case 'highlight': | |
| return ( | |
| <Box key={index} sx={{ mb: 2 }}> | |
| <Chip | |
| label={section.content} | |
| color="primary" | |
| variant="filled" | |
| sx={{ | |
| fontSize: '0.9rem', | |
| fontWeight: 'bold', | |
| mb: 1, | |
| '& .MuiChip-label': { | |
| px: 2, | |
| py: 1, | |
| }, | |
| }} | |
| /> | |
| </Box> | |
| ); | |
| case 'list': | |
| return ( | |
| <Box key={index} sx={{ mb: 2, ml: 2 }}> | |
| <List dense> | |
| {section.items?.map((item, idx) => ( | |
| <ListItem key={idx} sx={{ py: 0.25, pl: 0 }}> | |
| <ListItemText | |
| primary={item} | |
| primaryTypographyProps={{ | |
| variant: 'body2', | |
| color: 'text.secondary', | |
| }} | |
| /> | |
| </ListItem> | |
| ))} | |
| </List> | |
| </Box> | |
| ); | |
| case 'code': | |
| return ( | |
| <Card key={index} variant="outlined" sx={{ mb: 2, bgcolor: 'grey.100' }}> | |
| <CardContent sx={{ p: '8px 16px !important' }}> | |
| <Box | |
| sx={{ | |
| display: 'flex', | |
| alignItems: 'center', | |
| mb: 1, | |
| color: 'text.secondary', | |
| }} | |
| > | |
| <CodeIcon fontSize="small" sx={{ mr: 1 }} /> | |
| <Typography variant="caption">{section.language || 'code'}</Typography> | |
| </Box> | |
| <pre | |
| style={{ | |
| margin: 0, | |
| padding: '8px', | |
| backgroundColor: 'rgba(0, 0, 0, 0.05)', | |
| borderRadius: '4px', | |
| overflowX: 'auto', | |
| fontSize: '0.8rem', | |
| fontFamily: 'monospace', | |
| whiteSpace: 'pre-wrap', | |
| wordBreak: 'break-word', | |
| }} | |
| > | |
| {section.content} | |
| </pre> | |
| </CardContent> | |
| </Card> | |
| ); | |
| case 'table': | |
| return ( | |
| <TableContainer | |
| key={index} | |
| component={Card} | |
| variant="outlined" | |
| sx={{ mb: 2, maxWidth: '100%', overflowX: 'auto' }} | |
| > | |
| <Table size="small" aria-label="simple table"> | |
| <TableHead> | |
| <TableRow> | |
| {section.headers?.map((header, idx) => ( | |
| <TableCell | |
| key={idx} | |
| sx={{ | |
| fontWeight: 'bold', | |
| bgcolor: 'grey.100', | |
| whiteSpace: 'nowrap', | |
| }} | |
| > | |
| {header} | |
| </TableCell> | |
| ))} | |
| </TableRow> | |
| </TableHead> | |
| <TableBody> | |
| {section.rows?.map((row, rowIdx) => ( | |
| <TableRow | |
| key={rowIdx} | |
| sx={{ | |
| '&:nth-of-type(odd)': { bgcolor: 'action.hover' }, | |
| }} | |
| > | |
| {row.map((cell, cellIdx) => ( | |
| <TableCell key={cellIdx} sx={{ whiteSpace: 'nowrap' }}> | |
| {cell} | |
| </TableCell> | |
| ))} | |
| </TableRow> | |
| ))} | |
| </TableBody> | |
| </Table> | |
| </TableContainer> | |
| ); | |
| case 'link': | |
| return ( | |
| <Box key={index} sx={{ mb: 2 }}> | |
| <Link | |
| href={section.url} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| sx={{ | |
| display: 'inline-flex', | |
| alignItems: 'center', | |
| color: 'primary.main', | |
| textDecoration: 'none', | |
| '&:hover': { textDecoration: 'underline' }, | |
| }} | |
| > | |
| <LinkIcon fontSize="small" sx={{ mr: 0.5 }} /> | |
| {section.content || section.url} | |
| </Link> | |
| </Box> | |
| ); | |
| case 'metric': | |
| return ( | |
| <Box | |
| key={index} | |
| sx={{ | |
| display: 'inline-block', | |
| bgcolor: 'primary.light', | |
| color: 'primary.contrastText', | |
| px: 1.5, | |
| py: 0.5, | |
| borderRadius: 1, | |
| mb: 1, | |
| mr: 1, | |
| }} | |
| > | |
| <Typography variant="caption" fontWeight="medium"> | |
| {section.content} | |
| </Typography> | |
| </Box> | |
| ); | |
| default: | |
| return ( | |
| <Typography | |
| key={index} | |
| variant="body1" | |
| sx={{ mb: 2, whiteSpace: 'pre-line' }} | |
| color="text.primary" | |
| > | |
| {section.content} | |
| </Typography> | |
| ); | |
| } | |
| }; | |
| return ( | |
| <Box | |
| sx={{ | |
| width: '100%', | |
| '& > *:not(:last-child)': { | |
| mb: 2, | |
| }, | |
| }} | |
| > | |
| {sections.map((section, index) => renderSection(section, index))} | |
| </Box> | |
| ); | |
| }; | |
| export default ResponseRenderer; |