Merge branch 'develop' into recorder-ui-shift

This commit is contained in:
Rohit
2025-11-12 22:08:23 +05:30
committed by GitHub
12 changed files with 326 additions and 255 deletions

View File

@@ -1246,9 +1246,9 @@ export default class Interpreter extends EventEmitter {
if (checkLimit()) return allResults; if (checkLimit()) return allResults;
let loadMoreCounter = 0; let loadMoreCounter = 0;
// let previousResultCount = allResults.length; let previousResultCount = allResults.length;
// let noNewItemsCounter = 0; let noNewItemsCounter = 0;
// const MAX_NO_NEW_ITEMS = 2; const MAX_NO_NEW_ITEMS = 5;
while (true) { while (true) {
if (this.isAborted) { if (this.isAborted) {
@@ -1332,21 +1332,21 @@ export default class Interpreter extends EventEmitter {
await scrapeCurrentPage(); await scrapeCurrentPage();
// const currentResultCount = allResults.length; const currentResultCount = allResults.length;
// const newItemsAdded = currentResultCount > previousResultCount; const newItemsAdded = currentResultCount > previousResultCount;
// if (!newItemsAdded) { if (!newItemsAdded) {
// noNewItemsCounter++; noNewItemsCounter++;
// debugLog(`No new items added after click (${noNewItemsCounter}/${MAX_NO_NEW_ITEMS})`); debugLog(`No new items added after click (${noNewItemsCounter}/${MAX_NO_NEW_ITEMS})`);
// if (noNewItemsCounter >= MAX_NO_NEW_ITEMS) { if (noNewItemsCounter >= MAX_NO_NEW_ITEMS) {
// debugLog(`Stopping after ${MAX_NO_NEW_ITEMS} clicks with no new items`); debugLog(`Stopping after ${MAX_NO_NEW_ITEMS} clicks with no new items`);
// return allResults; return allResults;
// } }
// } else { } else {
// noNewItemsCounter = 0; noNewItemsCounter = 0;
// previousResultCount = currentResultCount; previousResultCount = currentResultCount;
// } }
if (checkLimit()) return allResults; if (checkLimit()) return allResults;

View File

@@ -55,7 +55,7 @@ export default class Preprocessor {
*/ */
static getParams(workflow: WorkflowFile): string[] { static getParams(workflow: WorkflowFile): string[] {
const getParamsRecurse = (object: any): string[] => { const getParamsRecurse = (object: any): string[] => {
if (typeof object === 'object') { if (typeof object === 'object' && object !== null) {
// Recursion base case // Recursion base case
if (object.$param) { if (object.$param) {
return [object.$param]; return [object.$param];
@@ -141,14 +141,24 @@ export default class Preprocessor {
} }
const out = object; const out = object;
// for every key (child) of the object
Object.keys(object!).forEach((key) => { Object.keys(object!).forEach((key) => {
// if the field has only one key, which is `k` const childValue = (<any>object)[key];
if (Object.keys((<any>object)[key]).length === 1 && (<any>object)[key][k]) {
// process the current special tag (init param, hydrate regex...) if (!childValue || typeof childValue !== 'object') {
(<any>out)[key] = f((<any>object)[key][k]); return;
} else { }
initSpecialRecurse((<any>object)[key], k, f);
try {
const childKeys = Object.keys(childValue);
if (childKeys.length === 1 && childValue[k]) {
(<any>out)[key] = f(childValue[k]);
} else {
initSpecialRecurse(childValue, k, f);
}
} catch (error) {
console.warn(`Error processing key "${key}" in initSpecialRecurse:`, error);
} }
}); });
return out; return out;

View File

@@ -201,6 +201,11 @@ export class RemoteBrowser {
private networkRequestTimeout: NodeJS.Timeout | null = null; private networkRequestTimeout: NodeJS.Timeout | null = null;
private pendingNetworkRequests: string[] = []; private pendingNetworkRequests: string[] = [];
private readonly NETWORK_QUIET_PERIOD = 8000; private readonly NETWORK_QUIET_PERIOD = 8000;
private readonly INITIAL_LOAD_QUIET_PERIOD = 3000;
private networkWaitStartTime: number = 0;
private progressInterval: NodeJS.Timeout | null = null;
private hasShownInitialLoader: boolean = false;
private isInitialLoadInProgress: boolean = false;
/** /**
* Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and * Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and
@@ -432,17 +437,19 @@ export class RemoteBrowser {
if (!this.currentPage) return; if (!this.currentPage) return;
this.currentPage.on("domcontentloaded", async () => { this.currentPage.on("domcontentloaded", async () => {
logger.info("DOM content loaded - triggering snapshot"); if (!this.isInitialLoadInProgress) {
await this.makeAndEmitDOMSnapshot(); logger.info("DOM content loaded - triggering snapshot");
await this.makeAndEmitDOMSnapshot();
}
}); });
this.currentPage.on("response", async (response) => { this.currentPage.on("response", async (response) => {
const url = response.url(); const url = response.url();
if ( const isDocumentRequest = response.request().resourceType() === "document";
response.request().resourceType() === "document" ||
url.includes("api/") || if (!this.hasShownInitialLoader && isDocumentRequest && !url.includes("about:blank")) {
url.includes("ajax") this.hasShownInitialLoader = true;
) { this.isInitialLoadInProgress = true;
this.pendingNetworkRequests.push(url); this.pendingNetworkRequests.push(url);
if (this.networkRequestTimeout) { if (this.networkRequestTimeout) {
@@ -450,24 +457,54 @@ export class RemoteBrowser {
this.networkRequestTimeout = null; this.networkRequestTimeout = null;
} }
if (this.progressInterval) {
clearInterval(this.progressInterval);
this.progressInterval = null;
}
this.networkWaitStartTime = Date.now();
this.progressInterval = setInterval(() => {
const elapsed = Date.now() - this.networkWaitStartTime;
const navigationProgress = Math.min((elapsed / this.INITIAL_LOAD_QUIET_PERIOD) * 40, 35);
const totalProgress = 60 + navigationProgress;
this.emitLoadingProgress(totalProgress, this.pendingNetworkRequests.length);
}, 500);
logger.debug( logger.debug(
`Network request received: ${url}. Total pending: ${this.pendingNetworkRequests.length}` `Initial load network request received: ${url}. Using ${this.INITIAL_LOAD_QUIET_PERIOD}ms quiet period`
); );
this.networkRequestTimeout = setTimeout(async () => { this.networkRequestTimeout = setTimeout(async () => {
logger.info( logger.info(
`Network quiet period reached. Processing ${this.pendingNetworkRequests.length} requests` `Initial load network quiet period reached (${this.INITIAL_LOAD_QUIET_PERIOD}ms)`
); );
if (this.progressInterval) {
clearInterval(this.progressInterval);
this.progressInterval = null;
}
this.emitLoadingProgress(100, this.pendingNetworkRequests.length);
this.pendingNetworkRequests = []; this.pendingNetworkRequests = [];
this.networkRequestTimeout = null; this.networkRequestTimeout = null;
this.isInitialLoadInProgress = false;
await this.makeAndEmitDOMSnapshot(); await this.makeAndEmitDOMSnapshot();
}, this.NETWORK_QUIET_PERIOD); }, this.INITIAL_LOAD_QUIET_PERIOD);
} }
}); });
} }
private emitLoadingProgress(progress: number, pendingRequests: number): void {
this.socket.emit("domLoadingProgress", {
progress: Math.round(progress),
pendingRequests,
userId: this.userId,
timestamp: Date.now(),
});
}
private async setupPageEventListeners(page: Page) { private async setupPageEventListeners(page: Page) {
page.on('framenavigated', async (frame) => { page.on('framenavigated', async (frame) => {
if (frame === page.mainFrame()) { if (frame === page.mainFrame()) {
@@ -521,7 +558,13 @@ export class RemoteBrowser {
const MAX_RETRIES = 3; const MAX_RETRIES = 3;
let retryCount = 0; let retryCount = 0;
let success = false; let success = false;
this.socket.emit("dom-snapshot-loading", {
userId: this.userId,
timestamp: Date.now(),
});
this.emitLoadingProgress(0, 0);
while (!success && retryCount < MAX_RETRIES) { while (!success && retryCount < MAX_RETRIES) {
try { try {
this.browser = <Browser>(await chromium.launch({ this.browser = <Browser>(await chromium.launch({
@@ -545,7 +588,9 @@ export class RemoteBrowser {
if (!this.browser || this.browser.isConnected() === false) { if (!this.browser || this.browser.isConnected() === false) {
throw new Error('Browser failed to launch or is not connected'); throw new Error('Browser failed to launch or is not connected');
} }
this.emitLoadingProgress(20, 0);
const proxyConfig = await getDecryptedProxyConfig(userId); const proxyConfig = await getDecryptedProxyConfig(userId);
let proxyOptions: { server: string, username?: string, password?: string } = { server: '' }; let proxyOptions: { server: string, username?: string, password?: string } = { server: '' };
@@ -623,6 +668,8 @@ export class RemoteBrowser {
this.currentPage = await this.context.newPage(); this.currentPage = await this.context.newPage();
this.emitLoadingProgress(40, 0);
await this.setupPageEventListeners(this.currentPage); await this.setupPageEventListeners(this.currentPage);
const viewportSize = await this.currentPage.viewportSize(); const viewportSize = await this.currentPage.viewportSize();
@@ -645,7 +692,9 @@ export class RemoteBrowser {
// Still need to set up the CDP session even if blocker fails // Still need to set up the CDP session even if blocker fails
this.client = await this.currentPage.context().newCDPSession(this.currentPage); this.client = await this.currentPage.context().newCDPSession(this.currentPage);
} }
this.emitLoadingProgress(60, 0);
success = true; success = true;
logger.log('debug', `Browser initialized successfully for user ${userId}`); logger.log('debug', `Browser initialized successfully for user ${userId}`);
} catch (error: any) { } catch (error: any) {
@@ -1521,9 +1570,6 @@ export class RemoteBrowser {
this.isDOMStreamingActive = true; this.isDOMStreamingActive = true;
logger.info("DOM streaming started successfully"); logger.info("DOM streaming started successfully");
// Initial DOM snapshot
await this.makeAndEmitDOMSnapshot();
this.setupScrollEventListener(); this.setupScrollEventListener();
this.setupPageChangeListeners(); this.setupPageChangeListeners();
} catch (error) { } catch (error) {

View File

@@ -116,6 +116,16 @@ export class WorkflowInterpreter {
*/ */
private currentScrapeListIndex: number = 0; private currentScrapeListIndex: number = 0;
/**
* Track action counts to generate unique names
*/
private actionCounts: Record<string, number> = {};
/**
* Track used action names to prevent duplicates
*/
private usedActionNames: Set<string> = new Set();
/** /**
* Current run ID for real-time persistence * Current run ID for real-time persistence
*/ */
@@ -379,6 +389,8 @@ export class WorkflowInterpreter {
}; };
this.binaryData = []; this.binaryData = [];
this.currentScrapeListIndex = 0; this.currentScrapeListIndex = 0;
this.actionCounts = {};
this.usedActionNames = new Set();
this.currentRunId = null; this.currentRunId = null;
this.persistenceBuffer = []; this.persistenceBuffer = [];
this.persistenceInProgress = false; this.persistenceInProgress = false;
@@ -394,6 +406,43 @@ export class WorkflowInterpreter {
logger.log('debug', `Set run ID for real-time persistence: ${runId}`); logger.log('debug', `Set run ID for real-time persistence: ${runId}`);
}; };
/**
* Generates a unique action name for data storage
* @param actionType The type of action (scrapeList, scrapeSchema, etc.)
* @param providedName Optional name provided by the action
* @returns A unique action name
*/
private getUniqueActionName = (actionType: string, providedName?: string | null): string => {
if (providedName && providedName.trim() !== '' && !this.usedActionNames.has(providedName)) {
this.usedActionNames.add(providedName);
return providedName;
}
if (!this.actionCounts[actionType]) {
this.actionCounts[actionType] = 0;
}
let uniqueName: string;
let counter = this.actionCounts[actionType];
do {
counter++;
if (actionType === 'scrapeList') {
uniqueName = `List ${counter}`;
} else if (actionType === 'scrapeSchema') {
uniqueName = `Text ${counter}`;
} else if (actionType === 'screenshot') {
uniqueName = `Screenshot ${counter}`;
} else {
uniqueName = `${actionType} ${counter}`;
}
} while (this.usedActionNames.has(uniqueName));
this.actionCounts[actionType] = counter;
this.usedActionNames.add(uniqueName);
return uniqueName;
};
/** /**
* Persists extracted data to database with intelligent batching for performance * Persists extracted data to database with intelligent batching for performance
* Falls back to immediate persistence for critical operations * Falls back to immediate persistence for critical operations
@@ -525,20 +574,8 @@ export class WorkflowInterpreter {
} }
let actionName = this.currentActionName || ""; let actionName = this.currentActionName || "";
if (typeKey === "scrapeList") {
if (!actionName) { actionName = this.getUniqueActionName(typeKey, this.currentActionName);
if (!Array.isArray(data) && Object.keys(data).length === 1) {
const soleKey = Object.keys(data)[0];
const soleValue = data[soleKey];
if (Array.isArray(soleValue) || typeof soleValue === "object") {
actionName = soleKey;
data = soleValue;
}
}
}
if (!actionName) {
actionName = "Unnamed Action";
} }
const flattened = Array.isArray(data) const flattened = Array.isArray(data)
@@ -570,9 +607,10 @@ export class WorkflowInterpreter {
const { name, data, mimeType } = payload; const { name, data, mimeType } = payload;
const base64Data = data.toString("base64"); const base64Data = data.toString("base64");
const uniqueName = this.getUniqueActionName('screenshot', name);
const binaryItem = { const binaryItem = {
name, name: uniqueName,
mimeType, mimeType,
data: base64Data data: base64Data
}; };
@@ -582,7 +620,7 @@ export class WorkflowInterpreter {
await this.persistBinaryDataToDatabase(binaryItem); await this.persistBinaryDataToDatabase(binaryItem);
this.socket.emit("binaryCallback", { this.socket.emit("binaryCallback", {
name, name: uniqueName,
data: base64Data, data: base64Data,
mimeType mimeType
}); });

View File

@@ -13,7 +13,7 @@ import {
export const BrowserContent = () => { export const BrowserContent = () => {
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const [tabs, setTabs] = useState<string[]>(["current"]); const [tabs, setTabs] = useState<string[]>(["Loading..."]);
const [tabIndex, setTabIndex] = React.useState(0); const [tabIndex, setTabIndex] = React.useState(0);
const [showOutputData, setShowOutputData] = useState(false); const [showOutputData, setShowOutputData] = useState(false);
const { browserWidth } = useBrowserDimensionsStore(); const { browserWidth } = useBrowserDimensionsStore();
@@ -125,7 +125,7 @@ export const BrowserContent = () => {
useEffect(() => { useEffect(() => {
getCurrentTabs() getCurrentTabs()
.then((response) => { .then((response) => {
if (response) { if (response && response.length > 0) {
setTabs(response); setTabs(response);
} }
}) })

View File

@@ -1,8 +1,6 @@
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import Canvas from "../recorder/Canvas";
import { Highlighter } from "../recorder/Highlighter";
import { GenericModal } from '../ui/GenericModal'; import { GenericModal } from '../ui/GenericModal';
import { useActionContext } from '../../context/browserActions'; import { useActionContext } from '../../context/browserActions';
import { useBrowserSteps, TextStep, ListStep } from '../../context/browserSteps'; import { useBrowserSteps, TextStep, ListStep } from '../../context/browserSteps';
@@ -38,12 +36,6 @@ interface AttributeOption {
value: string; value: string;
} }
interface ScreencastData {
image: string;
userId: string;
viewport?: ViewportInfo | null;
}
interface ViewportInfo { interface ViewportInfo {
width: number; width: number;
height: number; height: number;
@@ -146,8 +138,6 @@ const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null):
export const BrowserWindow = () => { export const BrowserWindow = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { browserWidth, browserHeight } = useBrowserDimensionsStore(); const { browserWidth, browserHeight } = useBrowserDimensionsStore();
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
const [screenShot, setScreenShot] = useState<string>("");
const [highlighterData, setHighlighterData] = useState<{ const [highlighterData, setHighlighterData] = useState<{
rect: DOMRect; rect: DOMRect;
selector: string; selector: string;
@@ -1174,16 +1164,31 @@ export const BrowserWindow = () => {
undefined, undefined,
false false
); );
if (pendingNotification) {
notify(pendingNotification.type, pendingNotification.message);
setPendingNotification(null);
}
} else {
console.warn(`Failed to extract any fields from list selector: ${listSelector}`);
setListSelector(null);
setFields({});
setCachedListSelector(null);
setCachedChildSelectors([]);
setCurrentListId(null);
setInitialAutoFieldIds(new Set());
setPendingNotification(null);
notify(
"error",
"The list you have selected is not valid. Please reselect it."
);
} }
} catch (error) { } catch (error) {
console.error("Error during child selector caching:", error); console.error("Error during child selector caching:", error);
} finally { } finally {
setIsCachingChildSelectors(false); setIsCachingChildSelectors(false);
if (pendingNotification) {
notify(pendingNotification.type, pendingNotification.message);
setPendingNotification(null);
}
} }
}, 100); }, 100);
} else { } else {
@@ -1303,17 +1308,6 @@ export const BrowserWindow = () => {
}, []); }, []);
const onMouseMove = (e: MouseEvent) => { const onMouseMove = (e: MouseEvent) => {
if (canvasRef && canvasRef.current && highlighterData) {
const canvasRect = canvasRef.current.getBoundingClientRect();
if (
e.pageX < canvasRect.left
|| e.pageX > canvasRect.right
|| e.pageY < canvasRect.top
|| e.pageY > canvasRect.bottom
) {
setHighlighterData(null);
}
}
}; };
const resetListState = useCallback(() => { const resetListState = useCallback(() => {
@@ -1331,35 +1325,15 @@ export const BrowserWindow = () => {
} }
}, [getList, resetListState]); }, [getList, resetListState]);
const screencastHandler = useCallback((data: string | ScreencastData) => {
if (typeof data === 'string') {
setScreenShot(data);
} else if (data && typeof data === 'object' && 'image' in data) {
if (!data.userId || data.userId === user?.id) {
setScreenShot(data.image);
if (data.viewport) {
setViewportInfo(data.viewport);
}
}
}
}, [user?.id]);
useEffect(() => { useEffect(() => {
if (socket) { if (socket) {
socket.on("screencast", screencastHandler);
socket.on("domcast", rrwebSnapshotHandler); socket.on("domcast", rrwebSnapshotHandler);
socket.on("dom-mode-enabled", domModeHandler); socket.on("dom-mode-enabled", domModeHandler);
socket.on("dom-mode-error", domModeErrorHandler); socket.on("dom-mode-error", domModeErrorHandler);
} }
if (canvasRef?.current && !isDOMMode && screenShot) {
drawImage(screenShot, canvasRef.current);
}
return () => { return () => {
if (socket) { if (socket) {
socket.off("screencast", screencastHandler);
socket.off("domcast", rrwebSnapshotHandler); socket.off("domcast", rrwebSnapshotHandler);
socket.off("dom-mode-enabled", domModeHandler); socket.off("dom-mode-enabled", domModeHandler);
socket.off("dom-mode-error", domModeErrorHandler); socket.off("dom-mode-error", domModeErrorHandler);
@@ -1367,10 +1341,6 @@ export const BrowserWindow = () => {
}; };
}, [ }, [
socket, socket,
screenShot,
canvasRef,
isDOMMode,
screencastHandler,
rrwebSnapshotHandler, rrwebSnapshotHandler,
domModeHandler, domModeHandler,
domModeErrorHandler, domModeErrorHandler,
@@ -1710,16 +1680,17 @@ export const BrowserWindow = () => {
let cleanedSelector = highlighterData.selector; let cleanedSelector = highlighterData.selector;
setListSelector(cleanedSelector); setListSelector(cleanedSelector);
notify( setPendingNotification({
`info`, type: `info`,
t( message: t(
"browser_window.attribute_modal.notifications.list_select_success", "browser_window.attribute_modal.notifications.list_select_success",
{ {
count: highlighterData.groupInfo.groupSize, count: highlighterData.groupInfo.groupSize,
} }
) || ) ||
`Selected group with ${highlighterData.groupInfo.groupSize} similar elements` `Selected group with ${highlighterData.groupInfo.groupSize} similar elements`,
); count: highlighterData.groupInfo.groupSize,
});
setCurrentListId(Date.now()); setCurrentListId(Date.now());
setFields({}); setFields({});
@@ -1847,24 +1818,7 @@ export const BrowserWindow = () => {
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => { const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (highlighterData) { if (highlighterData) {
let shouldProcessClick = false; const shouldProcessClick = true;
if (!isDOMMode && canvasRef?.current) {
const canvasRect = canvasRef.current.getBoundingClientRect();
const clickX = e.clientX - canvasRect.left;
const clickY = e.clientY - canvasRect.top;
const highlightRect = highlighterData.rect;
const mappedRect =
coordinateMapper.mapBrowserRectToCanvas(highlightRect);
shouldProcessClick =
clickX >= mappedRect.left &&
clickX <= mappedRect.right &&
clickY >= mappedRect.top &&
clickY <= mappedRect.bottom;
} else {
shouldProcessClick = true;
}
if (shouldProcessClick) { if (shouldProcessClick) {
const options = getAttributeOptions( const options = getAttributeOptions(
@@ -2209,17 +2163,7 @@ export const BrowserWindow = () => {
!showAttributeModal && !showAttributeModal &&
highlighterData?.rect != null && ( highlighterData?.rect != null && (
<> <>
{!isDOMMode && canvasRef?.current && ( {highlighterData && (
<Highlighter
unmodifiedRect={highlighterData?.rect}
displayedSelector={highlighterData?.selector}
width={dimensions.width}
height={dimensions.height}
canvasRect={canvasRef.current.getBoundingClientRect()}
/>
)}
{isDOMMode && highlighterData && (
<div <div
id="dom-highlight-overlay" id="dom-highlight-overlay"
style={{ style={{
@@ -2355,31 +2299,27 @@ export const BrowserWindow = () => {
borderRadius: "0px 0px 5px 5px", borderRadius: "0px 0px 5px 5px",
}} }}
> >
{isDOMMode ? ( {currentSnapshot ? (
<> <>
{currentSnapshot ? ( <DOMBrowserRenderer
<DOMBrowserRenderer width={dimensions.width}
width={dimensions.width} height={dimensions.height}
height={dimensions.height} snapshot={currentSnapshot}
snapshot={currentSnapshot} getList={getList}
getList={getList} getText={getText}
getText={getText} listSelector={listSelector}
listSelector={listSelector} cachedChildSelectors={cachedChildSelectors}
cachedChildSelectors={cachedChildSelectors} paginationMode={paginationMode}
paginationMode={paginationMode} paginationType={paginationType}
paginationType={paginationType} limitMode={limitMode}
limitMode={limitMode} isCachingChildSelectors={isCachingChildSelectors}
isCachingChildSelectors={isCachingChildSelectors} onHighlight={domHighlighterHandler}
onHighlight={domHighlighterHandler} onElementSelect={handleDOMElementSelection}
onElementSelect={handleDOMElementSelection} onShowDatePicker={handleShowDatePicker}
onShowDatePicker={handleShowDatePicker} onShowDropdown={handleShowDropdown}
onShowDropdown={handleShowDropdown} onShowTimePicker={handleShowTimePicker}
onShowTimePicker={handleShowTimePicker} onShowDateTimePicker={handleShowDateTimePicker}
onShowDateTimePicker={handleShowDateTimePicker} />
/>
) : (
<DOMLoadingIndicator />
)}
{/* --- Loading overlay --- */} {/* --- Loading overlay --- */}
{isCachingChildSelectors && ( {isCachingChildSelectors && (
@@ -2492,11 +2432,7 @@ export const BrowserWindow = () => {
)} )}
</> </>
) : ( ) : (
<Canvas <DOMLoadingIndicator />
onCreateRef={setCanvasReference}
width={dimensions.width}
height={dimensions.height}
/>
)} )}
</div> </div>
</div> </div>
@@ -2591,26 +2527,6 @@ const DOMLoadingIndicator: React.FC = () => {
); );
}; };
const drawImage = (image: string, canvas: HTMLCanvasElement): void => {
const ctx = canvas.getContext('2d');
if (!ctx) return;
const img = new Image();
img.onload = () => {
requestAnimationFrame(() => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
});
if (image.startsWith('blob:')) {
URL.revokeObjectURL(image);
}
};
img.onerror = () => {
console.warn('Failed to load image');
};
img.src = image;
};
const modalStyle = { const modalStyle = {
top: '50%', top: '50%',
left: '50%', left: '50%',

View File

@@ -119,7 +119,7 @@ export const NavBar: React.FC<NavBarProps> = ({
} catch (error: any) { } catch (error: any) {
const status = error.response?.status; const status = error.response?.status;
let errorKey = 'unknown'; let errorKey = 'unknown';
switch (status) { switch (status) {
case 401: case 401:
errorKey = 'unauthorized'; errorKey = 'unauthorized';
@@ -132,7 +132,7 @@ export const NavBar: React.FC<NavBarProps> = ({
errorKey = 'network'; errorKey = 'network';
} }
} }
notify( notify(
'error', 'error',
t(`navbar.notifications.errors.logout.${errorKey}`, { t(`navbar.notifications.errors.logout.${errorKey}`, {
@@ -163,6 +163,9 @@ export const NavBar: React.FC<NavBarProps> = ({
onClick={toggleTheme} onClick={toggleTheme}
sx={{ sx={{
color: darkMode ? '#ffffff' : '#0000008A', color: darkMode ? '#ffffff' : '#0000008A',
'&:hover': {
background: 'inherit'
}
}} }}
> >
{darkMode ? <LightMode /> : <DarkMode />} {darkMode ? <LightMode /> : <DarkMode />}
@@ -253,6 +256,9 @@ export const NavBar: React.FC<NavBarProps> = ({
borderRadius: '5px', borderRadius: '5px',
padding: '8px', padding: '8px',
marginRight: '20px', marginRight: '20px',
'&:hover': {
background: 'inherit'
}
}}> }}>
<Update sx={{ marginRight: '5px' }} /> <Update sx={{ marginRight: '5px' }} />
<Typography variant="body1">{t('navbar.upgrade.button')}</Typography> <Typography variant="body1">{t('navbar.upgrade.button')}</Typography>
@@ -332,7 +338,7 @@ export const NavBar: React.FC<NavBarProps> = ({
docker-compose down docker-compose down
<br /> <br />
<br /> <br />
# Remove existing backend and frontend images # Remove existing backend and frontend images
<br /> <br />
docker rmi getmaxun/maxun-frontend:latest getmaxun/maxun-backend:latest docker rmi getmaxun/maxun-frontend:latest getmaxun/maxun-backend:latest
<br /> <br />
@@ -367,7 +373,7 @@ export const NavBar: React.FC<NavBarProps> = ({
padding: '8px', padding: '8px',
marginRight: '10px', marginRight: '10px',
'&:hover': { '&:hover': {
background: 'inherit' background: 'inherit'
} }
}}> }}>
<AccountCircle sx={{ marginRight: '5px' }} /> <AccountCircle sx={{ marginRight: '5px' }} />

View File

@@ -552,16 +552,12 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => {
pair.what.forEach((action, actionIndex) => { pair.what.forEach((action, actionIndex) => {
if (!editableActions.has(String(action.action))) return; if (!editableActions.has(String(action.action))) return;
let currentName = let currentName = action.name || '';
action.name ||
(action.args && action.args[0] && typeof action.args[0] === 'object') ||
'';
if (!currentName) { if (!currentName) {
switch (action.action) { switch (action.action) {
case 'scrapeSchema': case 'scrapeSchema':
textCount++; currentName = 'Texts';
currentName = `Text ${textCount}`;
break; break;
case 'screenshot': case 'screenshot':
screenshotCount++; screenshotCount++;
@@ -574,9 +570,6 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => {
} }
} else { } else {
switch (action.action) { switch (action.action) {
case 'scrapeSchema':
textCount++;
break;
case 'screenshot': case 'screenshot':
screenshotCount++; screenshotCount++;
break; break;
@@ -599,10 +592,7 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => {
switch (action.action) { switch (action.action) {
case 'scrapeSchema': { case 'scrapeSchema': {
const existingName = const existingName = currentName || "Texts";
currentName ||
(action.args && action.args[0] && typeof action.args[0] === "object") ||
"Texts";
if (!textInputs.length) { if (!textInputs.length) {
textInputs.push( textInputs.push(

View File

@@ -61,7 +61,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
const { captureStage, getText } = useActionContext(); const { captureStage, getText } = useActionContext();
const { browserWidth, outputPreviewHeight, outputPreviewWidth } = useBrowserDimensionsStore(); const { browserWidth, outputPreviewHeight, outputPreviewWidth } = useBrowserDimensionsStore();
const { currentWorkflowActionsState, shouldResetInterpretationLog, currentTextGroupName, setCurrentTextGroupName } = useGlobalInfoStore(); const { currentWorkflowActionsState, shouldResetInterpretationLog, currentTextGroupName, setCurrentTextGroupName, notify } = useGlobalInfoStore();
const [showPreviewData, setShowPreviewData] = useState<boolean>(false); const [showPreviewData, setShowPreviewData] = useState<boolean>(false);
const userClosedDrawer = useRef<boolean>(false); const userClosedDrawer = useRef<boolean>(false);
@@ -154,6 +154,28 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
} }
}; };
const checkForDuplicateName = (stepId: number, type: 'list' | 'text' | 'screenshot', newName: string): boolean => {
const trimmedName = newName.trim();
if (type === 'list') {
const listSteps = browserSteps.filter(step => step.type === 'list' && step.id !== stepId);
const duplicate = listSteps.find(step => step.name === trimmedName);
if (duplicate) {
notify('error', `A list with the name "${trimmedName}" already exists. Please choose a different name.`);
return true;
}
} else if (type === 'screenshot') {
const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot' && step.id !== stepId);
const duplicate = screenshotSteps.find(step => step.name === trimmedName);
if (duplicate) {
notify('error', `A screenshot with the name "${trimmedName}" already exists. Please choose a different name.`);
return true;
}
}
return false;
};
const startEdit = (stepId: number, type: 'list' | 'text' | 'screenshot', currentValue: string) => { const startEdit = (stepId: number, type: 'list' | 'text' | 'screenshot', currentValue: string) => {
setEditing({ stepId, type, value: currentValue }); setEditing({ stepId, type, value: currentValue });
}; };
@@ -168,6 +190,10 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
return; return;
} }
if (checkForDuplicateName(stepId, type, finalValue)) {
return;
}
if (type === 'list') { if (type === 'list') {
updateListStepName(stepId, finalValue); updateListStepName(stepId, finalValue);
} else if (type === 'text') { } else if (type === 'text') {
@@ -306,6 +332,8 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
shouldOpenDrawer = true; shouldOpenDrawer = true;
} }
lastListDataLength.current = captureListData.length; lastListDataLength.current = captureListData.length;
} else if (hasScrapeListAction && captureListData.length === 0) {
lastListDataLength.current = 0;
} }
if (hasScrapeSchemaAction && captureTextData.length > 0 && !getText) { if (hasScrapeSchemaAction && captureTextData.length > 0 && !getText) {
@@ -315,6 +343,8 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
shouldOpenDrawer = true; shouldOpenDrawer = true;
} }
lastTextDataLength.current = captureTextData.length; lastTextDataLength.current = captureTextData.length;
} else if (hasScrapeSchemaAction && captureTextData.length === 0) {
lastTextDataLength.current = 0;
} }
if (hasScreenshotAction && screenshotData.length > 0) { if (hasScreenshotAction && screenshotData.length > 0) {
@@ -324,6 +354,8 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
shouldOpenDrawer = true; shouldOpenDrawer = true;
} }
lastScreenshotDataLength.current = screenshotData.length; lastScreenshotDataLength.current = screenshotData.length;
} else if (hasScreenshotAction && screenshotData.length === 0) {
lastScreenshotDataLength.current = 0;
} }
const getLatestCaptureType = () => { const getLatestCaptureType = () => {
@@ -466,7 +498,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
{t('interpretation_log.titles.output_preview')} {t('interpretation_log.titles.output_preview')}
</Typography> </Typography>
{!(hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction) && ( {!(hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction) && !showPreviewData && availableTabs.length === 0 && (
<Grid container justifyContent="center" alignItems="center" style={{ height: '100%' }}> <Grid container justifyContent="center" alignItems="center" style={{ height: '100%' }}>
<Grid item> <Grid item>
<Typography variant="h6" gutterBottom align="left"> <Typography variant="h6" gutterBottom align="left">

View File

@@ -115,27 +115,29 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
const rawKeys = Object.keys(row.binaryOutput); const rawKeys = Object.keys(row.binaryOutput);
const isLegacyPattern = rawKeys.every(key => /^item-\d+-\d+$/.test(key)); const isLegacyPattern = rawKeys.every(key => /^item-\d+-\d+$/.test(key));
let normalizedScreenshotKeys: string[];
if (isLegacyPattern) { if (isLegacyPattern) {
const renamedKeys = rawKeys.map((_, index) => `Screenshot ${index + 1}`); // Legacy unnamed screenshots → Screenshot 1, Screenshot 2...
const keyMap: Record<string, string> = {}; normalizedScreenshotKeys = rawKeys.map((_, index) => `Screenshot ${index + 1}`);
renamedKeys.forEach((displayName, index) => {
keyMap[displayName] = rawKeys[index];
});
setScreenshotKeys(renamedKeys);
setScreenshotKeyMap(keyMap);
} else { } else {
const keyMap: Record<string, string> = {}; // Same rule as captured lists: if name missing or generic, auto-label
rawKeys.forEach(key => { normalizedScreenshotKeys = rawKeys.map((key, index) => {
keyMap[key] = key; if (!key || key.toLowerCase().includes("screenshot")) {
return `Screenshot ${index + 1}`;
}
return key;
}); });
setScreenshotKeys(rawKeys);
setScreenshotKeyMap(keyMap);
} }
const keyMap: Record<string, string> = {};
normalizedScreenshotKeys.forEach((displayName, index) => {
keyMap[displayName] = rawKeys[index];
});
setScreenshotKeys(normalizedScreenshotKeys);
setScreenshotKeyMap(keyMap);
setCurrentScreenshotIndex(0); setCurrentScreenshotIndex(0);
} else { } else {
setScreenshotKeys([]); setScreenshotKeys([]);
@@ -202,7 +204,14 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
const processSchemaData = (schemaOutput: any) => { const processSchemaData = (schemaOutput: any) => {
const keys = Object.keys(schemaOutput); const keys = Object.keys(schemaOutput);
setSchemaKeys(keys); const normalizedKeys = keys.map((key, index) => {
if (!key || key.toLowerCase().includes("scrapeschema")) {
return keys.length === 1 ? "Texts" : `Text ${index + 1}`;
}
return key;
});
setSchemaKeys(normalizedKeys);
const dataByKey: Record<string, any[]> = {}; const dataByKey: Record<string, any[]> = {};
const columnsByKey: Record<string, string[]> = {}; const columnsByKey: Record<string, string[]> = {};
@@ -248,8 +257,17 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
} }
}); });
setSchemaDataByKey(dataByKey); const remappedDataByKey: Record<string, any[]> = {};
setSchemaColumnsByKey(columnsByKey); const remappedColumnsByKey: Record<string, string[]> = {};
normalizedKeys.forEach((newKey, idx) => {
const oldKey = keys[idx];
remappedDataByKey[newKey] = dataByKey[oldKey];
remappedColumnsByKey[newKey] = columnsByKey[oldKey];
});
setSchemaDataByKey(remappedDataByKey);
setSchemaColumnsByKey(remappedColumnsByKey);
if (allData.length > 0) { if (allData.length > 0) {
const allColumns = new Set<string>(); const allColumns = new Set<string>();
@@ -290,7 +308,14 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
setListData(tablesList); setListData(tablesList);
setListColumns(columnsList); setListColumns(columnsList);
setListKeys(keys); const normalizedListKeys = keys.map((key, index) => {
if (!key || key.toLowerCase().includes("scrapelist")) {
return `List ${index + 1}`;
}
return key;
});
setListKeys(normalizedListKeys);
setCurrentListIndex(0); setCurrentListIndex(0);
}; };
@@ -617,10 +642,15 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
<TabContext value={tab}> <TabContext value={tab}>
<TabPanel value='output' sx={{ width: '100%', maxWidth: '900px' }}> <TabPanel value='output' sx={{ width: '100%', maxWidth: '900px' }}>
{row.status === 'running' || row.status === 'queued' ? ( {row.status === 'running' || row.status === 'queued' ? (
<Box sx={{ display: 'flex', alignItems: 'center' }}> <>
<CircularProgress size={22} sx={{ marginRight: '10px' }} /> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
{t('run_content.loading')} <CircularProgress size={22} sx={{ marginRight: '10px' }} />
</Box> {t('run_content.loading')}
</Box>
<Button color="error" onClick={abortRunHandler} sx={{ mt: 1 }}>
{t('run_content.buttons.stop')}
</Button>
</>
) : (!hasData && !hasScreenshots ) : (!hasData && !hasScreenshots
? <Typography>{t('run_content.empty_output')}</Typography> ? <Typography>{t('run_content.empty_output')}</Typography>
: null)} : null)}

View File

@@ -133,6 +133,10 @@ export const PageWrapper = () => {
path="/register" path="/register"
element={<Register />} element={<Register />}
/> />
<Route
path="/recording-setup"
element={<div />}
/>
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
</Box> </Box>

View File

@@ -43,7 +43,7 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
const { setId, socket } = useSocketStore(); const { setId, socket } = useSocketStore();
const { setWidth } = useBrowserDimensionsStore(); const { setWidth } = useBrowserDimensionsStore();
const { browserId, setBrowserId, recordingId, recordingUrl, setRecordingUrl, setRecordingName, setRetrainRobotId } = useGlobalInfoStore(); const { browserId, setBrowserId, recordingId, recordingUrl, setRecordingUrl, setRecordingName, setRetrainRobotId, setIsDOMMode } = useGlobalInfoStore();
const handleShowOutputData = useCallback(() => { const handleShowOutputData = useCallback(() => {
setShowOutputData(true); setShowOutputData(true);
@@ -77,6 +77,8 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
useEffect(() => { useEffect(() => {
let isCancelled = false; let isCancelled = false;
const handleRecording = async () => { const handleRecording = async () => {
setIsDOMMode(true);
const storedUrl = window.sessionStorage.getItem('recordingUrl'); const storedUrl = window.sessionStorage.getItem('recordingUrl');
if (storedUrl && !recordingUrl) { if (storedUrl && !recordingUrl) {
setRecordingUrl(storedUrl); setRecordingUrl(storedUrl);
@@ -137,9 +139,12 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
if (browserId === 'new-recording') { if (browserId === 'new-recording') {
socket?.emit('new-recording'); socket?.emit('new-recording');
} }
if (recordingUrl && socket) {
socket.emit('input:url', recordingUrl);
}
setIsLoaded(true); setIsLoaded(true);
} }
}, [socket, browserId, recordingName, recordingId, isLoaded]); }, [socket, browserId, recordingName, recordingId, recordingUrl, isLoaded]);
useEffect(() => { useEffect(() => {
socket?.on('loaded', handleLoaded); socket?.on('loaded', handleLoaded);
@@ -153,26 +158,20 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
<ActionProvider> <ActionProvider>
<BrowserStepsProvider> <BrowserStepsProvider>
<div id="browser-recorder"> <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' }}>
<Grid container direction="row" style={{ flexGrow: 1, height: '100%' }}> <div style={{ height: '100%', overflow: 'auto' }}>
<Grid item xs={12} md={9} lg={9} style={{ height: '100%', overflow: 'hidden', position: 'relative' }}> <BrowserContent />
<div style={{ height: '100%', overflow: 'hidden', display: 'flex', flexDirection: 'column', }}> <InterpretationLog isOpen={showOutputData} setIsOpen={setShowOutputData} />
<BrowserContent /> </div>
<InterpretationLog isOpen={showOutputData} setIsOpen={setShowOutputData} /> </Grid>
</div> <Grid item xs={12} md={3} lg={3} style={{ height: '100%', overflow: 'hidden' }}>
</Grid> <div className="right-side-panel" style={{ height: '100%' }}>
<Grid item xs={12} md={3} lg={3} style={{ height: '100%', overflow: 'hidden' }}> <RightSidePanel onFinishCapture={handleShowOutputData} />
<div className="right-side-panel" style={{ height: '100%' }}> <BrowserRecordingSave />
<RightSidePanel onFinishCapture={handleShowOutputData} /> </div>
<BrowserRecordingSave /> </Grid>
</div> </Grid>
</Grid>
</Grid>
</>
) : (
<Loader text={t('recording_page.loader.browser_startup', { url: recordingUrl })} />
)}
</div> </div>
</BrowserStepsProvider> </BrowserStepsProvider>
</ActionProvider> </ActionProvider>