Merge branch 'develop' into recorder-ui-shift
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()) {
|
||||||
@@ -522,6 +559,12 @@ export class RemoteBrowser {
|
|||||||
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({
|
||||||
@@ -546,6 +589,8 @@ export class RemoteBrowser {
|
|||||||
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();
|
||||||
@@ -646,6 +693,8 @@ export class RemoteBrowser {
|
|||||||
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) {
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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%',
|
||||||
|
|||||||
@@ -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' }} />
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -116,26 +116,28 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
|||||||
|
|
||||||
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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user