feat: create run ui directory
This commit is contained in:
186
src/components/run/InterpretationButtons.tsx
Normal file
186
src/components/run/InterpretationButtons.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Box, Button, Stack, Typography, CircularProgress } from "@mui/material";
|
||||
import { PlayCircle } from "@mui/icons-material";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { interpretCurrentRecording, stopCurrentInterpretation } from "../../api/recording";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { GenericModal } from "../ui/GenericModal";
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import HelpIcon from '@mui/icons-material/Help';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface InterpretationButtonsProps {
|
||||
enableStepping: (isPaused: boolean) => void;
|
||||
}
|
||||
|
||||
interface InterpretationInfo {
|
||||
running: boolean;
|
||||
isPaused: boolean;
|
||||
}
|
||||
|
||||
const interpretationInfo: InterpretationInfo = {
|
||||
running: false,
|
||||
isPaused: false,
|
||||
};
|
||||
|
||||
export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [info, setInfo] = useState<InterpretationInfo>(interpretationInfo);
|
||||
const [decisionModal, setDecisionModal] = useState<{
|
||||
pair: WhereWhatPair | null,
|
||||
actionType: string,
|
||||
selector: string,
|
||||
tagName: string,
|
||||
innerText: string,
|
||||
action: string,
|
||||
open: boolean
|
||||
}>({ pair: null, actionType: '', selector: '', action: '', tagName: '', innerText: '', open: false });
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
const { notify } = useGlobalInfoStore();
|
||||
|
||||
const finishedHandler = useCallback(() => {
|
||||
setInfo({ ...info, isPaused: false });
|
||||
enableStepping(false);
|
||||
}, [info, enableStepping]);
|
||||
|
||||
const breakpointHitHandler = useCallback(() => {
|
||||
setInfo({ running: false, isPaused: true });
|
||||
notify('warning', t('interpretation_buttons.messages.restart_required'));
|
||||
enableStepping(true);
|
||||
}, [enableStepping, t]);
|
||||
|
||||
const decisionHandler = useCallback(
|
||||
({ pair, actionType, lastData }: { pair: WhereWhatPair | null, actionType: string, lastData: { selector: string, action: string, tagName: string, innerText: string } }) => {
|
||||
const { selector, action, tagName, innerText } = lastData;
|
||||
setDecisionModal((prevState) => ({
|
||||
pair,
|
||||
actionType,
|
||||
selector,
|
||||
action,
|
||||
tagName,
|
||||
innerText,
|
||||
open: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleDecision = (decision: boolean) => {
|
||||
const { pair, actionType } = decisionModal;
|
||||
socket?.emit('decision', { pair, actionType, decision });
|
||||
setDecisionModal({ pair: null, actionType: '', selector: '', action: '', tagName: '', innerText: '', open: false });
|
||||
};
|
||||
|
||||
const handleDescription = () => {
|
||||
if (decisionModal.actionType === 'customAction') {
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
{t('interpretation_buttons.modal.use_previous')}
|
||||
</Typography>
|
||||
<Box style={{ marginTop: '4px' }}>
|
||||
<Typography>
|
||||
{t('interpretation_buttons.modal.previous_action')} <b>{decisionModal.action}</b>,
|
||||
{t('interpretation_buttons.modal.element_text')} <b>{decisionModal.innerText}</b>
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
socket.on('finished', finishedHandler);
|
||||
socket.on('breakpointHit', breakpointHitHandler);
|
||||
socket.on('decision', decisionHandler);
|
||||
}
|
||||
return () => {
|
||||
socket?.off('finished', finishedHandler);
|
||||
socket?.off('breakpointHit', breakpointHitHandler);
|
||||
socket?.off('decision', decisionHandler);
|
||||
};
|
||||
}, [socket, finishedHandler, breakpointHitHandler]);
|
||||
|
||||
const handlePlay = async () => {
|
||||
if (!info.running) {
|
||||
setInfo({ ...info, running: true });
|
||||
const finished = await interpretCurrentRecording();
|
||||
setInfo({ ...info, running: false });
|
||||
if (finished) {
|
||||
notify('info', t('interpretation_buttons.messages.run_finished'));
|
||||
} else {
|
||||
notify('error', t('interpretation_buttons.messages.run_failed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// pause and stop logic (do not delete - we wil bring this back!)
|
||||
/*
|
||||
const handlePause = async () => {
|
||||
if (info.running) {
|
||||
socket?.emit("pause");
|
||||
setInfo({ running: false, isPaused: true });
|
||||
notify('warning', 'Please restart the interpretation after updating the recording');
|
||||
enableStepping(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = async () => {
|
||||
setInfo({ running: false, isPaused: false });
|
||||
enableStepping(false);
|
||||
await stopCurrentInterpretation();
|
||||
};
|
||||
*/
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={3} sx={{ marginTop: '30px', marginBottom: '5px', justifyContent: 'center' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handlePlay}
|
||||
disabled={info.running}
|
||||
sx={{ display: 'grid' }}
|
||||
>
|
||||
{info.running ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CircularProgress size={22} color="inherit" sx={{ marginRight: '10px' }} />
|
||||
{t('interpretation_buttons.messages.extracting')}
|
||||
</Box>
|
||||
) : t('interpretation_buttons.buttons.preview')}
|
||||
</Button>
|
||||
<GenericModal
|
||||
onClose={() => { }}
|
||||
isOpen={decisionModal.open}
|
||||
canBeClosed={false}
|
||||
modalStyle={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 500,
|
||||
background: 'white',
|
||||
border: '2px solid #000',
|
||||
boxShadow: '24',
|
||||
height: 'fit-content',
|
||||
display: 'block',
|
||||
overflow: 'scroll',
|
||||
padding: '5px 25px 10px 25px',
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '15px' }}>
|
||||
<HelpIcon />
|
||||
{handleDescription()}
|
||||
<div style={{ float: 'right' }}>
|
||||
<Button onClick={() => handleDecision(true)} color='success'>
|
||||
{t('interpretation_buttons.buttons.yes')}
|
||||
</Button>
|
||||
<Button onClick={() => handleDecision(false)} color='error'>
|
||||
{t('interpretation_buttons.buttons.no')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
242
src/components/run/InterpretationLog.tsx
Normal file
242
src/components/run/InterpretationLog.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import * as React from 'react';
|
||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Button, TextField, Grid } from '@mui/material';
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
import { Buffer } from 'buffer';
|
||||
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
|
||||
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 Paper from '@mui/material/Paper';
|
||||
import StorageIcon from '@mui/icons-material/Storage';
|
||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||
import { SidePanelHeader } from '../molecules/SidePanelHeader';
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { useThemeMode } from '../../context/theme-provider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface InterpretationLogProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, setIsOpen }) => {
|
||||
const { t } = useTranslation();
|
||||
const [log, setLog] = useState<string>('');
|
||||
const [customValue, setCustomValue] = useState('');
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const [binaryData, setBinaryData] = useState<string | null>(null);
|
||||
|
||||
const logEndRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { width } = useBrowserDimensionsStore();
|
||||
const { socket } = useSocketStore();
|
||||
const { currentWorkflowActionsState, shouldResetInterpretationLog, notify } = useGlobalInfoStore();
|
||||
|
||||
const toggleDrawer = (newOpen: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
|
||||
if (
|
||||
event.type === 'keydown' &&
|
||||
((event as React.KeyboardEvent).key === 'Tab' ||
|
||||
(event as React.KeyboardEvent).key === 'Shift')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(newOpen);
|
||||
};
|
||||
|
||||
const scrollLogToBottom = () => {
|
||||
if (logEndRef.current) {
|
||||
logEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleLog = useCallback((msg: string, date: boolean = true) => {
|
||||
if (!date) {
|
||||
setLog((prevState) => prevState + '\n' + msg);
|
||||
} else {
|
||||
setLog((prevState) => prevState + '\n' + `[${new Date().toLocaleString()}] ` + msg);
|
||||
}
|
||||
scrollLogToBottom();
|
||||
}, [log, scrollLogToBottom]);
|
||||
|
||||
const handleSerializableCallback = useCallback((data: any) => {
|
||||
setLog((prevState) =>
|
||||
prevState + '\n' + t('interpretation_log.data_sections.serializable_received') + '\n'
|
||||
+ JSON.stringify(data, null, 2) + '\n' + t('interpretation_log.data_sections.separator'));
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
setTableData(data);
|
||||
}
|
||||
|
||||
scrollLogToBottom();
|
||||
}, [log, scrollLogToBottom, t]);
|
||||
|
||||
const handleBinaryCallback = useCallback(({ data, mimetype }: any) => {
|
||||
const base64String = Buffer.from(data).toString('base64');
|
||||
const imageSrc = `data:${mimetype};base64,${base64String}`;
|
||||
|
||||
setLog((prevState) =>
|
||||
prevState + '\n' + t('interpretation_log.data_sections.binary_received') + '\n'
|
||||
+ t('interpretation_log.data_sections.mimetype') + mimetype + '\n'
|
||||
+ t('interpretation_log.data_sections.image_below') + '\n'
|
||||
+ t('interpretation_log.data_sections.separator'));
|
||||
|
||||
setBinaryData(imageSrc);
|
||||
scrollLogToBottom();
|
||||
}, [log, scrollLogToBottom, t]);
|
||||
|
||||
|
||||
const handleCustomValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomValue(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldResetInterpretationLog) {
|
||||
setLog('');
|
||||
setTableData([]);
|
||||
setBinaryData(null);
|
||||
}
|
||||
}, [shouldResetInterpretationLog]);
|
||||
|
||||
useEffect(() => {
|
||||
socket?.on('log', handleLog);
|
||||
socket?.on('serializableCallback', handleSerializableCallback);
|
||||
socket?.on('binaryCallback', handleBinaryCallback);
|
||||
return () => {
|
||||
socket?.off('log', handleLog);
|
||||
socket?.off('serializableCallback', handleSerializableCallback);
|
||||
socket?.off('binaryCallback', handleBinaryCallback);
|
||||
};
|
||||
}, [socket, handleLog, handleSerializableCallback, handleBinaryCallback]);
|
||||
|
||||
// Extract columns dynamically from the first item of tableData
|
||||
const columns = tableData.length > 0 ? Object.keys(tableData[0]) : [];
|
||||
|
||||
const { hasScrapeListAction, hasScreenshotAction, hasScrapeSchemaAction } = currentWorkflowActionsState
|
||||
|
||||
useEffect(() => {
|
||||
if (hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}, [hasScrapeListAction, hasScrapeSchemaAction, hasScreenshotAction, setIsOpen]);
|
||||
|
||||
const { darkMode } = useThemeMode();
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={9} lg={9}>
|
||||
<Button
|
||||
onClick={toggleDrawer(true)}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{
|
||||
marginTop: '10px',
|
||||
color: 'white',
|
||||
position: 'absolute',
|
||||
background: '#ff00c3',
|
||||
border: 'none',
|
||||
padding: '10px 20px',
|
||||
width: '900px',
|
||||
overflow: 'hidden',
|
||||
textAlign: 'left',
|
||||
justifyContent: 'flex-start',
|
||||
'&:hover': {
|
||||
backgroundColor: '#ff00c3',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowUpwardIcon fontSize="inherit" sx={{ marginRight: '10px' }} />
|
||||
{t('interpretation_log.titles.output_preview')}
|
||||
</Button>
|
||||
<SwipeableDrawer
|
||||
anchor="bottom"
|
||||
open={isOpen}
|
||||
onClose={toggleDrawer(false)}
|
||||
onOpen={toggleDrawer(true)}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
background: `${darkMode ? '#1e2124' : 'white'}`,
|
||||
color: `${darkMode ? 'white' : 'black'}`,
|
||||
padding: '10px',
|
||||
height: 500,
|
||||
width: width - 10,
|
||||
display: 'flex',
|
||||
borderRadius: '10px 10px 0 0',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" gutterBottom style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<StorageIcon style={{ marginRight: '8px' }} />
|
||||
{t('interpretation_log.titles.output_preview')}
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
height: '50vh',
|
||||
overflow: 'none',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
{
|
||||
binaryData ? (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{t('interpretation_log.titles.screenshot')}
|
||||
</Typography>
|
||||
<img src={binaryData} alt={t('interpretation_log.titles.screenshot')} style={{ maxWidth: '100%' }} />
|
||||
</div>
|
||||
) : tableData.length > 0 ? (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} stickyHeader aria-label="output data table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column}>{column}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{tableData.slice(0, Math.min(5, tableData.length)).map((row, index) => (
|
||||
<TableRow key={index}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column}>{row[column]}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<span style={{ marginLeft: '15px', marginTop: '10px', fontSize: '12px' }}>
|
||||
{t('interpretation_log.messages.additional_rows')}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<Grid container justifyContent="center" alignItems="center" style={{ height: '100%' }}>
|
||||
<Grid item>
|
||||
{hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction ? (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom align="left">
|
||||
{t('interpretation_log.messages.successful_training')}
|
||||
</Typography>
|
||||
<SidePanelHeader />
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="h6" gutterBottom align="left">
|
||||
{t('interpretation_log.messages.no_selection')}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
<div style={{ float: 'left', clear: 'both' }} ref={logEndRef} />
|
||||
</div>
|
||||
</SwipeableDrawer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user