Merge pull request #965 from getmaxun/data-export

feat: revamp export for robot run data
This commit is contained in:
Karishma Shukla
2026-02-15 02:19:59 +05:30
committed by GitHub

View File

@@ -68,6 +68,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
const [screenshotKeys, setScreenshotKeys] = useState<string[]>([]);
const [screenshotKeyMap, setScreenshotKeyMap] = useState<Record<string, string>>({});
const [currentScreenshotIndex, setCurrentScreenshotIndex] = useState<number>(0);
const [currentSearchScreenshotTab, setCurrentSearchScreenshotTab] = useState<number>(0);
const [currentSchemaIndex, setCurrentSchemaIndex] = useState<number>(0);
const [legacyData, setLegacyData] = useState<any[]>([]);
@@ -517,18 +518,88 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
}, 100);
};
const downloadText = (content: string, filename: string) => {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
};
const downloadHTML = (content: string, filename: string) => {
const blob = new Blob([content], { type: 'text/html;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
};
const downloadAllCrawlsAsZip = async (crawlDataArray: any[], zipFilename: string) => {
const zip = new JSZip();
crawlDataArray.forEach((item, index) => {
for (let index = 0; index < crawlDataArray.length; index++) {
const item = crawlDataArray[index];
const url = item?.metadata?.url || item?.url || '';
const filename = url
? url.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') + '.json'
: `crawl_url_${index + 1}.json`;
const folderName = url
? url.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_')
: `page_${index + 1}`;
const pageFolder = zip.folder(folderName);
if (!pageFolder) continue;
const jsonContent = JSON.stringify(item, null, 2);
zip.file(filename, jsonContent);
});
pageFolder.file('metadata.json', JSON.stringify(item, null, 2));
if (item.text) {
const textContent = typeof item.text === 'object' ? JSON.stringify(item.text, null, 2) : String(item.text);
pageFolder.file('content.txt', textContent);
}
if (item.html) {
const htmlContent = typeof item.html === 'object' ? JSON.stringify(item.html, null, 2) : String(item.html);
pageFolder.file('content.html', htmlContent);
}
if (item.markdown) {
const mdContent = typeof item.markdown === 'object' ? JSON.stringify(item.markdown, null, 2) : String(item.markdown);
pageFolder.file('content.md', mdContent);
}
if (item.links && Array.isArray(item.links)) {
const uniqueLinks = Array.from(new Set(item.links));
pageFolder.file('links.txt', uniqueLinks.join('\n'));
}
const screenshots = [
{ id: item.screenshotVisible, name: 'screenshot_visible.png' },
{ id: item.screenshotFullpage, name: 'screenshot_full_page.png' }
];
for (const screenshot of screenshots) {
if (screenshot.id && row.binaryOutput && row.binaryOutput[screenshot.id]) {
const binaryData = row.binaryOutput[screenshot.id].data;
if (binaryData && !binaryData.startsWith('http')) {
const base64Data = binaryData.replace(/^data:image\/\w+;base64,/, "");
pageFolder.file(screenshot.name, base64Data, { base64: true });
}
}
}
}
const blob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(blob);
@@ -831,7 +902,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
onClick={() => downloadMarkdown(markdownContent, 'output.md')}
sx={{ color: '#FF00C3', textTransform: 'none' }}
>
Download
Download Markdown
</Button>
</Box>
</AccordionDetails>
@@ -866,7 +937,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
}}
sx={{ color: '#FF00C3', textTransform: 'none' }}
>
Download
Download HTML
</Button>
</Box>
</AccordionDetails>
@@ -930,6 +1001,39 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
/>
)}
</Box>
<Box sx={{ mt: 2 }}>
<Button
onClick={() => {
const key = screenshotKeys[currentScreenshotIndex];
const orig = screenshotKeyMap[key];
const src = row.binaryOutput[orig];
// Use fetch/blob for URL-based screenshots
if (src && typeof src === 'string' && src.startsWith('http')) {
fetch(src)
.then(res => res.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${key}.png`;
a.click();
window.URL.revokeObjectURL(url);
});
} else {
// Fallback for data URIs
const link = document.createElement('a');
link.href = src;
link.download = `${key}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Screenshot
</Button>
</Box>
</AccordionDetails>
</Accordion>
)}
@@ -1267,6 +1371,18 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
</TableBody>
</Table>
</TableContainer>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const pageUrl = crawlData[0][currentCrawlIndex]?.metadata?.url || crawlData[0][currentCrawlIndex]?.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `page_${currentCrawlIndex + 1}`;
downloadJSON(crawlData[0][currentCrawlIndex].metadata, `${baseFilename}_metadata.json`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Metadata
</Button>
</Box>
</AccordionDetails>
</Accordion>
@@ -1303,6 +1419,19 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
: crawlData[0][currentCrawlIndex].text}
</Typography>
</Paper>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const pageUrl = crawlData[0][currentCrawlIndex]?.metadata?.url || crawlData[0][currentCrawlIndex]?.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `page_${currentCrawlIndex + 1}`;
const content = typeof crawlData[0][currentCrawlIndex].text === 'object' ? JSON.stringify(crawlData[0][currentCrawlIndex].text, null, 2) : crawlData[0][currentCrawlIndex].text;
downloadText(content, `${baseFilename}_text.txt`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Text Content
</Button>
</Box>
</AccordionDetails>
</Accordion>
)}
@@ -1340,6 +1469,69 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
: crawlData[0][currentCrawlIndex].html}
</Typography>
</Paper>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const pageUrl = crawlData[0][currentCrawlIndex]?.metadata?.url || crawlData[0][currentCrawlIndex]?.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `page_${currentCrawlIndex + 1}`;
const content = typeof crawlData[0][currentCrawlIndex].html === 'object' ? JSON.stringify(crawlData[0][currentCrawlIndex].html, null, 2) : crawlData[0][currentCrawlIndex].html;
downloadHTML(content, `${baseFilename}.html`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download HTML
</Button>
</Box>
</AccordionDetails>
</Accordion>
)}
{crawlData[0][currentCrawlIndex].markdown && (
<Accordion sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant='h6'>
Markdown
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Paper
sx={{
p: 2,
maxHeight: '300px',
overflow: 'auto',
backgroundColor: darkMode ? '#1e1e1e' : '#f5f5f5'
}}
>
<Typography
component="pre"
sx={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '0.875rem',
m: 0
}}
>
{typeof crawlData[0][currentCrawlIndex].markdown === 'object'
? JSON.stringify(crawlData[0][currentCrawlIndex].markdown, null, 2)
: crawlData[0][currentCrawlIndex].markdown}
</Typography>
</Paper>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const pageUrl = crawlData[0][currentCrawlIndex]?.metadata?.url || crawlData[0][currentCrawlIndex]?.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `page_${currentCrawlIndex + 1}`;
const content = typeof crawlData[0][currentCrawlIndex].markdown === 'object' ? JSON.stringify(crawlData[0][currentCrawlIndex].markdown, null, 2) : crawlData[0][currentCrawlIndex].markdown;
downloadMarkdown(content, `${baseFilename}.md`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Markdown
</Button>
</Box>
</AccordionDetails>
</Accordion>
)}
@@ -1350,7 +1542,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
) || [];
return validLinks.length > 0 && (
<Accordion defaultExpanded sx={{ mb: 2 }}>
<Accordion sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant='h6'>
@@ -1359,13 +1551,29 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
</Box>
</AccordionSummary>
<AccordionDetails>
<Paper sx={{ maxHeight: 200, overflow: 'auto', p: 1 }}>
{validLinks.map((link: string, idx: number) => (
<Typography key={idx} sx={{ fontSize: '0.75rem', mb: 0.5 }}>
{link}
</Typography>
))}
<Paper sx={{ maxHeight: 200, overflow: 'auto', p: 2, backgroundColor: darkMode ? '#1e1e1e' : '#f5f5f5' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{(Array.from(new Set(validLinks)) as string[]).map((link: string, idx: number) => (
<Link key={idx} href={link} target="_blank" rel="noopener" sx={{ color: '#FF00C3', wordBreak: 'break-all', fontSize: '0.875rem' }}>
{link}
</Link>
))}
</Box>
</Paper>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const pageUrl = crawlData[0][currentCrawlIndex]?.metadata?.url || crawlData[0][currentCrawlIndex]?.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `page_${currentCrawlIndex + 1}`;
const uniqueLinks = Array.from(new Set(validLinks));
const content = uniqueLinks.join('\n');
downloadText(content, `${baseFilename}_links.txt`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Links
</Button>
</Box>
</AccordionDetails>
</Accordion>
);
@@ -1374,28 +1582,20 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
<Button
onClick={() => {
const currentUrl = crawlData[0][currentCrawlIndex]?.metadata?.url || crawlData[0][currentCrawlIndex]?.url || '';
const filename = currentUrl
? currentUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') + '.json'
: `crawl_url_${currentCrawlIndex + 1}.json`;
downloadJSON(
[crawlData[0][currentCrawlIndex]],
filename
const item = crawlData[0][currentCrawlIndex];
const pageUrl = item?.metadata?.url || item?.url || '';
const baseFilename = pageUrl
? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_')
: `page_${currentCrawlIndex + 1}`;
downloadAllCrawlsAsZip(
[item],
`${baseFilename}_bundle.zip`
);
}}
sx={{
color: '#FF00C3',
textTransform: 'none',
p: 0,
minWidth: 'auto',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent',
textDecoration: 'underline',
},
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download This Page as JSON
Download This Page
</Button>
<Button
@@ -1409,19 +1609,9 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
`${baseFilename}_all_urls.zip`
);
}}
sx={{
color: '#FF00C3',
textTransform: 'none',
p: 0,
minWidth: 'auto',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent',
textDecoration: 'underline',
},
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download All Pages as JSON
Download All Pages
</Button>
</Box>
</>
@@ -1525,6 +1715,19 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
</TableBody>
</Table>
</TableContainer>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const res = searchData[currentSearchIndex];
const pageUrl = res.metadata?.url || res.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `search_result_${currentSearchIndex + 1}`;
downloadJSON(res.metadata, `${baseFilename}_metadata.json`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Metadata
</Button>
</Box>
</AccordionDetails>
</Accordion>
@@ -1559,6 +1762,20 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
{searchData[currentSearchIndex].text}
</Typography>
</Paper>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const res = searchData[currentSearchIndex];
const pageUrl = res.metadata?.url || res.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `search_result_${currentSearchIndex + 1}`;
const content = typeof res.text === 'object' ? JSON.stringify(res.text, null, 2) : res.text;
downloadText(content, `${baseFilename}_text.txt`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Text Content
</Button>
</Box>
</AccordionDetails>
</Accordion>
)}
@@ -1596,12 +1813,26 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
: searchData[currentSearchIndex].html}
</Typography>
</Paper>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const res = searchData[currentSearchIndex];
const pageUrl = res.metadata?.url || res.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `search_result_${currentSearchIndex + 1}`;
const content = typeof res.html === 'object' ? JSON.stringify(res.html, null, 2) : res.html;
downloadHTML(content, `${baseFilename}.html`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download HTML
</Button>
</Box>
</AccordionDetails>
</Accordion>
)}
{searchData[currentSearchIndex].markdown && (
<Accordion defaultExpanded sx={{ mb: 2 }}>
<Accordion sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant='h6'>
@@ -1624,7 +1855,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '0.75rem',
fontSize: '0.875rem',
m: 0
}}
>
@@ -1633,6 +1864,20 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
: searchData[currentSearchIndex].markdown}
</Typography>
</Paper>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const res = searchData[currentSearchIndex];
const pageUrl = res.metadata?.url || res.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `search_result_${currentSearchIndex + 1}`;
const content = typeof res.markdown === 'object' ? JSON.stringify(res.markdown, null, 2) : res.markdown;
downloadMarkdown(content, `${baseFilename}.md`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Markdown
</Button>
</Box>
</AccordionDetails>
</Accordion>
)}
@@ -1652,61 +1897,153 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
</Box>
</AccordionSummary>
<AccordionDetails>
<Paper
sx={{
p: 2,
maxHeight: '300px',
overflow: 'auto',
backgroundColor: darkMode ? '#1e1e1e' : '#f5f5f5'
}}
>
{validLinks.map((link: string, linkIdx: number) => {
return (
<Box key={linkIdx} sx={{ mb: 0.5 }}>
<Link
href={link}
target="_blank"
rel="noopener noreferrer"
sx={{
color: '#FF00C3',
textDecoration: 'none',
fontSize: '0.75rem',
'&:hover': { textDecoration: 'underline' },
wordBreak: 'break-all'
}}
>
{link}
</Link>
</Box>
);
})}
</Paper>
</AccordionDetails>
</Accordion>
<Paper sx={{ maxHeight: 200, overflow: 'auto', p: 2, backgroundColor: darkMode ? '#1e1e1e' : '#f5f5f5' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{(Array.from(new Set(validLinks)) as string[]).map((link: string, idx: number) => (
<Link key={idx} href={link} target="_blank" rel="noopener" sx={{ color: '#FF00C3', wordBreak: 'break-all', fontSize: '0.875rem' }}>
{link}
</Link>
))}
</Box>
</Paper>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const res = searchData[currentSearchIndex];
const pageUrl = res.metadata?.url || res.url || '';
const baseFilename = pageUrl ? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_') : `search_result_${currentSearchIndex + 1}`;
const uniqueLinks = Array.from(new Set(validLinks)) as string[];
const content = uniqueLinks.join('\n');
downloadText(content, `${baseFilename}_links.txt`);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Links
</Button>
</Box>
</AccordionDetails>
</Accordion>
);
})()}
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2 }}>
{(searchData[currentSearchIndex].screenshotVisible || searchData[currentSearchIndex].screenshotFullpage) && (
<Accordion sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant='h6'>
Screenshots
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
{(() => {
const tabs: { key: string; label: string; value: string }[] = [];
if (searchData[currentSearchIndex].screenshotVisible)
tabs.push({ key: 'visible', label: 'Screenshot (Visible)', value: searchData[currentSearchIndex].screenshotVisible });
if (searchData[currentSearchIndex].screenshotFullpage)
tabs.push({ key: 'fullpage', label: 'Screenshot (Full Page)', value: searchData[currentSearchIndex].screenshotFullpage });
// Ensure activeTab is valid for current tabs array
const activeTab = Math.min(currentSearchScreenshotTab, tabs.length - 1);
const getImageSrc = (val: string) => {
if (val.startsWith('http')) return val;
if (row.binaryOutput && row.binaryOutput[val]) {
const binaryData = row.binaryOutput[val].data || row.binaryOutput[val];
return typeof binaryData === 'string' && binaryData.startsWith('http')
? binaryData
: typeof binaryData === 'string' && binaryData.startsWith('data:')
? binaryData
: `data:image/png;base64,${binaryData}`;
}
return `data:image/png;base64,${val}`;
};
return (
<>
{tabs.length > 1 && (
<Box sx={{ display: 'flex', borderBottom: '1px solid', borderColor: darkMode ? '#2a3441' : '#dee2e6', mb: 2 }}>
{tabs.map((tab, idx) => (
<Box
key={tab.key}
onClick={() => setCurrentSearchScreenshotTab(idx)}
sx={{
px: 3, py: 1,
cursor: 'pointer',
backgroundColor: activeTab === idx ? (darkMode ? '#121111ff' : '#e9ecef') : 'transparent',
borderBottom: activeTab === idx ? '3px solid #FF00C3' : 'none',
color: darkMode ? '#fff' : '#000',
}}
>
{tab.label}
</Box>
))}
</Box>
)}
{tabs.length > 0 && (
<>
<img
src={getImageSrc(tabs[activeTab >= 0 ? activeTab : 0].value)}
alt={tabs[activeTab >= 0 ? activeTab : 0].label}
style={{ maxWidth: '100%', borderRadius: '4px', border: '1px solid rgba(255,255,255,0.1)' }}
/>
<Box sx={{ mt: 1 }}>
<Button
onClick={() => {
const src = getImageSrc(tabs[activeTab >= 0 ? activeTab : 0].value);
const link = document.createElement('a');
link.href = src;
link.download = `${tabs[activeTab >= 0 ? activeTab : 0].label}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download Screenshot
</Button>
</Box>
</>
)}
</>
);
})()}
</AccordionDetails>
</Accordion>
)}
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
<Button
onClick={() => {
const result = searchData[currentSearchIndex];
const filename = `search_result_${currentSearchIndex + 1}.json`;
downloadJSON(result, filename);
}}
sx={{
color: '#FF00C3',
textTransform: 'none',
mr: 2,
p: 0,
minWidth: 'auto',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent',
textDecoration: 'underline',
},
const item = searchData[currentSearchIndex];
const pageUrl = item?.metadata?.url || item?.url || '';
const baseFilename = pageUrl
? pageUrl.replace(/^https?:\/\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_.-]/g, '_')
: `search_result_${currentSearchIndex + 1}`;
downloadAllCrawlsAsZip(
[item],
`${baseFilename}_bundle.zip`
);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download as JSON
Download This Page
</Button>
<Button
onClick={() => {
const firstUrl = searchData[0]?.metadata?.url || searchData[0]?.url || '';
const baseFilename = firstUrl
? firstUrl.replace(/^https?:\/\//, '').split('/')[0].replace(/[^a-zA-Z0-9_.-]/g, '_')
: 'search_results';
downloadAllCrawlsAsZip(
searchData,
`${baseFilename}_all_results.zip`
);
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download All Results
</Button>
</Box>
</>
@@ -1779,26 +2116,23 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
</Table>
</TableContainer>
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2 }}>
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
<Button
onClick={() => {
downloadJSON(searchData, 'search_results.json');
}}
sx={{
color: '#FF00C3',
textTransform: 'none',
mr: 2,
p: 0,
minWidth: 'auto',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent',
textDecoration: 'underline',
},
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download as JSON
</Button>
<Button
onClick={() => {
downloadCSV(searchData, ['title', 'url', 'description'], 'search_results.csv');
}}
sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}
>
Download as CSV
</Button>
</Box>
</>
)}
@@ -1874,6 +2208,26 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
/>
)}
</Box>
<Box sx={{ mt: 2 }}>
<Button onClick={() => {
const key = screenshotKeys[currentScreenshotIndex];
const orig = screenshotKeyMap[key];
const src = row.binaryOutput[orig];
if (src && src.startsWith && src.startsWith('http')) {
fetch(src).then(res => res.blob()).then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = key + '.png'; a.click();
});
} else {
const link = document.createElement('a');
link.href = src;
link.download = key + '.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}} sx={{ color: '#FF00C3', textTransform: 'none', p: 0, minWidth: 'auto', backgroundColor: 'transparent', '&:hover': { textDecoration: 'underline', backgroundColor: 'transparent' } }}>Download Screenshot</Button>
</Box>
</AccordionDetails>
</Accordion>
)}