import { Box, Tabs, Typography, Tab, Paper, Button, CircularProgress, Accordion, AccordionSummary, AccordionDetails } from "@mui/material"; import Highlight from "react-highlight"; import * as React from "react"; import { Data } from "./RunsTable"; import { TabPanel, TabContext } from "@mui/lab"; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { useEffect, useState } from "react"; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import 'highlight.js/styles/github.css'; import { useTranslation } from "react-i18next"; import { useThemeMode } from "../../context/theme-provider"; interface RunContentProps { row: Data, currentLog: string, interpretationInProgress: boolean, logEndRef: React.RefObject, abortRunHandler: () => void, } export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => { const { t } = useTranslation(); const [tab, setTab] = React.useState('output'); const [markdownContent, setMarkdownContent] = useState(''); const [htmlContent, setHtmlContent] = useState(''); const [schemaData, setSchemaData] = useState([]); const [schemaColumns, setSchemaColumns] = useState([]); const [schemaKeys, setSchemaKeys] = useState([]); const [schemaDataByKey, setSchemaDataByKey] = useState>({}); const [schemaColumnsByKey, setSchemaColumnsByKey] = useState>({}); const [isSchemaTabular, setIsSchemaTabular] = useState(false); const [listData, setListData] = useState([]); const [listColumns, setListColumns] = useState([]); const [listKeys, setListKeys] = useState([]); const [currentListIndex, setCurrentListIndex] = useState(0); const [screenshotKeys, setScreenshotKeys] = useState([]); const [screenshotKeyMap, setScreenshotKeyMap] = useState>({}); const [currentScreenshotIndex, setCurrentScreenshotIndex] = useState(0); const [currentSchemaIndex, setCurrentSchemaIndex] = useState(0); const [legacyData, setLegacyData] = useState([]); const [legacyColumns, setLegacyColumns] = useState([]); const [isLegacyData, setIsLegacyData] = useState(false); useEffect(() => { setTab(tab); }, [interpretationInProgress]); useEffect(() => { setMarkdownContent(''); setHtmlContent(''); if (row.serializableOutput?.markdown && Array.isArray(row.serializableOutput.markdown)) { const markdownData = row.serializableOutput.markdown[0]; if (markdownData?.content) { setMarkdownContent(markdownData.content); } } if (row.serializableOutput?.html && Array.isArray(row.serializableOutput.html)) { const htmlData = row.serializableOutput.html[0]; if (htmlData?.content) { setHtmlContent(htmlData.content); } } }, [row.serializableOutput]); useEffect(() => { if (row.status === 'running' || row.status === 'queued' || row.status === 'scheduled') { setSchemaData([]); setSchemaColumns([]); setSchemaKeys([]); setSchemaDataByKey({}); setSchemaColumnsByKey({}); setListData([]); setListColumns([]); setListKeys([]); setLegacyData([]); setLegacyColumns([]); setIsLegacyData(false); setIsSchemaTabular(false); return; } if (!row.serializableOutput) return; const hasLegacySchema = row.serializableOutput.scrapeSchema && Array.isArray(row.serializableOutput.scrapeSchema); const hasLegacyList = row.serializableOutput.scrapeList && Array.isArray(row.serializableOutput.scrapeList); const hasOldFormat = !row.serializableOutput.scrapeSchema && !row.serializableOutput.scrapeList && Object.keys(row.serializableOutput).length > 0; if (hasLegacySchema || hasLegacyList || hasOldFormat) { processLegacyData(row.serializableOutput); setIsLegacyData(false); return; } setIsLegacyData(false); if (row.serializableOutput.scrapeSchema && Object.keys(row.serializableOutput.scrapeSchema).length > 0) { processSchemaData(row.serializableOutput.scrapeSchema); } if (row.serializableOutput.scrapeList) { processScrapeList(row.serializableOutput.scrapeList); } }, [row.serializableOutput, row.status]); useEffect(() => { if (row.status === 'running' || row.status === 'queued' || row.status === 'scheduled') { setScreenshotKeys([]); setScreenshotKeyMap({}); setCurrentScreenshotIndex(0); return; } if (row.binaryOutput && Object.keys(row.binaryOutput).length > 0) { const rawKeys = Object.keys(row.binaryOutput); const isLegacyPattern = rawKeys.every(key => /^item-\d+-\d+$/.test(key)); let normalizedScreenshotKeys: string[]; if (isLegacyPattern) { // Legacy unnamed screenshots → Screenshot 1, Screenshot 2... normalizedScreenshotKeys = rawKeys.map((_, index) => `Screenshot ${index + 1}`); } else { // Same rule as captured lists: if name missing or generic, auto-label normalizedScreenshotKeys = rawKeys.map((key, index) => { if (!key || key.toLowerCase().includes("screenshot")) { return `Screenshot ${index + 1}`; } return key; }); } const keyMap: Record = {}; normalizedScreenshotKeys.forEach((displayName, index) => { keyMap[displayName] = rawKeys[index]; }); setScreenshotKeys(normalizedScreenshotKeys); setScreenshotKeyMap(keyMap); setCurrentScreenshotIndex(0); } else { setScreenshotKeys([]); setScreenshotKeyMap({}); setCurrentScreenshotIndex(0); } }, [row.binaryOutput, row.status]); const processLegacyData = (legacyOutput: Record) => { const convertedSchema: Record = {}; const convertedList: Record = {}; const keys = Object.keys(legacyOutput); keys.forEach((key) => { const data = legacyOutput[key]; if (Array.isArray(data)) { const firstNonNullElement = data.find(item => item !== null && item !== undefined); const isNestedArray = firstNonNullElement && Array.isArray(firstNonNullElement); if (isNestedArray) { data.forEach((subArray, index) => { if (subArray !== null && subArray !== undefined && Array.isArray(subArray) && subArray.length > 0) { const filteredData = subArray.filter(row => row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") ); if (filteredData.length > 0) { const autoName = `List ${Object.keys(convertedList).length + 1}`; convertedList[autoName] = filteredData; } } }); } else { const filteredData = data.filter(row => row && typeof row === 'object' && !Array.isArray(row) && Object.values(row).some(value => value !== undefined && value !== "") ); if (filteredData.length > 0) { const schemaCount = Object.keys(convertedSchema).length; const autoName = `Text ${schemaCount + 1}`; convertedSchema[autoName] = filteredData; } } } }); if (Object.keys(convertedSchema).length === 1) { const singleKey = Object.keys(convertedSchema)[0]; const singleData = convertedSchema[singleKey]; delete convertedSchema[singleKey]; convertedSchema["Texts"] = singleData; } if (Object.keys(convertedSchema).length > 0) { processSchemaData(convertedSchema); } if (Object.keys(convertedList).length > 0) { processScrapeList(convertedList); } }; const processSchemaData = (schemaOutput: any) => { const keys = Object.keys(schemaOutput); const normalizedKeys = keys.map((key, index) => { if (!key || key.toLowerCase().includes("scrapeschema")) { return keys.length === 1 ? "Texts" : `Text ${index + 1}`; } return key; }); setSchemaKeys(normalizedKeys); const dataByKey: Record = {}; const columnsByKey: Record = {}; if (Array.isArray(schemaOutput)) { const filteredData = schemaOutput.filter(row => row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") ); if (filteredData.length > 0) { const allColumns = new Set(); filteredData.forEach(item => { Object.keys(item).forEach(key => allColumns.add(key)); }); setSchemaData(filteredData); setSchemaColumns(Array.from(allColumns)); setIsSchemaTabular(filteredData.length > 1); return; } } let allData: any[] = []; let hasMultipleEntries = false; keys.forEach(key => { const data = schemaOutput[key]; if (Array.isArray(data)) { const filteredData = data.filter(row => row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") ); dataByKey[key] = filteredData; const columnsForKey = new Set(); filteredData.forEach(item => { Object.keys(item).forEach(col => columnsForKey.add(col)); }); columnsByKey[key] = Array.from(columnsForKey); allData = [...allData, ...filteredData]; if (filteredData.length > 1) hasMultipleEntries = true; } }); const remappedDataByKey: Record = {}; const remappedColumnsByKey: Record = {}; normalizedKeys.forEach((newKey, idx) => { const oldKey = keys[idx]; remappedDataByKey[newKey] = dataByKey[oldKey]; remappedColumnsByKey[newKey] = columnsByKey[oldKey]; }); setSchemaDataByKey(remappedDataByKey); setSchemaColumnsByKey(remappedColumnsByKey); if (allData.length > 0) { const allColumns = new Set(); allData.forEach(item => { Object.keys(item).forEach(key => allColumns.add(key)); }); setSchemaData(allData); setSchemaColumns(Array.from(allColumns)); setIsSchemaTabular(hasMultipleEntries || allData.length > 1); } }; const processScrapeList = (scrapeListData: any) => { const tablesList: any[][] = []; const columnsList: string[][] = []; const keys: string[] = []; if (typeof scrapeListData === 'object') { Object.keys(scrapeListData).forEach(key => { const tableData = scrapeListData[key]; if (Array.isArray(tableData) && tableData.length > 0) { const filteredData = tableData.filter(row => row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") ); if (filteredData.length > 0) { tablesList.push(filteredData); keys.push(key); const tableColumns = new Set(); filteredData.forEach(item => { Object.keys(item).forEach(key => tableColumns.add(key)); }); columnsList.push(Array.from(tableColumns)); } } }); } setListData(tablesList); setListColumns(columnsList); const normalizedListKeys = keys.map((key, index) => { if (!key || key.toLowerCase().includes("scrapelist")) { return `List ${index + 1}`; } return key; }); setListKeys(normalizedListKeys); setCurrentListIndex(0); }; const convertToCSV = (data: any[], columns: string[], isSchemaData: boolean = false, isTabular: boolean = false): string => { if (isSchemaData && !isTabular && data.length === 1) { const header = 'Label,Value'; const rows = columns.map(column => `"${column}","${data[0][column] || ""}"` ); return [header, ...rows].join('\n'); } else { const header = columns.map(col => `"${col}"`).join(','); const rows = data.map(row => columns.map(col => { const value = row[col] || ""; const escapedValue = String(value).replace(/"/g, '""'); return `"${escapedValue}"`; }).join(',') ); return [header, ...rows].join('\n'); } }; // Function to download a specific dataset as CSV const downloadCSV = (data: any[], columns: string[], filename: string, isSchemaData: boolean = false, isTabular: boolean = false) => { const csvContent = convertToCSV(data, columns, isSchemaData, isTabular); const blob = new Blob([csvContent], { type: 'text/csv;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 downloadJSON = (data: any[], filename: string) => { const jsonContent = JSON.stringify(data, null, 2); const blob = new Blob([jsonContent], { type: 'application/json;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 downloadMarkdown = (content: string, filename: string) => { const blob = new Blob([content], { type: 'text/markdown;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 renderDataTable = ( data: any[], columns: string[], title: string, csvFilename: string, jsonFilename: string, isPaginatedList: boolean = false, isSchemaData: boolean = false ) => { if (data.length === 0) return null; const shouldShowAsKeyValue = isSchemaData && !isSchemaTabular && data.length === 1; if (title === '') { return ( <> {shouldShowAsKeyValue ? ( <> theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > Label theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > Value ) : ( columns.map((column) => ( theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > {column} )) )} {shouldShowAsKeyValue ? ( columns.map((column) => ( {column} {data[0][column] === undefined || data[0][column] === "" ? "-" : (typeof data[0][column] === 'object' ? JSON.stringify(data[0][column]) : String(data[0][column]))} )) ) : ( data.map((row, index) => ( {columns.map((column) => ( {row[column] === undefined || row[column] === "" ? "-" : (typeof row[column] === 'object' ? JSON.stringify(row[column]) : String(row[column]))} ))} )) )}
); } return ( } aria-controls={`${title.toLowerCase()}-content`} id={`${title.toLowerCase()}-header`} > {title} {shouldShowAsKeyValue ? ( <> theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > Label theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > Value ) : ( columns.map((column) => ( theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > {column} )) )} {shouldShowAsKeyValue ? ( columns.map((column) => ( {column} {data[0][column] === undefined || data[0][column] === "" ? "-" : (typeof data[0][column] === 'object' ? JSON.stringify(data[0][column]) : String(data[0][column]))} )) ) : ( data.map((row, index) => ( {columns.map((column) => ( {row[column] === undefined || row[column] === "" ? "-" : (typeof row[column] === 'object' ? JSON.stringify(row[column]) : String(row[column]))} ))} )) )}
); }; const hasData = schemaData.length > 0 || listData.length > 0 || legacyData.length > 0; const hasScreenshots = row.binaryOutput && Object.keys(row.binaryOutput).length > 0; const hasMarkdown = markdownContent.length > 0; const hasHTML = htmlContent.length > 0; return ( {hasMarkdown || hasHTML ? ( <> {hasMarkdown && ( }> Markdown {markdownContent} )} {hasHTML && ( }> HTML {htmlContent} )} ) : ( // Extract robot output <> {row.status === 'running' || row.status === 'queued' ? ( <> {t('run_content.loading')} ) : (!hasData && !hasScreenshots ? {t('run_content.empty_output')} : null)} {hasData && ( {isLegacyData && ( renderDataTable( legacyData, legacyColumns, t('run_content.captured_data.title'), 'data.csv', 'data.json' ) )} {!isLegacyData && ( <> {schemaData.length > 0 && ( }> {t('run_content.captured_data.schema_title', 'Captured Texts')} {schemaKeys.length > 0 && ( {schemaKeys.map((key, idx) => ( setCurrentSchemaIndex(idx)} sx={{ px: 3, py: 1, cursor: 'pointer', backgroundColor: currentSchemaIndex === idx ? (theme) => theme.palette.mode === 'dark' ? '#121111ff' : '#e9ecef' : 'transparent', borderBottom: currentSchemaIndex === idx ? '3px solid #FF00C3' : 'none', color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000', }} > {key} ))} )} {renderDataTable( schemaDataByKey[schemaKeys[currentSchemaIndex]] || schemaData, schemaColumnsByKey[schemaKeys[currentSchemaIndex]] || schemaColumns, '', `${schemaKeys[currentSchemaIndex] || 'schema_data'}.csv`, `${schemaKeys[currentSchemaIndex] || 'schema_data'}.json`, false, true )} )} {listData.length > 0 && ( }> {t('run_content.captured_data.list_title', 'Captured Lists')} {listKeys.map((key, idx) => ( setCurrentListIndex(idx)} sx={{ px: 3, py: 1, cursor: 'pointer', backgroundColor: currentListIndex === idx ? (theme) => theme.palette.mode === 'dark' ? '#121111ff' : '#e9ecef' : 'transparent', borderBottom: currentListIndex === idx ? '3px solid #FF00C3' : 'none', color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000', }} > {key} ))} {(listColumns[currentListIndex] || []).map((column) => ( theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > {column} ))} {(listData[currentListIndex] || []).map((rowItem, idx) => ( {(listColumns[currentListIndex] || []).map((column) => ( {rowItem[column] === undefined || rowItem[column] === '' ? '-' : typeof rowItem[column] === 'object' ? JSON.stringify(rowItem[column]) : String(rowItem[column])} ))} ))}
)} )}
)} {hasScreenshots && ( }> {t('run_content.captured_screenshot.title', 'Captured Screenshots')} {screenshotKeys.length > 0 && ( {screenshotKeys.map((key, idx) => ( setCurrentScreenshotIndex(idx)} sx={{ px: 3, py: 1, cursor: 'pointer', backgroundColor: currentScreenshotIndex === idx ? (theme) => theme.palette.mode === 'dark' ? '#121111ff' : '#e9ecef' : 'transparent', borderBottom: currentScreenshotIndex === idx ? '3px solid #FF00C3' : 'none', color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000', }} > {key} ))} )} {screenshotKeys.length > 0 && ( {`Screenshot )} )} )}
); };