Merge branch 'develop' into ui-fix

This commit is contained in:
Rohit
2025-01-08 12:50:46 +05:30
committed by GitHub
67 changed files with 7978 additions and 1189 deletions

View File

@@ -19,6 +19,7 @@ import styled from 'styled-components';
import axios from 'axios';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { apiUrl } from '../../apiConfig';
import { useTranslation } from 'react-i18next';
const Container = styled(Box)`
display: flex;
@@ -29,24 +30,21 @@ const Container = styled(Box)`
`;
const ApiKeyManager = () => {
const { t } = useTranslation();
const [apiKey, setApiKey] = useState<string | null>(null);
const [apiKeyName, setApiKeyName] = useState<string>('Maxun API Key');
const [apiKeyName, setApiKeyName] = useState<string>(t('apikey.default_name'));
const [loading, setLoading] = useState<boolean>(true);
const [showKey, setShowKey] = useState<boolean>(false);
const [copySuccess, setCopySuccess] = useState<boolean>(false);
const { notify } = useGlobalInfoStore();
useEffect(() => {
const fetchApiKey = async () => {
try {
const { data } = await axios.get(`${apiUrl}/auth/api-key`);
setApiKey(data.api_key);
} catch (error: any) {
notify('error', `Failed to fetch API Key - ${error.message}`);
notify('error', t('apikey.notifications.fetch_error', { error: error.message }));
} finally {
setLoading(false);
}
@@ -62,9 +60,9 @@ const ApiKeyManager = () => {
const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`);
setApiKey(data.api_key);
notify('success', `Generated API Key successfully`);
notify('success', t('apikey.notifications.generate_success'));
} catch (error: any) {
notify('error', `Failed to generate API Key - ${error.message}`);
notify('error', t('apikey.notifications.generate_error', { error: error.message }));
} finally {
setLoading(false);
}
@@ -75,9 +73,9 @@ const ApiKeyManager = () => {
try {
await axios.delete(`${apiUrl}/auth/delete-api-key`);
setApiKey(null);
notify('success', 'API Key deleted successfully');
notify('success', t('apikey.notifications.delete_success'));
} catch (error: any) {
notify('error', `Failed to delete API Key - ${error.message}`);
notify('error', t('apikey.notifications.delete_error', { error: error.message }));
} finally {
setLoading(false);
}
@@ -88,7 +86,7 @@ const ApiKeyManager = () => {
navigator.clipboard.writeText(apiKey);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
notify('info', 'Copied API Key successfully');
notify('info', t('apikey.notifications.copy_success'));
}
};
@@ -111,34 +109,38 @@ const ApiKeyManager = () => {
return (
<Container sx={{ alignSelf: 'flex-start' }}>
<Typography variant="h6" gutterBottom component="div" style={{ marginBottom: '20px' }}>
Manage Your API Key
{t('apikey.title')}
</Typography>
{apiKey ? (
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
<Table>
<TableHead>
<TableRow>
<TableCell>API Key Name</TableCell>
<TableCell>API Key</TableCell>
<TableCell>Actions</TableCell>
<TableCell>{t('apikey.table.name')}</TableCell>
<TableCell>{t('apikey.table.key')}</TableCell>
<TableCell>{t('apikey.table.actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>{apiKeyName}</TableCell>
<TableCell>{showKey ? `${apiKey?.substring(0, 10)}...` : '***************'}</TableCell>
<TableCell>
<Tooltip title="Copy">
<Box sx={{ fontFamily: 'monospace', width: '10ch' }}>
{showKey ? `${apiKey?.substring(0, 10)}...` : '**********'}
</Box>
</TableCell>
<TableCell>
<Tooltip title={t('apikey.actions.copy')}>
<IconButton onClick={copyToClipboard}>
<ContentCopy />
</IconButton>
</Tooltip>
<Tooltip title={showKey ? 'Hide' : 'Show'}>
<Tooltip title={showKey ? t('apikey.actions.hide') : t('apikey.actions.show')}>
<IconButton onClick={() => setShowKey(!showKey)}>
<Visibility />
</IconButton>
</Tooltip>
<Tooltip title="Delete">
<Tooltip title={t('apikey.actions.delete')}>
<IconButton onClick={deleteApiKey} color="error">
<Delete />
</IconButton>
@@ -150,9 +152,9 @@ const ApiKeyManager = () => {
</TableContainer>
) : (
<>
<Typography>You haven't generated an API key yet.</Typography>
<Typography>{t('apikey.no_key_message')}</Typography>
<Button onClick={generateApiKey} variant="contained" color="primary" sx={{ marginTop: '15px' }}>
Generate API Key
{t('apikey.generate_button')}
</Button>
</>
)}

View File

@@ -7,11 +7,13 @@ import { GenericModal } from '../atoms/GenericModal';
import { useActionContext } from '../../context/browserActions';
import { useBrowserSteps, TextStep } from '../../context/browserSteps';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { useTranslation } from 'react-i18next';
interface ElementInfo {
tagName: string;
hasOnlyText?: boolean;
isIframeContent?: boolean;
isShadowRoot?: boolean;
innerText?: string;
url?: string;
imageUrl?: string;
@@ -52,6 +54,7 @@ const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null):
};
export const BrowserWindow = () => {
const { t } = useTranslation();
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
const [screenShot, setScreenShot] = useState<string>("");
const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] } | null>(null);
@@ -66,7 +69,7 @@ export const BrowserWindow = () => {
const { socket } = useSocketStore();
const { notify } = useGlobalInfoStore();
const { getText, getList, paginationMode, paginationType, limitMode } = useActionContext();
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
const { addTextStep, addListStep } = useBrowserSteps();
const onMouseMove = (e: MouseEvent) => {
@@ -115,34 +118,81 @@ export const BrowserWindow = () => {
}, [screenShot, canvasRef, socket, screencastHandler]);
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => {
console.log("LIST SELECTOR", listSelector);
console.log("DATA SELECTOR", data.selector);
console.log("CHILD SELECTORS", data.childSelectors);
if (getList === true) {
if (listSelector) {
socket?.emit('listSelector', { selector: listSelector });
const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0;
if (limitMode) {
setHighlighterData(null);
} else if (paginationMode) {
// only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
// Only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) {
setHighlighterData(data);
} else {
setHighlighterData(null);
}
} else if (data.childSelectors && data.childSelectors.includes(data.selector)) {
// highlight only valid child elements within the listSelector
// Highlight only valid child elements within the listSelector
setHighlighterData(data);
} else {
} else if (data.elementInfo?.isIframeContent && data.childSelectors) {
// Handle pure iframe elements - similar to previous shadow DOM logic but using iframe syntax
// Check if the selector matches any iframe child selectors
const isIframeChild = data.childSelectors.some(childSelector =>
data.selector.includes(':>>') && // Iframe uses :>> for traversal
childSelector.split(':>>').some(part =>
data.selector.includes(part.trim())
)
);
setHighlighterData(isIframeChild ? data : null);
} else if (data.selector.includes(':>>') && hasValidChildSelectors) {
// Handle mixed DOM cases with iframes
// Split the selector into parts and check each against child selectors
const selectorParts = data.selector.split(':>>').map(part => part.trim());
const isValidMixedSelector = selectorParts.some(part =>
// We know data.childSelectors is defined due to hasValidChildSelectors check
data.childSelectors!.some(childSelector =>
childSelector.includes(part)
)
);
setHighlighterData(isValidMixedSelector ? data : null);
} else if (data.elementInfo?.isShadowRoot && data.childSelectors) {
// New case: Handle pure Shadow DOM elements
// Check if the selector matches any shadow root child selectors
const isShadowChild = data.childSelectors.some(childSelector =>
data.selector.includes('>>') && // Shadow DOM uses >> for piercing
childSelector.split('>>').some(part =>
data.selector.includes(part.trim())
)
);
setHighlighterData(isShadowChild ? data : null);
} else if (data.selector.includes('>>') && hasValidChildSelectors) {
// New case: Handle mixed DOM cases
// Split the selector into parts and check each against child selectors
const selectorParts = data.selector.split('>>').map(part => part.trim());
const isValidMixedSelector = selectorParts.some(part =>
// Now we know data.childSelectors is defined
data.childSelectors!.some(childSelector =>
childSelector.includes(part)
)
);
setHighlighterData(isValidMixedSelector ? data : null);
} else {
// if !valid child in normal mode, clear the highlighter
setHighlighterData(null);
}
} else {
// set highlighterData for the initial listSelector selection
}
} else {
// Set highlighterData for the initial listSelector selection
setHighlighterData(data);
}
} else {
// for non-list steps
}
} else {
// For non-list steps
setHighlighterData(data);
}
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType]);
}
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
useEffect(() => {
@@ -156,6 +206,13 @@ export const BrowserWindow = () => {
};
}, [socket, onMouseMove]);
useEffect(() => {
if (captureStage === 'initial' && listSelector) {
socket?.emit('setGetList', { getList: true });
socket?.emit('listSelector', { selector: listSelector });
}
}, [captureStage, listSelector, socket]);
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (highlighterData && canvasRef?.current) {
const canvasRect = canvasRef.current.getBoundingClientRect();
@@ -183,6 +240,7 @@ export const BrowserWindow = () => {
addTextStep('', data, {
selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
attribute
});
} else {
@@ -190,7 +248,7 @@ export const BrowserWindow = () => {
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
info: highlighterData.elementInfo
info: highlighterData.elementInfo,
});
setShowAttributeModal(true);
}
@@ -200,7 +258,7 @@ export const BrowserWindow = () => {
// Only allow selection in pagination mode if type is not empty, 'scrollDown', or 'scrollUp'
if (paginationType !== '' && paginationType !== 'scrollDown' && paginationType !== 'scrollUp' && paginationType !== 'none') {
setPaginationSelector(highlighterData.selector);
notify(`info`, `Pagination element selected successfully.`);
notify(`info`, t('browser_window.attribute_modal.notifications.pagination_select_success'));
addListStep(listSelector!, fields, currentListId || 0, { type: paginationType, selector: highlighterData.selector });
}
return;
@@ -208,7 +266,7 @@ export const BrowserWindow = () => {
if (getList === true && !listSelector) {
setListSelector(highlighterData.selector);
notify(`info`, `List selected succesfully. Select the text data for extraction.`)
notify(`info`, t('browser_window.attribute_modal.notifications.list_select_success'));
setCurrentListId(Date.now());
setFields({});
} else if (getList === true && listSelector && currentListId) {
@@ -227,6 +285,7 @@ export const BrowserWindow = () => {
selectorObj: {
selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
attribute
}
};
@@ -274,6 +333,7 @@ export const BrowserWindow = () => {
addTextStep('', data, {
selector: selectedElement.selector,
tag: selectedElement.info?.tagName,
shadow: selectedElement.info?.isShadowRoot,
attribute: attribute
});
}
@@ -286,6 +346,7 @@ export const BrowserWindow = () => {
selectorObj: {
selector: selectedElement.selector,
tag: selectedElement.info?.tagName,
shadow: selectedElement.info?.isShadowRoot,
attribute: attribute
}
};
@@ -317,7 +378,6 @@ export const BrowserWindow = () => {
}
}, [paginationMode, resetPaginationSelector]);
return (
<div onClick={handleClick} style={{ width: '900px', height: "400px" , borderRadius: '8px 8px 0px 0px '}} id="browser-window">
{

View File

@@ -8,6 +8,9 @@ import { AutoAwesome, FormatListBulleted, VpnKey, Usb, Article, CloudQueue,Code,
import { apiUrl } from "../../apiConfig";
import { useTranslation } from 'react-i18next';
import i18n from '../../i18n';
interface MainMenuProps {
@@ -17,6 +20,7 @@ interface MainMenuProps {
export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => {
const theme = useTheme();
const {t} = useTranslation();
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
handleChangeContent(newValue);
@@ -70,28 +74,28 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
>
<Tab
value="recordings"
label="Robots"
label={t('mainmenu.recordings')}
icon={<AutoAwesome />}
iconPosition="start"
/>
<Tab
value="runs"
label="Runs"
label={t('mainmenu.runs')}
icon={<FormatListBulleted />}
iconPosition="start"
/>
<Tab
value="proxy"
label="Proxy"
label={t('mainmenu.proxy')}
icon={<Usb />}
iconPosition="start"
/>
<Tab
value="apikey"
label="API Key"
label={t('mainmenu.apikey')}
icon={<VpnKey />}
iconPosition="start"
@@ -99,13 +103,11 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
</Tabs>
<hr />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1rem', textAlign: 'left' }}>
<Button href="/api-docs" target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<Article />}>
API Docs
<Button href={`${apiUrl}/api-docs/`} target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<Code />}>
{t('mainmenu.apidocs')}
</Button>
<Button href="https://forms.gle/hXjgqDvkEhPcaBW76" target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<CloudQueue />}>
Join Maxun Cloud
{t('mainmenu.feedback')}
</Button>
</Box>
</Box>

View File

@@ -22,6 +22,7 @@ import {
import { sendProxyConfig, getProxyConfig, testProxyConfig, deleteProxyConfig } from '../../api/proxy';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { useThemeMode } from '../../context/theme-provider';
import { useTranslation } from 'react-i18next';
// Custom styled Tabs component
const CustomTabs = styled(Tabs)(({ theme }) => ({
@@ -44,6 +45,7 @@ const CustomTab = styled(Tab)(({ theme }) => ({
},
}));
const FormContainer = styled(Box)({
display: 'flex',
flexDirection: 'column',
@@ -56,6 +58,7 @@ const FormControl = styled(Box)({
});
const ProxyForm: React.FC = () => {
const { t } = useTranslation();
const [proxyConfigForm, setProxyConfigForm] = useState({
server_url: '',
username: '',
@@ -119,13 +122,13 @@ const ProxyForm: React.FC = () => {
try {
const response = await sendProxyConfig(proxyConfigForm);
if (response) {
notify('success', 'Proxy configuration submitted successfully');
notify('success', t('proxy.notifications.config_success'));
} else {
notify('error', `Failed to submit proxy configuration. Try again. ${response}`);
console.log(`Failed to submit proxy configuration. Try again. ${response}`)
notify('error', t('proxy.notifications.config_error'));
console.log(`${t('proxy.notifications.config_error')} ${response}`)
}
} catch (error: any) {
notify('error', `${error} : Failed to submit proxy configuration`);
notify('error', `${error} : ${t('proxy.notifications.config_error')}`);
}
};
@@ -136,9 +139,9 @@ const ProxyForm: React.FC = () => {
const testProxy = async () => {
await testProxyConfig().then((response) => {
if (response.success) {
notify('success', 'Proxy configuration is working');
notify('success', t('proxy.notifications.test_success'));
} else {
notify('error', 'Failed to test proxy configuration. Try again.');
notify('error', t('proxy.notifications.test_error'));
}
});
};
@@ -149,7 +152,7 @@ const ProxyForm: React.FC = () => {
if (response.proxy_url) {
setIsProxyConfigured(true);
setProxy(response);
notify('success', 'Proxy configuration fetched successfully');
notify('success', t('proxy.notifications.fetch_success'));
}
} catch (error: any) {
notify('error', error);
@@ -159,11 +162,11 @@ const ProxyForm: React.FC = () => {
const removeProxy = async () => {
await deleteProxyConfig().then((response) => {
if (response) {
notify('success', 'Proxy configuration removed successfully');
notify('success', t('proxy.notifications.remove_success'));
setIsProxyConfigured(false);
setProxy({ proxy_url: '', auth: false });
} else {
notify('error', 'Failed to remove proxy configuration. Try again.');
notify('error', t('proxy.notifications.remove_error'));
}
});
}
@@ -179,7 +182,7 @@ const ProxyForm: React.FC = () => {
<>
<FormContainer>
<Typography variant="h6" gutterBottom component="div" style={{ marginTop: '20px' }}>
Proxy Configuration
{t('proxy.title')}
</Typography>
<CustomTabs
value={tabIndex}
@@ -191,18 +194,24 @@ const ProxyForm: React.FC = () => {
}}
>
<CustomTab
label="Standard Proxy"
label={t('proxy.tab_standard')}
style={{
color: tabIndex === 0 ? '#FF69B4' : (isDarkMode ? 'white' : 'black')
}}
/>
<CustomTab
label="Automatic Proxy Rotation"
label={t('proxy.tab_rotation')}
style={{
color: tabIndex === 1 ? '#FF69B4' : (isDarkMode ? 'white' : 'black')
}}
/>
</CustomTabs>
<!-- <Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t('proxy.tab_standard')} />
<Tab label={t('proxy.tab_rotation')} />
</Tabs> -->
{tabIndex === 0 && (
isProxyConfigured ? (
<Box sx={{ maxWidth: 600, width: '100%', marginTop: '5px' }}>
@@ -210,8 +219,8 @@ const ProxyForm: React.FC = () => {
<Table>
<TableHead>
<TableRow>
<TableCell><strong>Proxy URL</strong></TableCell>
<TableCell><strong>Requires Authentication</strong></TableCell>
<TableCell><strong>{t('proxy.table.proxy_url')}</strong></TableCell>
<TableCell><strong>{t('proxy.table.requires_auth')}</strong></TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -223,39 +232,37 @@ const ProxyForm: React.FC = () => {
</Table>
</TableContainer>
<Button variant="outlined" color="primary" onClick={testProxy}>
Test Proxy
{t('proxy.test_proxy')}
</Button>
<Button variant="outlined" color="error" onClick={removeProxy} sx={{ marginLeft: '10px' }}>
Remove Proxy
{t('proxy.remove_proxy')}
</Button>
</Box>
) : (
<Box component="form" onSubmit={handleSubmit} sx={{ maxWidth: 400, width: '100%' }}>
<FormControl>
<TextField
label="Proxy Server URL"
label={t('proxy.server_url')}
name="server_url"
value={proxyConfigForm.server_url}
onChange={handleChange}
fullWidth
required
error={!!errors.server_url}
helperText={errors.server_url || `Proxy to be used for all robots. HTTP and SOCKS proxies are supported.
Example http://myproxy.com:3128 or socks5://myproxy.com:3128.
Short form myproxy.com:3128 is considered an HTTP proxy.`}
helperText={errors.server_url || t('proxy.server_url_helper')}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={<Switch checked={requiresAuth} onChange={handleAuthToggle} />}
label="Requires Authentication?"
label={t('proxy.requires_auth')}
/>
</FormControl>
{requiresAuth && (
<>
<FormControl>
<TextField
label="Username"
label={t('proxy.username')}
name="username"
value={proxyConfigForm.username}
onChange={handleChange}
@@ -267,7 +274,7 @@ const ProxyForm: React.FC = () => {
</FormControl>
<FormControl>
<TextField
label="Password"
label={t('proxy.password')}
name="password"
value={proxyConfigForm.password}
onChange={handleChange}
@@ -287,7 +294,7 @@ const ProxyForm: React.FC = () => {
fullWidth
disabled={!proxyConfigForm.server_url || (requiresAuth && (!proxyConfigForm.username || !proxyConfigForm.password))}
>
Add Proxy
{t('proxy.add_proxy')}
</Button>
</Box>
))}
@@ -295,31 +302,37 @@ const ProxyForm: React.FC = () => {
<Box sx={{ maxWidth: 400, width: '100%', textAlign: 'center', marginTop: '20px' }}>
<>
<Typography variant="body1" gutterBottom component="div">
Coming Soon - In Open Source (Basic Rotation) & Cloud (Advanced Rotation). If you don't want to manage the infrastructure, join our cloud waitlist to get early access.
{t('proxy.coming_soon')}
</Typography>
<Button variant="contained" color="primary" sx={{ marginTop: '20px',backgroundColor: '#ff00c3' }}>
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">Join Maxun Cloud Waitlist</a>
<!-- <Button variant="contained" color="primary" sx={{ marginTop: '20px',backgroundColor: '#ff00c3' }}>
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">Join Maxun Cloud Waitlist</a> -->
<Button variant="contained" color="primary" sx={{ marginTop: '20px' }}>
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">{t('proxy.join_waitlist')}</a>
</Button>
</>
</Box>
)}
</FormContainer>
<Alert severity="info" sx={{ marginTop: '80px', marginLeft: '50px', height: '230px', width: '450px', border: '1px solid #ff00c3', bgcolor: isDarkMode ? '#3b002d' : '#ffc4f1', color: isDarkMode ? 'white' : 'black' }}>
<AlertTitle>If your proxy requires a username and password, always provide them separately from the proxy URL. </AlertTitle>
<Alert severity="info" sx={{ marginTop: '80px', marginLeft: '50px', height: '230px', width: '450px', border: '1px solid #ff00c3' }}>
<AlertTitle>{t('proxy.alert.title')}</AlertTitle>
<br />
<b>The right way</b>
<b>{t('proxy.alert.right_way')}</b>
<br />
Proxy URL: http://proxy.com:1337
{t('proxy.alert.proxy_url')} http://proxy.com:1337
<br />
Username: myusername
{t('proxy.alert.username')} myusername
<br />
Password: mypassword
{t('proxy.alert.password')} mypassword
<br />
<br />
<b>The wrong way</b>
<br />
Proxy URL: http://myusername:mypassword@proxy.com:1337
<b>{t('proxy.alert.wrong_way')}</b>
<br />
{t('proxy.alert.proxy_url')} http://myusername:mypassword@proxy.com:1337
</Alert>
</>
);

View File

@@ -23,6 +23,7 @@ import { getActiveWorkflow } from "../../api/workflow";
import DeleteIcon from '@mui/icons-material/Delete';
import ActionDescriptionBox from '../molecules/ActionDescriptionBox';
import { useThemeMode } from '../../context/theme-provider';
import { useTranslation } from 'react-i18next';
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
getActiveWorkflow(id).then(
@@ -56,11 +57,14 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const [showCaptureText, setShowCaptureText] = useState(true);
const [hoverStates, setHoverStates] = useState<{ [id: string]: boolean }>({});
const [browserStepIdList, setBrowserStepIdList] = useState<number[]>([]);
const [isCaptureTextConfirmed, setIsCaptureTextConfirmed] = useState(false);
const [isCaptureListConfirmed, setIsCaptureListConfirmed] = useState(false);
const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState } = useGlobalInfoStore();
const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog } = 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();
const { t } = useTranslation();
const workflowHandler = useCallback((data: WorkflowFile) => {
setWorkflow(data);
@@ -129,6 +133,16 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const handlePairDelete = () => { }
const handleStartGetText = () => {
setIsCaptureTextConfirmed(false);
startGetText();
}
const handleStartGetList = () => {
setIsCaptureListConfirmed(false);
startGetList();
}
const handleTextLabelChange = (id: number, label: string, listId?: number, fieldKey?: string) => {
if (listId !== undefined && fieldKey !== undefined) {
// Prevent editing if the field is confirmed
@@ -140,7 +154,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
setTextLabels(prevLabels => ({ ...prevLabels, [id]: label }));
}
if (!label.trim()) {
setErrors(prevErrors => ({ ...prevErrors, [id]: 'Label cannot be empty' }));
setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') }));
} else {
setErrors(prevErrors => ({ ...prevErrors, [id]: '' }));
}
@@ -152,7 +166,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
updateBrowserTextStepLabel(id, label);
setConfirmedTextSteps(prev => ({ ...prev, [id]: true }));
} else {
setErrors(prevErrors => ({ ...prevErrors, [id]: 'Label cannot be empty' }));
setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') }));
}
};
@@ -168,6 +182,22 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
});
};
const handleTextStepDelete = (id: number) => {
deleteBrowserStep(id);
setTextLabels(prevLabels => {
const { [id]: _, ...rest } = prevLabels;
return rest;
});
setConfirmedTextSteps(prev => {
const { [id]: _, ...rest } = prev;
return rest;
});
setErrors(prevErrors => {
const { [id]: _, ...rest } = prevErrors;
return rest;
});
};
const handleListTextFieldConfirm = (listId: number, fieldKey: string) => {
setConfirmedListTextFields(prev => ({
...prev,
@@ -194,6 +224,22 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
});
};
const handleListTextFieldDelete = (listId: number, fieldKey: string) => {
removeListTextField(listId, fieldKey);
setConfirmedListTextFields(prev => {
const updatedListFields = { ...(prev[listId] || {}) };
delete updatedListFields[fieldKey];
return {
...prev,
[listId]: updatedListFields
};
});
setErrors(prev => {
const { [fieldKey]: _, ...rest } = prev;
return rest;
});
};
const getTextSettingsObject = useCallback(() => {
const settings: Record<string, { selector: string; tag?: string;[key: string]: any }> = {};
browserSteps.forEach(step => {
@@ -214,18 +260,19 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const stopCaptureAndEmitGetTextSettings = useCallback(() => {
const hasUnconfirmedTextSteps = browserSteps.some(step => step.type === 'text' && !confirmedTextSteps[step.id]);
if (hasUnconfirmedTextSteps) {
notify('error', 'Please confirm all text fields');
notify('error', t('right_panel.errors.confirm_text_fields'));
return;
}
stopGetText();
const settings = getTextSettingsObject();
console.log("SETTINGS", settings);
const hasTextSteps = browserSteps.some(step => step.type === 'text');
if (hasTextSteps) {
socket?.emit('action', { action: 'scrapeSchema', settings });
}
setIsCaptureTextConfirmed(true);
resetInterpretationLog();
onFinishCapture();
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps]);
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps, resetInterpretationLog]);
const getListSettingsObject = useCallback(() => {
let settings: {
@@ -279,7 +326,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
if (settings) {
socket?.emit('action', { action: 'scrapeList', settings });
} else {
notify('error', 'Unable to create list settings. Make sure you have defined a field for the list.');
notify('error', t('right_panel.errors.unable_create_settings'));
}
handleStopGetList();
onFinishCapture();
@@ -297,13 +344,13 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
case 'pagination':
if (!paginationType) {
notify('error', 'Please select a pagination type.');
notify('error', t('right_panel.errors.select_pagination'));
return;
}
const settings = getListSettingsObject();
const paginationSelector = settings.pagination?.selector;
if (['clickNext', 'clickLoadMore'].includes(paginationType) && !paginationSelector) {
notify('error', 'Please select the pagination element first.');
notify('error', t('right_panel.errors.select_pagination_element'));
return;
}
stopPaginationMode();
@@ -315,16 +362,17 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
case 'limit':
if (!limitType || (limitType === 'custom' && !customLimit)) {
notify('error', 'Please select a limit or enter a custom limit.');
notify('error', t('right_panel.errors.select_limit'));
return;
}
const limit = limitType === 'custom' ? parseInt(customLimit) : parseInt(limitType);
if (isNaN(limit) || limit <= 0) {
notify('error', 'Please enter a valid limit.');
notify('error', t('right_panel.errors.invalid_limit'));
return;
}
stopLimitMode();
setShowLimitOptions(false);
setIsCaptureListConfirmed(true);
stopCaptureAndEmitGetListSettings();
setCaptureStage('complete');
break;
@@ -335,6 +383,23 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
}
}, [captureStage, paginationType, limitType, customLimit, startPaginationMode, stopPaginationMode, startLimitMode, stopLimitMode, notify, stopCaptureAndEmitGetListSettings, getListSettingsObject]);
const handleBackCaptureList = useCallback(() => {
switch (captureStage) {
case 'limit':
stopLimitMode();
setShowLimitOptions(false);
startPaginationMode();
setShowPaginationOptions(true);
setCaptureStage('pagination');
break;
case 'pagination':
stopPaginationMode();
setShowPaginationOptions(false);
setCaptureStage('initial');
break;
}
}, [captureStage, stopLimitMode, startPaginationMode, stopPaginationMode]);
const handlePaginationSettingSelect = (option: PaginationType) => {
updatePaginationType(option);
};
@@ -349,7 +414,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
setTextLabels({});
setErrors({});
setConfirmedTextSteps({});
notify('error', 'Capture Text Discarded');
setIsCaptureTextConfirmed(false);
notify('error', t('right_panel.errors.capture_text_discarded'));
}, [browserSteps, stopGetText, deleteBrowserStep]);
const discardGetList = useCallback(() => {
@@ -364,7 +430,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
setShowLimitOptions(false);
setCaptureStage('initial');
setConfirmedListTextFields({});
notify('error', 'Capture List Discarded');
setIsCaptureListConfirmed(false);
notify('error', t('right_panel.errors.capture_list_discarded'));
}, [browserSteps, stopGetList, deleteBrowserStep, resetListState]);
@@ -420,37 +487,54 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
</SimpleBox> */}
<ActionDescriptionBox isDarkMode={isDarkMode} />
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px',background: isDarkMode?'#1E2124': 'inherit',color: isDarkMode ? 'white' : 'inherit' }}>
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetList}>Capture List</Button>}
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetList}>{t('right_panel.buttons.capture_list')}</Button>}
<!-- <ActionDescriptionBox />
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" onClick={startGetList}>{t('right_panel.buttons.capture_list')}</Button>}
--!>
{getList && (
<>
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
{(captureStage === 'pagination' || captureStage === 'limit') && (
<Button
variant="outlined"
onClick={handleBackCaptureList}
>
{t('right_panel.buttons.back')}
</Button>
)}
<Button
variant="outlined"
onClick={handleConfirmListCapture}
disabled={captureStage === 'initial' ? isConfirmCaptureDisabled : hasUnconfirmedListTextFields}
>
{captureStage === 'initial' ? 'Confirm Capture' :
captureStage === 'pagination' ? 'Confirm Pagination' :
captureStage === 'limit' ? 'Confirm Limit' : 'Finish Capture'}
{captureStage === 'initial' ? t('right_panel.buttons.confirm_capture') :
captureStage === 'pagination' ? t('right_panel.buttons.confirm_pagination') :
captureStage === 'limit' ? t('right_panel.buttons.confirm_limit') :
t('right_panel.buttons.finish_capture')}
</Button>
<Button variant="outlined" color="error" onClick={discardGetList}>
{t('right_panel.buttons.discard')}
</Button>
<Button variant="outlined" color="error" onClick={discardGetList}>Discard</Button>
</Box>
</>
)}
{showPaginationOptions && (
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
<Typography>How can we find the next list item on the page?</Typography>
<Button variant={paginationType === 'clickNext' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickNext')}>Click on next to navigate to the next page</Button>
<Button variant={paginationType === 'clickLoadMore' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickLoadMore')}>Click on load more to load more items</Button>
<Button variant={paginationType === 'scrollDown' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollDown')}>Scroll down to load more items</Button>
<Button variant={paginationType === 'scrollUp' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollUp')}>Scroll up to load more items</Button>
<Button variant={paginationType === 'none' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('none')}>No more items to load</Button>
<Typography>{t('right_panel.pagination.title')}</Typography>
<Button variant={paginationType === 'clickNext' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickNext')}>{t('right_panel.pagination.click_next')}</Button>
<Button variant={paginationType === 'clickLoadMore' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickLoadMore')}>{t('right_panel.pagination.click_load_more')}</Button>
<Button variant={paginationType === 'scrollDown' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollDown')}>{t('right_panel.pagination.scroll_down')}</Button>
<Button variant={paginationType === 'scrollUp' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollUp')}>{t('right_panel.pagination.scroll_up')}</Button>
<Button variant={paginationType === 'none' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('none')}>{t('right_panel.pagination.none')}</Button>
</Box>
)}
{showLimitOptions && (
<FormControl>
<FormLabel style={{ marginBottom: '10px', background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}>
<h4>What is the maximum number of rows you want to extract?</h4>
<h4>{t('right_panel.limit.title')}</h4>
<!-- <FormLabel>
<h4>{t('right_panel.limit.title')}</h4> -->
</FormLabel>
<RadioGroup
value={limitType}
@@ -464,13 +548,28 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
<FormControlLabel value="10" control={<Radio />} label="10" />
<FormControlLabel value="100" control={<Radio />} label="100" />
<div style={{ display: 'flex', alignItems: 'center' }}>
<FormControlLabel value="custom" control={<Radio />} label="Custom" />
<FormControlLabel value="custom" control={<Radio />} label={t('right_panel.limit.custom')} />
{limitType === 'custom' && (
<TextField
type="number"
value={customLimit}
onChange={(e) => updateCustomLimit(e.target.value)}
placeholder="Enter number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value);
// Only update if the value is greater than or equal to 1 or if the field is empty
if (e.target.value === '' || value >= 1) {
updateCustomLimit(e.target.value);
}
}}
inputProps={{
min: 1,
onKeyPress: (e: React.KeyboardEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value + e.key;
if (parseInt(value) < 1) {
e.preventDefault();
}
}
}}
placeholder={t('right_panel.limit.enter_number')}
sx={{
marginLeft: '10px',
'& input': {
@@ -487,21 +586,24 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
</RadioGroup>
</FormControl>
)}
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetText}>Capture Text</Button>}
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetText}>{t('right_panel.buttons.capture_text')}</Button>}
// {!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" onClick={handleStartGetText}>{t('right_panel.buttons.capture_text')}</Button>}
{getText &&
<>
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
<Button variant="outlined" onClick={stopCaptureAndEmitGetTextSettings} >Confirm</Button>
<Button variant="outlined" color="error" onClick={discardGetText} >Discard</Button>
<Button variant="outlined" onClick={stopCaptureAndEmitGetTextSettings} >{t('right_panel.buttons.confirm')}</Button>
<Button variant="outlined" color="error" onClick={discardGetText} >{t('right_panel.buttons.discard')}</Button>
</Box>
</>
}
{!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetScreenshot}>Capture Screenshot</Button>}
{!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetScreenshot}>{t('right_panel.buttons.capture_screenshot')}</Button>}
// {!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" onClick={startGetScreenshot}>{t('right_panel.buttons.capture_screenshot')}</Button>}
{getScreenshot && (
<Box display="flex" flexDirection="column" gap={2}>
<Button variant="contained" onClick={() => captureScreenshot(true)}>Capture Fullpage</Button>
<Button variant="contained" onClick={() => captureScreenshot(false)}>Capture Visible Part</Button>
<Button variant="outlined" color="error" onClick={stopGetScreenshot}>Discard</Button>
<Button variant="contained" onClick={() => captureScreenshot(true)}>{t('right_panel.screenshot.capture_fullpage')}</Button>
<Button variant="contained" onClick={() => captureScreenshot(false)}>{t('right_panel.screenshot.capture_visible')}</Button>
<Button variant="outlined" color="error" onClick={stopGetScreenshot}>{t('right_panel.buttons.discard')}</Button>
</Box>
)}
</Box>
@@ -512,7 +614,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
step.type === 'text' && (
<>
<TextField
label="Label"
label={t('right_panel.fields.label')}
value={textLabels[step.id] || step.label || ''}
onChange={(e) => handleTextLabelChange(step.id, e.target.value)}
fullWidth
@@ -531,7 +633,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
sx={{ background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}
/>
<TextField
label="Data"
label={t('right_panel.fields.data')}
value={step.data}
fullWidth
margin="normal"
@@ -545,10 +647,20 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
}}
/>
{!confirmedTextSteps[step.id] && (
{!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" color="error" onClick={() => handleTextStepDiscard(step.id)}>Discard</Button>
<Button variant="contained" onClick={() => handleTextStepConfirm(step.id)} disabled={!textLabels[step.id]?.trim()}>{t('right_panel.buttons.confirm')}</Button>
<Button variant="contained" color="error" onClick={() => handleTextStepDiscard(step.id)}>{t('right_panel.buttons.discard')}</Button>
</Box>
) : !isCaptureTextConfirmed && (
<Box display="flex" justifyContent="flex-end" gap={2}>
<Button
variant="contained"
color="error"
onClick={() => handleTextStepDelete(step.id)}
>
{t('right_panel.buttons.delete')}
</Button>
</Box>
)}
</>
@@ -557,17 +669,19 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
<Box display="flex" alignItems="center">
<DocumentScannerIcon sx={{ mr: 1 }} />
<Typography>
{`Take ${step.fullPage ? 'Fullpage' : 'Visible Part'} Screenshot`}
{step.fullPage ?
t('right_panel.screenshot.display_fullpage') :
t('right_panel.screenshot.display_visible')}
</Typography>
</Box>
)}
{step.type === 'list' && (
<>
<Typography>List Selected Successfully</Typography>
<Typography>{t('right_panel.messages.list_selected')}</Typography>
{Object.entries(step.fields).map(([key, field]) => (
<Box key={key} sx={{ padding: '10px', margin: '11px', borderRadius: '5px', position: 'relative', background: `${isDarkMode ? "#1E2124" : 'white'}` }}>
<TextField
label="Field Label"
label={t('right_panel.fields.field_label')}
value={field.label || ''}
onChange={(e) => handleTextLabelChange(field.id, e.target.value, step.id, key)}
fullWidth
@@ -584,7 +698,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
/>
<TextField
label="Field Data"
label={t('right_panel.fields.field_data')}
value={field.data || ''}
fullWidth
margin="normal"
@@ -598,21 +712,31 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
}}
/>
{!confirmedListTextFields[step.id]?.[key] && (
{!confirmedListTextFields[step.id]?.[key] ? (
<Box display="flex" justifyContent="space-between" gap={2}>
<Button
variant="contained"
onClick={() => handleListTextFieldConfirm(step.id, key)}
disabled={!field.label?.trim()}
>
Confirm
{t('right_panel.buttons.confirm')}
</Button>
<Button
variant="contained"
color="error"
onClick={() => handleListTextFieldDiscard(step.id, key)}
>
Discard
{t('right_panel.buttons.discard')}
</Button>
</Box>
) : !isCaptureListConfirmed && (
<Box display="flex" justifyContent="flex-end" gap={2}>
<Button
variant="contained"
color="error"
onClick={() => handleListTextFieldDelete(step.id, key)}
>
{t('right_panel.buttons.delete')}
</Button>
</Box>
)}