Merge pull request #76 from amhsirak/develop

feat: browser revamp
This commit is contained in:
Karishma Shukla
2024-10-21 00:00:09 +05:30
committed by GitHub
29 changed files with 813 additions and 432 deletions

View File

@@ -90,7 +90,12 @@ export class RemoteBrowser {
*/
public initialize = async (options: RemoteBrowserOptions): Promise<void> => {
this.browser = <Browser>(await options.browser.launch(options.launchOptions));
const context = await this.browser.newContext();
const context = await this.browser.newContext(
{
viewport: { height: 400, width: 900 },
// recordVideo: { dir: 'videos/' }
}
);
this.currentPage = await context.newPage();
const blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch);
await blocker.enableBlockingInPage(this.currentPage);
@@ -275,7 +280,7 @@ export class RemoteBrowser {
if (page) {
await this.stopScreencast();
this.currentPage = page;
await this.currentPage.setViewportSize({ height: 500, width: 1280 })
await this.currentPage.setViewportSize({ height: 400, width: 900 })
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
this.socket.emit('urlChanged', this.currentPage.url());
await this.makeAndEmitScreenshot();

View File

@@ -1,15 +1,69 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { GlobalInfoProvider } from "./context/globalInfo";
import { PageWrapper } from "./pages/PageWrappper";
const theme = createTheme({
palette: {
primary: {
main: "#ff00c3",
contrastText: "#ffffff",
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
// Default styles for all buttons (optional)
textTransform: "none",
},
containedPrimary: {
// Styles for 'contained' variant with 'primary' color
'&:hover': {
backgroundColor: "#ff66d9",
},
},
outlined: {
// Apply white background for all 'outlined' variant buttons
backgroundColor: "#ffffff",
'&:hover': {
backgroundColor: "#f0f0f0", // Optional lighter background on hover
},
},
},
},
MuiLink: {
styleOverrides: {
root: {
'&:hover': {
color: "#ff00c3",
},
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
'&:hover': {
color: "#ff66d9",
},
},
},
},
},
});
function App() {
return (
<GlobalInfoProvider>
<Routes>
<Route path="/*" element={<PageWrapper />} />
</Routes>
</GlobalInfoProvider>
<ThemeProvider theme={theme}>
<GlobalInfoProvider>
<Routes>
<Route path="/*" element={<PageWrapper />} />
</Routes>
</GlobalInfoProvider>
</ThemeProvider>
);
}

View File

@@ -33,11 +33,11 @@ const defaultModalStyle = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
width: 1000,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
height: '30%',
height: '50%',
display: 'block',
overflow: 'scroll',
padding: '5px 25px 10px 25px',

View File

@@ -1,86 +1,73 @@
import styled from "styled-components";
import { Stack } from "@mui/material";
export const Loader = () => {
return (
<Stack direction="column" sx={{ margin: "30px 0px 291px 0px" }}>
<StyledLoader />
<StyledParagraph>
Loading...
</StyledParagraph>
</Stack>
);
interface LoaderProps {
text: string;
}
export const Loader: React.FC<LoaderProps> = ({ text }) => {
return (
<Stack direction="column" sx={{ margin: "30px 0px", alignItems: "center" }}>
<DotsContainer>
<Dot />
<Dot />
<Dot />
<Dot />
</DotsContainer>
<StyledParagraph>{text}</StyledParagraph>
</Stack>
);
};
const StyledParagraph = styled.p`
font-size: x-large;
font-size: large;
font-family: inherit;
color: #1976d2;
display: grid;
justify-content: center;
color: #333;
margin-top: 20px;
`;
const StyledLoader = styled.div`
const DotsContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
gap: 15px; /* Space between dots */
`;
const Dot = styled.div`
width: 15px;
height: 15px;
background-color: #ff00c3;
border-radius: 50%;
color: #1976d2;
font-size: 11px;
text-indent: -99999em;
margin: 55px auto;
position: relative;
width: 10em;
height: 10em;
box-shadow: inset 0 0 0 1em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
&:before {
position: absolute;
content: '';
border-radius: 50%;
width: 5.2em;
height: 10.2em;
background: #ffffff;
border-radius: 10.2em 0 0 10.2em;
top: -0.1em;
left: -0.1em;
-webkit-transform-origin: 5.1em 5.1em;
transform-origin: 5.1em 5.1em;
-webkit-animation: load2 2s infinite ease 1.5s;
animation: load2 2s infinite ease 1.5s;
animation: intensePulse 1.2s infinite ease-in-out both, bounceAndPulse 1.5s infinite ease-in-out;
&:nth-child(1) {
animation-delay: -0.3s;
}
&:after {
position: absolute;
content: '';
border-radius: 50%;
width: 5.2em;
height: 10.2em;
background: #ffffff;
border-radius: 0 10.2em 10.2em 0;
top: -0.1em;
left: 4.9em;
-webkit-transform-origin: 0.1em 5.1em;
transform-origin: 0.1em 5.1em;
-webkit-animation: load2 2s infinite ease;
animation: load2 2s infinite ease;
&:nth-child(2) {
animation-delay: -0.2s;
}
@-webkit-keyframes load2 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
&:nth-child(3) {
animation-delay: -0.1s;
}
&:nth-child(4) {
animation-delay: 0s;
}
@keyframes bounceAndPulse {
0%, 100% {
transform: translateY(0) scale(1);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
50% {
transform: translateY(-10px) scale(1.3);
}
}
@keyframes load2 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
@keyframes intensePulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(255, 0, 195, 0.7);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
50% {
box-shadow: 0 0 15px 10px rgba(255, 0, 195, 0.3);
}
}
`;

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
export const NavBarButton = styled.button<{ disabled: boolean }>`
margin-left: 5px;
margin-left: 10px;
margin-right: 5px;
padding: 0;
border: none;
@@ -26,7 +26,7 @@ export const NavBarButton = styled.button<{ disabled: boolean }>`
export const UrlFormButton = styled.button`
position: absolute;
top: 0;
right: 0;
right: 10px;
padding: 0;
border: none;
background-color: transparent;

View File

@@ -45,34 +45,35 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
}, [getText, getList]);
const onMouseEvent = useCallback((event: MouseEvent) => {
if (socket) {
const coordinates = {
x: event.clientX,
y: event.clientY,
}
if (socket && canvasRef.current) {
// Get the canvas bounding rectangle
const rect = canvasRef.current.getBoundingClientRect();
const clickCoordinates = {
x: event.clientX - rect.left, // Use relative x coordinate
y: event.clientY - rect.top, // Use relative y coordinate
};
switch (event.type) {
case 'mousedown':
const clickCoordinates = getMappedCoordinates(event, canvasRef.current, width, height);
if (getTextRef.current === true) {
console.log('Capturing Text...');
} else if (getListRef.current === true){
} else if (getListRef.current === true) {
console.log('Capturing List...');
}else {
} else {
socket.emit('input:mousedown', clickCoordinates);
}
notifyLastAction('click');
break;
case 'mousemove':
const coordinates = getMappedCoordinates(event, canvasRef.current, width, height);
if (lastMousePosition.current.x !== coordinates.x ||
lastMousePosition.current.y !== coordinates.y) {
if (lastMousePosition.current.x !== clickCoordinates.x ||
lastMousePosition.current.y !== clickCoordinates.y) {
lastMousePosition.current = {
x: coordinates.x,
y: coordinates.y,
x: clickCoordinates.x,
y: clickCoordinates.y,
};
socket.emit('input:mousemove', {
x: coordinates.x,
y: coordinates.y,
x: clickCoordinates.x,
y: clickCoordinates.y,
});
notifyLastAction('move');
}
@@ -140,8 +141,8 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
<canvas
tabIndex={0}
ref={canvasRef}
height={720}
width={1280}
height={400}
width={900}
/>
);

View File

@@ -0,0 +1,121 @@
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import { PaginationType, useActionContext, LimitType } from '../../context/browserActions';
import { Typography, FormControlLabel, Checkbox, Box } from '@mui/material';
const CustomBoxContainer = styled.div`
position: relative;
min-width: 250px;
width: auto;
min-height: 100px;
height: auto;
border: 2px solid #ff00c3;
background-color: white;
margin: 30px 15px;
`;
const Triangle = styled.div`
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-bottom: 20px solid #ff00c3;
`;
const Content = styled.div`
padding: 20px;
text-align: left;
`;
const ActionDescriptionBox = () => {
const { getText, getScreenshot, getList, captureStage } = useActionContext();
const messages = [
{
stage: 'initial' as const,
text: 'Select the list you want to extract along with the texts inside it',
},
{
stage: 'pagination' as const,
text: 'Select how the robot can capture the rest of the list',
},
{
stage: 'limit' as const,
text: 'Choose the number of items to extract',
},
{
stage: 'complete' as const,
text: 'Capture is complete',
},
];
const renderActionDescription = () => {
if (getText) {
return (
<>
<Typography variant="subtitle2" gutterBottom>Capture Text</Typography>
<Typography variant="body2" gutterBottom>Hover over the texts you want to extract and click to select them</Typography>
</>
)
} else if (getScreenshot) {
return (
<>
<Typography variant="subtitle2" gutterBottom>Capture Screenshot</Typography>
<Typography variant="body2" gutterBottom>Capture a partial or full page screenshot of the current page. </Typography>
</>
)
} else if (getList) {
return (
<>
<Typography variant="subtitle2" gutterBottom>Capture List</Typography>
<Typography variant="body2" gutterBottom>
Hover over the list you want to extract. Once selected, you can hover over all texts inside the list you selected. Click to select them.
</Typography>
<Box>
{messages.map(({ stage, text }, index) => (
<FormControlLabel
key={stage}
control={
<Checkbox
checked={
(stage === 'initial' && captureStage !== '') || // Checked if captureStage is at least 'initial'
(stage === 'pagination' && (captureStage === 'pagination' || captureStage === 'limit' || captureStage === 'complete')) || // captureStage is at least 'pagination'
(stage === 'limit' && (captureStage === 'limit' || captureStage === 'complete')) || // captureStage is at least 'limit'
(stage === 'complete' && captureStage === 'complete') // captureStage is 'complete'
}
disabled
/>
}
label={
<Typography variant="body2" gutterBottom>{text}</Typography>
}
/>
))}
</Box>
</>
);
} else {
return (
<>
<Typography variant="subtitle2" gutterBottom>What data do you want to extract?</Typography>
<Typography variant="body2" gutterBottom>A robot is designed to perform one action at a time. You can choose any of the options below.</Typography>
</>
)
}
}
return (
<CustomBoxContainer>
<Triangle />
<Content>
{renderActionDescription()}
</Content>
</CustomBoxContainer>
);
};
export default ActionDescriptionBox;

View File

@@ -16,7 +16,7 @@ import { useGlobalInfoStore } from '../../context/globalInfo';
const StyledNavBar = styled.div<{ browserWidth: number }>`
display: flex;
padding: 5px;
padding: 12px 0px;
background-color: #f6f6f6;
width: ${({ browserWidth }) => browserWidth}px;
`;

View File

@@ -0,0 +1,49 @@
import React from 'react'
import { Paper, Grid, IconButton, Button, Box } from '@mui/material';
import { SaveRecording } from "./SaveRecording";
import { Circle, Add, Logout, Clear } from "@mui/icons-material";
import { useGlobalInfoStore } from '../../context/globalInfo';
import { stopRecording } from "../../api/recording";
import { Link, useLocation, useNavigate } from 'react-router-dom';
const BrowserRecordingSave = () => {
const { recordingName, browserId, setBrowserId, notify } = useGlobalInfoStore();
const navigate = useNavigate();
const goToMainMenu = async () => {
if (browserId) {
await stopRecording(browserId);
notify('warning', 'Current Recording was terminated');
setBrowserId(null);
}
navigate('/');
};
return (
<Grid container>
<Grid item xs={12} md={3} lg={3}>
<div style={{
marginTop: '10px',
marginLeft: '10px',
color: 'white',
position: 'absolute',
background: '#ff00c3',
border: 'none',
padding: '7.5px',
width: 'calc(100% - 20px)', // Ensure it takes full width but with padding
overflow: 'hidden',
display: 'flex',
justifyContent: 'space-between',
}}>
<Button onClick={goToMainMenu} variant="outlined" sx={{ marginLeft: "20px" }} size="small" color="error">
Discard
</Button>
<SaveRecording fileName={recordingName} />
</div>
</Grid>
</Grid>
);
}
export default BrowserRecordingSave

View File

@@ -46,6 +46,9 @@ export const BrowserTabs = (
<Tab
key={`tab-${index}`}
id={`tab-${index}`}
sx={{
background: 'white',
}}
icon={<CloseButton closeTab={() => {
tabWasClosed = true;
handleCloseTab(index);
@@ -64,7 +67,7 @@ export const BrowserTabs = (
})}
</Tabs>
</Box>
<AddButton handleClick={handleAddNewTab} />
<AddButton handleClick={handleAddNewTab} style={{ background: 'white' }} />
</Box>
);
}

View File

@@ -16,10 +16,10 @@ interface CollapsibleRowProps {
abortRunHandler: () => void;
runningRecordingName: string;
}
export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler,runningRecordingName }: CollapsibleRowProps) => {
export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName }: CollapsibleRowProps) => {
const [open, setOpen] = useState(isOpen);
const logEndRef = useRef<HTMLDivElement|null>(null);
const logEndRef = useRef<HTMLDivElement | null>(null);
const scrollToLogBottom = () => {
if (logEndRef.current) {
@@ -52,7 +52,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
</TableCell>
{columns.map((column) => {
// @ts-ignore
const value : any = row[column.id];
const value: any = row[column.id];
if (value !== undefined) {
return (
<TableCell key={column.id} align={column.align}>
@@ -64,14 +64,14 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
case 'delete':
return (
<TableCell key={column.id} align={column.align}>
<IconButton aria-label="add" size= "small" onClick={() => {
<IconButton aria-label="add" size="small" onClick={() => {
deleteRunFromStorage(`${row.runId}`).then((result: boolean) => {
if (result) {
handleDelete();
}
})
}} sx={{'&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}>
<DeleteForever/>
}}>
<DeleteForever />
</IconButton>
</TableCell>
);
@@ -85,7 +85,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit>
<RunContent row={row} abortRunHandler={handleAbort} currentLog={currentLog}
logEndRef={logEndRef} interpretationInProgress={runningRecordingName === row.name} />
logEndRef={logEndRef} interpretationInProgress={runningRecordingName === row.name} />
</Collapse>
</TableCell>
</TableRow>

View File

@@ -1,5 +1,5 @@
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
import { PauseCircle, PlayCircle, StopCircle } from "@mui/icons-material";
import { Box, Button, Stack, Typography } 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";
@@ -20,10 +20,10 @@ interface InterpretationInfo {
const interpretationInfo: InterpretationInfo = {
running: false,
isPaused: false,
}
};
export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsProps) => {
const [info, setInfo] = React.useState<InterpretationInfo>(interpretationInfo);
const [info, setInfo] = useState<InterpretationInfo>(interpretationInfo);
const [decisionModal, setDecisionModal] = useState<{
pair: WhereWhatPair | null,
actionType: string,
@@ -44,52 +44,47 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
const breakpointHitHandler = useCallback(() => {
setInfo({ running: false, isPaused: true });
notify('warning', 'Please restart the interpretation, after updating the recording');
notify('warning', 'Please restart the interpretation after updating the recording');
enableStepping(true);
}, [info, enableStepping]);
}, [enableStepping]);
const decisionHandler = useCallback(
({ pair, actionType, lastData }
: { pair: WhereWhatPair | null, actionType: string, lastData: { selector: string, action: string, tagName: string, innerText: string } }) => {
({ 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) => {
return {
pair,
actionType,
selector,
action,
tagName,
innerText,
open: true,
}
})
}, [decisionModal]);
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 = () => {
switch (decisionModal.actionType) {
case 'customAction':
return (
<React.Fragment>
if (decisionModal.actionType === 'customAction') {
return (
<>
<Typography>
Do you want to use your previous selection as a condition for performing this action?
</Typography>
<Box style={{ marginTop: '4px' }}>
<Typography>
Do you want to use your previous selection as a condition for performing this action?
Your previous action was: <b>{decisionModal.action}</b>, on an element with text <b>{decisionModal.innerText}</b>
</Typography>
<Box style={{ marginTop: '4px' }}>
<Typography>Your previous action was:
<b>{decisionModal.action.charAt(0).toUpperCase() + decisionModal.action.slice(1)} </b>,
on an element with text
<b>{decisionModal.innerText} </b>
</Typography>
</Box>
</React.Fragment>);
default: return null;
</Box>
</>
);
}
}
return null;
};
useEffect(() => {
if (socket) {
@@ -101,15 +96,11 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
socket?.off('finished', finishedHandler);
socket?.off('breakpointHit', breakpointHitHandler);
socket?.off('decision', decisionHandler);
}
};
}, [socket, finishedHandler, breakpointHitHandler]);
const handlePlay = async () => {
if (info.isPaused) {
socket?.emit("resume");
setInfo({ running: true, isPaused: false });
enableStepping(false);
} else {
if (!info.running) {
setInfo({ ...info, running: true });
const finished = await interpretCurrentRecording();
setInfo({ ...info, running: false });
@@ -121,40 +112,39 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
}
};
// 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();
};
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);
}
};
*/
return (
<Stack direction="row" spacing={3}
sx={{ marginTop: '10px', marginBottom: '5px', justifyContent: 'space-evenly', }} >
<IconButton disabled={!info.running} sx={{ display: 'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}
aria-label="pause" size="small" title="Pause" onClick={handlePause}>
<PauseCircle sx={{ fontSize: 30, justifySelf: 'center' }} />
Pause
</IconButton>
<IconButton disabled={info.running} sx={{ display: 'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}
aria-label="play" size="small" title="Play" onClick={handlePlay}>
<PlayCircle sx={{ fontSize: 30, justifySelf: 'center' }} />
{info.isPaused ? 'Resume' : 'Start'}
</IconButton>
<IconButton disabled={!info.running && !info.isPaused} sx={{ display: 'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}
aria-label="stop" size="small" title="Stop" onClick={handleStop}>
<StopCircle sx={{ fontSize: 30, justifySelf: 'center' }} />
Stop
</IconButton>
<GenericModal onClose={() => { }} isOpen={decisionModal.open} canBeClosed={false}
<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 ? 'Extracting data...almost there!' : 'Get Preview of Output Data'}
</Button>
<GenericModal
onClose={() => { }}
isOpen={decisionModal.open}
canBeClosed={false}
modalStyle={{
position: 'absolute',
top: '50%',
@@ -168,15 +158,14 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
display: 'block',
overflow: 'scroll',
padding: '5px 25px 10px 25px',
}}>
}}
>
<div style={{ padding: '15px' }}>
<HelpIcon />
{
handleDescription()
}
{handleDescription()}
<div style={{ float: 'right' }}>
<Button onClick={() => handleDecision(true)} color='success'>yes</Button>
<Button onClick={() => handleDecision(false)} color='error'>no</Button>
<Button onClick={() => handleDecision(true)} color='success'>Yes</Button>
<Button onClick={() => handleDecision(false)} color='error'>No</Button>
</div>
</div>
</GenericModal>

View File

@@ -3,7 +3,7 @@ import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import Typography from '@mui/material/Typography';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import { Button, TextField } from '@mui/material';
import { Button, TextField, Grid } from '@mui/material';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
@@ -19,6 +19,8 @@ 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 { SidePanelHeader } from './SidePanelHeader';
import { useGlobalInfoStore } from '../../context/globalInfo';
interface InterpretationLogProps {
isOpen: boolean;
@@ -27,7 +29,6 @@ interface InterpretationLogProps {
export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, setIsOpen }) => {
const [log, setLog] = useState<string>('');
const [selectedOption, setSelectedOption] = useState<string>('10');
const [customValue, setCustomValue] = useState('');
const [tableData, setTableData] = useState<any[]>([]);
@@ -35,6 +36,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
const { width } = useBrowserDimensionsStore();
const { socket } = useSocketStore();
const { currentWorkflowActionsState } = useGlobalInfoStore();
const toggleDrawer = (newOpen: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
if (
@@ -83,10 +85,6 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
scrollLogToBottom();
}, [log, scrollLogToBottom]);
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedOption(event.target.value);
};
const handleCustomValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCustomValue(event.target.value);
};
@@ -105,107 +103,108 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
// 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]);
return (
<div>
<button
onClick={toggleDrawer(true)}
style={{
color: 'white',
background: '#3f4853',
border: 'none',
padding: '10px 20px',
width: 1280,
textAlign: 'left'
}}>
Interpretation Log
</button>
<SwipeableDrawer
anchor="bottom"
open={isOpen}
onClose={toggleDrawer(false)}
onOpen={toggleDrawer(true)}
PaperProps={{
sx: {
background: 'white',
color: 'black',
padding: '10px',
height: 720,
width: width - 10,
display: 'flex'
}
}}
>
<Typography variant="h6" gutterBottom>
<StorageIcon /> Output Data Preview
</Typography>
<div style={{
height: '50vh',
overflow: 'none',
padding: '10px',
}}>
{/* <Highlight className="javascript">
{log}
</Highlight> */}
{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.map((row, index) => (
<TableRow key={index}>
{columns.map((column) => (
<TableCell key={column}>{row[column]}</TableCell>
<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',
},
}}
>
Output Data Preview
</Button>
<SwipeableDrawer
anchor="bottom"
open={isOpen}
onClose={toggleDrawer(false)}
onOpen={toggleDrawer(true)}
PaperProps={{
sx: {
background: 'white',
color: 'black',
padding: '10px',
height: 500,
width: width - 10,
display: 'flex',
},
}}
>
<Typography variant="h6" gutterBottom>
<StorageIcon /> Output Data Preview
</Typography>
<div
style={{
height: '50vh',
overflow: 'none',
padding: '10px',
}}
>
{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.map((row, index) => (
<TableRow key={index}>
{columns.map((column) => (
<TableCell key={column}>{row[column]}</TableCell>
))}
</TableRow>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '200px' }}>
<FormControl>
<FormLabel>
<h4>What is the maximum number of rows you want to extract?</h4>
</FormLabel>
<RadioGroup row value={selectedOption} onChange={handleRadioChange} sx={{ width: '500px' }}>
<FormControlLabel value="10" control={<Radio />} label="10" />
<FormControlLabel value="100" control={<Radio />} label="100" />
<FormControlLabel value="custom" control={<Radio />} label="Custom" />
{selectedOption === 'custom' && (
<TextField
type="number"
value={customValue}
onChange={handleCustomValueChange}
placeholder="Enter number"
sx={{
marginLeft: '10px',
marginTop: '-3px',
'& input': {
padding: '10px',
},
}}
/>
)}
</RadioGroup>
</FormControl>
<div style={{ paddingBottom: '40px' }}>
<h4>How can we find the next item?</h4>
<p>Select and review the pagination setting this webpage is using</p>
<Button variant="outlined">
Select Pagination Setting
</Button>
</TableBody>
</Table>
</TableContainer>
) : (
<Grid container justifyContent="center" alignItems="center" style={{ height: '100%' }}>
<Grid item>
{hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction ? (
<>
<Typography variant="h6" gutterBottom align="left">
You've successfully trained the robot to perform actions! Click on the button below to get a preview of the data your robot will extract.
</Typography>
<SidePanelHeader />
</>
) : (
<Typography variant="h6" gutterBottom align="left">
It looks like you have not selected anything for extraction yet. Once you do, the robot will show a preview of your selections here.
</Typography>
)}
</Grid>
</Grid>
)}
<div style={{ float: 'left', clear: 'both' }} ref={logEndRef} />
</div>
</div>
<div style={{ float: "left", clear: "both" }}
ref={logEndRef} />
</div>
</SwipeableDrawer>
</div>
);
</SwipeableDrawer>
</Grid>
</Grid>
);
}

View File

@@ -3,7 +3,7 @@ import axios from 'axios';
import styled from "styled-components";
import { stopRecording } from "../../api/recording";
import { useGlobalInfoStore } from "../../context/globalInfo";
import { Button, IconButton } from "@mui/material";
import { Button, IconButton, Typography } from "@mui/material";
import { RecordingIcon } from "../atoms/RecorderIcon";
import { SaveRecording } from "./SaveRecording";
import { Circle, Add, Logout, Clear } from "@mui/icons-material";
@@ -121,7 +121,9 @@ export const NavBar: React.FC<NavBarProps> = ({ newRecording, recordingName, isR
<Logout sx={{ marginRight: '5px' }} />
Logout</IconButton>
</>
) : <IconButton sx={{
) :
<>
<IconButton sx={{
width: '140px',
borderRadius: '5px',
padding: '8px',
@@ -137,23 +139,20 @@ export const NavBar: React.FC<NavBarProps> = ({ newRecording, recordingName, isR
}} onClick={goToMainMenu}>
<Clear sx={{ marginRight: '5px' }} />
Discard</IconButton>
}
{
recordingLength > 0
? <SaveRecording fileName={recordingName} />
: null
<SaveRecording fileName={recordingName} />
</>
}
</div>
<GenericModal isOpen={isModalOpen} onClose={() => setModalOpen(false)}>
<div style={{ padding: '20px' }}>
<h2>Enter URL</h2>
<Typography variant="h6" gutterBottom>Enter URL To Extract Data</Typography>
<TextField
label="URL"
variant="outlined"
fullWidth
value={recordingUrl}
onChange={(e: any) => setRecordingUrl(e.target.value)}
style={{ marginBottom: '20px' }}
style={{ marginBottom: '20px', marginTop: '20px' }}
/>
<Button
variant="contained"
@@ -161,7 +160,7 @@ export const NavBar: React.FC<NavBarProps> = ({ newRecording, recordingName, isR
onClick={startRecording}
disabled={!recordingUrl}
>
Submit & Start Recording
Start Training Robot
</Button>
</div>
</GenericModal>

View File

@@ -200,7 +200,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
fetchRecordings();
}
})
}} sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
}}>
<DeleteForever />
</IconButton>
</TableCell>
@@ -239,7 +239,7 @@ const InterpretButton = ({ handleInterpret }: InterpretButtonProps) => {
<IconButton aria-label="add" size="small" onClick={() => {
handleInterpret();
}}
sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
>
<PlayCircle />
</IconButton>
)
@@ -255,7 +255,7 @@ const ScheduleButton = ({ handleSchedule }: ScheduleButtonProps) => {
<IconButton aria-label="add" size="small" onClick={() => {
handleSchedule();
}}
sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
>
<Schedule />
</IconButton>
)
@@ -270,7 +270,7 @@ const IntegrateButton = ({ handleIntegrate }: IntegrateButtonProps) => {
<IconButton aria-label="add" size="small" onClick={() => {
handleIntegrate();
}}
sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
>
<LinkIcon />
</IconButton>
)

View File

@@ -69,22 +69,9 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
return (
<div>
<IconButton sx={{
width: '140px',
background: 'green',
color: 'white',
'&:hover': { background: 'green', color: 'white' },
padding: '13px',
marginRight: '10px',
borderRadius: '5px',
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
fontWeight: '500',
fontSize: '0.875rem',
lineHeight: '1.75',
letterSpacing: '0.02857em',
}} onClick={() => setOpenModal(true)}>
<DoneAll sx={{ marginRight: '5px' }} /> Finish
</IconButton>
<Button onClick={() => setOpenModal(true)} variant="outlined" sx={{ marginRight: '20px' }} size="small" color="success">
Finish
</Button>
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
<form onSubmit={handleSaveRecording} style={{ paddingTop: '20px', display: 'flex', flexDirection: 'column' }} >

View File

@@ -12,18 +12,18 @@ import { useGlobalInfoStore } from "../../context/globalInfo";
export const SidePanelHeader = () => {
const [steppingIsDisabled, setSteppingIsDisabled] = useState(true);
const [steppingIsDisabled, setSteppingIsDisabled] = useState(true);
const { socket } = useSocketStore();
const { socket } = useSocketStore();
const handleStep = () => {
socket?.emit('step');
};
const handleStep = () => {
socket?.emit('step');
};
return (
<div style={{width: 'inherit'}}>
<InterpretationButtons enableStepping={(isPaused) => setSteppingIsDisabled(!isPaused)}/>
<Button
return (
<div style={{ width: 'inherit' }}>
<InterpretationButtons enableStepping={(isPaused) => setSteppingIsDisabled(!isPaused)} />
{/* <Button
variant='outlined'
disabled={steppingIsDisabled}
onClick={handleStep}
@@ -31,8 +31,8 @@ export const SidePanelHeader = () => {
>
step
<FastForward/>
</Button>
<hr/>
</div>
);
</Button> */}
<hr />
</div>
);
};

View File

@@ -62,6 +62,7 @@ export const UrlForm = ({
type="text"
value={address}
onChange={onChange}
readOnly
/>
<UrlFormButton type="submit">
<KeyboardArrowRightIcon />

View File

@@ -7,18 +7,20 @@ import { BrowserTabs } from "../molecules/BrowserTabs";
import { useSocketStore } from "../../context/socket";
import { getCurrentTabs, getCurrentUrl, interpretCurrentRecording } from "../../api/recording";
import { Box } from '@mui/material';
import { InterpretationLog } from "../molecules/InterpretationLog";
// TODO: Tab !show currentUrl after recordingUrl global state
export const BrowserContent = () => {
const { width } = useBrowserDimensionsStore();
const { socket } = useSocketStore();
const { width } = useBrowserDimensionsStore();
const { socket } = useSocketStore();
const [tabs, setTabs] = useState<string[]>(['current']);
const [tabIndex, setTabIndex] = React.useState(0);
const [tabs, setTabs] = useState<string[]>(['current']);
const [tabIndex, setTabIndex] = React.useState(0);
const [showOutputData, setShowOutputData] = useState(false);
const handleChangeIndex = useCallback((index: number) => {
setTabIndex(index);
}, [tabIndex])
const handleChangeIndex = useCallback((index: number) => {
setTabIndex(index);
}, [tabIndex])
const handleCloseTab = useCallback((index: number) => {
// the tab needs to be closed on the backend
@@ -51,17 +53,17 @@ export const BrowserContent = () => {
handleChangeIndex(tabs.length);
}, [socket, tabs]);
const handleNewTab = useCallback((tab: string) => {
// Adds a new tab to the end of the tabs array and shifts focus
setTabs((prevState) => [...prevState, tab]);
// changes focus on the new tab - same happens in the remote browser
handleChangeIndex(tabs.length);
handleTabChange(tabs.length);
}, [tabs]);
const handleNewTab = useCallback((tab: string) => {
// Adds a new tab to the end of the tabs array and shifts focus
setTabs((prevState) => [...prevState, tab]);
// changes focus on the new tab - same happens in the remote browser
handleChangeIndex(tabs.length);
handleTabChange(tabs.length);
}, [tabs]);
const handleTabChange = useCallback((index: number) => {
// page screencast and focus needs to be changed on backend
socket?.emit('changeTab', index);
socket?.emit('changeTab', index);
}, [socket]);
const handleUrlChanged = (url: string) => {
@@ -91,18 +93,18 @@ export const BrowserContent = () => {
handleCloseTab(index);
}, [handleCloseTab])
useEffect(() => {
if (socket) {
socket.on('newTab', handleNewTab);
socket.on('tabHasBeenClosed', tabHasBeenClosedHandler);
}
return () => {
if (socket) {
socket.off('newTab', handleNewTab);
socket.off('tabHasBeenClosed', tabHasBeenClosedHandler);
}
}
}, [socket, handleNewTab])
useEffect(() => {
if (socket) {
socket.on('newTab', handleNewTab);
socket.on('tabHasBeenClosed', tabHasBeenClosedHandler);
}
return () => {
if (socket) {
socket.off('newTab', handleNewTab);
socket.off('tabHasBeenClosed', tabHasBeenClosedHandler);
}
}
}, [socket, handleNewTab])
useEffect(() => {
getCurrentTabs().then((response) => {
@@ -115,7 +117,7 @@ export const BrowserContent = () => {
}, [])
return (
<>
<div id="browser">
<BrowserTabs
tabs={tabs}
handleTabChange={handleTabChange}
@@ -129,8 +131,8 @@ export const BrowserContent = () => {
browserWidth={900}
handleUrlChanged={handleUrlChanged}
/>
<BrowserWindow/>
</>
<BrowserWindow />
</div>
);
}

View File

@@ -64,7 +64,7 @@ export const BrowserWindow = () => {
const [paginationSelector, setPaginationSelector] = useState<string>('');
const { socket } = useSocketStore();
const { width, height } = useBrowserDimensionsStore();
//const { width, height } = useBrowserDimensionsStore();
const { getText, getList, paginationMode, paginationType, limitMode } = useActionContext();
const { addTextStep, addListStep } = useBrowserSteps();
@@ -316,7 +316,7 @@ export const BrowserWindow = () => {
return (
<div onClick={handleClick}>
<div onClick={handleClick} style={{ width: '900px'}} id="browser-window">
{
getText === true || getList === true ? (
<GenericModal
@@ -356,20 +356,22 @@ export const BrowserWindow = () => {
</GenericModal>
) : null
}
<div style={{ height: '400px', overflow: 'hidden' }}>
{((getText === true || getList === true) && !showAttributeModal && highlighterData?.rect != null && highlighterData?.rect.top != null) && canvasRef?.current ?
<Highlighter
unmodifiedRect={highlighterData?.rect}
displayedSelector={highlighterData?.selector}
width={width}
height={height}
width={900}
height={400}
canvasRect={canvasRef.current.getBoundingClientRect()}
/>
: null}
<Canvas
onCreateRef={setCanvasReference}
width={width}
height={height}
width={900}
height={400}
/>
</div>
</div>
);
};
@@ -383,7 +385,7 @@ const drawImage = (image: string, canvas: HTMLCanvasElement): void => {
img.src = image;
img.onload = () => {
URL.revokeObjectURL(img.src);
ctx?.drawImage(img, 0, 0, 1280, 720);
ctx?.drawImage(img, 0, 0, 900, 400);
};
};

View File

@@ -105,7 +105,7 @@ export const LeftSidePanel = (
flexDirection: 'column',
}}
>
<SidePanelHeader />
{/* <SidePanelHeader /> */}
<TabContext value={tab}>
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)}>
<Tab label="Recording" value='recording' />

View File

@@ -21,6 +21,7 @@ import RadioGroup from '@mui/material/RadioGroup';
import { emptyWorkflow } from "../../shared/constants";
import { getActiveWorkflow } from "../../api/workflow";
import DeleteIcon from '@mui/icons-material/Delete';
import ActionDescriptionBox from '../molecules/ActionDescriptionBox';
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
getActiveWorkflow(id).then(
@@ -52,11 +53,10 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const [showCaptureList, setShowCaptureList] = useState(true);
const [showCaptureScreenshot, setShowCaptureScreenshot] = useState(true);
const [showCaptureText, setShowCaptureText] = useState(true);
const [captureStage, setCaptureStage] = useState<'initial' | 'pagination' | 'limit' | 'complete'>('initial');
const [hoverStates, setHoverStates] = useState<{ [id: string]: boolean }>({});
const { lastAction, notify } = useGlobalInfoStore();
const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, getList, startGetList, stopGetList, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode } = useActionContext();
const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState } = useGlobalInfoStore();
const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, getList, startGetList, stopGetList, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode, captureStage, setCaptureStage } = useActionContext();
const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField } = useBrowserSteps();
const { id, socket } = useSocketStore();
@@ -87,7 +87,6 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
useEffect(() => {
const hasPairs = workflow.workflow.length > 0;
if (!hasPairs) {
setShowCaptureList(true);
setShowCaptureScreenshot(true);
@@ -96,19 +95,25 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
}
const hasScrapeListAction = workflow.workflow.some(pair =>
pair.what.some(action => action.action === "scrapeList")
pair.what.some(action => action.action === 'scrapeList')
);
const hasScreenshotAction = workflow.workflow.some(pair =>
pair.what.some(action => action.action === "screenshot")
pair.what.some(action => action.action === 'screenshot')
);
const hasScrapeSchemaAction = workflow.workflow.some(pair =>
pair.what.some(action => action.action === "scrapeSchema")
pair.what.some(action => action.action === 'scrapeSchema')
);
setShowCaptureList(!(hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction));
setShowCaptureScreenshot(!(hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction));
setCurrentWorkflowActionsState({
hasScrapeListAction,
hasScreenshotAction,
hasScrapeSchemaAction,
});
const shouldHideActions = hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction;
setShowCaptureList(!shouldHideActions);
setShowCaptureScreenshot(!shouldHideActions);
setShowCaptureText(!(hasScrapeListAction || hasScreenshotAction));
}, [workflow]);
@@ -369,11 +374,11 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
};
return (
<Paper variant="outlined" sx={{ height: '100%', width: '100%', backgroundColor: 'white', alignItems: "center" }}>
<Paper variant="outlined" sx={{ height: '520px', width: 'auto', alignItems: "center", background: 'inherit' }} id="browser-actions">
<SimpleBox height={60} width='100%' background='lightGray' radius='0%'>
<Typography sx={{ padding: '10px' }}>Last action: {` ${lastAction}`}</Typography>
</SimpleBox>
<SidePanelHeader />
<ActionDescriptionBox />
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '15px' }}>
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" onClick={startGetList}>Capture List</Button>}
{getList && (
@@ -458,7 +463,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
</Box>
<Box>
{browserSteps.map(step => (
<Box key={step.id} sx={{ boxShadow: 5, padding: '10px', margin: '13px', borderRadius: '4px', position: 'relative', }} onMouseEnter={() => handleMouseEnter(step.id)} onMouseLeave={() => handleMouseLeave(step.id)}>
<Box key={step.id} onMouseEnter={() => handleMouseEnter(step.id)} onMouseLeave={() => handleMouseLeave(step.id)} sx={{ boxShadow: 5, padding: '10px', margin: '13px', borderRadius: '4px', position: 'relative', background: 'white' }}>
{
step.type === 'text' && (
<>
@@ -514,7 +519,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
{!confirmedTextSteps[step.id] && (
<Box display="flex" justifyContent="space-between" gap={2}>
<Button variant="contained" onClick={() => handleTextStepConfirm(step.id)} disabled={!textLabels[step.id]?.trim()}>Confirm</Button>
<Button variant="contained" onClick={() => handleTextStepDiscard(step.id)}>Discard</Button>
<Button variant="contained" color="error" onClick={() => handleTextStepDiscard(step.id)}>Discard</Button>
</Box>
)}
</>
@@ -572,6 +577,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
</Button>
<Button
variant="contained"
color="error"
onClick={() => handleListTextFieldDiscard(step.id, key)}
>
Discard

View File

@@ -1,5 +1,5 @@
export const VIEWPORT_W = 1280;
export const VIEWPORT_H = 720;
export const VIEWPORT_W = 900;
export const VIEWPORT_H = 400;
export const ONE_PERCENT_OF_VIEWPORT_W = VIEWPORT_W / 100;
export const ONE_PERCENT_OF_VIEWPORT_H = VIEWPORT_H / 100;

View File

@@ -3,6 +3,7 @@ import { useSocketStore } from './socket';
export type PaginationType = 'scrollDown' | 'scrollUp' | 'clickNext' | 'clickLoadMore' | 'none' | '';
export type LimitType = '10' | '100' | 'custom' | '';
export type CaptureStage = 'initial' | 'pagination' | 'limit' | 'complete';
interface ActionContextProps {
getText: boolean;
@@ -13,6 +14,8 @@ interface ActionContextProps {
paginationType: PaginationType;
limitType: LimitType;
customLimit: string;
captureStage: CaptureStage; // New captureStage property
setCaptureStage: (stage: CaptureStage) => void; // Setter for captureStage
startPaginationMode: () => void;
startGetText: () => void;
stopGetText: () => void;
@@ -39,17 +42,26 @@ export const ActionProvider = ({ children }: { children: ReactNode }) => {
const [paginationType, setPaginationType] = useState<PaginationType>('');
const [limitType, setLimitType] = useState<LimitType>('');
const [customLimit, setCustomLimit] = useState<string>('');
const [captureStage, setCaptureStage] = useState<CaptureStage>('initial'); // New captureStage state
const {socket} = useSocketStore();
const { socket } = useSocketStore();
const updatePaginationType = (type: PaginationType) => setPaginationType(type);
const updateLimitType = (type: LimitType) => setLimitType(type);
const updateCustomLimit = (limit: string) => setCustomLimit(limit);
const startPaginationMode = () => setPaginationMode(true);
const startPaginationMode = () => {
setPaginationMode(true);
setCaptureStage('pagination');
};
const stopPaginationMode = () => setPaginationMode(false);
const startLimitMode = () => setLimitMode(true);
const startLimitMode = () => {
setLimitMode(true);
setCaptureStage('limit');
};
const stopLimitMode = () => setLimitMode(false);
const startGetText = () => setGetText(true);
@@ -59,35 +71,38 @@ export const ActionProvider = ({ children }: { children: ReactNode }) => {
setGetList(true);
socket?.emit('setGetList', { getList: true });
}
const stopGetList = () => {
setGetList(false);
socket?.emit('setGetList', { getList: false });
setPaginationType('');
setLimitType('');
setCustomLimit('');
setCaptureStage('initial'); // Reset captureStage when stopping getList
};
const startGetScreenshot = () => setGetScreenshot(true);
const stopGetScreenshot = () => setGetScreenshot(false);
return (
<ActionContext.Provider value={{
getText,
getList,
getScreenshot,
paginationMode,
<ActionContext.Provider value={{
getText,
getList,
getScreenshot,
paginationMode,
limitMode,
paginationType,
paginationType,
limitType,
customLimit,
startGetText,
stopGetText,
startGetList,
stopGetList,
startGetScreenshot,
stopGetScreenshot,
startPaginationMode,
captureStage,
setCaptureStage,
startGetText,
stopGetText,
startGetList,
stopGetList,
startGetScreenshot,
stopGetScreenshot,
startPaginationMode,
stopPaginationMode,
startLimitMode,
stopLimitMode,
@@ -106,4 +121,4 @@ export const useActionContext = () => {
throw new Error('useActionContext must be used within an ActionProvider');
}
return context;
};
};

View File

@@ -6,9 +6,9 @@ interface BrowserDimensions {
setWidth: (newWidth: number) => void;
};
class BrowserDimensionsStore implements Partial<BrowserDimensions>{
width: number = 1280;
height: number = 720;
class BrowserDimensionsStore implements Partial<BrowserDimensions> {
width: number = 900;
height: number = 400;
};
const browserDimensionsStore = new BrowserDimensionsStore();

View File

@@ -22,6 +22,16 @@ interface GlobalInfo {
setRecordingName: (recordingName: string) => void;
recordingUrl: string;
setRecordingUrl: (recordingUrl: string) => void;
currentWorkflowActionsState: {
hasScrapeListAction: boolean;
hasScreenshotAction: boolean;
hasScrapeSchemaAction: boolean;
};
setCurrentWorkflowActionsState: (actionsState: {
hasScrapeListAction: boolean;
hasScreenshotAction: boolean;
hasScrapeSchemaAction: boolean;
}) => void;
};
class GlobalInfoStore implements Partial<GlobalInfo> {
@@ -38,6 +48,11 @@ class GlobalInfoStore implements Partial<GlobalInfo> {
rerenderRuns = false;
recordingName = '';
recordingUrl = 'https://';
currentWorkflowActionsState = {
hasScrapeListAction: false,
hasScreenshotAction: false,
hasScrapeSchemaAction: false,
};
};
const globalInfoStore = new GlobalInfoStore();
@@ -55,6 +70,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
const [recordingId, setRecordingId] = useState<string | null>(globalInfoStore.recordingId);
const [recordingName, setRecordingName] = useState<string>(globalInfoStore.recordingName);
const [recordingUrl, setRecordingUrl] = useState<string>(globalInfoStore.recordingUrl);
const [currentWorkflowActionsState, setCurrentWorkflowActionsState] = useState(globalInfoStore.currentWorkflowActionsState);
const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => {
setNotification({ severity, message, isOpen: true });
@@ -93,6 +109,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
setRecordingName,
recordingUrl,
setRecordingUrl,
currentWorkflowActionsState,
setCurrentWorkflowActionsState,
}}
>
{children}

View File

@@ -5,12 +5,18 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
scrollbar-gutter: stable;
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
scrollbar-gutter: stable;
overflow-y: auto;
}
html {
width: 100%;
height: 100%;
overflow-y: auto;
}
code {
@@ -18,6 +24,124 @@ code {
monospace;
}
html {
overflow-y:scroll;
#browser-actions {
right: 0;
overflow-x: hidden;
}
#browser-recorder {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
}
#browser-content {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
transform: scale(1); /* Ensure no scaling */
transform-origin: top left; /* Keep the position fixed */
}
#browser {
}
#browser-window {
overflow-y: auto;
height: 100%;
}
.right-side-panel {
margin: 0;
transform: scale(1);
transform-origin: top left;
overflow: hidden;
position: relative;
}
/* For laptops (between 1024px and 1440px) */
@media (min-width: 1024px) and (max-width: 1440px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 0.6rem);
margin: 0.3rem;
}
}
/* For desktops (between 1441px and 1920px) */
@media (min-width: 1441px) and (max-width: 1500px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem;
}
}
@media (min-width: 1501px) and (max-width: 1700px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 8rem;
}
}
@media (min-width: 1701px) and (max-width: 1800px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 14rem;
}
}
@media (min-width: 1801px) and (max-width: 1900px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 18.5rem;
}
}
@media (min-width: 1900px) and (max-width: 1920px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 20rem;
}
}
/* For very large desktops (greater than 1920px) */
@media (min-width: 1921px) and (max-width: 2000px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 20rem;
}
}
@media (min-width: 2001px) and (max-width: 2500px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 24rem;
}
}
@media (min-width: 2501px) and (max-width: 2999px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 40rem;
}
}
@media (min-width: 3000px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 55rem;
}
}

View File

@@ -57,7 +57,11 @@ export const PageWrapper = () => {
<AuthProvider>
<SocketProvider>
<React.Fragment>
<NavBar newRecording={handleNewRecording} recordingName={recordingName} isRecording={!!browserId} />
{
!!browserId ? (
""
) : <NavBar newRecording={handleNewRecording} recordingName={recordingName} isRecording={!!browserId} />
}
<Routes>
<Route element={<UserRoute />}>
<Route path="/" element={<MainPage handleEditRecording={handleEditRecording} />} />

View File

@@ -14,6 +14,7 @@ import { useGlobalInfoStore } from "../context/globalInfo";
import { editRecordingFromStorage } from "../api/storage";
import { WhereWhatPair } from "maxun-core";
import styled from "styled-components";
import BrowserRecordingSave from '../components/molecules/BrowserRecordingSave';
interface RecordingPageProps {
recordingName?: string;
@@ -54,6 +55,17 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
useEffect(() => changeBrowserDimensions(), [isLoaded])
useEffect(() => {
document.body.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 1) 0%, rgba(232, 191, 222, 1) 100%, rgba(255, 255, 255, 1) 100%)';
document.body.style.filter = 'progid:DXImageTransform.Microsoft.gradient(startColorstr="#ffffff",endColorstr="#ffffff",GradientType=1);'
return () => {
// Cleanup the background when leaving the page
document.body.style.background = '';
document.body.style.filter = '';
};
}, []);
useEffect(() => {
let isCancelled = false;
const handleRecording = async () => {
@@ -110,35 +122,38 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
}
}, [socket, handleLoaded]);
return (
<ActionProvider>
<BrowserStepsProvider>
<div>
{isLoaded ?
<Grid container direction="row" spacing={0}>
<Grid item xs={2} ref={workflowListRef} style={{ display: "flex", flexDirection: "row" }}>
<LeftSidePanel
sidePanelRef={workflowListRef.current}
alreadyHasScrollbar={hasScrollbar}
recordingName={recordingName ? recordingName : ''}
handleSelectPairForEdit={handleSelectPairForEdit}
/>
<div id="browser-recorder">
{isLoaded ? (
<>
<Grid container direction="row" style={{ flexGrow: 1, height: '100%' }}>
<Grid item xs={12} md={9} lg={9} style={{ height: '100%', overflow: 'hidden', position: 'relative' }}>
<div style={{ height: '100%', overflow: 'auto' }}>
<BrowserContent />
<InterpretationLog isOpen={showOutputData} setIsOpen={setShowOutputData} />
</div>
</Grid>
<Grid item xs={12} md={3} lg={3} style={{ height: '100%', overflow: 'hidden' }}>
<div className="right-side-panel" style={{ height: '100%' }}>
<RightSidePanel onFinishCapture={handleShowOutputData} />
<BrowserRecordingSave />
</div>
</Grid>
</Grid>
<Grid id="browser-content" ref={browserContentRef} item xs>
<BrowserContent />
<InterpretationLog isOpen={showOutputData} setIsOpen={setShowOutputData} />
</Grid>
<Grid item xs={2}>
<RightSidePanel onFinishCapture={handleShowOutputData} />
</Grid>
</Grid>
: <Loader />}
</>
) : (
<Loader text={'Spinning up a browser just for you...'} />
)}
</div>
</BrowserStepsProvider>
</ActionProvider>
);
};
const RecordingPageWrapper = styled.div`
position: relative;
width: 100vw;