BizInsights-Frontend / src /components /ReportRenderer.tsx
pranav8tripathi@gmail.com
changed report looks
9b124fd
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;