@@ -7,10 +7,11 @@ COPY package*.json ./
|
||||
COPY maxun-core ./maxun-core
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
# Copy frontend source code and config
|
||||
COPY src ./src
|
||||
COPY public ./public
|
||||
COPY index.html ./
|
||||
COPY vite.config.js ./
|
||||
COPY tsconfig.json ./
|
||||
|
||||
13
README.md
13
README.md
@@ -30,14 +30,15 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web
|
||||
<img src="https://static.scarf.sh/a.png?x-pxid=c12a77cc-855e-4602-8a0f-614b2d0da56a" />
|
||||
|
||||
# Installation
|
||||
1. First, create a file named `.env` in the root folder of the project
|
||||
2. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). Copy all content of example env to your `.env` file.
|
||||
3. Choose your installation method below
|
||||
1. Create a root folder for your project (e.g. 'maxun')
|
||||
2. Create a file named `.env` in the root folder of the project
|
||||
3. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). Copy all content of example env to your `.env` file.
|
||||
4. Choose your installation method below
|
||||
|
||||
### Docker Compose
|
||||
1. Copy paste the [docker-compose.yml file](https://github.com/getmaxun/maxun/blob/master/docker-compose.yml)
|
||||
2. Ensure you have setup the `.env` file
|
||||
3. Run the command below
|
||||
1. Copy paste the [docker-compose.yml file](https://github.com/getmaxun/maxun/blob/master/docker-compose.yml) into your root folder
|
||||
2. Ensure you have setup the `.env` file in that same folder
|
||||
3. Run the command below from a terminal
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
@@ -43,7 +43,7 @@ services:
|
||||
#build:
|
||||
#context: .
|
||||
#dockerfile: server/Dockerfile
|
||||
image: getmaxun/maxun-backend:v0.0.7
|
||||
image: getmaxun/maxun-backend:v0.0.9
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}"
|
||||
env_file: .env
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
#build:
|
||||
#context: .
|
||||
#dockerfile: Dockerfile
|
||||
image: getmaxun/maxun-frontend:v0.0.3
|
||||
image: getmaxun/maxun-frontend:v0.0.5
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}"
|
||||
env_file: .env
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "maxun-core",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "Core package for Maxun, responsible for data extraction",
|
||||
"main": "build/index.js",
|
||||
"typings": "build/index.d.ts",
|
||||
|
||||
@@ -265,41 +265,72 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
||||
const scrapedData = [];
|
||||
|
||||
while (scrapedData.length < limit) {
|
||||
// Get all parent elements matching the listSelector
|
||||
const parentElements = Array.from(document.querySelectorAll(listSelector));
|
||||
let parentElements = Array.from(document.querySelectorAll(listSelector));
|
||||
|
||||
// If we only got one element or none, try a more generic approach
|
||||
if (limit > 1 && parentElements.length <= 1) {
|
||||
const [containerSelector, _] = listSelector.split('>').map(s => s.trim());
|
||||
const container = document.querySelector(containerSelector);
|
||||
|
||||
if (container) {
|
||||
const allChildren = Array.from(container.children);
|
||||
|
||||
const firstMatch = document.querySelector(listSelector);
|
||||
if (firstMatch) {
|
||||
// Get classes from the first matching element
|
||||
const firstMatchClasses = Array.from(firstMatch.classList);
|
||||
|
||||
// Find similar elements by matching most of their classes
|
||||
parentElements = allChildren.filter(element => {
|
||||
const elementClasses = Array.from(element.classList);
|
||||
|
||||
// Iterate through each parent element
|
||||
for (const parent of parentElements) {
|
||||
if (scrapedData.length >= limit) break;
|
||||
const record = {};
|
||||
|
||||
// For each field, select the corresponding element within the parent
|
||||
for (const [label, { selector, attribute }] of Object.entries(fields)) {
|
||||
const fieldElement = parent.querySelector(selector);
|
||||
|
||||
if (fieldElement) {
|
||||
if (attribute === 'innerText') {
|
||||
record[label] = fieldElement.innerText.trim();
|
||||
} else if (attribute === 'innerHTML') {
|
||||
record[label] = fieldElement.innerHTML.trim();
|
||||
} else if (attribute === 'src') {
|
||||
// Handle relative 'src' URLs
|
||||
const src = fieldElement.getAttribute('src');
|
||||
record[label] = src ? new URL(src, window.location.origin).href : null;
|
||||
} else if (attribute === 'href') {
|
||||
// Handle relative 'href' URLs
|
||||
const href = fieldElement.getAttribute('href');
|
||||
record[label] = href ? new URL(href, window.location.origin).href : null;
|
||||
} else {
|
||||
record[label] = fieldElement.getAttribute(attribute);
|
||||
// Element should share at least 70% of classes with the first match
|
||||
const commonClasses = firstMatchClasses.filter(cls =>
|
||||
elementClasses.includes(cls));
|
||||
return commonClasses.length >= Math.floor(firstMatchClasses.length * 0.7);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scrapedData.push(record);
|
||||
}
|
||||
|
||||
// Iterate through each parent element
|
||||
for (const parent of parentElements) {
|
||||
if (scrapedData.length >= limit) break;
|
||||
const record = {};
|
||||
|
||||
// For each field, select the corresponding element within the parent
|
||||
for (const [label, { selector, attribute }] of Object.entries(fields)) {
|
||||
const fieldElement = parent.querySelector(selector);
|
||||
|
||||
if (fieldElement) {
|
||||
if (attribute === 'innerText') {
|
||||
record[label] = fieldElement.innerText.trim();
|
||||
} else if (attribute === 'innerHTML') {
|
||||
record[label] = fieldElement.innerHTML.trim();
|
||||
} else if (attribute === 'src') {
|
||||
// Handle relative 'src' URLs
|
||||
const src = fieldElement.getAttribute('src');
|
||||
record[label] = src ? new URL(src, window.location.origin).href : null;
|
||||
} else if (attribute === 'href') {
|
||||
// Handle relative 'href' URLs
|
||||
const href = fieldElement.getAttribute('href');
|
||||
record[label] = href ? new URL(href, window.location.origin).href : null;
|
||||
} else {
|
||||
record[label] = fieldElement.getAttribute(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
scrapedData.push(record);
|
||||
}
|
||||
|
||||
// If we've processed all available elements and still haven't reached the limit,
|
||||
// break to avoid infinite loop
|
||||
if (parentElements.length === 0 || scrapedData.length >= parentElements.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return scrapedData
|
||||
};
|
||||
return scrapedData;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,13 +111,21 @@ export default class Interpreter extends EventEmitter {
|
||||
|
||||
private async applyAdBlocker(page: Page): Promise<void> {
|
||||
if (this.blocker) {
|
||||
await this.blocker.enableBlockingInPage(page);
|
||||
try {
|
||||
await this.blocker.enableBlockingInPage(page);
|
||||
} catch (err) {
|
||||
this.log(`Ad-blocker operation failed:`, Level.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async disableAdBlocker(page: Page): Promise<void> {
|
||||
if (this.blocker) {
|
||||
await this.blocker.disableBlockingInPage(page);
|
||||
try {
|
||||
await this.blocker.disableBlockingInPage(page);
|
||||
} catch (err) {
|
||||
this.log(`Ad-blocker operation failed:`, Level.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,8 +200,8 @@ export default class Interpreter extends EventEmitter {
|
||||
// const actionable = async (selector: string): Promise<boolean> => {
|
||||
// try {
|
||||
// const proms = [
|
||||
// page.isEnabled(selector, { timeout: 5000 }),
|
||||
// page.isVisible(selector, { timeout: 5000 }),
|
||||
// page.isEnabled(selector, { timeout: 10000 }),
|
||||
// page.isVisible(selector, { timeout: 10000 }),
|
||||
// ];
|
||||
|
||||
// return await Promise.all(proms).then((bools) => bools.every((x) => x));
|
||||
@@ -214,6 +222,17 @@ export default class Interpreter extends EventEmitter {
|
||||
// return [];
|
||||
// }),
|
||||
// ).then((x) => x.flat());
|
||||
|
||||
const presentSelectors: SelectorArray = await Promise.all(
|
||||
selectors.map(async (selector) => {
|
||||
try {
|
||||
await page.waitForSelector(selector, { state: 'attached' });
|
||||
return [selector];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
).then((x) => x.flat());
|
||||
|
||||
const action = workflowCopy[workflowCopy.length - 1];
|
||||
|
||||
@@ -233,7 +252,7 @@ export default class Interpreter extends EventEmitter {
|
||||
...p,
|
||||
[cookie.name]: cookie.value,
|
||||
}), {}),
|
||||
selectors,
|
||||
selectors: presentSelectors,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -506,7 +525,11 @@ export default class Interpreter extends EventEmitter {
|
||||
try {
|
||||
await executeAction(invokee, methodName, step.args);
|
||||
} catch (error) {
|
||||
await executeAction(invokee, methodName, [step.args[0], { force: true }]);
|
||||
try{
|
||||
await executeAction(invokee, methodName, [step.args[0], { force: true }]);
|
||||
} catch (error) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await executeAction(invokee, methodName, step.args);
|
||||
@@ -647,7 +670,11 @@ export default class Interpreter extends EventEmitter {
|
||||
const workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow));
|
||||
|
||||
// apply ad-blocker to the current page
|
||||
await this.applyAdBlocker(p);
|
||||
try {
|
||||
await this.applyAdBlocker(p);
|
||||
} catch (error) {
|
||||
this.log(`Failed to apply ad-blocker: ${error.message}`, Level.ERROR);
|
||||
}
|
||||
const usedActions: string[] = [];
|
||||
let selectors: string[] = [];
|
||||
let lastAction = null;
|
||||
@@ -767,6 +794,8 @@ export default class Interpreter extends EventEmitter {
|
||||
public async run(page: Page, params?: ParamType): Promise<void> {
|
||||
this.log('Starting the workflow.', Level.LOG);
|
||||
const context = page.context();
|
||||
|
||||
page.setDefaultNavigationTimeout(100000);
|
||||
|
||||
// Check proxy settings from context options
|
||||
const contextOptions = (context as any)._options;
|
||||
|
||||
@@ -3,36 +3,36 @@
|
||||
*/
|
||||
export default class Concurrency {
|
||||
/**
|
||||
* Maximum number of workers running in parallel. If set to `null`, there is no limit.
|
||||
*/
|
||||
* Maximum number of workers running in parallel. If set to `null`, there is no limit.
|
||||
*/
|
||||
maxConcurrency: number = 1;
|
||||
|
||||
/**
|
||||
* Number of currently active workers.
|
||||
*/
|
||||
* Number of currently active workers.
|
||||
*/
|
||||
activeWorkers: number = 0;
|
||||
|
||||
/**
|
||||
* Queue of jobs waiting to be completed.
|
||||
*/
|
||||
* Queue of jobs waiting to be completed.
|
||||
*/
|
||||
private jobQueue: Function[] = [];
|
||||
|
||||
/**
|
||||
* "Resolve" callbacks of the waitForCompletion() promises.
|
||||
*/
|
||||
* "Resolve" callbacks of the waitForCompletion() promises.
|
||||
*/
|
||||
private waiting: Function[] = [];
|
||||
|
||||
/**
|
||||
* Constructs a new instance of concurrency manager.
|
||||
* @param {number} maxConcurrency Maximum number of workers running in parallel.
|
||||
*/
|
||||
* Constructs a new instance of concurrency manager.
|
||||
* @param {number} maxConcurrency Maximum number of workers running in parallel.
|
||||
*/
|
||||
constructor(maxConcurrency: number) {
|
||||
this.maxConcurrency = maxConcurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a waiting job out of the queue and runs it.
|
||||
*/
|
||||
* Takes a waiting job out of the queue and runs it.
|
||||
*/
|
||||
private runNextJob(): void {
|
||||
const job = this.jobQueue.pop();
|
||||
|
||||
@@ -53,12 +53,12 @@ export default class Concurrency {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass a job (a time-demanding async function) to the concurrency manager. \
|
||||
* The time of the job's execution depends on the concurrency manager itself
|
||||
* (given a generous enough `maxConcurrency` value, it might be immediate,
|
||||
* but this is not guaranteed).
|
||||
* @param worker Async function to be executed (job to be processed).
|
||||
*/
|
||||
* Pass a job (a time-demanding async function) to the concurrency manager. \
|
||||
* The time of the job's execution depends on the concurrency manager itself
|
||||
* (given a generous enough `maxConcurrency` value, it might be immediate,
|
||||
* but this is not guaranteed).
|
||||
* @param worker Async function to be executed (job to be processed).
|
||||
*/
|
||||
addJob(job: () => Promise<any>): void {
|
||||
// console.debug("Adding a worker!");
|
||||
this.jobQueue.push(job);
|
||||
@@ -72,11 +72,11 @@ export default class Concurrency {
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until there is no running nor waiting job. \
|
||||
* If the concurrency manager is idle at the time of calling this function,
|
||||
* it waits until at least one job is completed (can be "presubscribed").
|
||||
* @returns Promise, resolved after there is no running/waiting worker.
|
||||
*/
|
||||
* Waits until there is no running nor waiting job. \
|
||||
* If the concurrency manager is idle at the time of calling this function,
|
||||
* it waits until at least one job is completed (can be "presubscribed").
|
||||
* @returns Promise, resolved after there is no running/waiting worker.
|
||||
*/
|
||||
waitForCompletion(): Promise<void> {
|
||||
return new Promise((res) => {
|
||||
this.waiting.push(res);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "maxun",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"author": "Maxun",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -36,6 +36,9 @@
|
||||
"fortawesome": "^0.0.1-security",
|
||||
"google-auth-library": "^9.14.1",
|
||||
"googleapis": "^144.0.0",
|
||||
"i18next": "^24.0.2",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
"idcac-playwright": "^0.1.3",
|
||||
"ioredis": "^5.4.1",
|
||||
"joi": "^17.6.0",
|
||||
@@ -43,7 +46,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.8.0",
|
||||
"loglevel-plugin-remote": "^0.6.8",
|
||||
"maxun-core": "^0.0.6",
|
||||
"maxun-core": "^0.0.7",
|
||||
"minio": "^8.0.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"node-cron": "^3.0.3",
|
||||
@@ -57,6 +60,7 @@
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-highlight": "0.15.0",
|
||||
"react-i18next": "^15.1.3",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-simple-code-editor": "^0.11.2",
|
||||
"react-transition-group": "^4.4.2",
|
||||
|
||||
490
public/locales/de.json
Normal file
490
public/locales/de.json
Normal file
@@ -0,0 +1,490 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Willkommen zurück!",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"button": "Einloggen",
|
||||
"loading": "Lädt",
|
||||
"register_prompt": "Noch keinen Account?",
|
||||
"register_link": "Registrieren",
|
||||
"welcome_notification": "Willkommen bei Maxun!",
|
||||
"error_notification": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut."
|
||||
},
|
||||
"register": {
|
||||
"title": "Konto registrieren",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"button": "Registrieren",
|
||||
"loading": "Lädt",
|
||||
"register_prompt": "Bereits ein Konto?",
|
||||
"login_link": "Einloggen",
|
||||
"welcome_notification": "Willkommen bei Maxun!",
|
||||
"error_notification": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut."
|
||||
},
|
||||
"recordingtable": {
|
||||
"run": "Ausführen",
|
||||
"name": "Name",
|
||||
"schedule": "Zeitplan",
|
||||
"integrate": "Integrieren",
|
||||
"settings": "Einstellungen",
|
||||
"options": "Optionen",
|
||||
"heading": "Meine Roboter",
|
||||
"new": "Roboter erstellen",
|
||||
"modal": {
|
||||
"title": "Geben Sie die URL ein",
|
||||
"label": "URL",
|
||||
"button": "Aufnahme starten"
|
||||
},
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"duplicate": "Duplizieren",
|
||||
"notifications": {
|
||||
"delete_warning": "Roboter kann nicht gelöscht werden, da zugehörige Ausführungen vorhanden sind",
|
||||
"delete_success": "Roboter erfolgreich gelöscht"
|
||||
}
|
||||
},
|
||||
"mainmenu": {
|
||||
"recordings": "Roboter",
|
||||
"runs": "Ausführungen",
|
||||
"proxy": "Proxy",
|
||||
"apikey": "API-Schlüssel",
|
||||
"feedback": "Maxun Cloud beitreten",
|
||||
"apidocs": "Website zu API"
|
||||
},
|
||||
"runstable": {
|
||||
"runs": "Alle Ausführungen",
|
||||
"runStatus": "Status",
|
||||
"runName": "Name",
|
||||
"startedAt": "Gestartet am",
|
||||
"finishedAt": "Beendet am",
|
||||
"delete": "Löschen",
|
||||
"settings": "Einstellungen",
|
||||
"search": "Ausführungen suchen...",
|
||||
"notifications": {
|
||||
"no_runs": "Keine Ausführungen gefunden. Bitte versuchen Sie es erneut.",
|
||||
"delete_success": "Ausführung erfolgreich gelöscht"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"title": "Proxy-Konfiguration",
|
||||
"tab_standard": "Standard-Proxy",
|
||||
"tab_rotation": "Automatische Proxy-Rotation",
|
||||
"server_url": "Proxy-Server-URL",
|
||||
"server_url_helper": "Proxy für alle Roboter. HTTP- und SOCKS-Proxys werden unterstützt. Beispiel http://myproxy.com:3128 oder socks5://myproxy.com:3128. Kurzform myproxy.com:3128 wird als HTTP-Proxy behandelt.",
|
||||
"requires_auth": "Authentifizierung erforderlich?",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"add_proxy": "Proxy hinzufügen",
|
||||
"test_proxy": "Proxy testen",
|
||||
"remove_proxy": "Proxy entfernen",
|
||||
"table": {
|
||||
"proxy_url": "Proxy-URL",
|
||||
"requires_auth": "Authentifizierung erforderlich"
|
||||
},
|
||||
"coming_soon": "Demnächst verfügbar - In Open Source (Basis-Rotation) & Cloud (Erweiterte Rotation). Wenn Sie die Infrastruktur nicht selbst verwalten möchten, tragen Sie sich in unsere Cloud-Warteliste ein.",
|
||||
"join_waitlist": "Maxun Cloud Warteliste beitreten",
|
||||
"alert": {
|
||||
"title": "Wenn Ihr Proxy einen Benutzernamen und ein Passwort erfordert, geben Sie diese immer separat von der Proxy-URL an.",
|
||||
"right_way": "Der richtige Weg",
|
||||
"wrong_way": "Der falsche Weg",
|
||||
"proxy_url": "Proxy-URL:",
|
||||
"username": "Benutzername:",
|
||||
"password": "Passwort:"
|
||||
},
|
||||
"notifications": {
|
||||
"config_success": "Proxy-Konfiguration erfolgreich übermittelt",
|
||||
"config_error": "Fehler beim Übermitteln der Proxy-Konfiguration. Bitte erneut versuchen.",
|
||||
"test_success": "Proxy-Konfiguration funktioniert",
|
||||
"test_error": "Fehler beim Testen der Proxy-Konfiguration. Bitte erneut versuchen.",
|
||||
"fetch_success": "Proxy-Konfiguration erfolgreich abgerufen",
|
||||
"remove_success": "Proxy-Konfiguration erfolgreich entfernt",
|
||||
"remove_error": "Fehler beim Entfernen der Proxy-Konfiguration. Bitte erneut versuchen."
|
||||
}
|
||||
},
|
||||
"apikey": {
|
||||
"title": "API-Schlüssel verwalten",
|
||||
"default_name": "Maxun API-Schlüssel",
|
||||
"table": {
|
||||
"name": "API-Schlüssel Name",
|
||||
"key": "API-Schlüssel",
|
||||
"actions": "Aktionen"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Kopieren",
|
||||
"show": "Anzeigen",
|
||||
"hide": "Ausblenden",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"no_key_message": "Sie haben noch keinen API-Schlüssel generiert.",
|
||||
"generate_button": "API-Schlüssel generieren",
|
||||
"notifications": {
|
||||
"fetch_error": "API-Schlüssel konnte nicht abgerufen werden - ${error}",
|
||||
"generate_success": "API-Schlüssel erfolgreich generiert",
|
||||
"generate_error": "API-Schlüssel konnte nicht generiert werden - ${error}",
|
||||
"delete_success": "API-Schlüssel erfolgreich gelöscht",
|
||||
"delete_error": "API-Schlüssel konnte nicht gelöscht werden - ${error}",
|
||||
"copy_success": "API-Schlüssel erfolgreich kopiert"
|
||||
}
|
||||
},
|
||||
"action_description": {
|
||||
"text": {
|
||||
"title": "Text erfassen",
|
||||
"description": "Fahren Sie über die Texte, die Sie extrahieren möchten, und klicken Sie, um sie auszuwählen"
|
||||
},
|
||||
"screenshot": {
|
||||
"title": "Screenshot erfassen",
|
||||
"description": "Erfassen Sie einen Teil- oder Vollbildschirmfoto der aktuellen Seite."
|
||||
},
|
||||
"list": {
|
||||
"title": "Liste erfassen",
|
||||
"description": "Fahren Sie über die Liste, die Sie extrahieren möchten. Nach der Auswahl können Sie über alle Texte in der ausgewählten Liste fahren. Klicken Sie zum Auswählen."
|
||||
},
|
||||
"default": {
|
||||
"title": "Welche Daten möchten Sie extrahieren?",
|
||||
"description": "Ein Roboter ist darauf ausgelegt, eine Aktion nach der anderen auszuführen. Sie können eine der folgenden Optionen wählen."
|
||||
},
|
||||
"list_stages": {
|
||||
"initial": "Wählen Sie die Liste aus, die Sie extrahieren möchten, zusammen mit den darin enthaltenen Texten",
|
||||
"pagination": "Wählen Sie aus, wie der Roboter den Rest der Liste erfassen kann",
|
||||
"limit": "Wählen Sie die Anzahl der zu extrahierenden Elemente",
|
||||
"complete": "Erfassung ist abgeschlossen"
|
||||
}
|
||||
},
|
||||
"right_panel": {
|
||||
"buttons": {
|
||||
"capture_list": "Liste erfassen",
|
||||
"capture_text": "Text erfassen",
|
||||
"capture_screenshot": "Screenshot erfassen",
|
||||
"confirm": "Bestätigen",
|
||||
"discard": "Verwerfen",
|
||||
"confirm_capture": "Erfassung bestätigen",
|
||||
"confirm_pagination": "Paginierung bestätigen",
|
||||
"confirm_limit": "Limit bestätigen",
|
||||
"finish_capture": "Erfassung abschließen",
|
||||
"finish": "Fertig",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "Vollständige Seite erfassen",
|
||||
"capture_visible": "Sichtbaren Bereich erfassen",
|
||||
"display_fullpage": "Vollständige Seite Screenshot",
|
||||
"display_visible": "Sichtbarer Bereich Screenshot"
|
||||
},
|
||||
"pagination": {
|
||||
"title": "Wie können wir das nächste Listenelement auf der Seite finden?",
|
||||
"click_next": "Auf 'Weiter' klicken, um zur nächsten Seite zu navigieren",
|
||||
"click_load_more": "Auf 'Mehr laden' klicken, um weitere Elemente zu laden",
|
||||
"scroll_down": "Nach unten scrollen, um mehr Elemente zu laden",
|
||||
"scroll_up": "Nach oben scrollen, um mehr Elemente zu laden",
|
||||
"none": "Keine weiteren Elemente zu laden"
|
||||
},
|
||||
"limit": {
|
||||
"title": "Wie viele Zeilen möchten Sie maximal extrahieren?",
|
||||
"custom": "Benutzerdefiniert",
|
||||
"enter_number": "Nummer eingeben"
|
||||
},
|
||||
"fields": {
|
||||
"label": "Bezeichnung",
|
||||
"data": "Daten",
|
||||
"field_label": "Feldbezeichnung",
|
||||
"field_data": "Felddaten"
|
||||
},
|
||||
"messages": {
|
||||
"list_selected": "Liste erfolgreich ausgewählt"
|
||||
},
|
||||
"errors": {
|
||||
"select_pagination": "Bitte wählen Sie einen Paginierungstyp aus.",
|
||||
"select_pagination_element": "Bitte wählen Sie zuerst das Paginierungselement aus.",
|
||||
"select_limit": "Bitte wählen Sie ein Limit oder geben Sie ein benutzerdefiniertes Limit ein.",
|
||||
"invalid_limit": "Bitte geben Sie ein gültiges Limit ein.",
|
||||
"confirm_text_fields": "Bitte bestätigen Sie alle Textfelder",
|
||||
"unable_create_settings": "Listeneinstellungen können nicht erstellt werden. Stellen Sie sicher, dass Sie ein Feld für die Liste definiert haben.",
|
||||
"capture_text_discarded": "Texterfassung verworfen",
|
||||
"capture_list_discarded": "Listenerfassung verworfen"
|
||||
}
|
||||
},
|
||||
"save_recording": {
|
||||
"title": "Roboter speichern",
|
||||
"robot_name": "Roboter Name",
|
||||
"buttons": {
|
||||
"save": "Speichern",
|
||||
"confirm": "Bestätigen"
|
||||
},
|
||||
"notifications": {
|
||||
"save_success": "Roboter erfolgreich gespeichert"
|
||||
},
|
||||
"errors": {
|
||||
"user_not_logged": "Benutzer nicht angemeldet. Aufnahme kann nicht gespeichert werden.",
|
||||
"exists_warning": "Ein Roboter mit diesem Namen existiert bereits, bitte bestätigen Sie das Überschreiben des Roboters."
|
||||
},
|
||||
"tooltips": {
|
||||
"saving": "Workflow wird optimiert und gespeichert"
|
||||
}
|
||||
},
|
||||
"browser_recording": {
|
||||
"modal": {
|
||||
"confirm_discard": "Sind Sie sicher, dass Sie die Aufnahme verwerfen möchten?"
|
||||
},
|
||||
"notifications": {
|
||||
"terminated": "Aktuelle Aufnahme wurde beendet"
|
||||
}
|
||||
},
|
||||
"interpretation_log": {
|
||||
"titles": {
|
||||
"output_preview": "Vorschau der Ausgabedaten",
|
||||
"screenshot": "Bildschirmfoto"
|
||||
},
|
||||
"messages": {
|
||||
"additional_rows": "Weitere Datenzeilen werden nach Abschluss der Aufnahme extrahiert.",
|
||||
"successful_training": "Sie haben den Roboter erfolgreich für Aktionen trainiert! Klicken Sie auf die Schaltfläche unten, um eine Vorschau der Daten zu erhalten, die Ihr Roboter extrahieren wird.",
|
||||
"no_selection": "Sie haben noch nichts zur Extraktion ausgewählt. Sobald Sie dies tun, wird der Roboter hier eine Vorschau Ihrer Auswahl anzeigen."
|
||||
},
|
||||
"data_sections": {
|
||||
"binary_received": "---------- Binäre Ausgabedaten empfangen ----------",
|
||||
"serializable_received": "---------- Serialisierbare Ausgabedaten empfangen ----------",
|
||||
"mimetype": "Medientyp: ",
|
||||
"image_below": "Bild wird unten angezeigt:",
|
||||
"separator": "--------------------------------------------------"
|
||||
},
|
||||
"notifications": {
|
||||
"reset_success": "Vorschau erfolgreich zurückgesetzt"
|
||||
}
|
||||
},
|
||||
"interpretation_buttons": {
|
||||
"buttons": {
|
||||
"preview": "Vorschau der Ausgabedaten anzeigen",
|
||||
"reset": "Zurücksetzen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein"
|
||||
},
|
||||
"messages": {
|
||||
"extracting": "Daten werden extrahiert...bitte warten Sie 10 Sekunden bis 1 Minute",
|
||||
"restart_required": "Bitte starten Sie die Interpretation nach der Aktualisierung der Aufnahme neu",
|
||||
"run_finished": "Durchlauf beendet",
|
||||
"run_failed": "Start fehlgeschlagen"
|
||||
},
|
||||
"modal": {
|
||||
"use_previous": "Möchten Sie Ihre vorherige Auswahl als Bedingung für diese Aktion verwenden?",
|
||||
"previous_action": "Ihre vorherige Aktion war: ",
|
||||
"element_text": "auf einem Element mit Text "
|
||||
}
|
||||
},
|
||||
"recording_page": {
|
||||
"loader": {
|
||||
"browser_startup": "Browser wird gestartet...Navigation zu {{url}}"
|
||||
}
|
||||
},
|
||||
"integration_settings": {
|
||||
"title": "Mit Google Sheet integrieren",
|
||||
"descriptions": {
|
||||
"sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung dieses Roboters die erfassten Daten in Ihrem Google Sheet ergänzt.",
|
||||
"authenticated_as": "Authentifiziert als: {{email}}"
|
||||
},
|
||||
"alerts": {
|
||||
"success": {
|
||||
"title": "Google Sheet erfolgreich integriert.",
|
||||
"content": "Jedes Mal, wenn dieser Roboter eine erfolgreiche Ausführung erstellt, werden die erfassten Daten Ihrem Google Sheet {{sheetName}} hinzugefügt. Sie können die Datenaktualisierungen",
|
||||
"here": "hier",
|
||||
"note": "Hinweis:",
|
||||
"sync_limitation": "Die vor der Integration mit Google Sheets extrahierten Daten werden nicht im Google Sheet synchronisiert. Nur die nach der Integration extrahierten Daten werden synchronisiert."
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"authenticate": "Mit Google authentifizieren",
|
||||
"fetch_sheets": "Google Sheets abrufen",
|
||||
"remove_integration": "Integration entfernen",
|
||||
"submit": "Absenden"
|
||||
},
|
||||
"fields": {
|
||||
"select_sheet": "Google Sheet auswählen",
|
||||
"selected_sheet": "Ausgewähltes Sheet: {{name}} (ID: {{id}})"
|
||||
}
|
||||
},
|
||||
"robot_duplication": {
|
||||
"title": "Roboter duplizieren",
|
||||
"descriptions": {
|
||||
"purpose": "Die Roboter-Duplizierung ist nützlich, um Daten von Seiten mit der gleichen Struktur zu extrahieren.",
|
||||
"example": "Beispiel: Wenn Sie einen Roboter für {{url1}} erstellt haben, können Sie ihn duplizieren, um ähnliche Seiten wie {{url2}} zu durchsuchen, ohne einen Roboter von Grund auf neu zu trainieren.",
|
||||
"warning": "⚠️ Stellen Sie sicher, dass die neue Seite die gleiche Struktur wie die Originalseite hat."
|
||||
},
|
||||
"fields": {
|
||||
"target_url": "Roboter Ziel-URL"
|
||||
},
|
||||
"buttons": {
|
||||
"duplicate": "Roboter duplizieren",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"notifications": {
|
||||
"robot_not_found": "Roboterdetails konnten nicht gefunden werden. Bitte versuchen Sie es erneut.",
|
||||
"url_required": "Ziel-URL ist erforderlich.",
|
||||
"duplicate_success": "Roboter erfolgreich dupliziert.",
|
||||
"duplicate_error": "Fehler beim Aktualisieren der Ziel-URL. Bitte versuchen Sie es erneut.",
|
||||
"unknown_error": "Beim Aktualisieren der Ziel-URL ist ein Fehler aufgetreten."
|
||||
}
|
||||
},
|
||||
"robot_settings": {
|
||||
"title": "Roboter-Einstellungen",
|
||||
"target_url": "Roboter-Ziel-URL",
|
||||
"robot_id": "Roboter-ID",
|
||||
"robot_limit": "Roboter-Limit",
|
||||
"created_by_user": "Erstellt von Benutzer",
|
||||
"created_at": "Erstellungsdatum des Roboters",
|
||||
"errors": {
|
||||
"robot_not_found": "Roboterdetails konnten nicht gefunden werden. Bitte versuchen Sie es erneut."
|
||||
}
|
||||
},
|
||||
"robot_edit": {
|
||||
"title": "Roboter bearbeiten",
|
||||
"change_name": "Roboternamen ändern",
|
||||
"robot_limit": "Roboter-Limit",
|
||||
"save": "Änderungen speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"notifications": {
|
||||
"update_success": "Roboter erfolgreich aktualisiert.",
|
||||
"update_failed": "Aktualisierung des Roboters fehlgeschlagen. Bitte erneut versuchen.",
|
||||
"update_error": "Beim Aktualisieren des Roboters ist ein Fehler aufgetreten."
|
||||
}
|
||||
},
|
||||
"schedule_settings": {
|
||||
"title": "Zeitplan-Einstellungen",
|
||||
"run_every": "Ausführen alle",
|
||||
"start_from": "Beginnen ab",
|
||||
"on_day": "An Tag",
|
||||
"at_around": "Um",
|
||||
"timezone": "Zeitzone",
|
||||
"buttons": {
|
||||
"delete_schedule": "Zeitplan löschen",
|
||||
"save_schedule": "Zeitplan speichern",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"labels": {
|
||||
"in_between": "Zwischen",
|
||||
"run_once_every": "Ausführen alle",
|
||||
"start_from_label": "Beginnen ab",
|
||||
"on_day_of_month": "Tag des Monats",
|
||||
"on_day": {
|
||||
"st": ".",
|
||||
"nd": ".",
|
||||
"rd": ".",
|
||||
"th": "."
|
||||
}
|
||||
}
|
||||
},
|
||||
"main_page": {
|
||||
"notifications": {
|
||||
"interpretation_success": "Interpretation des Roboters {{name}} erfolgreich",
|
||||
"interpretation_failed": "Interpretation des Roboters {{name}} fehlgeschlagen",
|
||||
"run_started": "Roboter wird ausgeführt: {{name}}",
|
||||
"run_start_failed": "Fehler beim Ausführen des Roboters: {{name}}",
|
||||
"schedule_success": "Roboter {{name}} erfolgreich geplant",
|
||||
"schedule_failed": "Planen des Roboters {{name}} fehlgeschlagen",
|
||||
"abort_success": "Interpretation des Roboters {{name}} erfolgreich abgebrochen",
|
||||
"abort_failed": "Abbrechen der Interpretation des Roboters {{name}} fehlgeschlagen"
|
||||
},
|
||||
"menu": {
|
||||
"recordings": "Roboter",
|
||||
"runs": "Ausführungen",
|
||||
"proxy": "Proxy",
|
||||
"apikey": "API-Schlüssel"
|
||||
}
|
||||
},
|
||||
"browser_window": {
|
||||
"attribute_modal": {
|
||||
"title": "Attribut auswählen",
|
||||
"notifications": {
|
||||
"list_select_success": "Liste erfolgreich ausgewählt. Wählen Sie die zu extrahierenden Textdaten.",
|
||||
"pagination_select_success": "Paginierungselement erfolgreich ausgewählt."
|
||||
}
|
||||
},
|
||||
"attribute_options": {
|
||||
"anchor": {
|
||||
"text": "Text: {{text}}",
|
||||
"url": "URL: {{url}}"
|
||||
},
|
||||
"image": {
|
||||
"alt_text": "Alt-Text: {{altText}}",
|
||||
"image_url": "Bild-URL: {{imageUrl}}"
|
||||
},
|
||||
"default": {
|
||||
"text": "Text: {{text}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"runs_table": {
|
||||
"run_type_chips": {
|
||||
"manual_run": "Manuelle Ausführung",
|
||||
"scheduled_run": "Geplante Ausführung",
|
||||
"api": "API",
|
||||
"unknown_run_type": "Unbekannter Ausführungstyp"
|
||||
},
|
||||
"run_status_chips": {
|
||||
"success": "Erfolg",
|
||||
"running": "Läuft",
|
||||
"scheduled": "Geplant",
|
||||
"failed": "Fehlgeschlagen"
|
||||
},
|
||||
"run_settings_modal": {
|
||||
"title": "Ausführungseinstellungen",
|
||||
"labels": {
|
||||
"run_id": "Ausführungs-ID",
|
||||
"run_by_user": "Ausgeführt von Benutzer",
|
||||
"run_by_schedule": "Ausgeführt nach Zeitplan-ID",
|
||||
"run_by_api": "Ausgeführt durch API",
|
||||
"run_type": "Ausführungstyp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"run_content": {
|
||||
"tabs": {
|
||||
"output_data": "Ausgabedaten",
|
||||
"log": "Protokoll"
|
||||
},
|
||||
"empty_output": "Die Ausgabe ist leer.",
|
||||
"captured_data": {
|
||||
"title": "Erfasste Daten",
|
||||
"download_json": "Als JSON herunterladen",
|
||||
"download_csv": "Als CSV herunterladen"
|
||||
},
|
||||
"captured_screenshot": {
|
||||
"title": "Erfasster Screenshot",
|
||||
"download": "Screenshot herunterladen",
|
||||
"render_failed": "Das Bild konnte nicht gerendert werden"
|
||||
},
|
||||
"buttons": {
|
||||
"stop": "Stoppen"
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
"project_name": "Maxun",
|
||||
"upgrade": {
|
||||
"button": "Upgrade",
|
||||
"modal": {
|
||||
"up_to_date": "🎉 Du bist auf dem neuesten Stand!",
|
||||
"new_version_available": "Eine neue Version ist verfügbar: {{version}}. Aktualisieren Sie auf die neueste Version für Fehlerkorrekturen, Verbesserungen und neue Funktionen!",
|
||||
"view_updates": "Alle Updates anzeigen",
|
||||
"view_updates_link": "hier",
|
||||
"tabs": {
|
||||
"manual_setup": "Manuelles Setup-Upgrade",
|
||||
"docker_setup": "Docker Compose Setup-Upgrade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu_items": {
|
||||
"logout": "Abmelden",
|
||||
"discord": "Discord",
|
||||
"youtube": "YouTube",
|
||||
"twitter": "Twitter (X)",
|
||||
"language": "Sprache"
|
||||
},
|
||||
"recording": {
|
||||
"discard": "Verwerfen"
|
||||
}
|
||||
},
|
||||
"language_menu": {
|
||||
"en": "Englisch",
|
||||
"es": "Spanisch",
|
||||
"ja": "Japanisch",
|
||||
"zh": "Chinesisch",
|
||||
"de": "Deutsch"
|
||||
}
|
||||
}
|
||||
500
public/locales/en.json
Normal file
500
public/locales/en.json
Normal file
@@ -0,0 +1,500 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Welcome Back!",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"button": "Login",
|
||||
"loading": "Loading",
|
||||
"register_prompt": "Don't have an account?",
|
||||
"register_link": "Register",
|
||||
"welcome_notification": "Welcome to Maxun!",
|
||||
"error_notification": "Login Failed. Please try again."
|
||||
},
|
||||
"register": {
|
||||
"title": "Register Account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"button": "Register",
|
||||
"loading": "Loading",
|
||||
"register_prompt": "Already have an account?",
|
||||
"login_link": "Login",
|
||||
"welcome_notification": "Welcome to Maxun!",
|
||||
"error_notification": "Registeration Failed. Please try again."
|
||||
},
|
||||
"recordingtable":{
|
||||
"run": "Run",
|
||||
"name": "Name",
|
||||
"schedule": "Schedule",
|
||||
"integrate": "Integrate",
|
||||
"settings": "Settings",
|
||||
"options": "Options",
|
||||
"heading":"My Robots",
|
||||
"new":"Create Robot",
|
||||
"modal":{
|
||||
"title":"Enter the URL",
|
||||
"label":"URL",
|
||||
"button":"Start Recording"
|
||||
},
|
||||
"edit":"Edit",
|
||||
"delete":"Delete",
|
||||
"duplicate":"Duplicate",
|
||||
"search":"Search Robots...",
|
||||
"notifications": {
|
||||
"delete_warning": "Cannot delete robot as it has associated runs",
|
||||
"delete_success": "Robot deleted successfully"
|
||||
}
|
||||
},
|
||||
"mainmenu":{
|
||||
"recordings": "Robots",
|
||||
"runs": "Runs",
|
||||
"proxy": "Proxy",
|
||||
"apikey": "API Key",
|
||||
"feedback":"Join Maxun Cloud",
|
||||
"apidocs":"Website To API"
|
||||
},
|
||||
"runstable":{
|
||||
"runs":"All Runs",
|
||||
"runStatus":"Status",
|
||||
"runName":"Name",
|
||||
"startedAt":"Started At",
|
||||
"finishedAt":"Finished At",
|
||||
"delete":"Delete",
|
||||
"settings":"Settings",
|
||||
"search":"Search Runs...",
|
||||
"notifications": {
|
||||
"no_runs": "No runs found. Please try again.",
|
||||
"delete_success": "Run deleted successfully"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"title": "Proxy Configuration",
|
||||
"tab_standard": "Standard Proxy",
|
||||
"tab_rotation": "Automatic Proxy Rotation",
|
||||
"server_url": "Proxy Server URL",
|
||||
"server_url_helper": "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.",
|
||||
"requires_auth": "Requires Authentication?",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"add_proxy": "Add Proxy",
|
||||
"test_proxy": "Test Proxy",
|
||||
"remove_proxy": "Remove Proxy",
|
||||
"table": {
|
||||
"proxy_url": "Proxy URL",
|
||||
"requires_auth": "Requires Authentication"
|
||||
},
|
||||
"coming_soon": "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.",
|
||||
"join_waitlist": "Join Maxun Cloud Waitlist",
|
||||
"alert": {
|
||||
"title": "If your proxy requires a username and password, always provide them separately from the proxy URL.",
|
||||
"right_way": "The right way",
|
||||
"wrong_way": "The wrong way",
|
||||
"proxy_url": "Proxy URL:",
|
||||
"username": "Username:",
|
||||
"password": "Password:"
|
||||
},
|
||||
"notifications": {
|
||||
"config_success": "Proxy configuration submitted successfully",
|
||||
"config_error": "Failed to submit proxy configuration. Try again.",
|
||||
"test_success": "Proxy configuration is working",
|
||||
"test_error": "Failed to test proxy configuration. Try again.",
|
||||
"fetch_success": "Proxy configuration fetched successfully",
|
||||
"remove_success": "Proxy configuration removed successfully",
|
||||
"remove_error": "Failed to remove proxy configuration. Try again."
|
||||
}
|
||||
},
|
||||
"apikey": {
|
||||
"title": "Manage Your API Key",
|
||||
"default_name": "Maxun API Key",
|
||||
"table": {
|
||||
"name": "API Key Name",
|
||||
"key": "API Key",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copy",
|
||||
"show": "Show",
|
||||
"hide": "Hide",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"no_key_message": "You haven't generated an API key yet.",
|
||||
"generate_button": "Generate API Key",
|
||||
"notifications": {
|
||||
"fetch_error": "Failed to fetch API Key - ${error}",
|
||||
"generate_success": "Generated API Key successfully",
|
||||
"generate_error": "Failed to generate API Key - ${error}",
|
||||
"delete_success": "API Key deleted successfully",
|
||||
"delete_error": "Failed to delete API Key - ${error}",
|
||||
"copy_success": "Copied API Key successfully"
|
||||
}
|
||||
},
|
||||
"action_description": {
|
||||
"text": {
|
||||
"title": "Capture Text",
|
||||
"description": "Hover over the texts you want to extract and click to select them"
|
||||
},
|
||||
"screenshot": {
|
||||
"title": "Capture Screenshot",
|
||||
"description": "Capture a partial or full page screenshot of the current page."
|
||||
},
|
||||
"list": {
|
||||
"title": "Capture List",
|
||||
"description": "Hover over the list you want to extract. Once selected, you can hover over all texts inside the list you selected. Click to select them."
|
||||
},
|
||||
"default": {
|
||||
"title": "What data do you want to extract?",
|
||||
"description": "A robot is designed to perform one action at a time. You can choose any of the options below."
|
||||
},
|
||||
"list_stages": {
|
||||
"initial": "Select the list you want to extract along with the texts inside it",
|
||||
"pagination": "Select how the robot can capture the rest of the list",
|
||||
"limit": "Choose the number of items to extract",
|
||||
"complete": "Capture is complete"
|
||||
}
|
||||
},
|
||||
"right_panel": {
|
||||
"buttons": {
|
||||
"capture_list": "Capture List",
|
||||
"capture_text": "Capture Text",
|
||||
"capture_screenshot": "Capture Screenshot",
|
||||
"confirm": "Confirm",
|
||||
"discard": "Discard",
|
||||
"confirm_capture": "Confirm Capture",
|
||||
"confirm_pagination": "Confirm Pagination",
|
||||
"confirm_limit": "Confirm Limit",
|
||||
"finish_capture": "Finish Capture",
|
||||
"finish": "Finish",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "Capture Fullpage",
|
||||
"capture_visible": "Capture Visible Part",
|
||||
"display_fullpage": "Take Fullpage Screenshot",
|
||||
"display_visible": "Take Visible Part Screenshot"
|
||||
},
|
||||
"pagination": {
|
||||
"title": "How can we find the next list item on the page?",
|
||||
"click_next": "Click on next to navigate to the next page",
|
||||
"click_load_more": "Click on load more to load more items",
|
||||
"scroll_down": "Scroll down to load more items",
|
||||
"scroll_up": "Scroll up to load more items",
|
||||
"none": "No more items to load"
|
||||
},
|
||||
"limit": {
|
||||
"title": "What is the maximum number of rows you want to extract?",
|
||||
"custom": "Custom",
|
||||
"enter_number": "Enter number"
|
||||
},
|
||||
"fields": {
|
||||
"label": "Label",
|
||||
"data": "Data",
|
||||
"field_label": "Field Label",
|
||||
"field_data": "Field Data"
|
||||
},
|
||||
"messages": {
|
||||
"list_selected": "List Selected Successfully"
|
||||
},
|
||||
"errors": {
|
||||
"select_pagination": "Please select a pagination type.",
|
||||
"select_pagination_element": "Please select the pagination element first.",
|
||||
"select_limit": "Please select a limit or enter a custom limit.",
|
||||
"invalid_limit": "Please enter a valid limit.",
|
||||
"confirm_text_fields": "Please confirm all text fields",
|
||||
"unable_create_settings": "Unable to create list settings. Make sure you have defined a field for the list.",
|
||||
"capture_text_discarded": "Capture Text Discarded",
|
||||
"capture_list_discarded": "Capture List Discarded"
|
||||
}
|
||||
},
|
||||
"save_recording": {
|
||||
"title": "Save Robot",
|
||||
"robot_name": "Robot Name",
|
||||
"buttons": {
|
||||
"save": "Save",
|
||||
"confirm": "Confirm"
|
||||
},
|
||||
"notifications": {
|
||||
"save_success": "Robot saved successfully"
|
||||
},
|
||||
"errors": {
|
||||
"user_not_logged": "User not logged in. Cannot save recording.",
|
||||
"exists_warning": "Robot with this name already exists, please confirm the Robot's overwrite."
|
||||
},
|
||||
"tooltips": {
|
||||
"saving": "Optimizing and saving the workflow"
|
||||
}
|
||||
},
|
||||
"browser_recording": {
|
||||
"modal": {
|
||||
"confirm_discard": "Are you sure you want to discard the recording?"
|
||||
},
|
||||
"notifications": {
|
||||
"terminated": "Current Recording was terminated"
|
||||
}
|
||||
},
|
||||
"interpretation_log": {
|
||||
"titles": {
|
||||
"output_preview": "Output Data Preview",
|
||||
"screenshot": "Screenshot"
|
||||
},
|
||||
"messages": {
|
||||
"additional_rows": "Additional rows of data will be extracted once you finish recording.",
|
||||
"successful_training": "You've successfully trained the robot to perform actions! Click on the button below to get a preview of the data your robot will extract.",
|
||||
"no_selection": "It looks like you have not selected anything for extraction yet. Once you do, the robot will show a preview of your selections here."
|
||||
},
|
||||
"data_sections": {
|
||||
"binary_received": "---------- Binary output data received ----------",
|
||||
"serializable_received": "---------- Serializable output data received ----------",
|
||||
"mimetype": "mimetype: ",
|
||||
"image_below": "Image is rendered below:",
|
||||
"separator": "--------------------------------------------------"
|
||||
},
|
||||
"notifications": {
|
||||
"reset_success": "Output Preview reset successfully"
|
||||
}
|
||||
},
|
||||
"interpretation_buttons": {
|
||||
"buttons": {
|
||||
"preview": "Get Preview of Output Data",
|
||||
"reset": "Reset",
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
},
|
||||
"messages": {
|
||||
"extracting": "Extracting data...please wait for 10secs to 1min",
|
||||
"restart_required": "Please restart the interpretation after updating the recording",
|
||||
"run_finished": "Run finished",
|
||||
"run_failed": "Run failed to start"
|
||||
},
|
||||
"modal": {
|
||||
"use_previous": "Do you want to use your previous selection as a condition for performing this action?",
|
||||
"previous_action": "Your previous action was: ",
|
||||
"element_text": "on an element with text "
|
||||
}
|
||||
},
|
||||
"recording_page": {
|
||||
"loader": {
|
||||
"browser_startup": "Spinning up a browser...Navigating to {{url}}"
|
||||
}
|
||||
},
|
||||
"integration_settings": {
|
||||
"title": "Integrate with Google Sheet",
|
||||
"descriptions": {
|
||||
"sync_info": "If you enable this option, every time this robot runs a task successfully, its captured data will be appended to your Google Sheet.",
|
||||
"authenticated_as": "Authenticated as: {{email}}"
|
||||
},
|
||||
"alerts": {
|
||||
"success": {
|
||||
"title": "Google Sheet Integrated Successfully.",
|
||||
"content": "Every time this robot creates a successful run, its captured data is appended to your {{sheetName}} Google Sheet. You can check the data updates",
|
||||
"here": "here",
|
||||
"note": "Note:",
|
||||
"sync_limitation": "The data extracted before integrating with Google Sheets will not be synced in the Google Sheet. Only the data extracted after the integration will be synced."
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"authenticate": "Authenticate with Google",
|
||||
"fetch_sheets": "Fetch Google Spreadsheets",
|
||||
"remove_integration": "Remove Integration",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"fields": {
|
||||
"select_sheet": "Select Google Sheet",
|
||||
"selected_sheet": "Selected Sheet: {{name}} (ID: {{id}})"
|
||||
},
|
||||
"errors": {
|
||||
"auth_error": "Error authenticating with Google",
|
||||
"fetch_error": "Error fetching spreadsheet files: {{message}}",
|
||||
"update_error": "Error updating Google Sheet ID: {{message}}",
|
||||
"remove_error": "Error removing Google Sheets integration: {{message}}"
|
||||
},
|
||||
"notifications": {
|
||||
"sheet_selected": "Google Sheet selected successfully"
|
||||
}
|
||||
},
|
||||
"robot_duplication": {
|
||||
"title": "Duplicate Robot",
|
||||
"descriptions": {
|
||||
"purpose": "Robot duplication is useful to extract data from pages with the same structure.",
|
||||
"example": "Example: If you've created a robot for {{url1}}, you can duplicate it to scrape similar pages like {{url2}} without training a robot from scratch.",
|
||||
"warning": "⚠️ Ensure the new page has the same structure as the original page."
|
||||
},
|
||||
"fields": {
|
||||
"target_url": "Robot Target URL"
|
||||
},
|
||||
"buttons": {
|
||||
"duplicate": "Duplicate Robot",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"notifications": {
|
||||
"robot_not_found": "Could not find robot details. Please try again.",
|
||||
"url_required": "Target URL is required.",
|
||||
"duplicate_success": "Robot duplicated successfully.",
|
||||
"duplicate_error": "Failed to update the Target URL. Please try again.",
|
||||
"unknown_error": "An error occurred while updating the Target URL."
|
||||
}
|
||||
},
|
||||
"robot_settings": {
|
||||
"title": "Robot Settings",
|
||||
"target_url": "Robot Target URL",
|
||||
"robot_id": "Robot ID",
|
||||
"robot_limit": "Robot Limit",
|
||||
"created_by_user": "Created By User",
|
||||
"created_at": "Robot Created At",
|
||||
"errors": {
|
||||
"robot_not_found": "Could not find robot details. Please try again."
|
||||
}
|
||||
},
|
||||
"robot_edit": {
|
||||
"title": "Edit Robot",
|
||||
"change_name": "Change Robot Name",
|
||||
"robot_limit": "Robot Limit",
|
||||
"save": "Save Changes",
|
||||
"cancel": "Cancel",
|
||||
"notifications": {
|
||||
"update_success": "Robot updated successfully.",
|
||||
"update_failed": "Failed to update the robot. Please try again.",
|
||||
"update_error": "An error occurred while updating the robot."
|
||||
}
|
||||
},
|
||||
"schedule_settings": {
|
||||
"title": "Schedule Settings",
|
||||
"run_every": "Run every",
|
||||
"start_from": "Start From",
|
||||
"on_day": "On day",
|
||||
"at_around": "At around",
|
||||
"timezone": "Timezone",
|
||||
"buttons": {
|
||||
"delete_schedule": "Delete Schedule",
|
||||
"save_schedule": "Save Schedule",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"labels": {
|
||||
"in_between": "In Between",
|
||||
"run_once_every": "Run once every",
|
||||
"start_from_label": "Start From",
|
||||
"on_day_of_month": "On Day of the Month",
|
||||
"on_day": {
|
||||
"st": "st",
|
||||
"nd": "nd",
|
||||
"rd": "rd",
|
||||
"th": "th"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main_page": {
|
||||
"notifications": {
|
||||
"interpretation_success": "Interpretation of robot {{name}} succeeded",
|
||||
"interpretation_failed": "Failed to interpret robot {{name}}",
|
||||
"run_started": "Running robot: {{name}}",
|
||||
"run_start_failed": "Failed to run robot: {{name}}",
|
||||
"schedule_success": "Robot {{name}} scheduled successfully",
|
||||
"schedule_failed": "Failed to schedule robot {{name}}",
|
||||
"abort_success": "Interpretation of robot {{name}} aborted successfully",
|
||||
"abort_failed": "Failed to abort the interpretation of robot {{name}}"
|
||||
},
|
||||
"menu": {
|
||||
"recordings": "Robots",
|
||||
"runs": "Runs",
|
||||
"proxy": "Proxy",
|
||||
"apikey": "API Key"
|
||||
}
|
||||
},
|
||||
"browser_window": {
|
||||
"attribute_modal": {
|
||||
"title": "属性を選択",
|
||||
"notifications": {
|
||||
"list_select_success": "リストが正常に選択されました。抽出するテキストデータを選択してください。",
|
||||
"pagination_select_success": "ページネーション要素が正常に選択されました。"
|
||||
}
|
||||
},
|
||||
"attribute_options": {
|
||||
"anchor": {
|
||||
"text": "テキスト: {{text}}",
|
||||
"url": "URL: {{url}}"
|
||||
},
|
||||
"image": {
|
||||
"alt_text": "代替テキスト: {{altText}}",
|
||||
"image_url": "画像URL: {{imageUrl}}"
|
||||
},
|
||||
"default": {
|
||||
"text": "テキスト: {{text}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"runs_table": {
|
||||
"run_type_chips": {
|
||||
"manual_run": "Manual Run",
|
||||
"scheduled_run": "Scheduled Run",
|
||||
"api": "API",
|
||||
"unknown_run_type": "Unknown Run Type"
|
||||
},
|
||||
"run_status_chips": {
|
||||
"success": "Success",
|
||||
"running": "Running",
|
||||
"scheduled": "Scheduled",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"run_settings_modal": {
|
||||
"title": "Run Settings",
|
||||
"labels": {
|
||||
"run_id": "Run ID",
|
||||
"run_by_user": "Run by User",
|
||||
"run_by_schedule": "Run by Schedule ID",
|
||||
"run_by_api": "Run by API",
|
||||
"run_type": "Run Type"
|
||||
}
|
||||
}
|
||||
},
|
||||
"run_content": {
|
||||
"tabs": {
|
||||
"output_data": "Output Data",
|
||||
"log": "Log"
|
||||
},
|
||||
"empty_output": "The output is empty.",
|
||||
"captured_data": {
|
||||
"title": "Captured Data",
|
||||
"download_json": "Download as JSON",
|
||||
"download_csv": "Download as CSV"
|
||||
},
|
||||
"captured_screenshot": {
|
||||
"title": "Captured Screenshot",
|
||||
"download": "Download Screenshot",
|
||||
"render_failed": "The image failed to render"
|
||||
},
|
||||
"buttons": {
|
||||
"stop": "Stop"
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
"project_name": "Maxun",
|
||||
"upgrade": {
|
||||
"button": "Upgrade",
|
||||
"modal": {
|
||||
"up_to_date": "🎉 You're up to date!",
|
||||
"new_version_available": "A new version is available: {{version}}. Upgrade to the latest version for bug fixes, enhancements and new features!",
|
||||
"view_updates": "View all the new updates",
|
||||
"view_updates_link": "here",
|
||||
"tabs": {
|
||||
"manual_setup": "Manual Setup Upgrade",
|
||||
"docker_setup": "Docker Compose Setup Upgrade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu_items": {
|
||||
"logout": "Logout",
|
||||
"discord": "Discord",
|
||||
"youtube": "YouTube",
|
||||
"twitter": "Twitter (X)",
|
||||
"language": "Language"
|
||||
},
|
||||
"recording": {
|
||||
"discard": "Discard"
|
||||
}
|
||||
},
|
||||
"language_menu": {
|
||||
"en": "English",
|
||||
"es": "Spanish",
|
||||
"ja": "Japanese",
|
||||
"zh": "Chinese",
|
||||
"de": "German"
|
||||
}
|
||||
}
|
||||
491
public/locales/es.json
Normal file
491
public/locales/es.json
Normal file
@@ -0,0 +1,491 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "¡Bienvenido de nuevo!",
|
||||
"email": "Correo electrónico",
|
||||
"password": "Contraseña",
|
||||
"button": "Iniciar sesión",
|
||||
"loading": "Cargando",
|
||||
"register_prompt": "¿No tienes una cuenta?",
|
||||
"register_link": "Registrarse",
|
||||
"welcome_notification": "¡Bienvenido a Maxun!",
|
||||
"error_notification": "Error al iniciar sesión. Por favor, inténtalo de nuevo."
|
||||
},
|
||||
"register": {
|
||||
"title": "Crear cuenta",
|
||||
"email": "Correo electrónico",
|
||||
"password": "Contraseña",
|
||||
"button": "Registrarse",
|
||||
"loading": "Cargando",
|
||||
"register_prompt": "¿Ya tienes una cuenta?",
|
||||
"login_link": "Iniciar sesión",
|
||||
"welcome_notification": "¡Bienvenido a Maxun!",
|
||||
"error_notification": "Error en el registro. Por favor, inténtalo de nuevo."
|
||||
},
|
||||
"recordingtable": {
|
||||
"run": "Ejecutar",
|
||||
"name": "Nombre",
|
||||
"schedule": "Programar",
|
||||
"integrate": "Integrar",
|
||||
"settings": "Ajustes",
|
||||
"options": "Opciones",
|
||||
"heading": "Mis Robots",
|
||||
"new": "Crear Robot",
|
||||
"modal": {
|
||||
"title": "Ingresa la URL",
|
||||
"label": "URL",
|
||||
"button": "Comenzar grabación"
|
||||
},
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"duplicate": "Duplicar",
|
||||
"search": "Buscar robots...",
|
||||
"notifications": {
|
||||
"delete_warning": "No se puede eliminar el robot ya que tiene ejecuciones asociadas",
|
||||
"delete_success": "Robot eliminado exitosamente"
|
||||
}
|
||||
},
|
||||
"mainmenu": {
|
||||
"recordings": "Robots",
|
||||
"runs": "Ejecuciones",
|
||||
"proxy": "Proxy",
|
||||
"apikey": "Clave API",
|
||||
"feedback": "Unirse a Maxun Cloud",
|
||||
"apidocs": "Sitio Web a API"
|
||||
},
|
||||
"runstable": {
|
||||
"runs": "Todas las ejecuciones",
|
||||
"runStatus": "Estado",
|
||||
"runName": "Nombre",
|
||||
"startedAt": "Iniciado el",
|
||||
"finishedAt": "Finalizado el",
|
||||
"delete": "Eliminar",
|
||||
"settings": "Ajustes",
|
||||
"search": "Buscar ejecuciones...",
|
||||
"notifications": {
|
||||
"no_runs": "No se encontraron ejecuciones. Por favor, inténtelo de nuevo.",
|
||||
"delete_success": "Ejecución eliminada con éxito"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"title": "Configuración del Proxy",
|
||||
"tab_standard": "Proxy Estándar",
|
||||
"tab_rotation": "Rotación Automática de Proxy",
|
||||
"server_url": "URL del Servidor Proxy",
|
||||
"server_url_helper": "Proxy para usar en todos los robots. Se admiten proxies HTTP y SOCKS. Ejemplo http://myproxy.com:3128 o socks5://myproxy.com:3128. La forma corta myproxy.com:3128 se considera un proxy HTTP.",
|
||||
"requires_auth": "¿Requiere Autenticación?",
|
||||
"username": "Usuario",
|
||||
"password": "Contraseña",
|
||||
"add_proxy": "Agregar Proxy",
|
||||
"test_proxy": "Probar Proxy",
|
||||
"remove_proxy": "Eliminar Proxy",
|
||||
"table": {
|
||||
"proxy_url": "URL del Proxy",
|
||||
"requires_auth": "Requiere Autenticación"
|
||||
},
|
||||
"coming_soon": "Próximamente - En Open Source (Rotación Básica) y Cloud (Rotación Avanzada). Si no desea administrar la infraestructura, únase a nuestra lista de espera en la nube para obtener acceso anticipado.",
|
||||
"join_waitlist": "Unirse a la Lista de Espera de Maxun Cloud",
|
||||
"alert": {
|
||||
"title": "Si su proxy requiere un nombre de usuario y contraseña, proporcione siempre estos datos por separado de la URL del proxy.",
|
||||
"right_way": "La forma correcta",
|
||||
"wrong_way": "La forma incorrecta",
|
||||
"proxy_url": "URL del Proxy:",
|
||||
"username": "Usuario:",
|
||||
"password": "Contraseña:"
|
||||
},
|
||||
"notifications": {
|
||||
"config_success": "Configuración del proxy enviada con éxito",
|
||||
"config_error": "Error al enviar la configuración del proxy. Inténtelo de nuevo.",
|
||||
"test_success": "La configuración del proxy funciona correctamente",
|
||||
"test_error": "Error al probar la configuración del proxy. Inténtelo de nuevo.",
|
||||
"fetch_success": "Configuración del proxy recuperada con éxito",
|
||||
"remove_success": "Configuración del proxy eliminada con éxito",
|
||||
"remove_error": "Error al eliminar la configuración del proxy. Inténtelo de nuevo."
|
||||
}
|
||||
},
|
||||
"apikey": {
|
||||
"title": "Gestionar tu Clave API",
|
||||
"default_name": "Clave API de Maxun",
|
||||
"table": {
|
||||
"name": "Nombre de la Clave API",
|
||||
"key": "Clave API",
|
||||
"actions": "Acciones"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copiar",
|
||||
"show": "Mostrar",
|
||||
"hide": "Ocultar",
|
||||
"delete": "Eliminar"
|
||||
},
|
||||
"no_key_message": "Aún no has generado una clave API.",
|
||||
"generate_button": "Generar Clave API",
|
||||
"notifications": {
|
||||
"fetch_error": "Error al obtener la clave API - ${error}",
|
||||
"generate_success": "Clave API generada con éxito",
|
||||
"generate_error": "Error al generar la clave API - ${error}",
|
||||
"delete_success": "Clave API eliminada con éxito",
|
||||
"delete_error": "Error al eliminar la clave API - ${error}",
|
||||
"copy_success": "Clave API copiada con éxito"
|
||||
}
|
||||
},
|
||||
"action_description": {
|
||||
"text": {
|
||||
"title": "Capturar Texto",
|
||||
"description": "Pase el cursor sobre los textos que desea extraer y haga clic para seleccionarlos"
|
||||
},
|
||||
"screenshot": {
|
||||
"title": "Capturar Pantalla",
|
||||
"description": "Capture una captura de pantalla parcial o completa de la página actual."
|
||||
},
|
||||
"list": {
|
||||
"title": "Capturar Lista",
|
||||
"description": "Pase el cursor sobre la lista que desea extraer. Una vez seleccionada, puede pasar el cursor sobre todos los textos dentro de la lista seleccionada. Haga clic para seleccionarlos."
|
||||
},
|
||||
"default": {
|
||||
"title": "¿Qué datos desea extraer?",
|
||||
"description": "Un robot está diseñado para realizar una acción a la vez. Puede elegir cualquiera de las siguientes opciones."
|
||||
},
|
||||
"list_stages": {
|
||||
"initial": "Seleccione la lista que desea extraer junto con los textos que contiene",
|
||||
"pagination": "Seleccione cómo puede el robot capturar el resto de la lista",
|
||||
"limit": "Elija el número de elementos a extraer",
|
||||
"complete": "Captura completada"
|
||||
}
|
||||
},
|
||||
"right_panel": {
|
||||
"buttons": {
|
||||
"capture_list": "Capturar Lista",
|
||||
"capture_text": "Capturar Texto",
|
||||
"capture_screenshot": "Capturar Pantalla",
|
||||
"confirm": "Confirmar",
|
||||
"discard": "Descartar",
|
||||
"confirm_capture": "Confirmar Captura",
|
||||
"confirm_pagination": "Confirmar Paginación",
|
||||
"confirm_limit": "Confirmar Límite",
|
||||
"finish_capture": "Finalizar Captura",
|
||||
"finish": "Finalizar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "Capturar Página Completa",
|
||||
"capture_visible": "Capturar Parte Visible",
|
||||
"display_fullpage": "Capturar Screenshot de Página Completa",
|
||||
"display_visible": "Capturar Screenshot de Parte Visible"
|
||||
},
|
||||
"pagination": {
|
||||
"title": "¿Cómo podemos encontrar el siguiente elemento de la lista en la página?",
|
||||
"click_next": "Hacer clic en siguiente para navegar a la siguiente página",
|
||||
"click_load_more": "Hacer clic en cargar más para cargar más elementos",
|
||||
"scroll_down": "Desplazarse hacia abajo para cargar más elementos",
|
||||
"scroll_up": "Desplazarse hacia arriba para cargar más elementos",
|
||||
"none": "No hay más elementos para cargar"
|
||||
},
|
||||
"limit": {
|
||||
"title": "¿Cuál es el número máximo de filas que desea extraer?",
|
||||
"custom": "Personalizado",
|
||||
"enter_number": "Ingrese número"
|
||||
},
|
||||
"fields": {
|
||||
"label": "Etiqueta",
|
||||
"data": "Datos",
|
||||
"field_label": "Etiqueta del Campo",
|
||||
"field_data": "Datos del Campo"
|
||||
},
|
||||
"messages": {
|
||||
"list_selected": "Lista seleccionada exitosamente"
|
||||
},
|
||||
"errors": {
|
||||
"select_pagination": "Por favor seleccione un tipo de paginación.",
|
||||
"select_pagination_element": "Por favor seleccione primero el elemento de paginación.",
|
||||
"select_limit": "Por favor seleccione un límite o ingrese un límite personalizado.",
|
||||
"invalid_limit": "Por favor ingrese un límite válido.",
|
||||
"confirm_text_fields": "Por favor confirme todos los campos de texto",
|
||||
"unable_create_settings": "No se pueden crear las configuraciones de la lista. Asegúrese de haber definido un campo para la lista.",
|
||||
"capture_text_discarded": "Captura de texto descartada",
|
||||
"capture_list_discarded": "Captura de lista descartada"
|
||||
}
|
||||
},
|
||||
"save_recording": {
|
||||
"title": "Guardar Robot",
|
||||
"robot_name": "Nombre del Robot",
|
||||
"buttons": {
|
||||
"save": "Guardar",
|
||||
"confirm": "Confirmar"
|
||||
},
|
||||
"notifications": {
|
||||
"save_success": "Robot guardado exitosamente"
|
||||
},
|
||||
"errors": {
|
||||
"user_not_logged": "Usuario no conectado. No se puede guardar la grabación.",
|
||||
"exists_warning": "Ya existe un robot con este nombre, por favor confirme la sobrescritura del robot."
|
||||
},
|
||||
"tooltips": {
|
||||
"saving": "Optimizando y guardando el flujo de trabajo"
|
||||
}
|
||||
},
|
||||
"browser_recording": {
|
||||
"modal": {
|
||||
"confirm_discard": "¿Está seguro de que desea descartar la grabación?"
|
||||
},
|
||||
"notifications": {
|
||||
"terminated": "La grabación actual fue terminada"
|
||||
}
|
||||
},
|
||||
"interpretation_log": {
|
||||
"titles": {
|
||||
"output_preview": "Vista Previa de Datos de Salida",
|
||||
"screenshot": "Captura de pantalla"
|
||||
},
|
||||
"messages": {
|
||||
"additional_rows": "Se extraerán filas adicionales de datos una vez que termine la grabación.",
|
||||
"successful_training": "¡Has entrenado exitosamente al robot para realizar acciones! Haz clic en el botón de abajo para obtener una vista previa de los datos que tu robot extraerá.",
|
||||
"no_selection": "Parece que aún no has seleccionado nada para extraer. Una vez que lo hagas, el robot mostrará una vista previa de tus selecciones aquí."
|
||||
},
|
||||
"data_sections": {
|
||||
"binary_received": "---------- Datos binarios de salida recibidos ----------",
|
||||
"serializable_received": "---------- Datos serializables de salida recibidos ----------",
|
||||
"mimetype": "tipo MIME: ",
|
||||
"image_below": "La imagen se muestra a continuación:",
|
||||
"separator": "--------------------------------------------------"
|
||||
}
|
||||
},
|
||||
"interpretation_buttons": {
|
||||
"buttons": {
|
||||
"preview": "Obtener Vista Previa de Datos de Salida",
|
||||
"reset": "Restablecer",
|
||||
"yes": "Sí",
|
||||
"no": "No"
|
||||
},
|
||||
"messages": {
|
||||
"extracting": "Extrayendo datos...espere de 10 segundos a 1 minuto",
|
||||
"restart_required": "Por favor, reinicie la interpretación después de actualizar la grabación",
|
||||
"run_finished": "Ejecución finalizada",
|
||||
"run_failed": "Error al iniciar la ejecución"
|
||||
},
|
||||
"modal": {
|
||||
"use_previous": "¿Desea usar su selección anterior como condición para realizar esta acción?",
|
||||
"previous_action": "Su acción anterior fue: ",
|
||||
"element_text": "en un elemento con texto "
|
||||
},
|
||||
"notifications": {
|
||||
"reset_success": "Vista previa restablecida correctamente"
|
||||
}
|
||||
},
|
||||
"recording_page": {
|
||||
"loader": {
|
||||
"browser_startup": "Iniciando el navegador...Navegando a {{url}}"
|
||||
}
|
||||
},
|
||||
"integration_settings": {
|
||||
"title": "Integrar con Google Sheet",
|
||||
"descriptions": {
|
||||
"sync_info": "Si habilitas esta opción, cada vez que este robot ejecute una tarea exitosamente, sus datos capturados se añadirán a tu Google Sheet.",
|
||||
"authenticated_as": "Autenticado como: {{email}}"
|
||||
},
|
||||
"alerts": {
|
||||
"success": {
|
||||
"title": "Google Sheet integrado exitosamente.",
|
||||
"content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a tu Google Sheet {{sheetName}}. Puedes verificar las actualizaciones de datos",
|
||||
"here": "aquí",
|
||||
"note": "Nota:",
|
||||
"sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en el Google Sheet. Solo se sincronizarán los datos extraídos después de la integración."
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"authenticate": "Autenticar con Google",
|
||||
"fetch_sheets": "Obtener Google Sheets",
|
||||
"remove_integration": "Eliminar integración",
|
||||
"submit": "Enviar"
|
||||
},
|
||||
"fields": {
|
||||
"select_sheet": "Seleccionar Google Sheet",
|
||||
"selected_sheet": "Hoja seleccionada: {{name}} (ID: {{id}})"
|
||||
}
|
||||
},
|
||||
"robot_duplication": {
|
||||
"title": "Duplicar Robot",
|
||||
"descriptions": {
|
||||
"purpose": "La duplicación de robots es útil para extraer datos de páginas con la misma estructura.",
|
||||
"example": "Ejemplo: Si has creado un robot para {{url1}}, puedes duplicarlo para extraer páginas similares como {{url2}} sin tener que entrenar un robot desde cero.",
|
||||
"warning": "⚠️ Asegúrate de que la nueva página tenga la misma estructura que la página original."
|
||||
},
|
||||
"fields": {
|
||||
"target_url": "URL Destino del Robot"
|
||||
},
|
||||
"buttons": {
|
||||
"duplicate": "Duplicar Robot",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"notifications": {
|
||||
"robot_not_found": "No se pudieron encontrar los detalles del robot. Por favor, inténtalo de nuevo.",
|
||||
"url_required": "Se requiere la URL de destino.",
|
||||
"duplicate_success": "Robot duplicado con éxito.",
|
||||
"duplicate_error": "Error al actualizar la URL de destino. Por favor, inténtalo de nuevo.",
|
||||
"unknown_error": "Ocurrió un error al actualizar la URL de destino."
|
||||
}
|
||||
},
|
||||
"robot_settings": {
|
||||
"title": "Configuración del Robot",
|
||||
"target_url": "URL de Destino del Robot",
|
||||
"robot_id": "ID del Robot",
|
||||
"robot_limit": "Límite del Robot",
|
||||
"created_by_user": "Creado por Usuario",
|
||||
"created_at": "Fecha de Creación del Robot",
|
||||
"errors": {
|
||||
"robot_not_found": "No se pudieron encontrar los detalles del robot. Inténtelo de nuevo."
|
||||
}
|
||||
},
|
||||
"robot_edit": {
|
||||
"title": "Editar Robot",
|
||||
"change_name": "Cambiar Nombre del Robot",
|
||||
"robot_limit": "Límite del Robot",
|
||||
"save": "Guardar Cambios",
|
||||
"cancel": "Cancelar",
|
||||
"notifications": {
|
||||
"update_success": "Robot actualizado exitosamente.",
|
||||
"update_failed": "Error al actualizar el robot. Intente de nuevo.",
|
||||
"update_error": "Ocurrió un error al actualizar el robot."
|
||||
}
|
||||
},
|
||||
"schedule_settings": {
|
||||
"title": "Configuración de Programación",
|
||||
"run_every": "Ejecutar cada",
|
||||
"start_from": "Iniciar desde",
|
||||
"on_day": "En día",
|
||||
"at_around": "Alrededor de",
|
||||
"timezone": "Zona horaria",
|
||||
"buttons": {
|
||||
"delete_schedule": "Eliminar Programación",
|
||||
"save_schedule": "Guardar Programación",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"labels": {
|
||||
"in_between": "Entre",
|
||||
"run_once_every": "Ejecutar cada",
|
||||
"start_from_label": "Iniciar desde",
|
||||
"on_day_of_month": "Día del mes",
|
||||
"on_day": {
|
||||
"st": "º",
|
||||
"nd": "º",
|
||||
"rd": "º",
|
||||
"th": "º"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main_page": {
|
||||
"notifications": {
|
||||
"interpretation_success": "Interpretación del robot {{name}} completada con éxito",
|
||||
"interpretation_failed": "Error al interpretar el robot {{name}}",
|
||||
"run_started": "Ejecutando robot: {{name}}",
|
||||
"run_start_failed": "Error al ejecutar el robot: {{name}}",
|
||||
"schedule_success": "Robot {{name}} programado exitosamente",
|
||||
"schedule_failed": "Error al programar el robot {{name}}",
|
||||
"abort_success": "Interpretación del robot {{name}} abortada exitosamente",
|
||||
"abort_failed": "Error al abortar la interpretación del robot {{name}}"
|
||||
},
|
||||
"menu": {
|
||||
"recordings": "Robots",
|
||||
"runs": "Ejecuciones",
|
||||
"proxy": "Proxy",
|
||||
"apikey": "Clave API"
|
||||
}
|
||||
},
|
||||
"browser_window": {
|
||||
"attribute_modal": {
|
||||
"title": "Seleccionar Atributo",
|
||||
"notifications": {
|
||||
"list_select_success": "Lista seleccionada correctamente. Seleccione los datos de texto para extracción.",
|
||||
"pagination_select_success": "Elemento de paginación seleccionado correctamente."
|
||||
}
|
||||
},
|
||||
"attribute_options": {
|
||||
"anchor": {
|
||||
"text": "Texto: {{text}}",
|
||||
"url": "URL: {{url}}"
|
||||
},
|
||||
"image": {
|
||||
"alt_text": "Texto Alt: {{altText}}",
|
||||
"image_url": "URL de Imagen: {{imageUrl}}"
|
||||
},
|
||||
"default": {
|
||||
"text": "Texto: {{text}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"runs_table": {
|
||||
"run_type_chips": {
|
||||
"manual_run": "Ejecución Manual",
|
||||
"scheduled_run": "Ejecución Programada",
|
||||
"api": "API",
|
||||
"unknown_run_type": "Tipo de Ejecución Desconocido"
|
||||
},
|
||||
"run_status_chips": {
|
||||
"success": "Éxito",
|
||||
"running": "Ejecutando",
|
||||
"scheduled": "Programado",
|
||||
"failed": "Fallido"
|
||||
},
|
||||
"run_settings_modal": {
|
||||
"title": "Configuración de Ejecución",
|
||||
"labels": {
|
||||
"run_id": "ID de Ejecución",
|
||||
"run_by_user": "Ejecutado por Usuario",
|
||||
"run_by_schedule": "Ejecutado por ID de Programación",
|
||||
"run_by_api": "Ejecutado por API",
|
||||
"run_type": "Tipo de Ejecución"
|
||||
}
|
||||
}
|
||||
},
|
||||
"run_content": {
|
||||
"tabs": {
|
||||
"output_data": "Datos de Salida",
|
||||
"log": "Registro"
|
||||
},
|
||||
"empty_output": "La salida está vacía.",
|
||||
"captured_data": {
|
||||
"title": "Datos Capturados",
|
||||
"download_json": "Descargar como JSON",
|
||||
"download_csv": "Descargar como CSV"
|
||||
},
|
||||
"captured_screenshot": {
|
||||
"title": "Captura de Pantalla",
|
||||
"download": "Descargar Captura",
|
||||
"render_failed": "No se pudo renderizar la imagen"
|
||||
},
|
||||
"buttons": {
|
||||
"stop": "Detener"
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
"project_name": "Maxun",
|
||||
"upgrade": {
|
||||
"button": "Actualizar",
|
||||
"modal": {
|
||||
"up_to_date": "¡Estás actualizado!",
|
||||
"new_version_available": "Hay una nueva versión disponible: {{version}}. ¡Actualice a la última versión para correcciones de errores, mejoras y nuevas características!",
|
||||
"view_updates": "Ver todas las actualizaciones",
|
||||
"view_updates_link": "aquí",
|
||||
"tabs": {
|
||||
"manual_setup": "Actualización de Configuración Manual",
|
||||
"docker_setup": "Actualización de Configuración Docker Compose"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu_items": {
|
||||
"logout": "Cerrar sesión",
|
||||
"discord": "Discord",
|
||||
"youtube": "YouTube",
|
||||
"twitter": "Twitter (X)",
|
||||
"language": "Idioma"
|
||||
},
|
||||
"recording": {
|
||||
"discard": "Descartar"
|
||||
}
|
||||
},
|
||||
"language_menu": {
|
||||
"en": "Inglés",
|
||||
"es": "Español",
|
||||
"ja": "Japonés",
|
||||
"zh": "Chino",
|
||||
"de": "Alemán"
|
||||
}
|
||||
}
|
||||
491
public/locales/ja.json
Normal file
491
public/locales/ja.json
Normal file
@@ -0,0 +1,491 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "お帰りなさい!",
|
||||
"email": "メールアドレス",
|
||||
"password": "パスワード",
|
||||
"button": "ログイン",
|
||||
"loading": "読み込み中",
|
||||
"register_prompt": "アカウントをお持ちでないですか?",
|
||||
"register_link": "登録する",
|
||||
"welcome_notification": "Maxunへようこそ!",
|
||||
"error_notification": "ログインに失敗しました。もう一度お試しください。"
|
||||
},
|
||||
"register": {
|
||||
"title": "アカウントを登録する",
|
||||
"email": "メールアドレス",
|
||||
"password": "パスワード",
|
||||
"button": "登録する",
|
||||
"loading": "読み込み中",
|
||||
"register_prompt": "既にアカウントをお持ちですか?",
|
||||
"login_link": "ログイン",
|
||||
"welcome_notification": "Maxunへようこそ!",
|
||||
"error_notification": "登録に失敗しました。もう一度お試しください。"
|
||||
},
|
||||
"recordingtable": {
|
||||
"run": "実行",
|
||||
"name": "名前",
|
||||
"schedule": "スケジュール",
|
||||
"integrate": "統合",
|
||||
"settings": "設定",
|
||||
"options": "オプション",
|
||||
"heading": "私のロボット",
|
||||
"new": "ロボットを作成",
|
||||
"modal": {
|
||||
"title": "URLを入力してください",
|
||||
"label": "URL",
|
||||
"button": "録画を開始"
|
||||
},
|
||||
"edit": "編集",
|
||||
"delete": "削除",
|
||||
"duplicate": "複製",
|
||||
"search": "ロボットを検索...",
|
||||
"notifications": {
|
||||
"delete_warning": "関連する実行があるため、ロボットを削除できません",
|
||||
"delete_success": "ロボットが正常に削除されました"
|
||||
}
|
||||
},
|
||||
"mainmenu": {
|
||||
"recordings": "ロボット",
|
||||
"runs": "実行",
|
||||
"proxy": "プロキシ",
|
||||
"apikey": "APIキー",
|
||||
"feedback": "Maxunクラウドに参加する",
|
||||
"apidocs": "WebサイトからAPI"
|
||||
},
|
||||
"runstable": {
|
||||
"runs": "すべての実行",
|
||||
"runStatus": "ステータス",
|
||||
"runName": "名前",
|
||||
"startedAt": "開始日時",
|
||||
"finishedAt": "終了日時",
|
||||
"delete": "削除",
|
||||
"settings": "設定",
|
||||
"search": "実行を検索...",
|
||||
"notifications": {
|
||||
"no_runs": "実行が見つかりません。もう一度お試しください。",
|
||||
"delete_success": "実行が正常に削除されました"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"title": "プロキシ設定",
|
||||
"tab_standard": "標準プロキシ",
|
||||
"tab_rotation": "自動プロキシローテーション",
|
||||
"server_url": "プロキシサーバーURL",
|
||||
"server_url_helper": "すべてのロボットで使用するプロキシ。HTTPとSOCKSプロキシがサポートされています。例:http://myproxy.com:3128 または socks5://myproxy.com:3128。短縮形 myproxy.com:3128 はHTTPプロキシとして扱われます。",
|
||||
"requires_auth": "認証が必要ですか?",
|
||||
"username": "ユーザー名",
|
||||
"password": "パスワード",
|
||||
"add_proxy": "プロキシを追加",
|
||||
"test_proxy": "プロキシをテスト",
|
||||
"remove_proxy": "プロキシを削除",
|
||||
"table": {
|
||||
"proxy_url": "プロキシURL",
|
||||
"requires_auth": "認証が必要"
|
||||
},
|
||||
"coming_soon": "近日公開 - オープンソース(基本ローテーション)とクラウド(高度なローテーション)。インフラストラクチャを管理したくない場合は、クラウドの待機リストに参加して早期アクセスを取得してください。",
|
||||
"join_waitlist": "Maxun Cloud待機リストに参加",
|
||||
"alert": {
|
||||
"title": "プロキシにユーザー名とパスワードが必要な場合は、必ずプロキシURLとは別に指定してください。",
|
||||
"right_way": "正しい方法",
|
||||
"wrong_way": "間違った方法",
|
||||
"proxy_url": "プロキシURL:",
|
||||
"username": "ユーザー名:",
|
||||
"password": "パスワード:"
|
||||
},
|
||||
"notifications": {
|
||||
"config_success": "プロキシ設定が正常に送信されました",
|
||||
"config_error": "プロキシ設定の送信に失敗しました。もう一度お試しください。",
|
||||
"test_success": "プロキシ設定は正常に動作しています",
|
||||
"test_error": "プロキシ設定のテストに失敗しました。もう一度お試しください。",
|
||||
"fetch_success": "プロキシ設定の取得に成功しました",
|
||||
"remove_success": "プロキシ設定が正常に削除されました",
|
||||
"remove_error": "プロキシ設定の削除に失敗しました。もう一度お試しください。"
|
||||
}
|
||||
},
|
||||
"apikey": {
|
||||
"title": "APIキーの管理",
|
||||
"default_name": "Maxun APIキー",
|
||||
"table": {
|
||||
"name": "APIキー名",
|
||||
"key": "APIキー",
|
||||
"actions": "アクション"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "コピー",
|
||||
"show": "表示",
|
||||
"hide": "非表示",
|
||||
"delete": "削除"
|
||||
},
|
||||
"no_key_message": "APIキーはまだ生成されていません。",
|
||||
"generate_button": "APIキーを生成",
|
||||
"notifications": {
|
||||
"fetch_error": "APIキーの取得に失敗しました - ${error}",
|
||||
"generate_success": "APIキーが正常に生成されました",
|
||||
"generate_error": "APIキーの生成に失敗しました - ${error}",
|
||||
"delete_success": "APIキーが正常に削除されました",
|
||||
"delete_error": "APIキーの削除に失敗しました - ${error}",
|
||||
"copy_success": "APIキーがコピーされました"
|
||||
}
|
||||
},
|
||||
"action_description": {
|
||||
"text": {
|
||||
"title": "テキストを取得",
|
||||
"description": "抽出したいテキストにカーソルを合わせ、クリックして選択してください"
|
||||
},
|
||||
"screenshot": {
|
||||
"title": "スクリーンショットを取得",
|
||||
"description": "現在のページの部分的または全体のスクリーンショットを取得します。"
|
||||
},
|
||||
"list": {
|
||||
"title": "リストを取得",
|
||||
"description": "抽出したいリストにカーソルを合わせてください。選択後、選択したリスト内のすべてのテキストにカーソルを合わせることができます。クリックして選択してください。"
|
||||
},
|
||||
"default": {
|
||||
"title": "どのデータを抽出しますか?",
|
||||
"description": "ロボットは一度に1つのアクションを実行するように設計されています。以下のオプションから選択できます。"
|
||||
},
|
||||
"list_stages": {
|
||||
"initial": "抽出したいリストとその中のテキストを選択してください",
|
||||
"pagination": "ロボットがリストの残りをどのように取得するか選択してください",
|
||||
"limit": "抽出するアイテムの数を選択してください",
|
||||
"complete": "取得が完了しました"
|
||||
}
|
||||
},
|
||||
"right_panel": {
|
||||
"buttons": {
|
||||
"capture_list": "リストを取得",
|
||||
"capture_text": "テキストを取得",
|
||||
"capture_screenshot": "スクリーンショットを取得",
|
||||
"confirm": "確認",
|
||||
"discard": "破棄",
|
||||
"confirm_capture": "取得を確認",
|
||||
"confirm_pagination": "ページネーションを確認",
|
||||
"confirm_limit": "制限を確認",
|
||||
"finish_capture": "取得を完了",
|
||||
"finish": "完了",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "フルページを取得",
|
||||
"capture_visible": "表示部分を取得",
|
||||
"display_fullpage": "フルページスクリーンショットを撮影",
|
||||
"display_visible": "表示部分のスクリーンショットを撮影"
|
||||
},
|
||||
"pagination": {
|
||||
"title": "次のリスト項目をページ上でどのように見つけますか?",
|
||||
"click_next": "次へをクリックして次のページへ移動",
|
||||
"click_load_more": "もっと読み込むをクリックして項目を追加",
|
||||
"scroll_down": "下にスクロールして項目を追加",
|
||||
"scroll_up": "上にスクロールして項目を追加",
|
||||
"none": "これ以上読み込む項目はありません"
|
||||
},
|
||||
"limit": {
|
||||
"title": "抽出する最大行数はいくつですか?",
|
||||
"custom": "カスタム",
|
||||
"enter_number": "数値を入力"
|
||||
},
|
||||
"fields": {
|
||||
"label": "ラベル",
|
||||
"data": "データ",
|
||||
"field_label": "フィールドラベル",
|
||||
"field_data": "フィールドデータ"
|
||||
},
|
||||
"messages": {
|
||||
"list_selected": "リストが正常に選択されました"
|
||||
},
|
||||
"errors": {
|
||||
"select_pagination": "ページネーションタイプを選択してください。",
|
||||
"select_pagination_element": "まずページネーション要素を選択してください。",
|
||||
"select_limit": "制限を選択するかカスタム制限を入力してください。",
|
||||
"invalid_limit": "有効な制限を入力してください。",
|
||||
"confirm_text_fields": "すべてのテキストフィールドを確認してください",
|
||||
"unable_create_settings": "リスト設定を作成できません。リストのフィールドを定義したことを確認してください。",
|
||||
"capture_text_discarded": "テキスト取得が破棄されました",
|
||||
"capture_list_discarded": "リスト取得が破棄されました"
|
||||
}
|
||||
},
|
||||
"save_recording": {
|
||||
"title": "ロボットを保存",
|
||||
"robot_name": "ロボット名",
|
||||
"buttons": {
|
||||
"save": "保存",
|
||||
"confirm": "確認"
|
||||
},
|
||||
"notifications": {
|
||||
"save_success": "ロボットが正常に保存されました"
|
||||
},
|
||||
"errors": {
|
||||
"user_not_logged": "ユーザーがログインしていません。録画を保存できません。",
|
||||
"exists_warning": "この名前のロボットは既に存在します。ロボットの上書きを確認してください。"
|
||||
},
|
||||
"tooltips": {
|
||||
"saving": "ワークフローを最適化して保存中"
|
||||
}
|
||||
},
|
||||
"browser_recording": {
|
||||
"modal": {
|
||||
"confirm_discard": "録画を破棄してもよろしいですか?"
|
||||
},
|
||||
"notifications": {
|
||||
"terminated": "現在の録画は終了しました"
|
||||
}
|
||||
},
|
||||
"interpretation_log": {
|
||||
"titles": {
|
||||
"output_preview": "出力データプレビュー",
|
||||
"screenshot": "スクリーンショット"
|
||||
},
|
||||
"messages": {
|
||||
"additional_rows": "記録が完了すると、追加のデータ行が抽出されます。",
|
||||
"successful_training": "ロボットのアクショントレーニングが成功しました!下のボタンをクリックすると、ロボットが抽出するデータのプレビューが表示されます。",
|
||||
"no_selection": "まだ抽出対象が選択されていません。選択すると、ロボットがここで選択内容のプレビューを表示します。"
|
||||
},
|
||||
"data_sections": {
|
||||
"binary_received": "---------- バイナリ出力データを受信 ----------",
|
||||
"serializable_received": "---------- シリアライズ可能な出力データを受信 ----------",
|
||||
"mimetype": "MIMEタイプ: ",
|
||||
"image_below": "画像は以下に表示されます:",
|
||||
"separator": "--------------------------------------------------"
|
||||
},
|
||||
"notifications": {
|
||||
"reset_success": "出力プレビューが正常にリセットされました"
|
||||
}
|
||||
},
|
||||
"interpretation_buttons": {
|
||||
"buttons": {
|
||||
"preview": "出力データのプレビューを取得",
|
||||
"reset": "リセット",
|
||||
"yes": "はい",
|
||||
"no": "いいえ"
|
||||
},
|
||||
"messages": {
|
||||
"extracting": "データ抽出中...10秒から1分ほどお待ちください",
|
||||
"restart_required": "録画を更新した後、解釈を再起動してください",
|
||||
"run_finished": "実行完了",
|
||||
"run_failed": "実行の開始に失敗しました"
|
||||
},
|
||||
"modal": {
|
||||
"use_previous": "この操作の条件として前回の選択を使用しますか?",
|
||||
"previous_action": "前回の操作: ",
|
||||
"element_text": "テキスト要素 "
|
||||
}
|
||||
},
|
||||
"recording_page": {
|
||||
"loader": {
|
||||
"browser_startup": "ブラウザを起動中...{{url}}に移動中"
|
||||
}
|
||||
},
|
||||
"integration_settings": {
|
||||
"title": "Google Sheetと連携",
|
||||
"descriptions": {
|
||||
"sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle Sheetに追加されます。",
|
||||
"authenticated_as": "認証済みユーザー: {{email}}"
|
||||
},
|
||||
"alerts": {
|
||||
"success": {
|
||||
"title": "Google Sheetの連携が完了しました。",
|
||||
"content": "このロボットが正常に実行を完了するたびに、取得したデータはGoogle Sheet {{sheetName}}に追加されます。データの更新は",
|
||||
"here": "こちら",
|
||||
"note": "注意:",
|
||||
"sync_limitation": "Google Sheetsとの連携前に抽出されたデータは同期されません。連携後に抽出されたデータのみが同期されます。"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"authenticate": "Googleで認証",
|
||||
"fetch_sheets": "Google Sheetsを取得",
|
||||
"remove_integration": "連携を解除",
|
||||
"submit": "送信"
|
||||
},
|
||||
"fields": {
|
||||
"select_sheet": "Google Sheetを選択",
|
||||
"selected_sheet": "選択したシート: {{name}} (ID: {{id}})"
|
||||
}
|
||||
},
|
||||
"robot_duplication": {
|
||||
"title": "ロボットを複製",
|
||||
"descriptions": {
|
||||
"purpose": "ロボットの複製は、同じ構造のページからデータを抽出する際に便利です。",
|
||||
"example": "例:{{url1}}用のロボットを作成した場合、ロボットを一から作り直すことなく、{{url2}}のような類似のページをスクレイピングするために複製できます。",
|
||||
"warning": "⚠️ 新しいページが元のページと同じ構造であることを確認してください。"
|
||||
},
|
||||
"fields": {
|
||||
"target_url": "ロボットのターゲットURL"
|
||||
},
|
||||
"buttons": {
|
||||
"duplicate": "ロボットを複製",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"notifications": {
|
||||
"robot_not_found": "ロボットの詳細が見つかりません。もう一度お試しください。",
|
||||
"url_required": "ターゲットURLが必要です。",
|
||||
"duplicate_success": "ロボットが正常に複製されました。",
|
||||
"duplicate_error": "ターゲットURLの更新に失敗しました。もう一度お試しください。",
|
||||
"unknown_error": "ターゲットURLの更新中にエラーが発生しました。"
|
||||
}
|
||||
},
|
||||
"robot_settings": {
|
||||
"title": "ロボット設定",
|
||||
"target_url": "ロボットのターゲットURL",
|
||||
"robot_id": "ロボットID",
|
||||
"robot_limit": "ロボットの制限",
|
||||
"created_by_user": "作成したユーザー",
|
||||
"created_at": "作成日時",
|
||||
"errors": {
|
||||
"robot_not_found": "ロボットの詳細が見つかりませんでした。もう一度試してください。"
|
||||
}
|
||||
},
|
||||
"robot_edit": {
|
||||
"title": "ロボットを編集",
|
||||
"change_name": "ロボット名の変更",
|
||||
"robot_limit": "ロボットの制限",
|
||||
"save": "変更を保存",
|
||||
"cancel": "キャンセル",
|
||||
"notifications": {
|
||||
"update_success": "ロボットが正常に更新されました。",
|
||||
"update_failed": "ロボットの更新に失敗しました。もう一度試してください。",
|
||||
"update_error": "ロボットの更新中にエラーが発生しました。"
|
||||
}
|
||||
},
|
||||
"schedule_settings": {
|
||||
"title": "スケジュール設定",
|
||||
"run_every": "実行間隔",
|
||||
"start_from": "開始日",
|
||||
"on_day": "日付",
|
||||
"at_around": "時刻",
|
||||
"timezone": "タイムゾーン",
|
||||
"buttons": {
|
||||
"delete_schedule": "スケジュールを削除",
|
||||
"save_schedule": "スケジュールを保存",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"labels": {
|
||||
"in_between": "間隔",
|
||||
"run_once_every": "実行間隔",
|
||||
"start_from_label": "開始日",
|
||||
"on_day_of_month": "月の日付",
|
||||
"on_day": {
|
||||
"st": "日",
|
||||
"nd": "日",
|
||||
"rd": "日",
|
||||
"th": "日"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main_page": {
|
||||
"notifications": {
|
||||
"interpretation_success": "ロボット{{name}}の解釈に成功しました",
|
||||
"interpretation_failed": "ロボット{{name}}の解釈に失敗しました",
|
||||
"run_started": "ロボット{{name}}を実行中",
|
||||
"run_start_failed": "ロボット{{name}}の実行に失敗しました",
|
||||
"schedule_success": "ロボット{{name}}のスケジュールが正常に設定されました",
|
||||
"schedule_failed": "ロボット{{name}}のスケジュール設定に失敗しました",
|
||||
"abort_success": "ロボット{{name}}の解釈を中止しました",
|
||||
"abort_failed": "ロボット{{name}}の解釈中止に失敗しました"
|
||||
},
|
||||
"menu": {
|
||||
"recordings": "ロボット",
|
||||
"runs": "実行",
|
||||
"proxy": "プロキシ",
|
||||
"apikey": "APIキー"
|
||||
}
|
||||
},
|
||||
"browser_window": {
|
||||
"attribute_modal": {
|
||||
"title": "属性を選択",
|
||||
"notifications": {
|
||||
"list_select_success": "リストが正常に選択されました。抽出するテキストデータを選択してください。",
|
||||
"pagination_select_success": "ページネーション要素が正常に選択されました。"
|
||||
}
|
||||
},
|
||||
"attribute_options": {
|
||||
"anchor": {
|
||||
"text": "テキスト: {{text}}",
|
||||
"url": "URL: {{url}}"
|
||||
},
|
||||
"image": {
|
||||
"alt_text": "代替テキスト: {{altText}}",
|
||||
"image_url": "画像URL: {{imageUrl}}"
|
||||
},
|
||||
"default": {
|
||||
"text": "テキスト: {{text}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"runs_table": {
|
||||
"run_type_chips": {
|
||||
"manual_run": "手動実行",
|
||||
"scheduled_run": "スケジュール実行",
|
||||
"api": "API",
|
||||
"unknown_run_type": "不明な実行タイプ"
|
||||
},
|
||||
"run_status_chips": {
|
||||
"success": "成功",
|
||||
"running": "実行中",
|
||||
"scheduled": "スケジュール済み",
|
||||
"failed": "失敗"
|
||||
},
|
||||
"run_settings_modal": {
|
||||
"title": "実行設定",
|
||||
"labels": {
|
||||
"run_id": "実行ID",
|
||||
"run_by_user": "ユーザーによる実行",
|
||||
"run_by_schedule": "スケジュールによる実行",
|
||||
"run_by_api": "APIによる実行",
|
||||
"run_type": "実行タイプ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"run_content": {
|
||||
"tabs": {
|
||||
"output_data": "出力データ",
|
||||
"log": "ログ"
|
||||
},
|
||||
"empty_output": "出力は空です。",
|
||||
"captured_data": {
|
||||
"title": "キャプチャされたデータ",
|
||||
"download_json": "JSONとしてダウンロード",
|
||||
"download_csv": "CSVとしてダウンロード"
|
||||
},
|
||||
"captured_screenshot": {
|
||||
"title": "キャプチャされたスクリーンショット",
|
||||
"download": "スクリーンショットをダウンロード",
|
||||
"render_failed": "画像のレンダリングに失敗しました"
|
||||
},
|
||||
"buttons": {
|
||||
"stop": "停止"
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
"project_name": "Maxun",
|
||||
"upgrade": {
|
||||
"button": "アップグレード",
|
||||
"modal": {
|
||||
"up_to_date": "最新版です!",
|
||||
"new_version_available": "新しいバージョンが利用可能です: {{version}}。バグ修正、機能強化のために最新版にアップグレードしてください。",
|
||||
"view_updates": "すべての更新を",
|
||||
"view_updates_link": "こちら",
|
||||
"tabs": {
|
||||
"manual_setup": "手動セットアップ",
|
||||
"docker_setup": "Docker Composeセットアップ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu_items": {
|
||||
"logout": "ログアウト",
|
||||
"discord": "Discord",
|
||||
"youtube": "YouTube",
|
||||
"twitter": "Twitter (X)",
|
||||
"language": "言語"
|
||||
},
|
||||
"recording": {
|
||||
"discard": "破棄"
|
||||
}
|
||||
},
|
||||
"language_menu": {
|
||||
"en": "英語",
|
||||
"es": "スペイン語",
|
||||
"ja": "日本語",
|
||||
"zh": "中国語",
|
||||
"de": "ドイツ語"
|
||||
}
|
||||
}
|
||||
491
public/locales/zh.json
Normal file
491
public/locales/zh.json
Normal file
@@ -0,0 +1,491 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "欢迎回来!",
|
||||
"email": "电子邮箱",
|
||||
"password": "密码",
|
||||
"button": "登录",
|
||||
"loading": "加载中",
|
||||
"register_prompt": "还没有账号?",
|
||||
"register_link": "注册",
|
||||
"welcome_notification": "欢迎使用 Maxun!",
|
||||
"error_notification": "登录失败。请重试。"
|
||||
},
|
||||
"register": {
|
||||
"title": "注册账号",
|
||||
"email": "电子邮箱",
|
||||
"password": "密码",
|
||||
"button": "注册",
|
||||
"loading": "加载中",
|
||||
"register_prompt": "已有账号?",
|
||||
"login_link": "登录",
|
||||
"welcome_notification": "欢迎使用 Maxun!",
|
||||
"error_notification": "注册失败。请重试。"
|
||||
},
|
||||
"recordingtable": {
|
||||
"run": "运行",
|
||||
"name": "名称",
|
||||
"schedule": "计划",
|
||||
"integrate": "集成",
|
||||
"settings": "设置",
|
||||
"options": "选项",
|
||||
"heading": "我的机器人",
|
||||
"new": "创建机器人",
|
||||
"modal": {
|
||||
"title": "输入URL",
|
||||
"label": "URL",
|
||||
"button": "开始录制"
|
||||
},
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"duplicate": "复制",
|
||||
"search": "搜索机器人...",
|
||||
"notifications": {
|
||||
"delete_warning": "无法删除机器人,因为它有关联的运行记录",
|
||||
"delete_success": "机器人删除成功"
|
||||
}
|
||||
},
|
||||
"mainmenu": {
|
||||
"recordings": "机器人",
|
||||
"runs": "运行记录",
|
||||
"proxy": "代理",
|
||||
"apikey": "API密钥",
|
||||
"feedback": "加入 Maxun Cloud",
|
||||
"apidocs": "网站转API"
|
||||
},
|
||||
"runstable": {
|
||||
"runs": "所有运行记录",
|
||||
"runStatus": "状态",
|
||||
"runName": "名称",
|
||||
"startedAt": "开始时间",
|
||||
"finishedAt": "结束时间",
|
||||
"delete": "删除",
|
||||
"settings": "设置",
|
||||
"search": "搜索运行记录...",
|
||||
"notifications": {
|
||||
"no_runs": "未找到运行记录。请重试。",
|
||||
"delete_success": "运行记录删除成功"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"title": "代理设置",
|
||||
"tab_standard": "标准代理",
|
||||
"tab_rotation": "自动代理轮换",
|
||||
"server_url": "代理服务器URL",
|
||||
"server_url_helper": "用于所有机器人的代理。支持HTTP和SOCKS代理。示例 http://myproxy.com:3128 或 socks5://myproxy.com:3128。简短形式 myproxy.com:3128 被视为HTTP代理。",
|
||||
"requires_auth": "需要认证?",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"add_proxy": "添加代理",
|
||||
"test_proxy": "测试代理",
|
||||
"remove_proxy": "删除代理",
|
||||
"table": {
|
||||
"proxy_url": "代理URL",
|
||||
"requires_auth": "需要认证"
|
||||
},
|
||||
"coming_soon": "即将推出 - 开源版(基础轮换)和云版(高级轮换)。如果您不想管理基础设施,请加入我们的云服务等候名单以获得早期访问权限。",
|
||||
"join_waitlist": "加入Maxun Cloud等候名单",
|
||||
"alert": {
|
||||
"title": "如果您的代理需要用户名和密码,请务必将它们与代理URL分开提供。",
|
||||
"right_way": "正确方式",
|
||||
"wrong_way": "错误方式",
|
||||
"proxy_url": "代理URL:",
|
||||
"username": "用户名:",
|
||||
"password": "密码:"
|
||||
},
|
||||
"notifications": {
|
||||
"config_success": "代理配置提交成功",
|
||||
"config_error": "提交代理配置失败。请重试。",
|
||||
"test_success": "代理配置运行正常",
|
||||
"test_error": "测试代理配置失败。请重试。",
|
||||
"fetch_success": "成功获取代理配置",
|
||||
"remove_success": "成功删除代理配置",
|
||||
"remove_error": "删除代理配置失败。请重试。"
|
||||
}
|
||||
},
|
||||
"apikey": {
|
||||
"title": "管理API密钥",
|
||||
"default_name": "Maxun API密钥",
|
||||
"table": {
|
||||
"name": "API密钥名称",
|
||||
"key": "API密钥",
|
||||
"actions": "操作"
|
||||
},
|
||||
"actions": {
|
||||
"copy": "复制",
|
||||
"show": "显示",
|
||||
"hide": "隐藏",
|
||||
"delete": "删除"
|
||||
},
|
||||
"no_key_message": "您还未生成API密钥。",
|
||||
"generate_button": "生成API密钥",
|
||||
"notifications": {
|
||||
"fetch_error": "获取API密钥失败 - ${error}",
|
||||
"generate_success": "API密钥生成成功",
|
||||
"generate_error": "生成API密钥失败 - ${error}",
|
||||
"delete_success": "API密钥删除成功",
|
||||
"delete_error": "删除API密钥失败 - ${error}",
|
||||
"copy_success": "API密钥复制成功"
|
||||
}
|
||||
},
|
||||
"action_description": {
|
||||
"text": {
|
||||
"title": "捕获文本",
|
||||
"description": "将鼠标悬停在要提取的文本上并点击选择"
|
||||
},
|
||||
"screenshot": {
|
||||
"title": "捕获截图",
|
||||
"description": "捕获当前页面的部分或全部截图。"
|
||||
},
|
||||
"list": {
|
||||
"title": "捕获列表",
|
||||
"description": "将鼠标悬停在要提取的列表上。选择后,您可以将鼠标悬停在所选列表中的所有文本上。点击选择它们。"
|
||||
},
|
||||
"default": {
|
||||
"title": "您想提取什么数据?",
|
||||
"description": "机器人设计为一次执行一个操作。您可以选择以下任何选项。"
|
||||
},
|
||||
"list_stages": {
|
||||
"initial": "选择要提取的列表及其中的文本",
|
||||
"pagination": "选择机器人如何捕获列表的其余部分",
|
||||
"limit": "选择要提取的项目数量",
|
||||
"complete": "捕获完成"
|
||||
}
|
||||
},
|
||||
"right_panel": {
|
||||
"buttons": {
|
||||
"capture_list": "捕获列表",
|
||||
"capture_text": "捕获文本",
|
||||
"capture_screenshot": "捕获截图",
|
||||
"confirm": "确认",
|
||||
"discard": "放弃",
|
||||
"confirm_capture": "确认捕获",
|
||||
"confirm_pagination": "确认分页",
|
||||
"confirm_limit": "确认限制",
|
||||
"finish_capture": "完成捕获",
|
||||
"finish": "完成",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "捕获整页",
|
||||
"capture_visible": "捕获可见部分",
|
||||
"display_fullpage": "获取整页截图",
|
||||
"display_visible": "获取可见部分截图"
|
||||
},
|
||||
"pagination": {
|
||||
"title": "如何在页面上找到下一个列表项?",
|
||||
"click_next": "点击下一页导航到下一页",
|
||||
"click_load_more": "点击加载更多来加载更多项目",
|
||||
"scroll_down": "向下滚动加载更多项目",
|
||||
"scroll_up": "向上滚动加载更多项目",
|
||||
"none": "没有更多项目可加载"
|
||||
},
|
||||
"limit": {
|
||||
"title": "您想要提取的最大行数是多少?",
|
||||
"custom": "自定义",
|
||||
"enter_number": "输入数字"
|
||||
},
|
||||
"fields": {
|
||||
"label": "标签",
|
||||
"data": "数据",
|
||||
"field_label": "字段标签",
|
||||
"field_data": "字段数据"
|
||||
},
|
||||
"messages": {
|
||||
"list_selected": "列表选择成功"
|
||||
},
|
||||
"errors": {
|
||||
"select_pagination": "请选择分页类型。",
|
||||
"select_pagination_element": "请先选择分页元素。",
|
||||
"select_limit": "请选择限制或输入自定义限制。",
|
||||
"invalid_limit": "请输入有效的限制。",
|
||||
"confirm_text_fields": "请确认所有文本字段",
|
||||
"unable_create_settings": "无法创建列表设置。请确保您已为列表定义了字段。",
|
||||
"capture_text_discarded": "文本捕获已放弃",
|
||||
"capture_list_discarded": "列表捕获已放弃"
|
||||
}
|
||||
},
|
||||
"save_recording": {
|
||||
"title": "保存机器人",
|
||||
"robot_name": "机器人名称",
|
||||
"buttons": {
|
||||
"save": "保存",
|
||||
"confirm": "确认"
|
||||
},
|
||||
"notifications": {
|
||||
"save_success": "机器人保存成功"
|
||||
},
|
||||
"errors": {
|
||||
"user_not_logged": "用户未登录。无法保存录制。",
|
||||
"exists_warning": "已存在同名机器人,请确认是否覆盖机器人。"
|
||||
},
|
||||
"tooltips": {
|
||||
"saving": "正在优化并保存工作流程"
|
||||
}
|
||||
},
|
||||
"browser_recording": {
|
||||
"modal": {
|
||||
"confirm_discard": "您确定要放弃录制吗?"
|
||||
},
|
||||
"notifications": {
|
||||
"terminated": "当前录制已终止"
|
||||
}
|
||||
},
|
||||
"interpretation_log": {
|
||||
"titles": {
|
||||
"output_preview": "输出数据预览",
|
||||
"screenshot": "截图"
|
||||
},
|
||||
"messages": {
|
||||
"additional_rows": "完成录制后将提取更多数据行。",
|
||||
"successful_training": "您已成功训练机器人执行操作!点击下方按钮预览机器人将提取的数据。",
|
||||
"no_selection": "看起来您还没有选择要提取的内容。选择后,机器人将在此处显示您的选择预览。"
|
||||
},
|
||||
"data_sections": {
|
||||
"binary_received": "---------- 已接收二进制输出数据 ----------",
|
||||
"serializable_received": "---------- 已接收可序列化输出数据 ----------",
|
||||
"mimetype": "MIME类型:",
|
||||
"image_below": "图片显示如下:",
|
||||
"separator": "--------------------------------------------------"
|
||||
},
|
||||
"notifications": {
|
||||
"reset_success": "输出预览已成功重置"
|
||||
}
|
||||
},
|
||||
"interpretation_buttons": {
|
||||
"buttons": {
|
||||
"preview": "获取输出数据预览",
|
||||
"reset": "重置",
|
||||
"yes": "是",
|
||||
"no": "否"
|
||||
},
|
||||
"messages": {
|
||||
"extracting": "正在提取数据...请等待10秒到1分钟",
|
||||
"restart_required": "更新录制后请重新启动解释",
|
||||
"run_finished": "运行完成",
|
||||
"run_failed": "运行启动失败"
|
||||
},
|
||||
"modal": {
|
||||
"use_previous": "您要将之前的选择用作执行此操作的条件吗?",
|
||||
"previous_action": "您之前的操作是:",
|
||||
"element_text": "在文本元素上 "
|
||||
}
|
||||
},
|
||||
"recording_page": {
|
||||
"loader": {
|
||||
"browser_startup": "正在启动浏览器...正在导航至{{url}}"
|
||||
}
|
||||
},
|
||||
"integration_settings": {
|
||||
"title": "与Google Sheet集成",
|
||||
"descriptions": {
|
||||
"sync_info": "如果启用此选项,每次机器人成功运行任务时,捕获的数据都会追加到您的Google Sheet中。",
|
||||
"authenticated_as": "已验证身份: {{email}}"
|
||||
},
|
||||
"alerts": {
|
||||
"success": {
|
||||
"title": "Google Sheet集成成功。",
|
||||
"content": "每次此机器人创建成功运行时,捕获的数据都会追加到您的Google Sheet {{sheetName}}中。您可以查看数据更新",
|
||||
"here": "在此处",
|
||||
"note": "注意:",
|
||||
"sync_limitation": "与Google Sheets集成之前提取的数据将不会同步到Google Sheet中。只有集成后提取的数据才会同步。"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"authenticate": "使用Google验证",
|
||||
"fetch_sheets": "获取Google Sheets",
|
||||
"remove_integration": "移除集成",
|
||||
"submit": "提交"
|
||||
},
|
||||
"fields": {
|
||||
"select_sheet": "选择Google Sheet",
|
||||
"selected_sheet": "已选择表格: {{name}} (ID: {{id}})"
|
||||
}
|
||||
},
|
||||
"robot_duplication": {
|
||||
"title": "复制机器人",
|
||||
"descriptions": {
|
||||
"purpose": "机器人复制功能用于从具有相同结构的页面提取数据。",
|
||||
"example": "示例:如果您已经为{{url1}}创建了机器人,您可以复制它来抓取类似的页面(如{{url2}}),而无需从头开始训练机器人。",
|
||||
"warning": "⚠️ 确保新页面与原始页面具有相同的结构。"
|
||||
},
|
||||
"fields": {
|
||||
"target_url": "机器人目标URL"
|
||||
},
|
||||
"buttons": {
|
||||
"duplicate": "复制机器人",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"notifications": {
|
||||
"robot_not_found": "找不到机器人详细信息。请重试。",
|
||||
"url_required": "需要目标URL。",
|
||||
"duplicate_success": "机器人复制成功。",
|
||||
"duplicate_error": "更新目标URL失败。请重试。",
|
||||
"unknown_error": "更新目标URL时发生错误。"
|
||||
}
|
||||
},
|
||||
"robot_settings": {
|
||||
"title": "机器人设置",
|
||||
"target_url": "机器人目标URL",
|
||||
"robot_id": "机器人ID",
|
||||
"robot_limit": "机器人限制",
|
||||
"created_by_user": "由用户创建",
|
||||
"created_at": "机器人创建时间",
|
||||
"errors": {
|
||||
"robot_not_found": "无法找到机器人详细信息。请重试。"
|
||||
}
|
||||
},
|
||||
"robot_edit": {
|
||||
"title": "编辑机器人",
|
||||
"change_name": "更改机器人名称",
|
||||
"robot_limit": "机器人限制",
|
||||
"save": "保存更改",
|
||||
"cancel": "取消",
|
||||
"notifications": {
|
||||
"update_success": "机器人更新成功。",
|
||||
"update_failed": "无法更新机器人。请重试。",
|
||||
"update_error": "更新机器人时发生错误。"
|
||||
}
|
||||
},
|
||||
"schedule_settings": {
|
||||
"title": "计划设置",
|
||||
"run_every": "每次运行",
|
||||
"start_from": "开始于",
|
||||
"on_day": "在日",
|
||||
"at_around": "大约在",
|
||||
"timezone": "时区",
|
||||
"buttons": {
|
||||
"delete_schedule": "删除计划",
|
||||
"save_schedule": "保存计划",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"labels": {
|
||||
"in_between": "之间",
|
||||
"run_once_every": "每次运行",
|
||||
"start_from_label": "开始于",
|
||||
"on_day_of_month": "月份日期",
|
||||
"on_day": {
|
||||
"st": "日",
|
||||
"nd": "日",
|
||||
"rd": "日",
|
||||
"th": "日"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main_page": {
|
||||
"notifications": {
|
||||
"interpretation_success": "机器人{{name}}解释成功",
|
||||
"interpretation_failed": "机器人{{name}}解释失败",
|
||||
"run_started": "正在运行机器人:{{name}}",
|
||||
"run_start_failed": "机器人运行失败:{{name}}",
|
||||
"schedule_success": "机器人{{name}}调度成功",
|
||||
"schedule_failed": "机器人{{name}}调度失败",
|
||||
"abort_success": "成功中止机器人{{name}}的解释",
|
||||
"abort_failed": "中止机器人{{name}}的解释失败"
|
||||
},
|
||||
"menu": {
|
||||
"recordings": "机器人",
|
||||
"runs": "运行",
|
||||
"proxy": "代理",
|
||||
"apikey": "API密钥"
|
||||
}
|
||||
},
|
||||
"browser_window": {
|
||||
"attribute_modal": {
|
||||
"title": "选择属性",
|
||||
"notifications": {
|
||||
"list_select_success": "列表选择成功。选择要提取的文本数据。",
|
||||
"pagination_select_success": "分页元素选择成功。"
|
||||
}
|
||||
},
|
||||
"attribute_options": {
|
||||
"anchor": {
|
||||
"text": "文本: {{text}}",
|
||||
"url": "URL: {{url}}"
|
||||
},
|
||||
"image": {
|
||||
"alt_text": "替代文本: {{altText}}",
|
||||
"image_url": "图像URL: {{imageUrl}}"
|
||||
},
|
||||
"default": {
|
||||
"text": "文本: {{text}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"runs_table": {
|
||||
"run_type_chips": {
|
||||
"manual_run": "手动运行",
|
||||
"scheduled_run": "计划运行",
|
||||
"api": "API",
|
||||
"unknown_run_type": "未知运行类型"
|
||||
},
|
||||
"run_status_chips": {
|
||||
"success": "成功",
|
||||
"running": "运行中",
|
||||
"scheduled": "已计划",
|
||||
"failed": "失败"
|
||||
},
|
||||
"run_settings_modal": {
|
||||
"title": "运行设置",
|
||||
"labels": {
|
||||
"run_id": "运行ID",
|
||||
"run_by_user": "由用户运行",
|
||||
"run_by_schedule": "按计划ID运行",
|
||||
"run_by_api": "由API运行",
|
||||
"run_type": "运行类型"
|
||||
}
|
||||
}
|
||||
},
|
||||
"run_content": {
|
||||
"tabs": {
|
||||
"output_data": "输出数据",
|
||||
"log": "日志"
|
||||
},
|
||||
"empty_output": "输出为空。",
|
||||
"captured_data": {
|
||||
"title": "捕获的数据",
|
||||
"download_json": "下载为JSON",
|
||||
"download_csv": "下载为CSV"
|
||||
},
|
||||
"captured_screenshot": {
|
||||
"title": "捕获的截图",
|
||||
"download": "下载截图",
|
||||
"render_failed": "图像渲染失败"
|
||||
},
|
||||
"buttons": {
|
||||
"stop": "停止"
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
"project_name": "Maxun",
|
||||
"upgrade": {
|
||||
"button": "升级",
|
||||
"modal": {
|
||||
"up_to_date": "🎉 您已是最新版本!",
|
||||
"new_version_available": "新版本已可用:{{version}}。升级到最新版本以获取错误修复、增强和新功能!",
|
||||
"view_updates": "查看所有新更新",
|
||||
"view_updates_link": "此处",
|
||||
"tabs": {
|
||||
"manual_setup": "手动设置升级",
|
||||
"docker_setup": "Docker Compose设置升级"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu_items": {
|
||||
"logout": "退出登录",
|
||||
"discord": "Discord",
|
||||
"youtube": "YouTube",
|
||||
"twitter": "Twitter (X)",
|
||||
"language": "语言"
|
||||
},
|
||||
"recording": {
|
||||
"discard": "丢弃"
|
||||
}
|
||||
},
|
||||
"language_menu": {
|
||||
"en": "英语",
|
||||
"es": "西班牙语",
|
||||
"ja": "日语",
|
||||
"zh": "中文",
|
||||
"de": "德语"
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,14 @@ WORKDIR /app
|
||||
COPY package*.json ./
|
||||
COPY maxun-core ./maxun-core
|
||||
COPY src ./src
|
||||
COPY public ./public
|
||||
COPY server ./server
|
||||
COPY tsconfig.json ./
|
||||
COPY server/tsconfig.json ./server/
|
||||
# COPY server/start.sh ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
# Install Playwright browsers and dependencies
|
||||
RUN npx playwright install --with-deps chromium
|
||||
|
||||
@@ -104,7 +104,7 @@ export class RemoteBrowser {
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a URL change is significant enough to emit
|
||||
@@ -130,11 +130,11 @@ export class RemoteBrowser {
|
||||
});
|
||||
|
||||
// Handle page load events with retry mechanism
|
||||
page.on('load', async () => {
|
||||
page.on('load', async () => {
|
||||
const injectScript = async (): Promise<boolean> => {
|
||||
try {
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 });
|
||||
|
||||
|
||||
await page.evaluate(getInjectableScript());
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
@@ -148,6 +148,19 @@ export class RemoteBrowser {
|
||||
});
|
||||
}
|
||||
|
||||
private getUserAgent() {
|
||||
const userAgents = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:117.0) Gecko/20100101 Firefox/117.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.1938.81 Safari/537.36 Edg/116.0.1938.81',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.96 Safari/537.36 OPR/101.0.4843.25',
|
||||
'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.62 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:118.0) Gecko/20100101 Firefox/118.0',
|
||||
];
|
||||
|
||||
return userAgents[Math.floor(Math.random() * userAgents.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* An asynchronous constructor for asynchronously initialized properties.
|
||||
* Must be called right after creating an instance of RemoteBrowser class.
|
||||
@@ -155,37 +168,17 @@ export class RemoteBrowser {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public initialize = async (userId: string): Promise<void> => {
|
||||
// const launchOptions = {
|
||||
// headless: true,
|
||||
// proxy: options.launchOptions?.proxy,
|
||||
// chromiumSandbox: false,
|
||||
// args: [
|
||||
// '--no-sandbox',
|
||||
// '--disable-setuid-sandbox',
|
||||
// '--headless=new',
|
||||
// '--disable-gpu',
|
||||
// '--disable-dev-shm-usage',
|
||||
// '--disable-software-rasterizer',
|
||||
// '--in-process-gpu',
|
||||
// '--disable-infobars',
|
||||
// '--single-process',
|
||||
// '--no-zygote',
|
||||
// '--disable-notifications',
|
||||
// '--disable-extensions',
|
||||
// '--disable-background-timer-throttling',
|
||||
// ...(options.launchOptions?.args || [])
|
||||
// ],
|
||||
// env: {
|
||||
// ...process.env,
|
||||
// CHROMIUM_FLAGS: '--disable-gpu --no-sandbox --headless=new'
|
||||
// }
|
||||
// };
|
||||
// console.log('Launch options before:', options.launchOptions);
|
||||
// this.browser = <Browser>(await options.browser.launch(launchOptions));
|
||||
|
||||
// console.log('Launch options after:', options.launchOptions)
|
||||
this.browser = <Browser>(await chromium.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"--disable-web-security",
|
||||
"--disable-features=IsolateOrigins,site-per-process",
|
||||
"--disable-site-isolation-trials",
|
||||
"--disable-extensions",
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
],
|
||||
}));
|
||||
const proxyConfig = await getDecryptedProxyConfig(userId);
|
||||
let proxyOptions: { server: string, username?: string, password?: string } = { server: '' };
|
||||
@@ -201,7 +194,7 @@ export class RemoteBrowser {
|
||||
const contextOptions: any = {
|
||||
viewport: { height: 400, width: 900 },
|
||||
// recordVideo: { dir: 'videos/' }
|
||||
// Force reduced motion to prevent animation issues
|
||||
// Force reduced motion to prevent animation issues
|
||||
reducedMotion: 'reduce',
|
||||
// Force JavaScript to be enabled
|
||||
javaScriptEnabled: true,
|
||||
@@ -210,7 +203,8 @@ export class RemoteBrowser {
|
||||
// Disable hardware acceleration
|
||||
forcedColors: 'none',
|
||||
isMobile: false,
|
||||
hasTouch: false
|
||||
hasTouch: false,
|
||||
userAgent: this.getUserAgent(),
|
||||
};
|
||||
|
||||
if (proxyOptions.server) {
|
||||
@@ -220,22 +214,48 @@ export class RemoteBrowser {
|
||||
password: proxyOptions.password ? proxyOptions.password : undefined,
|
||||
};
|
||||
}
|
||||
const browserUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.38 Safari/537.36";
|
||||
|
||||
|
||||
contextOptions.userAgent = browserUserAgent;
|
||||
this.context = await this.browser.newContext(contextOptions);
|
||||
await this.context.addInitScript(
|
||||
`const defaultGetter = Object.getOwnPropertyDescriptor(
|
||||
Navigator.prototype,
|
||||
"webdriver"
|
||||
).get;
|
||||
defaultGetter.apply(navigator);
|
||||
defaultGetter.toString();
|
||||
Object.defineProperty(Navigator.prototype, "webdriver", {
|
||||
set: undefined,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: new Proxy(defaultGetter, {
|
||||
apply: (target, thisArg, args) => {
|
||||
Reflect.apply(target, thisArg, args);
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
});
|
||||
const patchedGetter = Object.getOwnPropertyDescriptor(
|
||||
Navigator.prototype,
|
||||
"webdriver"
|
||||
).get;
|
||||
patchedGetter.apply(navigator);
|
||||
patchedGetter.toString();`
|
||||
);
|
||||
this.currentPage = await this.context.newPage();
|
||||
|
||||
await this.setupPageEventListeners(this.currentPage);
|
||||
|
||||
// await this.currentPage.setExtraHTTPHeaders({
|
||||
// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
|
||||
// });
|
||||
const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']);
|
||||
await blocker.enableBlockingInPage(this.currentPage);
|
||||
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||
await blocker.disableBlockingInPage(this.currentPage);
|
||||
try {
|
||||
const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']);
|
||||
await blocker.enableBlockingInPage(this.currentPage);
|
||||
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||
await blocker.disableBlockingInPage(this.currentPage);
|
||||
console.log('Adblocker initialized');
|
||||
} catch (error: any) {
|
||||
console.warn('Failed to initialize adblocker, continuing without it:', error.message);
|
||||
// Still need to set up the CDP session even if blocker fails
|
||||
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -456,7 +476,7 @@ export class RemoteBrowser {
|
||||
this.currentPage = newPage;
|
||||
if (this.currentPage) {
|
||||
await this.setupPageEventListeners(this.currentPage);
|
||||
|
||||
|
||||
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||
await this.subscribeToScreencast();
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Socket } from 'socket.io';
|
||||
|
||||
import logger from "../logger";
|
||||
import { Coordinates, ScrollDeltas, KeyboardInput } from '../types';
|
||||
import { Coordinates, ScrollDeltas, KeyboardInput, DatePickerEventData } from '../types';
|
||||
import { browserPool } from "../server";
|
||||
import { WorkflowGenerator } from "../workflow-management/classes/Generator";
|
||||
import { Page } from "playwright";
|
||||
@@ -223,6 +223,53 @@ const handleKeydown = async (generator: WorkflowGenerator, page: Page, { key, co
|
||||
logger.log('debug', `Key ${key} pressed`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the date selection event.
|
||||
* @param generator - the workflow generator {@link Generator}
|
||||
* @param page - the active page of the remote browser
|
||||
* @param data - the data of the date selection event {@link DatePickerEventData}
|
||||
* @category BrowserManagement
|
||||
*/
|
||||
const handleDateSelection = async (generator: WorkflowGenerator, page: Page, data: DatePickerEventData) => {
|
||||
await generator.onDateSelection(page, data);
|
||||
logger.log('debug', `Date ${data.value} selected`);
|
||||
}
|
||||
|
||||
const onDateSelection = async (data: DatePickerEventData) => {
|
||||
logger.log('debug', 'Handling date selection event emitted from client');
|
||||
await handleWrapper(handleDateSelection, data);
|
||||
}
|
||||
|
||||
const handleDropdownSelection = async (generator: WorkflowGenerator, page: Page, data: { selector: string, value: string }) => {
|
||||
await generator.onDropdownSelection(page, data);
|
||||
logger.log('debug', `Dropdown value ${data.value} selected`);
|
||||
}
|
||||
|
||||
const onDropdownSelection = async (data: { selector: string, value: string }) => {
|
||||
logger.log('debug', 'Handling dropdown selection event emitted from client');
|
||||
await handleWrapper(handleDropdownSelection, data);
|
||||
}
|
||||
|
||||
const handleTimeSelection = async (generator: WorkflowGenerator, page: Page, data: { selector: string, value: string }) => {
|
||||
await generator.onTimeSelection(page, data);
|
||||
logger.log('debug', `Time value ${data.value} selected`);
|
||||
}
|
||||
|
||||
const onTimeSelection = async (data: { selector: string, value: string }) => {
|
||||
logger.log('debug', 'Handling time selection event emitted from client');
|
||||
await handleWrapper(handleTimeSelection, data);
|
||||
}
|
||||
|
||||
const handleDateTimeLocalSelection = async (generator: WorkflowGenerator, page: Page, data: { selector: string, value: string }) => {
|
||||
await generator.onDateTimeLocalSelection(page, data);
|
||||
logger.log('debug', `DateTime Local value ${data.value} selected`);
|
||||
}
|
||||
|
||||
const onDateTimeLocalSelection = async (data: { selector: string, value: string }) => {
|
||||
logger.log('debug', 'Handling datetime-local selection event emitted from client');
|
||||
await handleWrapper(handleDateTimeLocalSelection, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper function for handling the keyup event.
|
||||
* @param keyboardInput - the keyboard input of the keyup event
|
||||
@@ -378,6 +425,10 @@ const registerInputHandlers = (socket: Socket) => {
|
||||
socket.on("input:refresh", onRefresh);
|
||||
socket.on("input:back", onGoBack);
|
||||
socket.on("input:forward", onGoForward);
|
||||
socket.on("input:date", onDateSelection);
|
||||
socket.on("input:dropdown", onDropdownSelection);
|
||||
socket.on("input:time", onTimeSelection);
|
||||
socket.on("input:datetime-local", onDateTimeLocalSelection);
|
||||
socket.on("action", onGenerateAction);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,16 @@ export interface Coordinates {
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* interface to handle date picker events.
|
||||
* @category Types
|
||||
*/
|
||||
export interface DatePickerEventData {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the deltas of a wheel/scroll event.
|
||||
* @category Types
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Action, ActionType, Coordinates, TagName } from "../../types";
|
||||
import { Action, ActionType, Coordinates, TagName, DatePickerEventData } from "../../types";
|
||||
import { WhereWhatPair, WorkflowFile } from 'maxun-core';
|
||||
import logger from "../../logger";
|
||||
import { Socket } from "socket.io";
|
||||
@@ -140,19 +140,22 @@ export class WorkflowGenerator {
|
||||
socket.on('decision', async ({ pair, actionType, decision }) => {
|
||||
const id = browserPool.getActiveBrowserId();
|
||||
if (id) {
|
||||
const activeBrowser = browserPool.getRemoteBrowser(id);
|
||||
const currentPage = activeBrowser?.getCurrentPage();
|
||||
if (decision) {
|
||||
// const activeBrowser = browserPool.getRemoteBrowser(id);
|
||||
// const currentPage = activeBrowser?.getCurrentPage();
|
||||
if (!decision) {
|
||||
switch (actionType) {
|
||||
case 'customAction':
|
||||
pair.where.selectors = [this.generatedData.lastUsedSelector];
|
||||
// pair.where.selectors = [this.generatedData.lastUsedSelector];
|
||||
pair.where.selectors = pair.where.selectors.filter(
|
||||
(selector: string) => selector !== this.generatedData.lastUsedSelector
|
||||
);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
if (currentPage) {
|
||||
await this.addPairToWorkflowAndNotifyClient(pair, currentPage);
|
||||
}
|
||||
// if (currentPage) {
|
||||
// await this.addPairToWorkflowAndNotifyClient(pair, currentPage);
|
||||
// }
|
||||
}
|
||||
})
|
||||
socket.on('updatePair', (data) => {
|
||||
@@ -252,6 +255,85 @@ export class WorkflowGenerator {
|
||||
logger.log('info', `Workflow emitted`);
|
||||
};
|
||||
|
||||
public onDateSelection = async (page: Page, data: DatePickerEventData) => {
|
||||
const { selector, value } = data;
|
||||
|
||||
try {
|
||||
await page.fill(selector, value);
|
||||
} catch (error) {
|
||||
console.error("Failed to fill date value:", error);
|
||||
}
|
||||
|
||||
const pair: WhereWhatPair = {
|
||||
where: { url: this.getBestUrl(page.url()) },
|
||||
what: [{
|
||||
action: 'fill',
|
||||
args: [selector, value],
|
||||
}],
|
||||
};
|
||||
|
||||
await this.addPairToWorkflowAndNotifyClient(pair, page);
|
||||
};
|
||||
|
||||
public onDropdownSelection = async (page: Page, data: { selector: string, value: string }) => {
|
||||
const { selector, value } = data;
|
||||
|
||||
try {
|
||||
await page.selectOption(selector, value);
|
||||
} catch (error) {
|
||||
console.error("Failed to fill date value:", error);
|
||||
}
|
||||
|
||||
const pair: WhereWhatPair = {
|
||||
where: { url: this.getBestUrl(page.url()) },
|
||||
what: [{
|
||||
action: 'selectOption',
|
||||
args: [selector, value],
|
||||
}],
|
||||
};
|
||||
|
||||
await this.addPairToWorkflowAndNotifyClient(pair, page);
|
||||
};
|
||||
|
||||
public onTimeSelection = async (page: Page, data: { selector: string, value: string }) => {
|
||||
const { selector, value } = data;
|
||||
|
||||
try {
|
||||
await page.fill(selector, value);
|
||||
} catch (error) {
|
||||
console.error("Failed to set time value:", error);
|
||||
}
|
||||
|
||||
const pair: WhereWhatPair = {
|
||||
where: { url: this.getBestUrl(page.url()) },
|
||||
what: [{
|
||||
action: 'fill',
|
||||
args: [selector, value],
|
||||
}],
|
||||
};
|
||||
|
||||
await this.addPairToWorkflowAndNotifyClient(pair, page);
|
||||
};
|
||||
|
||||
public onDateTimeLocalSelection = async (page: Page, data: { selector: string, value: string }) => {
|
||||
const { selector, value } = data;
|
||||
|
||||
try {
|
||||
await page.fill(selector, value);
|
||||
} catch (error) {
|
||||
console.error("Failed to fill datetime-local value:", error);
|
||||
}
|
||||
|
||||
const pair: WhereWhatPair = {
|
||||
where: { url: this.getBestUrl(page.url()) },
|
||||
what: [{
|
||||
action: 'fill',
|
||||
args: [selector, value],
|
||||
}],
|
||||
};
|
||||
|
||||
await this.addPairToWorkflowAndNotifyClient(pair, page);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a pair for the click event.
|
||||
@@ -263,6 +345,81 @@ export class WorkflowGenerator {
|
||||
let where: WhereWhatPair["where"] = { url: this.getBestUrl(page.url()) };
|
||||
const selector = await this.generateSelector(page, coordinates, ActionType.Click);
|
||||
logger.log('debug', `Element's selector: ${selector}`);
|
||||
|
||||
const elementInfo = await getElementInformation(page, coordinates, '', false);
|
||||
console.log("Element info: ", elementInfo);
|
||||
|
||||
// Check if clicked element is a select dropdown
|
||||
const isDropdown = elementInfo?.tagName === 'SELECT';
|
||||
|
||||
if (isDropdown && elementInfo.innerHTML) {
|
||||
// Parse options from innerHTML
|
||||
const options = elementInfo.innerHTML
|
||||
.split('<option')
|
||||
.slice(1) // Remove first empty element
|
||||
.map(optionHtml => {
|
||||
const valueMatch = optionHtml.match(/value="([^"]*)"/);
|
||||
const disabledMatch = optionHtml.includes('disabled="disabled"');
|
||||
const selectedMatch = optionHtml.includes('selected="selected"');
|
||||
|
||||
// Extract text content between > and </option>
|
||||
const textMatch = optionHtml.match(/>([^<]*)</);
|
||||
const text = textMatch
|
||||
? textMatch[1]
|
||||
.replace(/\n/g, '') // Remove all newlines
|
||||
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
|
||||
.trim()
|
||||
: '';
|
||||
|
||||
return {
|
||||
value: valueMatch ? valueMatch[1] : '',
|
||||
text,
|
||||
disabled: disabledMatch,
|
||||
selected: selectedMatch
|
||||
};
|
||||
});
|
||||
|
||||
// Notify client to show dropdown overlay
|
||||
this.socket.emit('showDropdown', {
|
||||
coordinates,
|
||||
selector,
|
||||
options
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicked element is a date input
|
||||
const isDateInput = elementInfo?.tagName === 'INPUT' && elementInfo?.attributes?.type === 'date';
|
||||
|
||||
if (isDateInput) {
|
||||
// Notify client to show datepicker overlay
|
||||
this.socket.emit('showDatePicker', {
|
||||
coordinates,
|
||||
selector
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isTimeInput = elementInfo?.tagName === 'INPUT' && elementInfo?.attributes?.type === 'time';
|
||||
|
||||
if (isTimeInput) {
|
||||
this.socket.emit('showTimePicker', {
|
||||
coordinates,
|
||||
selector
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isDateTimeLocal = elementInfo?.tagName === 'INPUT' && elementInfo?.attributes?.type === 'datetime-local';
|
||||
|
||||
if (isDateTimeLocal) {
|
||||
this.socket.emit('showDateTimePicker', {
|
||||
coordinates,
|
||||
selector
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//const element = await getElementMouseIsOver(page, coordinates);
|
||||
//logger.log('debug', `Element: ${JSON.stringify(element, null, 2)}`);
|
||||
if (selector) {
|
||||
@@ -360,6 +517,8 @@ export class WorkflowGenerator {
|
||||
}],
|
||||
}
|
||||
|
||||
await this.addPairToWorkflowAndNotifyClient(pair, page);
|
||||
|
||||
if (this.generatedData.lastUsedSelector) {
|
||||
const elementInfo = await this.getLastUsedSelectorInfo(page, this.generatedData.lastUsedSelector);
|
||||
|
||||
@@ -372,9 +531,7 @@ export class WorkflowGenerator {
|
||||
innerText: elementInfo.innerText,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await this.addPairToWorkflowAndNotifyClient(pair, page);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -541,9 +698,9 @@ export class WorkflowGenerator {
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
private generateSelector = async (page: Page, coordinates: Coordinates, action: ActionType) => {
|
||||
const elementInfo = await getElementInformation(page, coordinates, this.listSelector);
|
||||
const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList);
|
||||
const selectorBasedOnCustomAction = (this.getList === true)
|
||||
? await getNonUniqueSelectors(page, coordinates)
|
||||
? await getNonUniqueSelectors(page, coordinates, this.listSelector)
|
||||
: await getSelectors(page, coordinates);
|
||||
|
||||
const bestSelector = getBestSelectorForAction(
|
||||
@@ -569,9 +726,9 @@ export class WorkflowGenerator {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public generateDataForHighlighter = async (page: Page, coordinates: Coordinates) => {
|
||||
const rect = await getRect(page, coordinates, this.listSelector);
|
||||
const rect = await getRect(page, coordinates, this.listSelector, this.getList);
|
||||
const displaySelector = await this.generateSelector(page, coordinates, ActionType.Click);
|
||||
const elementInfo = await getElementInformation(page, coordinates, this.listSelector);
|
||||
const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList);
|
||||
if (rect) {
|
||||
if (this.getList === true) {
|
||||
if (this.listSelector !== '') {
|
||||
|
||||
@@ -17,9 +17,10 @@ export const getElementInformation = async (
|
||||
page: Page,
|
||||
coordinates: Coordinates,
|
||||
listSelector: string,
|
||||
getList: boolean
|
||||
) => {
|
||||
try {
|
||||
if (listSelector !== '') {
|
||||
if (!getList || listSelector !== '') {
|
||||
const elementInfo = await page.evaluate(
|
||||
async ({ x, y }) => {
|
||||
const el = document.elementFromPoint(x, y) as HTMLElement;
|
||||
@@ -53,6 +54,15 @@ export const getElementInformation = async (
|
||||
info.innerText = element.innerText ?? '';
|
||||
} else if (element?.tagName === 'IMG') {
|
||||
info.imageUrl = (element as HTMLImageElement).src;
|
||||
} else if (element?.tagName === 'SELECT') {
|
||||
const selectElement = element as HTMLSelectElement;
|
||||
info.innerText = selectElement.options[selectElement.selectedIndex]?.text ?? '';
|
||||
info.attributes = {
|
||||
...info.attributes,
|
||||
selectedValue: selectElement.value,
|
||||
};
|
||||
} else if (element?.tagName === 'INPUT' && (element as HTMLInputElement).type === 'time' || (element as HTMLInputElement).type === 'date') {
|
||||
info.innerText = (element as HTMLInputElement).value;
|
||||
} else {
|
||||
info.hasOnlyText = element?.children?.length === 0 &&
|
||||
element?.innerText?.length > 0;
|
||||
@@ -74,22 +84,10 @@ export const getElementInformation = async (
|
||||
if (originalEl) {
|
||||
let element = originalEl;
|
||||
|
||||
const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE',
|
||||
'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME',
|
||||
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET',
|
||||
'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT',
|
||||
'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT',
|
||||
'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET',
|
||||
'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A'
|
||||
];
|
||||
while (element.parentElement) {
|
||||
const parentRect = element.parentElement.getBoundingClientRect();
|
||||
const childRect = element.getBoundingClientRect();
|
||||
|
||||
if (!containerTags.includes(element.parentElement.tagName)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const fullyContained =
|
||||
parentRect.left <= childRect.left &&
|
||||
parentRect.right >= childRect.right &&
|
||||
@@ -167,9 +165,9 @@ export const getElementInformation = async (
|
||||
* @category WorkflowManagement-Selectors
|
||||
* @returns {Promise<Rectangle|undefined|null>}
|
||||
*/
|
||||
export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string) => {
|
||||
export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string, getList: boolean) => {
|
||||
try {
|
||||
if (listSelector !== '') {
|
||||
if (!getList || listSelector !== '') {
|
||||
const rect = await page.evaluate(
|
||||
async ({ x, y }) => {
|
||||
const el = document.elementFromPoint(x, y) as HTMLElement;
|
||||
@@ -202,22 +200,10 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector
|
||||
if (originalEl) {
|
||||
let element = originalEl;
|
||||
|
||||
const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE',
|
||||
'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME',
|
||||
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET',
|
||||
'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT',
|
||||
'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT',
|
||||
'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET',
|
||||
'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A'
|
||||
];
|
||||
while (element.parentElement) {
|
||||
const parentRect = element.parentElement.getBoundingClientRect();
|
||||
const childRect = element.getBoundingClientRect();
|
||||
|
||||
if (!containerTags.includes(element.parentElement.tagName)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const fullyContained =
|
||||
parentRect.left <= childRect.left &&
|
||||
parentRect.right >= childRect.right &&
|
||||
@@ -875,86 +861,123 @@ interface SelectorResult {
|
||||
* @returns {Promise<Selectors|null|undefined>}
|
||||
*/
|
||||
|
||||
export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates): Promise<SelectorResult> => {
|
||||
export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates, listSelector: string): Promise<SelectorResult> => {
|
||||
try {
|
||||
const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => {
|
||||
function getNonUniqueSelector(element: HTMLElement): string {
|
||||
let selector = element.tagName.toLowerCase();
|
||||
if (!listSelector) {
|
||||
console.log(`NON UNIQUE: MODE 1`)
|
||||
const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => {
|
||||
function getNonUniqueSelector(element: HTMLElement): string {
|
||||
let selector = element.tagName.toLowerCase();
|
||||
|
||||
if (element.className) {
|
||||
const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls));
|
||||
if (classes.length > 0) {
|
||||
const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':'));
|
||||
if (validClasses.length > 0) {
|
||||
selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.');
|
||||
if (element.className) {
|
||||
const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls));
|
||||
if (classes.length > 0) {
|
||||
const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':'));
|
||||
if (validClasses.length > 0) {
|
||||
selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
return selector;
|
||||
}
|
||||
function getSelectorPath(element: HTMLElement | null): string {
|
||||
const path: string[] = [];
|
||||
let depth = 0;
|
||||
const maxDepth = 2;
|
||||
|
||||
function getSelectorPath(element: HTMLElement | null): string {
|
||||
const path: string[] = [];
|
||||
let depth = 0;
|
||||
const maxDepth = 2;
|
||||
while (element && element !== document.body && depth < maxDepth) {
|
||||
const selector = getNonUniqueSelector(element);
|
||||
path.unshift(selector);
|
||||
element = element.parentElement;
|
||||
depth++;
|
||||
}
|
||||
|
||||
while (element && element !== document.body && depth < maxDepth) {
|
||||
const selector = getNonUniqueSelector(element);
|
||||
path.unshift(selector);
|
||||
element = element.parentElement;
|
||||
depth++;
|
||||
return path.join(' > ');
|
||||
}
|
||||
|
||||
return path.join(' > ');
|
||||
}
|
||||
const originalEl = document.elementFromPoint(x, y) as HTMLElement;
|
||||
if (!originalEl) return null;
|
||||
|
||||
const originalEl = document.elementFromPoint(x, y) as HTMLElement;
|
||||
if (!originalEl) return null;
|
||||
let element = originalEl;
|
||||
|
||||
let element = originalEl;
|
||||
// if (listSelector === '') {
|
||||
while (element.parentElement) {
|
||||
const parentRect = element.parentElement.getBoundingClientRect();
|
||||
const childRect = element.getBoundingClientRect();
|
||||
|
||||
const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE',
|
||||
'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME',
|
||||
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET',
|
||||
'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT',
|
||||
'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT',
|
||||
'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET',
|
||||
'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A'
|
||||
];
|
||||
const fullyContained =
|
||||
parentRect.left <= childRect.left &&
|
||||
parentRect.right >= childRect.right &&
|
||||
parentRect.top <= childRect.top &&
|
||||
parentRect.bottom >= childRect.bottom;
|
||||
|
||||
while (element.parentElement) {
|
||||
const parentRect = element.parentElement.getBoundingClientRect();
|
||||
const childRect = element.getBoundingClientRect();
|
||||
const significantOverlap =
|
||||
(childRect.width * childRect.height) /
|
||||
(parentRect.width * parentRect.height) > 0.5;
|
||||
|
||||
if (!containerTags.includes(element.parentElement.tagName)) {
|
||||
break;
|
||||
if (fullyContained && significantOverlap) {
|
||||
element = element.parentElement;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
const generalSelector = getSelectorPath(element);
|
||||
return {
|
||||
generalSelector,
|
||||
};
|
||||
}, coordinates);
|
||||
return selectors || { generalSelector: '' };
|
||||
} else {
|
||||
console.log(`NON UNIQUE: MODE 2`)
|
||||
const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => {
|
||||
function getNonUniqueSelector(element: HTMLElement): string {
|
||||
let selector = element.tagName.toLowerCase();
|
||||
|
||||
if (element.className) {
|
||||
const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls));
|
||||
if (classes.length > 0) {
|
||||
const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':'));
|
||||
if (validClasses.length > 0) {
|
||||
selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
const fullyContained =
|
||||
parentRect.left <= childRect.left &&
|
||||
parentRect.right >= childRect.right &&
|
||||
parentRect.top <= childRect.top &&
|
||||
parentRect.bottom >= childRect.bottom;
|
||||
function getSelectorPath(element: HTMLElement | null): string {
|
||||
const path: string[] = [];
|
||||
let depth = 0;
|
||||
const maxDepth = 2;
|
||||
|
||||
const significantOverlap =
|
||||
(childRect.width * childRect.height) /
|
||||
(parentRect.width * parentRect.height) > 0.5;
|
||||
while (element && element !== document.body && depth < maxDepth) {
|
||||
const selector = getNonUniqueSelector(element);
|
||||
path.unshift(selector);
|
||||
element = element.parentElement;
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (fullyContained && significantOverlap) {
|
||||
element = element.parentElement;
|
||||
} else {
|
||||
break;
|
||||
return path.join(' > ');
|
||||
}
|
||||
}
|
||||
|
||||
const generalSelector = getSelectorPath(element);
|
||||
return {
|
||||
generalSelector,
|
||||
};
|
||||
}, coordinates);
|
||||
const originalEl = document.elementFromPoint(x, y) as HTMLElement;
|
||||
if (!originalEl) return null;
|
||||
|
||||
let element = originalEl;
|
||||
|
||||
const generalSelector = getSelectorPath(element);
|
||||
return {
|
||||
generalSelector,
|
||||
};
|
||||
}, coordinates);
|
||||
return selectors || { generalSelector: '' };
|
||||
}
|
||||
|
||||
return selectors || { generalSelector: '' };
|
||||
} catch (error) {
|
||||
console.error('Error in getNonUniqueSelectors:', error);
|
||||
return { generalSelector: '' };
|
||||
|
||||
29
src/App.tsx
29
src/App.tsx
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import React from "react";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||
import { GlobalInfoProvider } from "./context/globalInfo";
|
||||
import { PageWrapper } from "./pages/PageWrappper";
|
||||
import i18n from "./i18n";
|
||||
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
@@ -20,14 +22,14 @@ const theme = createTheme({
|
||||
},
|
||||
containedPrimary: {
|
||||
// Styles for 'contained' variant with 'primary' color
|
||||
'&:hover': {
|
||||
"&:hover": {
|
||||
backgroundColor: "#ff66d9",
|
||||
},
|
||||
},
|
||||
outlined: {
|
||||
// Apply white background for all 'outlined' variant buttons
|
||||
backgroundColor: "#ffffff",
|
||||
'&:hover': {
|
||||
"&:hover": {
|
||||
backgroundColor: "#f0f0f0", // Optional lighter background on hover
|
||||
},
|
||||
},
|
||||
@@ -36,7 +38,7 @@ const theme = createTheme({
|
||||
MuiLink: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&:hover': {
|
||||
"&:hover": {
|
||||
color: "#ff00c3",
|
||||
},
|
||||
},
|
||||
@@ -63,7 +65,7 @@ const theme = createTheme({
|
||||
standardInfo: {
|
||||
backgroundColor: "#fce1f4",
|
||||
color: "#ff00c3",
|
||||
'& .MuiAlert-icon': {
|
||||
"& .MuiAlert-icon": {
|
||||
color: "#ff00c3",
|
||||
},
|
||||
},
|
||||
@@ -72,7 +74,7 @@ const theme = createTheme({
|
||||
MuiAlertTitle: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiAlert-icon': {
|
||||
"& .MuiAlert-icon": {
|
||||
color: "#ffffff",
|
||||
},
|
||||
},
|
||||
@@ -81,15 +83,16 @@ const theme = createTheme({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalInfoProvider>
|
||||
<Routes>
|
||||
<Route path="/*" element={<PageWrapper />} />
|
||||
</Routes>
|
||||
</GlobalInfoProvider>
|
||||
|
||||
<GlobalInfoProvider>
|
||||
<Routes>
|
||||
<Route path="/*" element={<PageWrapper />} />
|
||||
</Routes>
|
||||
</GlobalInfoProvider>
|
||||
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
74
src/components/atoms/DatePicker.tsx
Normal file
74
src/components/atoms/DatePicker.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface DatePickerProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DatePicker: React.FC<DatePickerProps> = ({ coordinates, selector, onClose }) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||
|
||||
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedDate(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (socket && selectedDate) {
|
||||
socket.emit('input:date', {
|
||||
selector,
|
||||
value: selectedDate
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${coordinates.x}px`,
|
||||
top: `${coordinates.y}px`,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||
padding: '10px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<input
|
||||
type="date"
|
||||
onChange={handleDateChange}
|
||||
value={selectedDate}
|
||||
className="p-2 border rounded"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={!selectedDate}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
selectedDate
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
74
src/components/atoms/DateTimeLocalPicker.tsx
Normal file
74
src/components/atoms/DateTimeLocalPicker.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface DateTimeLocalPickerProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DateTimeLocalPicker: React.FC<DateTimeLocalPickerProps> = ({ coordinates, selector, onClose }) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [selectedDateTime, setSelectedDateTime] = useState<string>('');
|
||||
|
||||
const handleDateTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedDateTime(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (socket && selectedDateTime) {
|
||||
socket.emit('input:datetime-local', {
|
||||
selector,
|
||||
value: selectedDateTime
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${coordinates.x}px`,
|
||||
top: `${coordinates.y}px`,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||
padding: '10px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<input
|
||||
type="datetime-local"
|
||||
onChange={handleDateTimeChange}
|
||||
value={selectedDateTime}
|
||||
className="p-2 border rounded"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={!selectedDateTime}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
selectedDateTime
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTimeLocalPicker;
|
||||
85
src/components/atoms/Dropdown.tsx
Normal file
85
src/components/atoms/Dropdown.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface DropdownProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
options: Array<{
|
||||
value: string;
|
||||
text: string;
|
||||
disabled: boolean;
|
||||
selected: boolean;
|
||||
}>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const Dropdown = ({ coordinates, selector, options, onClose }: DropdownProps) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
if (socket) {
|
||||
socket.emit('input:dropdown', { selector, value });
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: coordinates.x,
|
||||
top: coordinates.y,
|
||||
zIndex: 1000,
|
||||
width: '200px',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid rgb(169, 169, 169)',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||
};
|
||||
|
||||
const scrollContainerStyle: React.CSSProperties = {
|
||||
maxHeight: '180px',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
};
|
||||
|
||||
const getOptionStyle = (option: any, index: number): React.CSSProperties => ({
|
||||
fontSize: '13.333px',
|
||||
lineHeight: '18px',
|
||||
padding: '0 3px',
|
||||
cursor: option.disabled ? 'default' : 'default',
|
||||
backgroundColor: hoveredIndex === index ? '#0078D7' :
|
||||
option.selected ? '#0078D7' :
|
||||
option.disabled ? '#f8f8f8' : 'white',
|
||||
color: (hoveredIndex === index || option.selected) ? 'white' :
|
||||
option.disabled ? '#a0a0a0' : 'black',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={containerStyle}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div style={scrollContainerStyle}>
|
||||
{options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={getOptionStyle(option, index)}
|
||||
onMouseEnter={() => !option.disabled && setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
onClick={() => !option.disabled && handleSelect(option.value)}
|
||||
>
|
||||
{option.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
||||
130
src/components/atoms/TimePicker.tsx
Normal file
130
src/components/atoms/TimePicker.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface TimePickerProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [hoveredHour, setHoveredHour] = useState<number | null>(null);
|
||||
const [hoveredMinute, setHoveredMinute] = useState<number | null>(null);
|
||||
const [selectedHour, setSelectedHour] = useState<number | null>(null);
|
||||
const [selectedMinute, setSelectedMinute] = useState<number | null>(null);
|
||||
|
||||
const handleHourSelect = (hour: number) => {
|
||||
setSelectedHour(hour);
|
||||
// If minute is already selected, complete the selection
|
||||
if (selectedMinute !== null) {
|
||||
const formattedHour = hour.toString().padStart(2, '0');
|
||||
const formattedMinute = selectedMinute.toString().padStart(2, '0');
|
||||
if (socket) {
|
||||
socket.emit('input:time', {
|
||||
selector,
|
||||
value: `${formattedHour}:${formattedMinute}`
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMinuteSelect = (minute: number) => {
|
||||
setSelectedMinute(minute);
|
||||
// If hour is already selected, complete the selection
|
||||
if (selectedHour !== null) {
|
||||
const formattedHour = selectedHour.toString().padStart(2, '0');
|
||||
const formattedMinute = minute.toString().padStart(2, '0');
|
||||
if (socket) {
|
||||
socket.emit('input:time', {
|
||||
selector,
|
||||
value: `${formattedHour}:${formattedMinute}`
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: coordinates.x,
|
||||
top: coordinates.y,
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid rgb(169, 169, 169)',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||
};
|
||||
|
||||
const columnStyle: React.CSSProperties = {
|
||||
width: '60px',
|
||||
maxHeight: '180px',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
borderRight: '1px solid rgb(169, 169, 169)',
|
||||
};
|
||||
|
||||
const getOptionStyle = (value: number, isHour: boolean): React.CSSProperties => {
|
||||
const isHovered = isHour ? hoveredHour === value : hoveredMinute === value;
|
||||
const isSelected = isHour ? selectedHour === value : selectedMinute === value;
|
||||
|
||||
return {
|
||||
fontSize: '13.333px',
|
||||
lineHeight: '18px',
|
||||
padding: '0 3px',
|
||||
cursor: 'default',
|
||||
backgroundColor: isSelected ? '#0078D7' : isHovered ? '#0078D7' : 'white',
|
||||
color: (isSelected || isHovered) ? 'white' : 'black',
|
||||
userSelect: 'none',
|
||||
};
|
||||
};
|
||||
|
||||
const hours = Array.from({ length: 24 }, (_, i) => i);
|
||||
const minutes = Array.from({ length: 60 }, (_, i) => i);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={containerStyle}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{/* Hours column */}
|
||||
<div style={columnStyle}>
|
||||
{hours.map((hour) => (
|
||||
<div
|
||||
key={hour}
|
||||
style={getOptionStyle(hour, true)}
|
||||
onMouseEnter={() => setHoveredHour(hour)}
|
||||
onMouseLeave={() => setHoveredHour(null)}
|
||||
onClick={() => handleHourSelect(hour)}
|
||||
>
|
||||
{hour.toString().padStart(2, '0')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Minutes column */}
|
||||
<div style={{...columnStyle, borderRight: 'none'}}>
|
||||
{minutes.map((minute) => (
|
||||
<div
|
||||
key={minute}
|
||||
style={getOptionStyle(minute, false)}
|
||||
onMouseEnter={() => setHoveredMinute(minute)}
|
||||
onMouseLeave={() => setHoveredMinute(null)}
|
||||
onClick={() => handleMinuteSelect(minute)}
|
||||
>
|
||||
{minute.toString().padStart(2, '0')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimePicker;
|
||||
@@ -3,6 +3,10 @@ import { useSocketStore } from '../../context/socket';
|
||||
import { getMappedCoordinates } from "../../helpers/inputHelpers";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { useActionContext } from '../../context/browserActions';
|
||||
import DatePicker from './DatePicker';
|
||||
import Dropdown from './Dropdown';
|
||||
import TimePicker from './TimePicker';
|
||||
import DateTimeLocalPicker from './DateTimeLocalPicker';
|
||||
|
||||
interface CreateRefCallback {
|
||||
(ref: React.RefObject<HTMLCanvasElement>): void;
|
||||
@@ -31,6 +35,32 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
|
||||
const getTextRef = useRef(getText);
|
||||
const getListRef = useRef(getList);
|
||||
|
||||
const [datePickerInfo, setDatePickerInfo] = React.useState<{
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
} | null>(null);
|
||||
|
||||
const [dropdownInfo, setDropdownInfo] = React.useState<{
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
options: Array<{
|
||||
value: string;
|
||||
text: string;
|
||||
disabled: boolean;
|
||||
selected: boolean;
|
||||
}>;
|
||||
} | null>(null);
|
||||
|
||||
const [timePickerInfo, setTimePickerInfo] = React.useState<{
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
} | null>(null);
|
||||
|
||||
const [dateTimeLocalInfo, setDateTimeLocalInfo] = React.useState<{
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
} | null>(null);
|
||||
|
||||
const notifyLastAction = (action: string) => {
|
||||
if (lastAction !== action) {
|
||||
setLastAction(action);
|
||||
@@ -44,6 +74,42 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
|
||||
getListRef.current = getList;
|
||||
}, [getText, getList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
socket.on('showDatePicker', (info: {coordinates: Coordinates, selector: string}) => {
|
||||
setDatePickerInfo(info);
|
||||
});
|
||||
|
||||
socket.on('showDropdown', (info: {
|
||||
coordinates: Coordinates,
|
||||
selector: string,
|
||||
options: Array<{
|
||||
value: string;
|
||||
text: string;
|
||||
disabled: boolean;
|
||||
selected: boolean;
|
||||
}>;
|
||||
}) => {
|
||||
setDropdownInfo(info);
|
||||
});
|
||||
|
||||
socket.on('showTimePicker', (info: {coordinates: Coordinates, selector: string}) => {
|
||||
setTimePickerInfo(info);
|
||||
});
|
||||
|
||||
socket.on('showDateTimePicker', (info: {coordinates: Coordinates, selector: string}) => {
|
||||
setDateTimeLocalInfo(info);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off('showDatePicker');
|
||||
socket.off('showDropdown');
|
||||
socket.off('showTimePicker');
|
||||
socket.off('showDateTimePicker');
|
||||
};
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
const onMouseEvent = useCallback((event: MouseEvent) => {
|
||||
if (socket && canvasRef.current) {
|
||||
// Get the canvas bounding rectangle
|
||||
@@ -146,6 +212,35 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
|
||||
width={900}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
{datePickerInfo && (
|
||||
<DatePicker
|
||||
coordinates={datePickerInfo.coordinates}
|
||||
selector={datePickerInfo.selector}
|
||||
onClose={() => setDatePickerInfo(null)}
|
||||
/>
|
||||
)}
|
||||
{dropdownInfo && (
|
||||
<Dropdown
|
||||
coordinates={dropdownInfo.coordinates}
|
||||
selector={dropdownInfo.selector}
|
||||
options={dropdownInfo.options}
|
||||
onClose={() => setDropdownInfo(null)}
|
||||
/>
|
||||
)}
|
||||
{timePickerInfo && (
|
||||
<TimePicker
|
||||
coordinates={timePickerInfo.coordinates}
|
||||
selector={timePickerInfo.selector}
|
||||
onClose={() => setTimePickerInfo(null)}
|
||||
/>
|
||||
)}
|
||||
{dateTimeLocalInfo && (
|
||||
<DateTimeLocalPicker
|
||||
coordinates={dateTimeLocalInfo.coordinates}
|
||||
selector={dateTimeLocalInfo.selector}
|
||||
onClose={() => setDateTimeLocalInfo(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import styled from 'styled-components';
|
||||
import { Typography, FormControlLabel, Checkbox, Box } from '@mui/material';
|
||||
import { useActionContext } from '../../context/browserActions';
|
||||
import MaxunLogo from "../../assets/maxunlogo.png";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const CustomBoxContainer = styled.div`
|
||||
position: relative;
|
||||
@@ -44,6 +45,7 @@ const Content = styled.div`
|
||||
`;
|
||||
|
||||
const ActionDescriptionBox = () => {
|
||||
const { t } = useTranslation();
|
||||
const { getText, getScreenshot, getList, captureStage } = useActionContext() as {
|
||||
getText: boolean;
|
||||
getScreenshot: boolean;
|
||||
@@ -52,36 +54,36 @@ const ActionDescriptionBox = () => {
|
||||
};
|
||||
|
||||
const messages = [
|
||||
{ stage: 'initial' as const, text: 'Select the list you want to extract along with the texts inside it' },
|
||||
{ stage: 'pagination' as const, text: 'Select how the robot can capture the rest of the list' },
|
||||
{ stage: 'limit' as const, text: 'Choose the number of items to extract' },
|
||||
{ stage: 'complete' as const, text: 'Capture is complete' },
|
||||
{ stage: 'initial' as const, text: t('action_description.list_stages.initial') },
|
||||
{ stage: 'pagination' as const, text: t('action_description.list_stages.pagination') },
|
||||
{ stage: 'limit' as const, text: t('action_description.list_stages.limit') },
|
||||
{ stage: 'complete' as const, text: t('action_description.list_stages.complete') },
|
||||
];
|
||||
|
||||
const stages = messages.map(({ stage }) => stage); // Create a list of stages
|
||||
const currentStageIndex = stages.indexOf(captureStage); // Get the index of the current stage
|
||||
const stages = messages.map(({ stage }) => stage);
|
||||
const currentStageIndex = stages.indexOf(captureStage);
|
||||
|
||||
const renderActionDescription = () => {
|
||||
if (getText) {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>Capture Text</Typography>
|
||||
<Typography variant="body2" gutterBottom>Hover over the texts you want to extract and click to select them</Typography>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('action_description.text.title')}</Typography>
|
||||
<Typography variant="body2" gutterBottom>{t('action_description.text.description')}</Typography>
|
||||
</>
|
||||
);
|
||||
} else if (getScreenshot) {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>Capture Screenshot</Typography>
|
||||
<Typography variant="body2" gutterBottom>Capture a partial or full page screenshot of the current page.</Typography>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('action_description.screenshot.title')}</Typography>
|
||||
<Typography variant="body2" gutterBottom>{t('action_description.screenshot.description')}</Typography>
|
||||
</>
|
||||
);
|
||||
} else if (getList) {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>Capture List</Typography>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('action_description.list.title')}</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Hover over the list you want to extract. Once selected, you can hover over all texts inside the list you selected. Click to select them.
|
||||
{t('action_description.list.description')}
|
||||
</Typography>
|
||||
<Box>
|
||||
{messages.map(({ stage, text }, index) => (
|
||||
@@ -89,7 +91,7 @@ const ActionDescriptionBox = () => {
|
||||
key={stage}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={index < currentStageIndex} // Check the box if we are past this stage
|
||||
checked={index < currentStageIndex}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
@@ -102,8 +104,8 @@ const ActionDescriptionBox = () => {
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>What data do you want to extract?</Typography>
|
||||
<Typography variant="body2" gutterBottom>A robot is designed to perform one action at a time. You can choose any of the options below.</Typography>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('action_description.default.title')}</Typography>
|
||||
<Typography variant="body2" gutterBottom>{t('action_description.default.description')}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -111,7 +113,7 @@ const ActionDescriptionBox = () => {
|
||||
|
||||
return (
|
||||
<CustomBoxContainer>
|
||||
<Logo src={MaxunLogo} alt="Maxun Logo" />
|
||||
<Logo src={MaxunLogo} alt={t('common.maxun_logo')} />
|
||||
<Triangle />
|
||||
<Content>
|
||||
{renderActionDescription()}
|
||||
@@ -120,4 +122,4 @@ const ActionDescriptionBox = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionDescriptionBox;
|
||||
export default ActionDescriptionBox;
|
||||
@@ -5,8 +5,10 @@ import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { stopRecording } from "../../api/recording";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BrowserRecordingSave = () => {
|
||||
const { t } = useTranslation();
|
||||
const [openModal, setOpenModal] = useState<boolean>(false);
|
||||
const { recordingName, browserId, setBrowserId, notify } = useGlobalInfoStore();
|
||||
const navigate = useNavigate();
|
||||
@@ -14,7 +16,7 @@ const BrowserRecordingSave = () => {
|
||||
const goToMainMenu = async () => {
|
||||
if (browserId) {
|
||||
await stopRecording(browserId);
|
||||
notify('warning', 'Current Recording was terminated');
|
||||
notify('warning', t('browser_recording.notifications.terminated'));
|
||||
setBrowserId(null);
|
||||
}
|
||||
navigate('/');
|
||||
@@ -25,30 +27,29 @@ const BrowserRecordingSave = () => {
|
||||
<Grid item xs={12} md={3} lg={3}>
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
// marginLeft: '10px',
|
||||
color: 'white',
|
||||
position: 'absolute',
|
||||
background: '#ff00c3',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
padding: '7.5px',
|
||||
width: 'calc(100% - 20px)', // Ensure it takes full width but with padding
|
||||
width: 'calc(100% - 20px)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<Button onClick={() => setOpenModal(true)} variant="outlined" style={{ marginLeft: "25px" }} size="small" color="error">
|
||||
Discard
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
|
||||
<Box p={2}>
|
||||
<Typography variant="h6">Are you sure you want to discard the recording?</Typography>
|
||||
<Typography variant="h6">{t('browser_recording.modal.confirm_discard')}</Typography>
|
||||
<Box display="flex" justifyContent="space-between" mt={2}>
|
||||
<Button onClick={goToMainMenu} variant="contained" color="error">
|
||||
Discard
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
<Button onClick={() => setOpenModal(false)} variant="outlined">
|
||||
Cancel
|
||||
{t('right_panel.buttons.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -60,7 +61,7 @@ const BrowserRecordingSave = () => {
|
||||
);
|
||||
}
|
||||
|
||||
export default BrowserRecordingSave
|
||||
export default BrowserRecordingSave;
|
||||
|
||||
const modalStyle = {
|
||||
top: '25%',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RunContent } from "./RunContent";
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
import { getUserById } from "../../api/auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface RunTypeChipProps {
|
||||
runByUserId?: string;
|
||||
@@ -18,10 +19,12 @@ interface RunTypeChipProps {
|
||||
}
|
||||
|
||||
const RunTypeChip: React.FC<RunTypeChipProps> = ({ runByUserId, runByScheduledId, runByAPI }) => {
|
||||
if (runByUserId) return <Chip label="Manual Run" color="primary" variant="outlined" />;
|
||||
if (runByScheduledId) return <Chip label="Scheduled Run" color="primary" variant="outlined" />;
|
||||
if (runByAPI) return <Chip label="API" color="primary" variant="outlined" />;
|
||||
return <Chip label="Unknown Run Type" color="primary" variant="outlined" />;
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (runByUserId) return <Chip label={t('runs_table.run_type_chips.manual_run')} color="primary" variant="outlined" />;
|
||||
if (runByScheduledId) return <Chip label={t('runs_table.run_type_chips.scheduled_run')} color="primary" variant="outlined" />;
|
||||
if (runByAPI) return <Chip label={t('runs_table.run_type_chips.api')} color="primary" variant="outlined" />;
|
||||
return <Chip label={t('runs_table.run_type_chips.unknown_run_type')} color="primary" variant="outlined" />;
|
||||
};
|
||||
|
||||
interface CollapsibleRowProps {
|
||||
@@ -33,6 +36,7 @@ interface CollapsibleRowProps {
|
||||
runningRecordingName: string;
|
||||
}
|
||||
export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName }: CollapsibleRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(isOpen);
|
||||
const [openSettingsModal, setOpenSettingsModal] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<string | null>(null);
|
||||
@@ -99,12 +103,12 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
|
||||
} else {
|
||||
switch (column.id) {
|
||||
case 'runStatus':
|
||||
return (
|
||||
return (
|
||||
<TableCell key={column.id} align={column.align}>
|
||||
{row.status === 'success' && <Chip label="Success" color="success" variant="outlined" />}
|
||||
{row.status === 'running' && <Chip label="Running" color="warning" variant="outlined" />}
|
||||
{row.status === 'scheduled' && <Chip label="Scheduled" variant="outlined" />}
|
||||
{row.status === 'failed' && <Chip label="Failed" color="error" variant="outlined" />}
|
||||
{row.status === 'success' && <Chip label={t('runs_table.run_status_chips.success')} color="success" variant="outlined" />}
|
||||
{row.status === 'running' && <Chip label={t('runs_table.run_status_chips.running')} color="warning" variant="outlined" />}
|
||||
{row.status === 'scheduled' && <Chip label={t('runs_table.run_status_chips.scheduled')} variant="outlined" />}
|
||||
{row.status === 'failed' && <Chip label={t('runs_table.run_status_chips.failed')} color="error" variant="outlined" />}
|
||||
</TableCell>
|
||||
)
|
||||
case 'delete':
|
||||
@@ -133,21 +137,35 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
|
||||
modalStyle={modalStyle}
|
||||
>
|
||||
<>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>Run Settings</Typography>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>
|
||||
{t('runs_table.run_settings_modal.title')}
|
||||
</Typography>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<TextField
|
||||
label="Run ID"
|
||||
label={t('runs_table.run_settings_modal.labels.run_id')}
|
||||
value={row.runId}
|
||||
InputProps={{ readOnly: true }}
|
||||
/>
|
||||
<TextField
|
||||
label={row.runByUserId ? "Run by User" : row.runByScheduleId ? "Run by Schedule ID" : "Run by API"}
|
||||
label={
|
||||
row.runByUserId
|
||||
? t('runs_table.run_settings_modal.labels.run_by_user')
|
||||
: row.runByScheduleId
|
||||
? t('runs_table.run_settings_modal.labels.run_by_schedule')
|
||||
: t('runs_table.run_settings_modal.labels.run_by_api')
|
||||
}
|
||||
value={runByLabel}
|
||||
InputProps={{ readOnly: true }}
|
||||
/>
|
||||
<Box style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<Typography variant="body1">Run Type:</Typography>
|
||||
<RunTypeChip runByUserId={row.runByUserId} runByScheduledId={row.runByScheduleId} runByAPI={row.runByAPI ?? false} />
|
||||
<Typography variant="body1">
|
||||
{t('runs_table.run_settings_modal.labels.run_type')}:
|
||||
</Typography>
|
||||
<RunTypeChip
|
||||
runByUserId={row.runByUserId}
|
||||
runByScheduledId={row.runByScheduleId}
|
||||
runByAPI={row.runByAPI ?? false}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { getStoredRecording } from "../../api/storage";
|
||||
import { apiUrl } from "../../apiConfig.js";
|
||||
import Cookies from 'js-cookie';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface IntegrationProps {
|
||||
isOpen: boolean;
|
||||
@@ -33,6 +34,7 @@ export const IntegrationSettingsModal = ({
|
||||
handleStart,
|
||||
handleClose,
|
||||
}: IntegrationProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [settings, setSettings] = useState<IntegrationSettings>({
|
||||
spreadsheetId: "",
|
||||
spreadsheetName: "",
|
||||
@@ -77,8 +79,9 @@ export const IntegrationSettingsModal = ({
|
||||
);
|
||||
notify(
|
||||
"error",
|
||||
`Error fetching spreadsheet files: ${error.response?.data?.message || error.message
|
||||
}`
|
||||
t('integration_settings.errors.fetch_error', {
|
||||
message: error.response?.data?.message || error.message
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -107,7 +110,7 @@ export const IntegrationSettingsModal = ({
|
||||
},
|
||||
{ withCredentials: true }
|
||||
);
|
||||
notify(`success`, `Google Sheet selected successfully`)
|
||||
notify(`success`, t('integration_settings.notifications.sheet_selected'));
|
||||
console.log("Google Sheet ID updated:", response.data);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
@@ -168,38 +171,28 @@ export const IntegrationSettingsModal = ({
|
||||
|
||||
return (
|
||||
<GenericModal isOpen={isOpen} onClose={handleClose} modalStyle={modalStyle}>
|
||||
<div
|
||||
style={{
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
marginLeft: "65px",
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Typography variant="h6">
|
||||
Integrate with Google Sheet{" "}
|
||||
{/* <Chip label="beta" color="primary" variant="outlined" /> */}
|
||||
{t('integration_settings.title')}
|
||||
</Typography>
|
||||
|
||||
{recording && recording.google_sheet_id ? (
|
||||
<>
|
||||
<Alert severity="info" sx={{ marginTop: '10px', border: '1px solid #ff00c3' }}>
|
||||
<AlertTitle>Google Sheet Integrated Successfully.</AlertTitle>
|
||||
Every time this robot creates a successful run, its captured data
|
||||
is appended to your {recording.google_sheet_name} Google Sheet.
|
||||
You can check the data updates{" "}
|
||||
<a
|
||||
href={`https://docs.google.com/spreadsheets/d/${recording.google_sheet_id}`}
|
||||
<AlertTitle>{t('integration_settings.alerts.success.title')}</AlertTitle>
|
||||
{t('integration_settings.alerts.success.content', { sheetName: recording.google_sheet_name })}
|
||||
<a href={`https://docs.google.com/spreadsheets/d/${recording.google_sheet_id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
rel="noreferrer">
|
||||
{t('integration_settings.alerts.success.here')}
|
||||
</a>.
|
||||
<br />
|
||||
<strong>Note:</strong> The data extracted before integrating with
|
||||
Google Sheets will not be synced in the Google Sheet. Only the
|
||||
data extracted after the integration will be synced.
|
||||
<strong>{t('integration_settings.alerts.success.note')}</strong> {t('integration_settings.alerts.success.sync_limitation')}
|
||||
</Alert>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -207,31 +200,29 @@ export const IntegrationSettingsModal = ({
|
||||
onClick={removeIntegration}
|
||||
style={{ marginTop: "15px" }}
|
||||
>
|
||||
Remove Integration
|
||||
{t('integration_settings.buttons.remove_integration')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!recording?.google_sheet_email ? (
|
||||
<>
|
||||
<p>
|
||||
If you enable this option, every time this robot runs a task
|
||||
successfully, its captured data will be appended to your
|
||||
Google Sheet.
|
||||
</p>
|
||||
<p>{t('integration_settings.descriptions.sync_info')}</p>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={authenticateWithGoogle}
|
||||
>
|
||||
Authenticate with Google
|
||||
{t('integration_settings.buttons.authenticate')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{recording.google_sheet_email && (
|
||||
<Typography sx={{ margin: "20px 0px 30px 0px" }}>
|
||||
Authenticated as: {recording.google_sheet_email}
|
||||
{t('integration_settings.descriptions.authenticated_as', {
|
||||
email: recording.google_sheet_email
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -247,14 +238,14 @@ export const IntegrationSettingsModal = ({
|
||||
color="primary"
|
||||
onClick={fetchSpreadsheetFiles}
|
||||
>
|
||||
Fetch Google Spreadsheets
|
||||
{t('integration_settings.buttons.fetch_sheets')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={removeIntegration}
|
||||
>
|
||||
Remove Integration
|
||||
{t('integration_settings.buttons.remove_integration')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -263,7 +254,7 @@ export const IntegrationSettingsModal = ({
|
||||
<TextField
|
||||
sx={{ marginBottom: "15px" }}
|
||||
select
|
||||
label="Select Google Sheet"
|
||||
label={t('integration_settings.fields.select_sheet')}
|
||||
required
|
||||
value={settings.spreadsheetId}
|
||||
onChange={handleSpreadsheetSelect}
|
||||
@@ -278,13 +269,10 @@ export const IntegrationSettingsModal = ({
|
||||
|
||||
{settings.spreadsheetId && (
|
||||
<Typography sx={{ marginBottom: "10px" }}>
|
||||
Selected Sheet:{" "}
|
||||
{
|
||||
spreadsheets.find(
|
||||
(s) => s.id === settings.spreadsheetId
|
||||
)?.name
|
||||
}{" "}
|
||||
(ID: {settings.spreadsheetId})
|
||||
{t('integration_settings.fields.selected_sheet', {
|
||||
name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name,
|
||||
id: settings.spreadsheetId
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -298,7 +286,7 @@ export const IntegrationSettingsModal = ({
|
||||
style={{ marginTop: "10px" }}
|
||||
disabled={!settings.spreadsheetId || loading}
|
||||
>
|
||||
Submit
|
||||
{t('integration_settings.buttons.submit')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import HelpIcon from '@mui/icons-material/Help';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface InterpretationButtonsProps {
|
||||
enableStepping: (isPaused: boolean) => void;
|
||||
@@ -23,6 +24,7 @@ const interpretationInfo: InterpretationInfo = {
|
||||
};
|
||||
|
||||
export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [info, setInfo] = useState<InterpretationInfo>(interpretationInfo);
|
||||
const [decisionModal, setDecisionModal] = useState<{
|
||||
pair: WhereWhatPair | null,
|
||||
@@ -44,9 +46,9 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
|
||||
const breakpointHitHandler = useCallback(() => {
|
||||
setInfo({ running: false, isPaused: true });
|
||||
notify('warning', 'Please restart the interpretation after updating the recording');
|
||||
notify('warning', t('interpretation_buttons.messages.restart_required'));
|
||||
enableStepping(true);
|
||||
}, [enableStepping]);
|
||||
}, [enableStepping, t]);
|
||||
|
||||
const decisionHandler = useCallback(
|
||||
({ pair, actionType, lastData }: { pair: WhereWhatPair | null, actionType: string, lastData: { selector: string, action: string, tagName: string, innerText: string } }) => {
|
||||
@@ -73,11 +75,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
Do you want to use your previous selection as a condition for performing this action?
|
||||
{t('interpretation_buttons.modal.use_previous')}
|
||||
</Typography>
|
||||
<Box style={{ marginTop: '4px' }}>
|
||||
<Typography>
|
||||
Your previous action was: <b>{decisionModal.action}</b>, on an element with text <b>{decisionModal.innerText}</b>
|
||||
{t('interpretation_buttons.modal.previous_action')} <b>{decisionModal.action}</b>,
|
||||
{t('interpretation_buttons.modal.element_text')} <b>{decisionModal.innerText}</b>
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
@@ -105,9 +108,9 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
const finished = await interpretCurrentRecording();
|
||||
setInfo({ ...info, running: false });
|
||||
if (finished) {
|
||||
notify('info', 'Run finished');
|
||||
notify('info', t('interpretation_buttons.messages.run_finished'));
|
||||
} else {
|
||||
notify('error', 'Run failed to start');
|
||||
notify('error', t('interpretation_buttons.messages.run_failed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -139,9 +142,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
disabled={info.running}
|
||||
sx={{ display: 'grid' }}
|
||||
>
|
||||
{info.running ? <Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CircularProgress size={22} color="inherit" sx={{ marginRight: '10px' }} /> Extracting data...please wait for 10secs to 1min
|
||||
</Box> : 'Get Preview of Output Data'}
|
||||
{info.running ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CircularProgress size={22} color="inherit" sx={{ marginRight: '10px' }} />
|
||||
{t('interpretation_buttons.messages.extracting')}
|
||||
</Box>
|
||||
) : t('interpretation_buttons.buttons.preview')}
|
||||
</Button>
|
||||
<GenericModal
|
||||
onClose={() => { }}
|
||||
@@ -166,8 +172,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
<HelpIcon />
|
||||
{handleDescription()}
|
||||
<div style={{ float: 'right' }}>
|
||||
<Button onClick={() => handleDecision(true)} color='success'>Yes</Button>
|
||||
<Button onClick={() => handleDecision(false)} color='error'>No</Button>
|
||||
<Button onClick={() => handleDecision(true)} color='success'>
|
||||
{t('interpretation_buttons.buttons.yes')}
|
||||
</Button>
|
||||
<Button onClick={() => handleDecision(false)} color='error'>
|
||||
{t('interpretation_buttons.buttons.no')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
|
||||
@@ -17,6 +17,7 @@ import StorageIcon from '@mui/icons-material/Storage';
|
||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||
import { SidePanelHeader } from './SidePanelHeader';
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface InterpretationLogProps {
|
||||
isOpen: boolean;
|
||||
@@ -24,6 +25,7 @@ interface InterpretationLogProps {
|
||||
}
|
||||
|
||||
export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, setIsOpen }) => {
|
||||
const { t } = useTranslation();
|
||||
const [log, setLog] = useState<string>('');
|
||||
const [customValue, setCustomValue] = useState('');
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
@@ -33,7 +35,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
|
||||
const { width } = useBrowserDimensionsStore();
|
||||
const { socket } = useSocketStore();
|
||||
const { currentWorkflowActionsState } = useGlobalInfoStore();
|
||||
const { currentWorkflowActionsState, shouldResetInterpretationLog, notify } = useGlobalInfoStore();
|
||||
|
||||
const toggleDrawer = (newOpen: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
|
||||
if (
|
||||
@@ -63,34 +65,43 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
|
||||
const handleSerializableCallback = useCallback((data: any) => {
|
||||
setLog((prevState) =>
|
||||
prevState + '\n' + '---------- Serializable output data received ----------' + '\n'
|
||||
+ JSON.stringify(data, null, 2) + '\n' + '--------------------------------------------------');
|
||||
prevState + '\n' + t('interpretation_log.data_sections.serializable_received') + '\n'
|
||||
+ JSON.stringify(data, null, 2) + '\n' + t('interpretation_log.data_sections.separator'));
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
setTableData(data);
|
||||
}
|
||||
|
||||
scrollLogToBottom();
|
||||
}, [log, scrollLogToBottom]);
|
||||
}, [log, scrollLogToBottom, t]);
|
||||
|
||||
const handleBinaryCallback = useCallback(({ data, mimetype }: any) => {
|
||||
const base64String = Buffer.from(data).toString('base64');
|
||||
const imageSrc = `data:${mimetype};base64,${base64String}`;
|
||||
|
||||
setLog((prevState) =>
|
||||
prevState + '\n' + '---------- Binary output data received ----------' + '\n'
|
||||
+ `mimetype: ${mimetype}` + '\n' + 'Image is rendered below:' + '\n'
|
||||
+ '------------------------------------------------');
|
||||
prevState + '\n' + t('interpretation_log.data_sections.binary_received') + '\n'
|
||||
+ t('interpretation_log.data_sections.mimetype') + mimetype + '\n'
|
||||
+ t('interpretation_log.data_sections.image_below') + '\n'
|
||||
+ t('interpretation_log.data_sections.separator'));
|
||||
|
||||
setBinaryData(imageSrc);
|
||||
scrollLogToBottom();
|
||||
}, [log, scrollLogToBottom]);
|
||||
}, [log, scrollLogToBottom, t]);
|
||||
|
||||
|
||||
const handleCustomValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomValue(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldResetInterpretationLog) {
|
||||
setLog('');
|
||||
setTableData([]);
|
||||
setBinaryData(null);
|
||||
}
|
||||
}, [shouldResetInterpretationLog]);
|
||||
|
||||
useEffect(() => {
|
||||
socket?.on('log', handleLog);
|
||||
socket?.on('serializableCallback', handleSerializableCallback);
|
||||
@@ -136,7 +147,8 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowUpwardIcon fontSize="inherit" sx={{ marginRight: '10px'}} /> Output Data Preview
|
||||
<ArrowUpwardIcon fontSize="inherit" sx={{ marginRight: '10px'}} />
|
||||
{t('interpretation_log.titles.output_preview')}
|
||||
</Button>
|
||||
<SwipeableDrawer
|
||||
anchor="bottom"
|
||||
@@ -155,9 +167,10 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" gutterBottom style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<StorageIcon style={{ marginRight: '8px' }} /> Output Data Preview
|
||||
</Typography>
|
||||
<Typography variant="h6" gutterBottom style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<StorageIcon style={{ marginRight: '8px' }} />
|
||||
{t('interpretation_log.titles.output_preview')}
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
height: '50vh',
|
||||
@@ -168,8 +181,10 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
{
|
||||
binaryData ? (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<Typography variant="body1" gutterBottom>Screenshot</Typography>
|
||||
<img src={binaryData} alt="Binary Output" style={{ maxWidth: '100%' }} />
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{t('interpretation_log.titles.screenshot')}
|
||||
</Typography>
|
||||
<img src={binaryData} alt={t('interpretation_log.titles.screenshot')} style={{ maxWidth: '100%' }} />
|
||||
</div>
|
||||
) : tableData.length > 0 ? (
|
||||
<>
|
||||
@@ -193,7 +208,9 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<span style={{ marginLeft: '15px', marginTop: '10px', fontSize: '12px' }}>Additional rows of data will be extracted once you finish recording. </span>
|
||||
<span style={{ marginLeft: '15px', marginTop: '10px', fontSize: '12px' }}>
|
||||
{t('interpretation_log.messages.additional_rows')}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<Grid container justifyContent="center" alignItems="center" style={{ height: '100%' }}>
|
||||
@@ -201,13 +218,13 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
{hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction ? (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom align="left">
|
||||
You've successfully trained the robot to perform actions! Click on the button below to get a preview of the data your robot will extract.
|
||||
{t('interpretation_log.messages.successful_training')}
|
||||
</Typography>
|
||||
<SidePanelHeader />
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="h6" gutterBottom align="left">
|
||||
It looks like you have not selected anything for extraction yet. Once you do, the robot will show a preview of your selections here.
|
||||
{t('interpretation_log.messages.no_selection')}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
@@ -219,4 +236,4 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import styled from "styled-components";
|
||||
import { stopRecording } from "../../api/recording";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { IconButton, Menu, MenuItem, Typography, Chip, Button, Modal, Tabs, Tab, Box, Snackbar } from "@mui/material";
|
||||
import { AccountCircle, Logout, Clear, YouTube, X, Update, Close } from "@mui/icons-material";
|
||||
import { AccountCircle, Logout, Clear, YouTube, X, Update, Close, Language } from "@mui/icons-material";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AuthContext } from '../../context/auth';
|
||||
import { SaveRecording } from '../molecules/SaveRecording';
|
||||
@@ -13,18 +14,26 @@ import { apiUrl } from '../../apiConfig';
|
||||
import MaxunLogo from "../../assets/maxunlogo.png";
|
||||
import packageJson from "../../../package.json"
|
||||
|
||||
|
||||
interface NavBarProps {
|
||||
recordingName: string;
|
||||
isRecording: boolean;
|
||||
}
|
||||
|
||||
export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) => {
|
||||
const { notify, browserId, setBrowserId, recordingUrl } = useGlobalInfoStore();
|
||||
export const NavBar: React.FC<NavBarProps> = ({
|
||||
recordingName,
|
||||
isRecording,
|
||||
}) => {
|
||||
const { notify, browserId, setBrowserId } = useGlobalInfoStore();
|
||||
const { state, dispatch } = useContext(AuthContext);
|
||||
const { user } = state;
|
||||
const navigate = useNavigate();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const [langAnchorEl, setLangAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const currentVersion = packageJson.version;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -40,7 +49,7 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
return version;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch latest version:", error);
|
||||
return null; // Handle errors gracefully
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,33 +71,43 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleLangMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setLangAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setLangAnchorEl(null);
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
window.localStorage.removeItem('user');
|
||||
dispatch({ type: "LOGOUT" });
|
||||
window.localStorage.removeItem("user");
|
||||
const { data } = await axios.get(`${apiUrl}/auth/logout`);
|
||||
notify('success', data.message);
|
||||
navigate('/login');
|
||||
notify("success", data.message);
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
const goToMainMenu = async () => {
|
||||
if (browserId) {
|
||||
await stopRecording(browserId);
|
||||
notify('warning', 'Current Recording was terminated');
|
||||
notify("warning", t('browser_recording.notifications.terminated'));
|
||||
setBrowserId(null);
|
||||
}
|
||||
navigate('/');
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const changeLanguage = (lang: string) => {
|
||||
i18n.changeLanguage(lang);
|
||||
localStorage.setItem("language", lang);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkForUpdates = async () => {
|
||||
const latestVersion = await fetchLatestVersion();
|
||||
setLatestVersion(latestVersion); // Set the latest version state
|
||||
setLatestVersion(latestVersion);
|
||||
if (latestVersion && latestVersion !== currentVersion) {
|
||||
setIsUpdateAvailable(true); // Show a notification or highlight the "Upgrade" button
|
||||
setIsUpdateAvailable(true);
|
||||
}
|
||||
};
|
||||
checkForUpdates();
|
||||
@@ -101,7 +120,7 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
open={isUpdateAvailable}
|
||||
onClose={() => setIsUpdateAvailable(false)}
|
||||
message={
|
||||
`New version ${latestVersion} available! Click "Upgrade" to update.`
|
||||
`${t('navbar.upgrade.modal.new_version_available', { version: latestVersion })} ${t('navbar.upgrade.modal.view_updates')}`
|
||||
}
|
||||
action={
|
||||
<>
|
||||
@@ -118,7 +137,7 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
{t('navbar.upgrade.button')}
|
||||
</Button>
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -138,7 +157,6 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
)}
|
||||
<NavBarWrapper>
|
||||
<div style={{
|
||||
@@ -146,7 +164,7 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
justifyContent: 'flex-start',
|
||||
}}>
|
||||
<img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} />
|
||||
<div style={{ padding: '11px' }}><ProjectName>Maxun</ProjectName></div>
|
||||
<div style={{ padding: '11px' }}><ProjectName>{t('navbar.project_name')}</ProjectName></div>
|
||||
<Chip
|
||||
label={`${currentVersion}`}
|
||||
color="primary"
|
||||
@@ -165,7 +183,7 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
border: "#00000099 1px solid",
|
||||
'&:hover': { color: '#ff00c3', border: '#ff00c3 1px solid' }
|
||||
}}>
|
||||
<Update sx={{ marginRight: '5px' }} /> Upgrade Maxun
|
||||
<Update sx={{ marginRight: '5px' }} /> {t('navbar.upgrade.button')} Maxun
|
||||
</Button>
|
||||
<Modal open={open} onClose={handleUpdateClose}>
|
||||
<Box
|
||||
@@ -185,14 +203,14 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
<Typography>Checking for updates...</Typography>
|
||||
) : currentVersion === latestVersion ? (
|
||||
<Typography variant="h6" textAlign="center">
|
||||
🎉 You're up to date!
|
||||
{t('navbar.upgrade.modal.up_to_date')}
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="body1" textAlign="left" sx={{ marginLeft: '30px' }}>
|
||||
A new version is available: {latestVersion}. Upgrade to the latest version for bug fixes, enhancements and new features!
|
||||
{t('navbar.upgrade.modal.new_version_available', { version: latestVersion })}
|
||||
<br />
|
||||
View all the new updates
|
||||
{t('navbar.upgrade.modal.view_updates')}
|
||||
<a href="https://github.com/getmaxun/maxun/releases/" target="_blank" style={{ textDecoration: 'none' }}>{' '}here.</a>
|
||||
</Typography>
|
||||
<Tabs
|
||||
@@ -201,13 +219,18 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
sx={{ marginTop: 2, marginBottom: 2 }}
|
||||
centered
|
||||
>
|
||||
<Tab label="Manual Setup Upgrade" />
|
||||
<Tab label="Docker Compose Setup Upgrade" />
|
||||
<Tab label={t('navbar.upgrade.modal.tabs.manual_setup')} />
|
||||
<Tab label={t('navbar.upgrade.modal.tabs.docker_setup')} />
|
||||
</Tabs>
|
||||
{tab === 0 && (
|
||||
<Box sx={{ marginLeft: '30px', background: '#cfd0d1', padding: 1, borderRadius: 3 }}>
|
||||
<code style={{ color: 'black' }}>
|
||||
<p>Run the commands below</p>
|
||||
# cd to project directory (eg: maxun)
|
||||
<br />
|
||||
cd maxun
|
||||
<br />
|
||||
<br />
|
||||
# pull latest changes
|
||||
<br />
|
||||
git pull origin master
|
||||
@@ -228,6 +251,16 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
<Box sx={{ marginLeft: '30px', background: '#cfd0d1', padding: 1, borderRadius: 3 }}>
|
||||
<code style={{ color: 'black' }}>
|
||||
<p>Run the commands below</p>
|
||||
# cd to project directory (eg: maxun)
|
||||
<br />
|
||||
cd maxun
|
||||
<br />
|
||||
<br />
|
||||
# stop the working containers
|
||||
<br />
|
||||
docker-compose down
|
||||
<br />
|
||||
<br />
|
||||
# pull latest docker images
|
||||
<br />
|
||||
docker-compose pull
|
||||
@@ -270,7 +303,7 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
PaperProps={{ sx: { width: '180px' } }}
|
||||
>
|
||||
<MenuItem onClick={() => { handleMenuClose(); logout(); }}>
|
||||
<Logout sx={{ marginRight: '5px' }} /> Logout
|
||||
<Logout sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.logout')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://discord.gg/5GbPjBUkws', '_blank');
|
||||
@@ -287,6 +320,63 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
}}>
|
||||
<X sx={{ marginRight: '5px' }} /> Twiiter (X)
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLangMenuOpen}>
|
||||
<Language sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.language')}
|
||||
</MenuItem>
|
||||
<Menu
|
||||
anchorEl={langAnchorEl}
|
||||
open={Boolean(langAnchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("en");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
English
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("es");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Español
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("ja");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
日本語
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("zh");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
中文
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("de");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Deutsch
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Menu>
|
||||
</>
|
||||
) : (
|
||||
@@ -300,14 +390,80 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
'&:hover': { color: 'white', backgroundColor: 'red' }
|
||||
}}>
|
||||
<Clear sx={{ marginRight: '5px' }} />
|
||||
Discard
|
||||
{t('navbar.recording.discard')}
|
||||
</IconButton>
|
||||
<SaveRecording fileName={recordingName} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : ""
|
||||
}
|
||||
) : (
|
||||
<><IconButton
|
||||
onClick={handleLangMenuOpen}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: "5px",
|
||||
padding: "8px",
|
||||
marginRight: "10px",
|
||||
}}
|
||||
>
|
||||
<Language sx={{ marginRight: '5px' }} /><Typography variant="body1">{t("Language")}</Typography>
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={langAnchorEl}
|
||||
open={Boolean(langAnchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("en");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
English
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("es");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Español
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("ja");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
日本語
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("zh");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
中文
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("de");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Deutsch
|
||||
</MenuItem>
|
||||
</Menu></>
|
||||
)}
|
||||
</NavBarWrapper>
|
||||
</>
|
||||
);
|
||||
@@ -316,7 +472,7 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
||||
const NavBarWrapper = styled.div`
|
||||
grid-area: navbar;
|
||||
background-color: white;
|
||||
padding:5px;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
@@ -19,6 +20,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { stopRecording } from "../../api/recording";
|
||||
import { GenericModal } from '../atoms/GenericModal';
|
||||
|
||||
|
||||
/** TODO:
|
||||
* 1. allow editing existing robot after persisting browser steps
|
||||
*/
|
||||
@@ -31,30 +33,9 @@ interface Column {
|
||||
format?: (value: string) => string;
|
||||
}
|
||||
|
||||
const columns: readonly Column[] = [
|
||||
{ id: 'interpret', label: 'Run', minWidth: 80 },
|
||||
{ id: 'name', label: 'Name', minWidth: 80 },
|
||||
{
|
||||
id: 'schedule',
|
||||
label: 'Schedule',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'integrate',
|
||||
label: 'Integrate',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'options',
|
||||
label: 'Options',
|
||||
minWidth: 80,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
interface Data {
|
||||
id: string;
|
||||
@@ -76,12 +57,38 @@ interface RecordingsTableProps {
|
||||
}
|
||||
|
||||
export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => {
|
||||
const {t} = useTranslation();
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||
const [rows, setRows] = React.useState<Data[]>([]);
|
||||
const [isModalOpen, setModalOpen] = React.useState(false);
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
|
||||
const columns: readonly Column[] = [
|
||||
{ id: 'interpret', label: t('recordingtable.run'), minWidth: 80 },
|
||||
{ id: 'name', label: t('recordingtable.name'), minWidth: 80 },
|
||||
{
|
||||
id: 'schedule',
|
||||
label: t('recordingtable.schedule'),
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'integrate',
|
||||
label: t('recordingtable.integrate'),
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: t('recordingtable.settings'),
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'options',
|
||||
label: t('recordingtable.options'),
|
||||
minWidth: 80,
|
||||
},
|
||||
];
|
||||
|
||||
const { notify, setRecordings, browserId, setBrowserId, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -151,16 +158,17 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
row.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6" gutterBottom>
|
||||
My Robots
|
||||
{t('recordingtable.heading')}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Search robots..."
|
||||
placeholder={t('recordingtable.search')}
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
InputProps={{
|
||||
@@ -187,7 +195,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
'&:hover': { color: 'white', backgroundColor: '#ff00c3' }
|
||||
}}
|
||||
>
|
||||
<Add sx={{ marginRight: '5px' }} /> Create Robot
|
||||
<Add sx={{ marginRight: '5px' }} /> {t('recordingtable.new')}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -253,14 +261,14 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
|
||||
checkRunsForRecording(row.id).then((result: boolean) => {
|
||||
if (result) {
|
||||
notify('warning', 'Cannot delete robot as it has associated runs');
|
||||
notify('warning', t('recordingtable.notifications.delete_warning'));
|
||||
}
|
||||
})
|
||||
|
||||
deleteRecordingFromStorage(row.id).then((result: boolean) => {
|
||||
if (result) {
|
||||
setRows([]);
|
||||
notify('success', 'Robot deleted successfully');
|
||||
notify('success', t('recordingtable.notifications.delete_success'));
|
||||
fetchRecordings();
|
||||
}
|
||||
})
|
||||
@@ -297,9 +305,9 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
/>
|
||||
<GenericModal isOpen={isModalOpen} onClose={() => setModalOpen(false)} modalStyle={modalStyle}>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Typography variant="h6" gutterBottom>Enter URL To Extract Data</Typography>
|
||||
<Typography variant="h6" gutterBottom>{t('recordingtable.modal.title')}</Typography>
|
||||
<TextField
|
||||
label="URL"
|
||||
label={t('recordingtable.modal.label')}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={recordingUrl}
|
||||
@@ -312,7 +320,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
onClick={startRecording}
|
||||
disabled={!recordingUrl}
|
||||
>
|
||||
Start Training Robot
|
||||
{t('recordingtable.modal.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</GenericModal>
|
||||
@@ -397,6 +405,8 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -415,20 +425,23 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
|
||||
<ListItemIcon>
|
||||
<Edit fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { handleDuplicate(); handleClose(); }}>
|
||||
<ListItemIcon>
|
||||
<ContentCopy fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Duplicate</ListItemText>
|
||||
<ListItemText>{t('recordingtable.edit')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={() => { handleDelete(); handleClose(); }}>
|
||||
<ListItemIcon>
|
||||
<DeleteForever fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Delete</ListItemText>
|
||||
<ListItemText>{t('recordingtable.delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={() => { handleDuplicate(); handleClose(); }}>
|
||||
<ListItemIcon>
|
||||
<ContentCopy fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('recordingtable.duplicate')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { duplicateRecording, getStoredRecording } from '../../api/storage';
|
||||
import { WhereWhatPair } from 'maxun-core';
|
||||
import { getUserById } from "../../api/auth";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface RobotMeta {
|
||||
name: string;
|
||||
@@ -54,6 +55,7 @@ interface RobotSettingsProps {
|
||||
}
|
||||
|
||||
export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const [targetUrl, setTargetUrl] = useState<string | undefined>('');
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
@@ -65,7 +67,6 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update the targetUrl when the robot data is loaded
|
||||
if (robot) {
|
||||
const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1];
|
||||
const url = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
|
||||
@@ -78,22 +79,17 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
|
||||
const robot = await getStoredRecording(recordingId);
|
||||
setRobot(robot);
|
||||
} else {
|
||||
notify('error', 'Could not find robot details. Please try again.');
|
||||
notify('error', t('robot_duplication.notifications.robot_not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
// const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1];
|
||||
|
||||
// // Find the `goto` action in `what` and retrieve its arguments
|
||||
// const targetUrl = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
|
||||
|
||||
const handleTargetUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTargetUrl(e.target.value);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!robot || !targetUrl) {
|
||||
notify('error', 'Target URL is required.');
|
||||
notify('error', t('robot_duplication.notifications.url_required'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,18 +99,18 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
|
||||
const success = await duplicateRecording(robot.recording_meta.id, targetUrl);
|
||||
|
||||
if (success) {
|
||||
notify('success', 'Robot duplicated successfully.');
|
||||
handleStart(robot); // Inform parent about the updated robot
|
||||
notify('success', t('robot_duplication.notifications.duplicate_success'));
|
||||
handleStart(robot);
|
||||
handleClose();
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
notify('error', 'Failed to update the Target URL. Please try again.');
|
||||
notify('error', t('robot_duplication.notifications.duplicate_error'));
|
||||
}
|
||||
} catch (error) {
|
||||
notify('error', 'An error occurred while updating the Target URL.');
|
||||
notify('error', t('robot_duplication.notifications.unknown_error'));
|
||||
console.error('Error updating Target URL:', error);
|
||||
}
|
||||
};
|
||||
@@ -126,34 +122,40 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
|
||||
modalStyle={modalStyle}
|
||||
>
|
||||
<>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>Duplicate Robot</Typography>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>
|
||||
{t('robot_duplication.title')}
|
||||
</Typography>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{
|
||||
robot && (
|
||||
<>
|
||||
<span>Robot duplication is useful to extract data from pages with the same structure.</span>
|
||||
<span>
|
||||
{t('robot_duplication.descriptions.purpose')}
|
||||
</span>
|
||||
<br />
|
||||
<span dangerouslySetInnerHTML={{
|
||||
__html: t('robot_duplication.descriptions.example', {
|
||||
url1: '<code>producthunt.com/topics/api</code>',
|
||||
url2: '<code>producthunt.com/topics/database</code>'
|
||||
})
|
||||
}}/>
|
||||
<br />
|
||||
<span>
|
||||
Example: If you've created a robot for <code>producthunt.com/topics/api</code>, you can duplicate it to scrape similar pages
|
||||
like <code>producthunt.com/topics/database</code> without training a robot from scratch.
|
||||
</span>
|
||||
<br />
|
||||
<span>
|
||||
<b>⚠️ Ensure the new page has the same structure as the original page.</b>
|
||||
<b>{t('robot_duplication.descriptions.warning')}</b>
|
||||
</span>
|
||||
<TextField
|
||||
label="Robot Target URL"
|
||||
key="Robot Target URL"
|
||||
label={t('robot_duplication.fields.target_url')}
|
||||
key={t('robot_duplication.fields.target_url')}
|
||||
value={targetUrl}
|
||||
onChange={handleTargetUrlChange}
|
||||
style={{ marginBottom: '20px', marginTop: '30px' }}
|
||||
/>
|
||||
<Box mt={2} display="flex" justifyContent="flex-end" onClick={handleSave}>
|
||||
<Button variant="contained" color="primary">
|
||||
Duplicate Robot
|
||||
{t('robot_duplication.buttons.duplicate')}
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||
Cancel
|
||||
{t('robot_duplication.buttons.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { TextField, Typography, Box, Button } from "@mui/material";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
@@ -54,10 +55,10 @@ interface RobotSettingsProps {
|
||||
handleStart: (settings: RobotSettings) => void;
|
||||
handleClose: () => void;
|
||||
initialSettings?: RobotSettings | null;
|
||||
|
||||
}
|
||||
|
||||
export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
|
||||
@@ -72,7 +73,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
const robot = await getStoredRecording(recordingId);
|
||||
setRobot(robot);
|
||||
} else {
|
||||
notify('error', 'Could not find robot details. Please try again.');
|
||||
notify('error', t('robot_edit.notifications.update_failed'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +103,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!robot) return;
|
||||
|
||||
@@ -114,7 +116,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
const success = await updateRecording(robot.recording_meta.id, payload);
|
||||
|
||||
if (success) {
|
||||
notify('success', 'Robot updated successfully.');
|
||||
notify('success', t('robot_edit.notifications.update_success'));
|
||||
handleStart(robot); // Inform parent about the updated robot
|
||||
handleClose();
|
||||
|
||||
@@ -122,10 +124,10 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
notify('error', 'Failed to update the robot. Please try again.');
|
||||
notify('error', t('robot_edit.notifications.update_failed'));
|
||||
}
|
||||
} catch (error) {
|
||||
notify('error', 'An error occurred while updating the robot.');
|
||||
notify('error', t('robot_edit.notifications.update_error'));
|
||||
console.error('Error updating robot:', error);
|
||||
}
|
||||
};
|
||||
@@ -137,13 +139,15 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
modalStyle={modalStyle}
|
||||
>
|
||||
<>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>Edit Robot</Typography>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>
|
||||
{t('robot_edit.title')}
|
||||
</Typography>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{
|
||||
robot && (
|
||||
<>
|
||||
<TextField
|
||||
label="Change Robot Name"
|
||||
label={t('robot_edit.change_name')}
|
||||
key="Change Robot Name"
|
||||
type='text'
|
||||
value={robot.recording_meta.name}
|
||||
@@ -152,7 +156,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
/>
|
||||
{robot.recording.workflow?.[0]?.what?.[0]?.args?.[0]?.limit !== undefined && (
|
||||
<TextField
|
||||
label="Robot Limit"
|
||||
label={t('robot_edit.robot_limit')}
|
||||
type="number"
|
||||
value={robot.recording.workflow[0].what[0].args[0].limit || ''}
|
||||
onChange={(e) =>{
|
||||
@@ -168,10 +172,15 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
|
||||
<Box mt={2} display="flex" justifyContent="flex-end" onClick={handleSave}>
|
||||
<Button variant="contained" color="primary">
|
||||
Save Changes
|
||||
{t('robot_edit.save')}
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||
Cancel
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
style={{ marginLeft: '10px' }}
|
||||
>
|
||||
{t('robot_edit.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { TextField, Typography, Box } from "@mui/material";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
@@ -50,10 +51,10 @@ interface RobotSettingsProps {
|
||||
handleStart: (settings: RobotSettings) => void;
|
||||
handleClose: () => void;
|
||||
initialSettings?: RobotSettings | null;
|
||||
|
||||
}
|
||||
|
||||
export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const [userEmail, setUserEmail] = useState<string | null>(null);
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
@@ -69,7 +70,7 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
const robot = await getStoredRecording(recordingId);
|
||||
setRobot(robot);
|
||||
} else {
|
||||
notify('error', 'Could not find robot details. Please try again.');
|
||||
notify('error', t('robot_settings.errors.robot_not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +98,15 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
modalStyle={modalStyle}
|
||||
>
|
||||
<>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>Robot Settings</Typography>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>
|
||||
{t('robot_settings.title')}
|
||||
</Typography>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{
|
||||
robot && (
|
||||
<>
|
||||
<TextField
|
||||
label="Robot Target URL"
|
||||
label={t('robot_settings.target_url')}
|
||||
key="Robot Target URL"
|
||||
value={targetUrl}
|
||||
InputProps={{
|
||||
@@ -112,7 +115,7 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
style={{ marginBottom: '20px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Robot ID"
|
||||
label={t('robot_settings.robot_id')}
|
||||
key="Robot ID"
|
||||
value={robot.recording_meta.id}
|
||||
InputProps={{
|
||||
@@ -122,17 +125,17 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
/>
|
||||
{robot.recording.workflow?.[0]?.what?.[0]?.args?.[0]?.limit !== undefined && (
|
||||
<TextField
|
||||
label="Robot Limit"
|
||||
label={t('robot_settings.robot_limit')}
|
||||
type="number"
|
||||
value={robot.recording.workflow[0].what[0].args[0].limit || ''}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
readOnly: true,
|
||||
}}
|
||||
style={{ marginBottom: '20px' }}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
label="Created By User"
|
||||
label={t('robot_settings.created_by_user')}
|
||||
key="Created By User"
|
||||
value={userEmail ? userEmail : ''}
|
||||
InputProps={{
|
||||
@@ -141,7 +144,7 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
style={{ marginBottom: '20px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Robot Created At"
|
||||
label={t('robot_settings.created_at')}
|
||||
key="Robot Created At"
|
||||
value={robot.recording_meta.createdAt}
|
||||
InputProps={{
|
||||
@@ -156,4 +159,4 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
</>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import 'highlight.js/styles/github.css';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface RunContentProps {
|
||||
row: Data,
|
||||
@@ -23,6 +24,7 @@ interface RunContentProps {
|
||||
}
|
||||
|
||||
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTab] = React.useState<string>('log');
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const [columns, setColumns] = useState<string[]>([]);
|
||||
@@ -76,8 +78,8 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
<TabContext value={tab}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs">
|
||||
<Tab label="Output Data" value='output' />
|
||||
<Tab label="Log" value='log' />
|
||||
<Tab label={t('run_content.tabs.output_data')} value='output' />
|
||||
<Tab label={t('run_content.tabs.log')} value='log' />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<TabPanel value='log'>
|
||||
@@ -102,32 +104,32 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
color="error"
|
||||
onClick={abortRunHandler}
|
||||
>
|
||||
Stop
|
||||
{t('run_content.buttons.stop')}
|
||||
</Button> : null}
|
||||
</TabPanel>
|
||||
<TabPanel value='output' sx={{ width: '700px' }}>
|
||||
{!row || !row.serializableOutput || !row.binaryOutput
|
||||
|| (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0)
|
||||
? <Typography>The output is empty.</Typography> : null}
|
||||
? <Typography>{t('run_content.empty_output')}</Typography> : null}
|
||||
|
||||
{row.serializableOutput &&
|
||||
Object.keys(row.serializableOutput).length !== 0 &&
|
||||
<div>
|
||||
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ArticleIcon sx={{ marginRight: '15px' }} />
|
||||
Captured Data
|
||||
{t('run_content.captured_data.title')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2 }}>
|
||||
<Typography>
|
||||
<a style={{ textDecoration: 'none' }} href={`data:application/json;utf8,${JSON.stringify(row.serializableOutput, null, 2)}`}
|
||||
download="data.json">
|
||||
Download as JSON
|
||||
{t('run_content.captured_data.download_json')}
|
||||
</a>
|
||||
</Typography>
|
||||
<Typography
|
||||
onClick={downloadCSV}
|
||||
>
|
||||
<a style={{ textDecoration: 'none', cursor: 'pointer' }}>Download as CSV</a>
|
||||
<a style={{ textDecoration: 'none', cursor: 'pointer' }}>{t('run_content.captured_data.download_csv')}</a>
|
||||
</Typography>
|
||||
</Box>
|
||||
{tableData.length > 0 ? (
|
||||
@@ -171,7 +173,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
<div>
|
||||
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ImageIcon sx={{ marginRight: '15px' }} />
|
||||
Captured Screenshot
|
||||
{t('run_content.captured_screenshot.title')}
|
||||
</Typography>
|
||||
{Object.keys(row.binaryOutput).map((key) => {
|
||||
try {
|
||||
@@ -181,7 +183,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
width: 'max-content',
|
||||
}}>
|
||||
<Typography sx={{ margin: '20px 0px' }}>
|
||||
<a href={imageUrl} download={key} style={{ textDecoration: 'none' }}>Download Screenshot</a>
|
||||
<a href={imageUrl} download={key} style={{ textDecoration: 'none' }}>{t('run_content.captured_screenshot.download')}</a>
|
||||
</Typography>
|
||||
<img src={imageUrl} alt={key} height='auto' width='700px' />
|
||||
</Box>
|
||||
@@ -189,7 +191,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return <Typography key={`number-of-binary-output-${key}`}>
|
||||
{key}: The image failed to render
|
||||
{key}: {t('run_content.captured_screenshot.render_failed')}
|
||||
</Typography>
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
@@ -7,14 +9,24 @@ import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import { useEffect, useState } from "react";
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { getStoredRuns } from "../../api/storage";
|
||||
import { RunSettings } from "./RunSettings";
|
||||
import { CollapsibleRow } from "./ColapsibleRow";
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
// Export columns before the component
|
||||
export const columns: readonly Column[] = [
|
||||
{ id: 'runStatus', label: 'Status', minWidth: 80 },
|
||||
{ id: 'name', label: 'Name', minWidth: 80 },
|
||||
{ id: 'startedAt', label: 'Started At', minWidth: 80 },
|
||||
{ id: 'finishedAt', label: 'Finished At', minWidth: 80 },
|
||||
{ id: 'settings', label: 'Settings', minWidth: 80 },
|
||||
{ id: 'delete', label: 'Delete', minWidth: 80 },
|
||||
];
|
||||
|
||||
interface Column {
|
||||
id: 'runStatus' | 'name' | 'startedAt' | 'finishedAt' | 'delete' | 'settings';
|
||||
@@ -24,15 +36,6 @@ interface Column {
|
||||
format?: (value: string) => string;
|
||||
}
|
||||
|
||||
export const columns: readonly Column[] = [
|
||||
{ id: 'runStatus', label: 'Status', minWidth: 80 },
|
||||
{ id: 'name', label: 'Robot Name', minWidth: 80 },
|
||||
{ id: 'startedAt', label: 'Started at', minWidth: 80 },
|
||||
{ id: 'finishedAt', label: 'Finished at', minWidth: 80 },
|
||||
{ id: 'settings', label: 'Settings', minWidth: 80 },
|
||||
{ id: 'delete', label: 'Delete', minWidth: 80 },
|
||||
];
|
||||
|
||||
export interface Data {
|
||||
id: number;
|
||||
status: string;
|
||||
@@ -58,15 +61,25 @@ interface RunsTableProps {
|
||||
runningRecordingName: string;
|
||||
}
|
||||
|
||||
export const RunsTable = (
|
||||
{ currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsTableProps) => {
|
||||
export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
currentInterpretationLog,
|
||||
abortRunHandler,
|
||||
runId,
|
||||
runningRecordingName
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Update column labels using translation if needed
|
||||
const translatedColumns = columns.map(column => ({
|
||||
...column,
|
||||
label: t(`runstable.${column.id}`, column.label)
|
||||
}));
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [rows, setRows] = useState<Data[]>([]);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
|
||||
const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore();
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
@@ -86,16 +99,13 @@ export const RunsTable = (
|
||||
const fetchRuns = async () => {
|
||||
const runs = await getStoredRuns();
|
||||
if (runs) {
|
||||
const parsedRows: Data[] = [];
|
||||
runs.map((run: any, index) => {
|
||||
parsedRows.push({
|
||||
id: index,
|
||||
...run,
|
||||
});
|
||||
});
|
||||
const parsedRows: Data[] = runs.map((run: any, index: number) => ({
|
||||
id: index,
|
||||
...run,
|
||||
}));
|
||||
setRows(parsedRows);
|
||||
} else {
|
||||
notify('error', 'No runs found. Please try again.')
|
||||
notify('error', t('runstable.notifications.no_runs'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,15 +114,14 @@ export const RunsTable = (
|
||||
fetchRuns();
|
||||
setRerenderRuns(false);
|
||||
}
|
||||
}, [rerenderRuns]);
|
||||
}, [rerenderRuns, rows.length, setRerenderRuns]);
|
||||
|
||||
const handleDelete = () => {
|
||||
setRows([]);
|
||||
notify('success', 'Run deleted successfully');
|
||||
notify('success', t('runstable.notifications.delete_success'));
|
||||
fetchRuns();
|
||||
};
|
||||
|
||||
|
||||
// Filter rows based on search term
|
||||
const filteredRows = rows.filter((row) =>
|
||||
row.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
@@ -120,7 +129,6 @@ export const RunsTable = (
|
||||
|
||||
// Group filtered rows by robot meta id
|
||||
const groupedRows = filteredRows.reduce((acc, row) => {
|
||||
|
||||
if (!acc[row.robotMetaId]) {
|
||||
acc[row.robotMetaId] = [];
|
||||
}
|
||||
@@ -132,11 +140,11 @@ export const RunsTable = (
|
||||
<React.Fragment>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
All Runs
|
||||
{t('runstable.runs', 'Runs')}
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Search runs..."
|
||||
placeholder={t('runstable.search', 'Search runs...')}
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
InputProps={{
|
||||
@@ -149,16 +157,14 @@ export const RunsTable = (
|
||||
{Object.entries(groupedRows).map(([id, data]) => (
|
||||
<Accordion key={id}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
|
||||
<Typography variant="h6">{data[data.length - 1].name}</Typography>
|
||||
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Table stickyHeader aria-label="sticky table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
{columns.map((column) => (
|
||||
{translatedColumns.map((column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
align={column.align}
|
||||
@@ -200,4 +206,4 @@ export const RunsTable = (
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -9,13 +9,14 @@ import { TextField, Typography } from "@mui/material";
|
||||
import { WarningText } from "../atoms/texts";
|
||||
import NotificationImportantIcon from "@mui/icons-material/NotificationImportant";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SaveRecordingProps {
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [openModal, setOpenModal] = useState<boolean>(false);
|
||||
const [needConfirm, setNeedConfirm] = useState<boolean>(false);
|
||||
const [recordingName, setRecordingName] = useState<string>(fileName);
|
||||
@@ -46,7 +47,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
};
|
||||
|
||||
const exitRecording = useCallback(async () => {
|
||||
notify('success', 'Robot saved successfully');
|
||||
notify('success', t('save_recording.notifications.save_success'));
|
||||
if (browserId) {
|
||||
await stopRecording(browserId);
|
||||
}
|
||||
@@ -63,7 +64,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
setWaitingForSave(true);
|
||||
console.log(`Saving the recording as ${recordingName} for userId ${user.id}`);
|
||||
} else {
|
||||
console.error('User not logged in. Cannot save recording.');
|
||||
console.error(t('save_recording.notifications.user_not_logged'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,34 +78,38 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={() => setOpenModal(true)} variant="outlined" sx={{ marginRight: '20px' }} size="small" color="success">
|
||||
Finish
|
||||
{t('right_panel.buttons.finish')}
|
||||
</Button>
|
||||
|
||||
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
|
||||
<form onSubmit={handleSaveRecording} style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography variant="h6">Save Robot</Typography>
|
||||
<Typography variant="h6">{t('save_recording.title')}</Typography>
|
||||
<TextField
|
||||
required
|
||||
sx={{ width: '300px', margin: '15px 0px' }}
|
||||
onChange={handleChangeOfTitle}
|
||||
id="title"
|
||||
label="Robot Name"
|
||||
label={t('save_recording.robot_name')}
|
||||
variant="outlined"
|
||||
defaultValue={recordingName ? recordingName : null}
|
||||
/>
|
||||
{needConfirm
|
||||
?
|
||||
(<React.Fragment>
|
||||
<Button color="error" variant="contained" onClick={saveRecording} sx={{ marginTop: '10px' }}>Confirm</Button>
|
||||
<Button color="error" variant="contained" onClick={saveRecording} sx={{ marginTop: '10px' }}>
|
||||
{t('save_recording.buttons.confirm')}
|
||||
</Button>
|
||||
<WarningText>
|
||||
<NotificationImportantIcon color="warning" />
|
||||
Robot with this name already exists, please confirm the Robot's overwrite.
|
||||
{t('save_recording.warnings.robot_exists')}
|
||||
</WarningText>
|
||||
</React.Fragment>)
|
||||
: <Button type="submit" variant="contained" sx={{ marginTop: '10px' }}>Save</Button>
|
||||
: <Button type="submit" variant="contained" sx={{ marginTop: '10px' }}>
|
||||
{t('save_recording.buttons.save')}
|
||||
</Button>
|
||||
}
|
||||
{waitingForSave &&
|
||||
<Tooltip title='Optimizing and saving the workflow' placement={"bottom"}>
|
||||
<Tooltip title={t('save_recording.tooltips.optimizing')} placement={"bottom"}>
|
||||
<Box sx={{ width: '100%', marginTop: '10px' }}>
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { MenuItem, TextField, Typography, Box } from "@mui/material";
|
||||
import { Dropdown } from "../atoms/DropdownMui";
|
||||
@@ -25,6 +26,7 @@ export interface ScheduleSettings {
|
||||
}
|
||||
|
||||
export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: ScheduleSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [schedule, setSchedule] = useState<ScheduleSettings | null>(null);
|
||||
const [settings, setSettings] = useState<ScheduleSettings>({
|
||||
runEvery: 1,
|
||||
@@ -116,6 +118,25 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const getDayOrdinal = (day: string | undefined) => {
|
||||
if (!day) return '';
|
||||
const lastDigit = day.slice(-1);
|
||||
const lastTwoDigits = day.slice(-2);
|
||||
|
||||
// Special cases for 11, 12, 13
|
||||
if (['11', '12', '13'].includes(lastTwoDigits)) {
|
||||
return t('schedule_settings.labels.on_day.th');
|
||||
}
|
||||
|
||||
// Other cases
|
||||
switch (lastDigit) {
|
||||
case '1': return t('schedule_settings.labels.on_day.st');
|
||||
case '2': return t('schedule_settings.labels.on_day.nd');
|
||||
case '3': return t('schedule_settings.labels.on_day.rd');
|
||||
default: return t('schedule_settings.labels.on_day.th');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
isOpen={isOpen}
|
||||
@@ -129,30 +150,30 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
padding: '20px',
|
||||
'& > *': { marginBottom: '20px' },
|
||||
}}>
|
||||
<Typography variant="h6" sx={{ marginBottom: '20px' }}>Schedule Settings</Typography>
|
||||
<Typography variant="h6" sx={{ marginBottom: '20px' }}>{t('schedule_settings.title')}</Typography>
|
||||
<>
|
||||
{schedule !== null ? (
|
||||
<>
|
||||
<Typography>Run every: {schedule.runEvery} {schedule.runEveryUnit.toLowerCase()}</Typography>
|
||||
<Typography>{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? "Start From" : "On"} {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()}</Typography>
|
||||
<Typography>{t('schedule_settings.run_every')}: {schedule.runEvery} {schedule.runEveryUnit.toLowerCase()}</Typography>
|
||||
<Typography>{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.start_from') : t('schedule_settings.start_from')}: {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()}</Typography>
|
||||
{schedule.runEveryUnit === 'MONTHS' && (
|
||||
<Typography>On day: {schedule.dayOfMonth}{['1', '21', '31'].includes(schedule.dayOfMonth || '') ? 'st' : ['2', '22'].includes(schedule.dayOfMonth || '') ? 'nd' : ['3', '23'].includes(schedule.dayOfMonth || '') ? 'rd' : 'th'} of the month</Typography>
|
||||
<Typography>{t('schedule_settings.on_day')}: {schedule.dayOfMonth}{getDayOrdinal(schedule.dayOfMonth)} of the month</Typography>
|
||||
)}
|
||||
<Typography>At around: {schedule.atTimeStart}, {schedule.timezone} Timezone</Typography>
|
||||
<Typography>{t('schedule_settings.at_around')}: {schedule.atTimeStart}, {schedule.timezone} {t('schedule_settings.timezone')}</Typography>
|
||||
<Box mt={2} display="flex" justifyContent="space-between">
|
||||
<Button
|
||||
onClick={deleteRobotSchedule}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
Delete Schedule
|
||||
{t('schedule_settings.buttons.delete_schedule')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginRight: '10px' }}>Run once every</Typography>
|
||||
<Typography sx={{ marginRight: '10px' }}>{t('schedule_settings.labels.run_once_every')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
value={settings.runEvery}
|
||||
@@ -174,7 +195,9 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? "Start From" : "On"}</Typography>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>
|
||||
{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.labels.start_from_label') : t('schedule_settings.labels.start_from_label')}
|
||||
</Typography>
|
||||
<Dropdown
|
||||
label=""
|
||||
id="startFrom"
|
||||
@@ -190,7 +213,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
|
||||
{settings.runEveryUnit === 'MONTHS' && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>On Day of the Month</Typography>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>{t('schedule_settings.labels.on_day_of_month')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
value={settings.dayOfMonth}
|
||||
@@ -204,7 +227,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
{['MINUTES', 'HOURS'].includes(settings.runEveryUnit) ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Box sx={{ marginRight: '20px' }}>
|
||||
<Typography sx={{ marginBottom: '5px' }}>In Between</Typography>
|
||||
<Typography sx={{ marginBottom: '5px' }}>{t('schedule_settings.labels.in_between')}</Typography>
|
||||
<TextField
|
||||
type="time"
|
||||
value={settings.atTimeStart}
|
||||
@@ -221,7 +244,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '10px' }}>At Around</Typography>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '10px' }}>{t('schedule_settings.at_around')}</Typography>
|
||||
<TextField
|
||||
type="time"
|
||||
value={settings.atTimeStart}
|
||||
@@ -232,7 +255,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginRight: '10px' }}>Timezone</Typography>
|
||||
<Typography sx={{ marginRight: '10px' }}>{t('schedule_settings.timezone')}</Typography>
|
||||
<Dropdown
|
||||
label=""
|
||||
id="timezone"
|
||||
@@ -247,10 +270,10 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
</Box>
|
||||
<Box mt={2} display="flex" justifyContent="flex-end">
|
||||
<Button onClick={() => handleStart(settings)} variant="contained" color="primary">
|
||||
Save Schedule
|
||||
{t('schedule_settings.buttons.save_schedule')}
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||
Cancel
|
||||
{t('schedule_settings.buttons.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
@@ -271,4 +294,4 @@ const modalStyle = {
|
||||
height: 'fit-content',
|
||||
display: 'block',
|
||||
padding: '20px',
|
||||
};
|
||||
};
|
||||
@@ -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,16 +109,16 @@ 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>
|
||||
@@ -128,17 +126,17 @@ const ApiKeyManager = () => {
|
||||
<TableCell>{apiKeyName}</TableCell>
|
||||
<TableCell>{showKey ? `${apiKey?.substring(0, 10)}...` : '***************'}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title="Copy">
|
||||
<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 +148,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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 {
|
||||
@@ -52,6 +53,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);
|
||||
@@ -200,7 +202,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 +210,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) {
|
||||
|
||||
@@ -5,6 +5,9 @@ import Box from '@mui/material/Box';
|
||||
import { Paper, Button } from "@mui/material";
|
||||
import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Code } from "@mui/icons-material";
|
||||
import { apiUrl } from "../../apiConfig";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
|
||||
interface MainMenuProps {
|
||||
value: string;
|
||||
@@ -12,6 +15,7 @@ interface MainMenuProps {
|
||||
}
|
||||
|
||||
export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||
handleChangeContent(newValue);
|
||||
@@ -47,7 +51,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
fontSize: 'medium',
|
||||
}}
|
||||
value="recordings"
|
||||
label="Robots"
|
||||
label={t('mainmenu.recordings')}
|
||||
icon={<AutoAwesome />}
|
||||
iconPosition="start"
|
||||
/>
|
||||
@@ -58,7 +62,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
fontSize: 'medium',
|
||||
}}
|
||||
value="runs"
|
||||
label="Runs"
|
||||
label={t('mainmenu.runs')}
|
||||
icon={<FormatListBulleted />}
|
||||
iconPosition="start"
|
||||
/>
|
||||
@@ -69,7 +73,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
fontSize: 'medium',
|
||||
}}
|
||||
value="proxy"
|
||||
label="Proxy"
|
||||
label={t('mainmenu.proxy')}
|
||||
icon={<Usb />}
|
||||
iconPosition="start"
|
||||
/>
|
||||
@@ -80,7 +84,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
fontSize: 'medium',
|
||||
}}
|
||||
value="apikey"
|
||||
label="API Key"
|
||||
label={t('mainmenu.apikey')}
|
||||
icon={<VpnKey />}
|
||||
iconPosition="start"
|
||||
/>
|
||||
@@ -88,10 +92,10 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
<hr />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1rem', textAlign: 'left' }}>
|
||||
<Button href={`${apiUrl}/api-docs/`} target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<Code />}>
|
||||
Website To API
|
||||
{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>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { styled } from '@mui/system';
|
||||
import { Alert, AlertTitle, TextField, Button, Switch, FormControlLabel, Box, Typography, Tabs, Tab, Table, TableContainer, TableHead, TableRow, TableBody, TableCell, Paper } from '@mui/material';
|
||||
import { sendProxyConfig, getProxyConfig, testProxyConfig, deleteProxyConfig } from '../../api/proxy';
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const FormContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
@@ -16,6 +17,7 @@ const FormControl = styled(Box)({
|
||||
});
|
||||
|
||||
const ProxyForm: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [proxyConfigForm, setProxyConfigForm] = useState({
|
||||
server_url: '',
|
||||
username: '',
|
||||
@@ -79,13 +81,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')}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,9 +98,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'));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -109,7 +111,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);
|
||||
@@ -119,11 +121,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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -136,11 +138,11 @@ const ProxyForm: React.FC = () => {
|
||||
<>
|
||||
<FormContainer>
|
||||
<Typography variant="h6" gutterBottom component="div" style={{ marginTop: '20px' }}>
|
||||
Proxy Configuration
|
||||
{t('proxy.title')}
|
||||
</Typography>
|
||||
<Tabs value={tabIndex} onChange={handleTabChange}>
|
||||
<Tab label="Standard Proxy" />
|
||||
<Tab label="Automatic Proxy Rotation" />
|
||||
<Tab label={t('proxy.tab_standard')} />
|
||||
<Tab label={t('proxy.tab_rotation')} />
|
||||
</Tabs>
|
||||
{tabIndex === 0 && (
|
||||
isProxyConfigured ? (
|
||||
@@ -149,8 +151,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>
|
||||
@@ -162,39 +164,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}
|
||||
@@ -206,7 +206,7 @@ const ProxyForm: React.FC = () => {
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<TextField
|
||||
label="Password"
|
||||
label={t('proxy.password')}
|
||||
name="password"
|
||||
value={proxyConfigForm.password}
|
||||
onChange={handleChange}
|
||||
@@ -226,7 +226,7 @@ const ProxyForm: React.FC = () => {
|
||||
fullWidth
|
||||
disabled={!proxyConfigForm.server_url || (requiresAuth && (!proxyConfigForm.username || !proxyConfigForm.password))}
|
||||
>
|
||||
Add Proxy
|
||||
{t('proxy.add_proxy')}
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
@@ -234,33 +234,33 @@ 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' }}>
|
||||
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">Join Maxun Cloud Waitlist</a>
|
||||
<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' }}>
|
||||
<AlertTitle>If your proxy requires a username and password, always provide them separately from the proxy URL. </AlertTitle>
|
||||
<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>
|
||||
<b>{t('proxy.alert.wrong_way')}</b>
|
||||
<br />
|
||||
Proxy URL: http://myusername:mypassword@proxy.com:1337
|
||||
{t('proxy.alert.proxy_url')} http://myusername:mypassword@proxy.com:1337
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxyForm;
|
||||
export default ProxyForm;
|
||||
@@ -22,6 +22,7 @@ import { emptyWorkflow } from "../../shared/constants";
|
||||
import { getActiveWorkflow } from "../../api/workflow";
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ActionDescriptionBox from '../molecules/ActionDescriptionBox';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
|
||||
getActiveWorkflow(id).then(
|
||||
@@ -56,10 +57,11 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
const [hoverStates, setHoverStates] = useState<{ [id: string]: boolean }>({});
|
||||
const [browserStepIdList, setBrowserStepIdList] = useState<number[]>([]);
|
||||
|
||||
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);
|
||||
@@ -139,7 +141,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]: '' }));
|
||||
}
|
||||
@@ -151,7 +153,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') }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -213,7 +215,7 @@ 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();
|
||||
@@ -223,8 +225,9 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
if (hasTextSteps) {
|
||||
socket?.emit('action', { action: 'scrapeSchema', settings });
|
||||
}
|
||||
resetInterpretationLog();
|
||||
onFinishCapture();
|
||||
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps]);
|
||||
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps, resetInterpretationLog]);
|
||||
|
||||
const getListSettingsObject = useCallback(() => {
|
||||
let settings: {
|
||||
@@ -278,7 +281,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();
|
||||
@@ -296,13 +299,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();
|
||||
@@ -314,12 +317,12 @@ 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();
|
||||
@@ -348,7 +351,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
setTextLabels({});
|
||||
setErrors({});
|
||||
setConfirmedTextSteps({});
|
||||
notify('error', 'Capture Text Discarded');
|
||||
notify('error', t('right_panel.errors.capture_text_discarded'));
|
||||
}, [browserSteps, stopGetText, deleteBrowserStep]);
|
||||
|
||||
const discardGetList = useCallback(() => {
|
||||
@@ -363,7 +366,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
setShowLimitOptions(false);
|
||||
setCaptureStage('initial');
|
||||
setConfirmedListTextFields({});
|
||||
notify('error', 'Capture List Discarded');
|
||||
notify('error', t('right_panel.errors.capture_list_discarded'));
|
||||
}, [browserSteps, stopGetList, deleteBrowserStep, resetListState]);
|
||||
|
||||
|
||||
@@ -402,7 +405,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
</SimpleBox> */}
|
||||
<ActionDescriptionBox />
|
||||
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
|
||||
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" onClick={startGetList}>Capture List</Button>}
|
||||
{!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' }}>
|
||||
@@ -411,28 +414,29 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
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}>Discard</Button>
|
||||
<Button variant="outlined" color="error" onClick={discardGetList}>{t('right_panel.buttons.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>
|
||||
<h4>What is the maximum number of rows you want to extract?</h4>
|
||||
<h4>{t('right_panel.limit.title')}</h4>
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
value={limitType}
|
||||
@@ -446,13 +450,13 @@ 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"
|
||||
placeholder={t('right_panel.limit.enter_number')}
|
||||
sx={{
|
||||
marginLeft: '10px',
|
||||
'& input': {
|
||||
@@ -467,21 +471,21 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" onClick={startGetText}>Capture Text</Button>}
|
||||
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" onClick={startGetText}>{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" onClick={startGetScreenshot}>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>
|
||||
@@ -492,7 +496,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
|
||||
@@ -510,7 +514,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Data"
|
||||
label={t('right_panel.fields.data')}
|
||||
value={step.data}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
@@ -525,8 +529,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
/>
|
||||
{!confirmedTextSteps[step.id] && (
|
||||
<Box display="flex" justifyContent="space-between" gap={2}>
|
||||
<Button variant="contained" onClick={() => handleTextStepConfirm(step.id)} disabled={!textLabels[step.id]?.trim()}>Confirm</Button>
|
||||
<Button variant="contained" 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>
|
||||
)}
|
||||
</>
|
||||
@@ -535,17 +539,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}>
|
||||
<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
|
||||
@@ -560,7 +566,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"
|
||||
@@ -580,14 +586,14 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
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>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useReducer, createContext, useEffect } from 'react';
|
||||
import { useReducer, createContext, useEffect, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { apiUrl } from "../apiConfig";
|
||||
@@ -14,12 +14,16 @@ interface ActionType {
|
||||
|
||||
type InitialStateType = {
|
||||
user: any;
|
||||
lastActivityTime?: number;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
user: null,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
|
||||
const AUTO_LOGOUT_TIME = 4 * 60 * 60 * 1000; // 4 hours in milliseconds
|
||||
|
||||
const AuthContext = createContext<{
|
||||
state: InitialStateType;
|
||||
dispatch: React.Dispatch<ActionType>;
|
||||
@@ -34,11 +38,13 @@ const reducer = (state: InitialStateType, action: ActionType) => {
|
||||
return {
|
||||
...state,
|
||||
user: action.payload,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
case 'LOGOUT':
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
lastActivityTime: undefined,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
@@ -50,6 +56,39 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
const navigate = useNavigate();
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
const handleLogout = useCallback(async () => {
|
||||
try {
|
||||
await axios.get(`${apiUrl}/auth/logout`);
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
window.localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const checkAutoLogout = useCallback(() => {
|
||||
if (state.user && state.lastActivityTime) {
|
||||
const currentTime = Date.now();
|
||||
const timeSinceLastActivity = currentTime - state.lastActivityTime;
|
||||
|
||||
if (timeSinceLastActivity >= AUTO_LOGOUT_TIME) {
|
||||
handleLogout();
|
||||
}
|
||||
}
|
||||
}, [state.user, state.lastActivityTime, handleLogout]);
|
||||
|
||||
// Update last activity time on user interactions
|
||||
const updateActivityTime = useCallback(() => {
|
||||
if (state.user) {
|
||||
dispatch({
|
||||
type: 'LOGIN',
|
||||
payload: state.user // Reuse existing user data
|
||||
});
|
||||
}
|
||||
}, [state.user]);
|
||||
|
||||
// Initialize user from localStorage
|
||||
useEffect(() => {
|
||||
const storedUser = window.localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
@@ -57,21 +96,54 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Set up activity listeners
|
||||
useEffect(() => {
|
||||
if (state.user) {
|
||||
// List of events to track for user activity
|
||||
const events = ['mousedown', 'keydown', 'scroll', 'touchstart'];
|
||||
|
||||
// Throttled event handler
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const handleActivity = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timeoutId = setTimeout(updateActivityTime, 1000);
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
events.forEach(event => {
|
||||
window.addEventListener(event, handleActivity);
|
||||
});
|
||||
|
||||
// Set up periodic check for auto logout
|
||||
const checkInterval = setInterval(checkAutoLogout, 60000); // Check every minute
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
events.forEach(event => {
|
||||
window.removeEventListener(event, handleActivity);
|
||||
});
|
||||
clearInterval(checkInterval);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [state.user, updateActivityTime, checkAutoLogout]);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
const res = error.response;
|
||||
if (res.status === 401 && res.config && !res.config.__isRetryRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`${apiUrl}/auth/logout`)
|
||||
if (res?.status === 401 && res.config && !res.config.__isRetryRequest) {
|
||||
return new Promise((_, reject) => {
|
||||
handleLogout()
|
||||
.then(() => {
|
||||
console.log('/401 error > logout');
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
window.localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
reject(error);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('AXIOS INTERCEPTORS ERROR:', err);
|
||||
|
||||
@@ -14,8 +14,8 @@ interface ActionContextProps {
|
||||
paginationType: PaginationType;
|
||||
limitType: LimitType;
|
||||
customLimit: string;
|
||||
captureStage: CaptureStage; // New captureStage property
|
||||
setCaptureStage: (stage: CaptureStage) => void; // Setter for captureStage
|
||||
captureStage: CaptureStage;
|
||||
setCaptureStage: (stage: CaptureStage) => void;
|
||||
startPaginationMode: () => void;
|
||||
startGetText: () => void;
|
||||
stopGetText: () => void;
|
||||
|
||||
@@ -32,6 +32,8 @@ interface GlobalInfo {
|
||||
hasScreenshotAction: boolean;
|
||||
hasScrapeSchemaAction: boolean;
|
||||
}) => void;
|
||||
shouldResetInterpretationLog: boolean;
|
||||
resetInterpretationLog: () => void;
|
||||
};
|
||||
|
||||
class GlobalInfoStore implements Partial<GlobalInfo> {
|
||||
@@ -53,6 +55,7 @@ class GlobalInfoStore implements Partial<GlobalInfo> {
|
||||
hasScreenshotAction: false,
|
||||
hasScrapeSchemaAction: false,
|
||||
};
|
||||
shouldResetInterpretationLog = false;
|
||||
};
|
||||
|
||||
const globalInfoStore = new GlobalInfoStore();
|
||||
@@ -71,6 +74,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const [recordingName, setRecordingName] = useState<string>(globalInfoStore.recordingName);
|
||||
const [recordingUrl, setRecordingUrl] = useState<string>(globalInfoStore.recordingUrl);
|
||||
const [currentWorkflowActionsState, setCurrentWorkflowActionsState] = useState(globalInfoStore.currentWorkflowActionsState);
|
||||
const [shouldResetInterpretationLog, setShouldResetInterpretationLog] = useState<boolean>(globalInfoStore.shouldResetInterpretationLog);
|
||||
|
||||
const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => {
|
||||
setNotification({ severity, message, isOpen: true });
|
||||
@@ -87,6 +91,14 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const resetInterpretationLog = () => {
|
||||
setShouldResetInterpretationLog(true);
|
||||
// Reset the flag after a short delay to allow components to respond
|
||||
setTimeout(() => {
|
||||
setShouldResetInterpretationLog(false);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return (
|
||||
<globalInfoContext.Provider
|
||||
value={{
|
||||
@@ -111,6 +123,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
setRecordingUrl,
|
||||
currentWorkflowActionsState,
|
||||
setCurrentWorkflowActionsState,
|
||||
shouldResetInterpretationLog,
|
||||
resetInterpretationLog,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
22
src/i18n.ts
Normal file
22
src/i18n.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
debug: import.meta.env.DEV,
|
||||
supportedLngs: ['en', 'es', 'ja', 'zh','de'],
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes
|
||||
},
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}.json',
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import i18n from "./i18n"
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
|
||||
@@ -1,134 +1,142 @@
|
||||
import axios from "axios";
|
||||
import { useState, useContext, useEffect, FormEvent } from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { AuthContext } from "../context/auth";
|
||||
import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material";
|
||||
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||
import axios from "axios";
|
||||
import { useState, useContext, useEffect, FormEvent } from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { AuthContext } from "../context/auth";
|
||||
import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material";
|
||||
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||
import { apiUrl } from "../apiConfig";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '../i18n';
|
||||
|
||||
const Login = () => {
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { notify } = useGlobalInfoStore();
|
||||
const { email, password } = form;
|
||||
const { t } = useTranslation();
|
||||
console.log(i18n)
|
||||
console.log(t)
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { notify } = useGlobalInfoStore();
|
||||
const { email, password } = form;
|
||||
|
||||
const { state, dispatch } = useContext(AuthContext);
|
||||
const { user } = state;
|
||||
const { state, dispatch } = useContext(AuthContext);
|
||||
const { user } = state;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [user, navigate]);
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
const handleChange = (e: any) => {
|
||||
const { name, value } = e.target;
|
||||
setForm({ ...form, [name]: value });
|
||||
};
|
||||
const handleChange = (e: any) => {
|
||||
const { name, value } = e.target;
|
||||
setForm({ ...form, [name]: value });
|
||||
};
|
||||
|
||||
const submitForm = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await axios.post(`${apiUrl}/auth/login`, {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
dispatch({ type: "LOGIN", payload: data });
|
||||
notify("success", "Welcome to Maxun!");
|
||||
window.localStorage.setItem("user", JSON.stringify(data));
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
notify("error", "Login Failed. Please try again.");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const submitForm = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await axios.post(`${apiUrl}/auth/login`, {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
dispatch({ type: "LOGIN", payload: data });
|
||||
notify("success", t('login.welcome_notification')); // Translated notification
|
||||
window.localStorage.setItem("user", JSON.stringify(data));
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
notify("error", t('login.error_notification')); // Translated error
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
maxHeight: "100vh",
|
||||
mt: 6,
|
||||
padding: 4,
|
||||
}}
|
||||
>
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={submitForm}
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
backgroundColor: "#ffffff",
|
||||
padding: 6,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
maxWidth: 400,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} />
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Welcome Back!
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email"
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 2, mb: 2 }}
|
||||
disabled={loading || !email || !password}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<CircularProgress size={20} sx={{ mr: 2 }} />
|
||||
Loading
|
||||
</>
|
||||
) : (
|
||||
"Login"
|
||||
)}
|
||||
</Button>
|
||||
<Typography variant="body2" align="center">
|
||||
Don’t have an account?{" "}
|
||||
<Link to="/register" style={{ textDecoration: "none", color: "#ff33cc" }}>
|
||||
Register
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
);
|
||||
// Language switcher function
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
maxHeight: "100vh",
|
||||
mt: 6,
|
||||
padding: 4,
|
||||
}}
|
||||
>
|
||||
{/* Language Switcher Buttons */}
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={submitForm}
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
backgroundColor: "#ffffff",
|
||||
padding: 6,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
maxWidth: 400,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} />
|
||||
<Typography variant="h4" gutterBottom>
|
||||
{t('login.title')}
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('login.email')}
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('login.password')}
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 2, mb: 2 }}
|
||||
disabled={loading || !email || !password}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<CircularProgress size={20} sx={{ mr: 2 }} />
|
||||
{t('login.loading')}
|
||||
</>
|
||||
) : (
|
||||
t('login.button')
|
||||
)}
|
||||
</Button>
|
||||
<Typography variant="body2" align="center">
|
||||
{t('login.register_prompt')}{" "}
|
||||
<Link to="/register" style={{ textDecoration: "none", color: "#ff33cc" }}>
|
||||
{t('login.register_link')}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default Login;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MainMenu } from "../components/organisms/MainMenu";
|
||||
import { Stack } from "@mui/material";
|
||||
import { Recordings } from "../components/organisms/Recordings";
|
||||
@@ -30,7 +31,7 @@ export interface ScheduleRunResponse {
|
||||
}
|
||||
|
||||
export const MainPage = ({ handleEditRecording }: MainPageProps) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [content, setContent] = React.useState('recordings');
|
||||
const [sockets, setSockets] = React.useState<Socket[]>([]);
|
||||
const [runningRecordingId, setRunningRecordingId] = React.useState('');
|
||||
@@ -49,10 +50,10 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
|
||||
aborted = true;
|
||||
notifyAboutAbort(runId).then(async (response) => {
|
||||
if (response) {
|
||||
notify('success', `Interpretation of robot ${runningRecordingName} aborted successfully`);
|
||||
notify('success', t('main_page.notifications.abort_success', { name: runningRecordingName }));
|
||||
await stopRecording(ids.browserId);
|
||||
} else {
|
||||
notify('error', `Failed to abort the interpretation of ${runningRecordingName} robot`);
|
||||
notify('error', t('main_page.notifications.abort_failed', { name: runningRecordingName }));
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -67,9 +68,9 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
|
||||
interpretStoredRecording(runId).then(async (interpretation: boolean) => {
|
||||
if (!aborted) {
|
||||
if (interpretation) {
|
||||
notify('success', `Interpretation of robot ${runningRecordingName} succeeded`);
|
||||
notify('success', t('main_page.notifications.interpretation_success', { name: runningRecordingName }));
|
||||
} else {
|
||||
notify('success', `Failed to interpret ${runningRecordingName} robot`);
|
||||
notify('success', t('main_page.notifications.interpretation_failed', { name: runningRecordingName }));
|
||||
// destroy the created browser
|
||||
await stopRecording(browserId);
|
||||
}
|
||||
@@ -98,9 +99,9 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
|
||||
socket.on('debugMessage', debugMessageHandler);
|
||||
setContent('runs');
|
||||
if (browserId) {
|
||||
notify('info', `Running robot: ${runningRecordingName}`);
|
||||
notify('info', t('main_page.notifications.run_started', { name: runningRecordingName }));
|
||||
} else {
|
||||
notify('error', `Failed to run robot: ${runningRecordingName}`);
|
||||
notify('error', t('main_page.notifications.run_start_failed', { name: runningRecordingName }));
|
||||
}
|
||||
})
|
||||
return (socket: Socket, browserId: string, runId: string) => {
|
||||
@@ -113,9 +114,9 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
|
||||
scheduleStoredRecording(runningRecordingId, settings)
|
||||
.then(({ message, runId }: ScheduleRunResponse) => {
|
||||
if (message === 'success') {
|
||||
notify('success', `Robot ${runningRecordingName} scheduled successfully`);
|
||||
notify('success', t('main_page.notifications.schedule_success', { name: runningRecordingName }));
|
||||
} else {
|
||||
notify('error', `Failed to schedule robot ${runningRecordingName}`);
|
||||
notify('error', t('main_page.notifications.schedule_failed', { name: runningRecordingName }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -151,4 +152,4 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
|
||||
{DisplayContent()}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { editRecordingFromStorage } from "../api/storage";
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import styled from "styled-components";
|
||||
import BrowserRecordingSave from '../components/molecules/BrowserRecordingSave';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface RecordingPageProps {
|
||||
recordingName?: string;
|
||||
@@ -26,7 +27,7 @@ export interface PairForEdit {
|
||||
}
|
||||
|
||||
export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [isLoaded, setIsLoaded] = React.useState(false);
|
||||
const [hasScrollbar, setHasScrollbar] = React.useState(false);
|
||||
const [pairForEdit, setPairForEdit] = useState<PairForEdit>({
|
||||
@@ -145,7 +146,7 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
|
||||
</Grid>
|
||||
</>
|
||||
) : (
|
||||
<Loader text={`Spinning up a browser...Navigating to ${recordingUrl}`} />
|
||||
<Loader text={t('recording_page.loader.browser_startup', { url: recordingUrl })} />
|
||||
)}
|
||||
</div>
|
||||
</BrowserStepsProvider>
|
||||
|
||||
@@ -5,8 +5,13 @@ import { AuthContext } from "../context/auth";
|
||||
import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material";
|
||||
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||
import { apiUrl } from "../apiConfig";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '../i18n';
|
||||
|
||||
|
||||
|
||||
const Register = () => {
|
||||
const {t} = useTranslation();
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
@@ -40,11 +45,13 @@ const Register = () => {
|
||||
password,
|
||||
});
|
||||
dispatch({ type: "LOGIN", payload: data });
|
||||
notify("success", "Registration Successful!");
|
||||
notify("success", t('register.welcome_notification'));
|
||||
window.localStorage.setItem("user", JSON.stringify(data));
|
||||
navigate("/");
|
||||
} catch (error:any) {
|
||||
notify("error", `Registration Failed. Please try again. ${error.response.data}`);
|
||||
|
||||
notify("error", error.response.data || t('register.error_notification'));
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -78,11 +85,11 @@ const Register = () => {
|
||||
>
|
||||
<img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} />
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Create an Account
|
||||
{t('register.title')}
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email"
|
||||
label={t('register.email')}
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
@@ -92,7 +99,7 @@ const Register = () => {
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
label={t('register.password')}
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
@@ -115,13 +122,14 @@ const Register = () => {
|
||||
Loading
|
||||
</>
|
||||
) : (
|
||||
"Register"
|
||||
t('register.button')
|
||||
)}
|
||||
</Button>
|
||||
<Typography variant="body2" align="center">
|
||||
Already have an account?{" "}
|
||||
{t('register.register_prompt')}{" "}
|
||||
<Link to="/login" style={{ textDecoration: "none", color: "#ff33cc" }}>
|
||||
Login
|
||||
|
||||
{t('register.login_link')}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user