Compare commits
24 Commits
4dc9f3a130
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ce029534e | ||
| f98e5c88fe | |||
|
|
6498dfa4f4 | ||
|
|
1fa914900d | ||
|
|
7fa1901e5f | ||
|
|
550261e158 | ||
|
|
ca94044628 | ||
|
|
d7940235cb | ||
|
|
ae4f9d800b | ||
|
|
0b9f593597 | ||
|
|
8f88b109b9 | ||
|
|
7b60cc2672 | ||
|
|
6e90b02195 | ||
|
|
cbcb3d59de | ||
|
|
9d52302c1d | ||
|
|
fd4044d727 | ||
|
|
8acc3dfcca | ||
|
|
7cf8b789ff | ||
|
|
1a2f9bdd09 | ||
|
|
03204a7851 | ||
|
|
51156f5880 | ||
|
|
9f1b8ae6e3 | ||
|
|
d127f0c8f0 | ||
|
|
e413c1a4c3 |
1
.postcssrc
Normal file
1
.postcssrc
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -8,15 +8,22 @@ COPY package*.json ./
|
||||
# Install dependencies
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
# Copy frontend source code and config
|
||||
COPY src ./src
|
||||
COPY public ./public
|
||||
COPY index.html ./
|
||||
# Copy all configuration files first
|
||||
COPY vite.config.js ./
|
||||
COPY tsconfig.json ./
|
||||
COPY index.html ./
|
||||
COPY vite-env.d.ts ./
|
||||
COPY postcss.config.mjs ./
|
||||
|
||||
# Copy frontend source code
|
||||
COPY src ./src
|
||||
COPY public ./public
|
||||
|
||||
# Build production bundle
|
||||
RUN npm run build
|
||||
|
||||
# Expose the frontend port
|
||||
EXPOSE ${FRONTEND_PORT:-5173}
|
||||
|
||||
# Start the frontend using the client script
|
||||
CMD ["npm", "run", "client", "--", "--host"]
|
||||
# Serve static files using built-in Vite preview
|
||||
CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "5173"]
|
||||
110
README.md
110
README.md
@@ -1,9 +1,9 @@
|
||||
<h2 align="center">
|
||||
<h2 align="center">
|
||||
<div>
|
||||
<a href="https://www.maxun.dev/?ref=ghread">
|
||||
<img src="/src/assets/maxunlogo.png" width="70" />
|
||||
<a href="https://www.Dorod Parser.dev/?ref=ghread">
|
||||
<img src="/src/assets/Dorod Parserlogo.png" width="70" />
|
||||
<br>
|
||||
Maxun
|
||||
Dorod Parser
|
||||
</a>
|
||||
</div>
|
||||
Turn Any Website Into A Structured API
|
||||
@@ -11,44 +11,44 @@
|
||||
</h2>
|
||||
|
||||
<p align="center">
|
||||
✨ The unified open-source no-code platform for real-time web scraping, crawling, search and AI data extraction ✨
|
||||
вњЁ The unified open-source no-code platform for real-time web scraping, crawling, search and AI data extraction вњЁ
|
||||
|
||||
<p align="center">
|
||||
<a href="https://app.maxun.dev/?ref=ghread"><b>Go To App</b></a> •
|
||||
<a href="https://docs.maxun.dev/?ref=ghread"><b>Documentation</b></a> •
|
||||
<a href="https://www.maxun.dev/?ref=ghread"><b>Website</b></a> •
|
||||
<a href="https://discord.gg/5GbPjBUkws"><b>Discord</b></a> •
|
||||
<a href="https://www.youtube.com/@MaxunOSS?ref=ghread"><b>Watch Tutorials</b></a>
|
||||
<a href="https://app.Dorod Parser.dev/?ref=ghread"><b>Go To App</b></a> •
|
||||
<a href="https://docs.Dorod Parser.dev/?ref=ghread"><b>Documentation</b></a> •
|
||||
<a href="https://www.Dorod Parser.dev/?ref=ghread"><b>Website</b></a> •
|
||||
<a href="https://discord.gg/5GbPjBUkws"><b>Discord</b></a> •
|
||||
<a href="https://www.youtube.com/@Dorod ParserOSS?ref=ghread"><b>Watch Tutorials</b></a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://trendshift.io/repositories/12113" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12113" alt="getmaxun%2Fmaxun | Trendshift" style="width: 250px; height: 55px; margin-top: 10px;" width="250" height="55"/></a>
|
||||
<a href="https://trendshift.io/repositories/12113" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12113" alt="getDorod Parser%2FDorod Parser | Trendshift" style="width: 250px; height: 55px; margin-top: 10px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
## What is Maxun?
|
||||
## What is Dorod Parser?
|
||||
|
||||
Maxun is an open-source no-code web data platform for turning the web into structured, reliable data.
|
||||
It supports extraction, crawling, scraping, and search — designed to scale from simple use cases to complex, automated workflows.
|
||||
Dorod Parser is an open-source no-code web data platform for turning the web into structured, reliable data.
|
||||
It supports extraction, crawling, scraping, and search — designed to scale from simple use cases to complex, automated workflows.
|
||||
|
||||
### Ecosystem
|
||||
|
||||
1. **[Extract](https://docs.maxun.dev/category/extract)** – Emulate real user behavior and collect structured data from any website.
|
||||
* **[Recorder Mode](https://docs.maxun.dev/robot/extract/robot-actions)** - Record your actions as you browse; Maxun turns them into a reusable extraction robot.
|
||||
* **[AI Mode](https://docs.maxun.dev/robot/extract/llm-extraction)** - Describe what you want in natural language and let LLM-powered extraction do the rest.
|
||||
1. **[Extract](https://docs.Dorod Parser.dev/category/extract)** – Emulate real user behavior and collect structured data from any website.
|
||||
* **[Recorder Mode](https://docs.Dorod Parser.dev/robot/extract/robot-actions)** - Record your actions as you browse; Dorod Parser turns them into a reusable extraction robot.
|
||||
* **[AI Mode](https://docs.Dorod Parser.dev/robot/extract/llm-extraction)** - Describe what you want in natural language and let LLM-powered extraction do the rest.
|
||||
|
||||
2. **[Scrape](https://docs.maxun.dev/robot/scrape/scrape-robots)** – Convert full webpages into clean Markdown or HTML and capture screenshots.
|
||||
3. **[Crawl](https://docs.maxun.dev/robot/crawl/crawl-introduction)** - Crawl entire websites and extract content from every relevant page, with full control over scope and discovery.
|
||||
4. **[Search](https://docs.maxun.dev/robot/search/search-introduction)** - Run automated web searches to discover or scrape results, with support for time-based filters.
|
||||
5. **[SDK](https://docs.maxun.dev/sdk/sdk-overview)** – A complete developer toolkit for scraping, extraction, scheduling, and end-to-end data automation.
|
||||
2. **[Scrape](https://docs.Dorod Parser.dev/robot/scrape/scrape-robots)** – Convert full webpages into clean Markdown or HTML and capture screenshots.
|
||||
3. **[Crawl](https://docs.Dorod Parser.dev/robot/crawl/crawl-introduction)** - Crawl entire websites and extract content from every relevant page, with full control over scope and discovery.
|
||||
4. **[Search](https://docs.Dorod Parser.dev/robot/search/search-introduction)** - Run automated web searches to discover or scrape results, with support for time-based filters.
|
||||
5. **[SDK](https://docs.Dorod Parser.dev/sdk/sdk-overview)** – A complete developer toolkit for scraping, extraction, scheduling, and end-to-end data automation.
|
||||
|
||||
## How Does It Work?
|
||||
|
||||
Maxun robots are automated tools that help you collect data from websites without writing any code. Think of them as your personal web assistants that can navigate websites, extract information, and organize data just like you would manually - but faster and more efficiently.
|
||||
Dorod Parser robots are automated tools that help you collect data from websites without writing any code. Think of them as your personal web assistants that can navigate websites, extract information, and organize data just like you would manually - but faster and more efficiently.
|
||||
|
||||
There are four types of robots, each designed for a different job.
|
||||
|
||||
### 1. Extract
|
||||
Extract emulates real user behavior and captures structured data.
|
||||
- <a href="/robot/extract/robot-actions">Recorder Mode</a> - Record your actions as you browse; Maxun turns them into a reusable extraction robot.
|
||||
- <a href="/robot/extract/robot-actions">Recorder Mode</a> - Record your actions as you browse; Dorod Parser turns them into a reusable extraction robot.
|
||||
### Example: Extract 10 Property Listings from Airbnb
|
||||
|
||||
[https://github.com/user-attachments/assets/recorder-mode-demo-video](https://github.com/user-attachments/assets/c6baa75f-b950-482c-8d26-8a8b6c5382c3)
|
||||
@@ -62,41 +62,41 @@ Learn more <a href="/category/extract">here</a>.
|
||||
### 2. Scrape
|
||||
Scrape converts full webpages into clean Markdown, HTML and can capture screenshots. Ideal for AI workflows, agents, and document processing.
|
||||
|
||||
Learn more <a href="https://docs.maxun.dev/robot/scrape/scrape-robots">here</a>.
|
||||
Learn more <a href="https://docs.Dorod Parser.dev/robot/scrape/scrape-robots">here</a>.
|
||||
|
||||
### 3. Crawl
|
||||
Crawl entire websites and extract content from every relevant page, with full control over scope and discovery.
|
||||
|
||||
Learn more <a href="https://docs.maxun.dev/robot/crawl/crawl-introduction">here</a>.
|
||||
Learn more <a href="https://docs.Dorod Parser.dev/robot/crawl/crawl-introduction">here</a>.
|
||||
|
||||
### 4. Search
|
||||
Run automated web searches to discover or scrape results, with support for time-based filters.
|
||||
|
||||
Learn more <a href="https://docs.maxun.dev/robot/search/search-introduction">here</a>.
|
||||
Learn more <a href="https://docs.Dorod Parser.dev/robot/search/search-introduction">here</a>.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Getting Started
|
||||
The simplest & fastest way to get started is to use the hosted version: https://app.maxun.dev. You can self-host if you prefer!
|
||||
The simplest & fastest way to get started is to use the hosted version: https://app.Dorod Parser.dev. You can self-host if you prefer!
|
||||
|
||||
### Installation
|
||||
Maxun can run locally with or without Docker
|
||||
1. [Setup with Docker Compose](https://docs.maxun.dev/installation/docker)
|
||||
2. [Setup without Docker](https://docs.maxun.dev/installation/local)
|
||||
3. [Environment Variables](https://docs.maxun.dev/installation/environment_variables)
|
||||
4. [SDK](https://github.com/getmaxun/node-sdk)
|
||||
Dorod Parser can run locally with or without Docker
|
||||
1. [Setup with Docker Compose](https://docs.Dorod Parser.dev/installation/docker)
|
||||
2. [Setup without Docker](https://docs.Dorod Parser.dev/installation/local)
|
||||
3. [Environment Variables](https://docs.Dorod Parser.dev/installation/environment_variables)
|
||||
4. [SDK](https://github.com/getDorod Parser/node-sdk)
|
||||
|
||||
### Upgrading & Self Hosting
|
||||
1. [Self Host Maxun With Docker & Portainer](https://docs.maxun.dev/self-host)
|
||||
2. [Upgrade Maxun With Docker Compose Setup](https://docs.maxun.dev/installation/upgrade#upgrading-with-docker-compose)
|
||||
3. [Upgrade Maxun Without Docker Compose Setup](https://docs.maxun.dev/installation/upgrade#upgrading-with-local-setup)
|
||||
1. [Self Host Dorod Parser With Docker & Portainer](https://docs.Dorod Parser.dev/self-host)
|
||||
2. [Upgrade Dorod Parser With Docker Compose Setup](https://docs.Dorod Parser.dev/installation/upgrade#upgrading-with-docker-compose)
|
||||
3. [Upgrade Dorod Parser Without Docker Compose Setup](https://docs.Dorod Parser.dev/installation/upgrade#upgrading-with-local-setup)
|
||||
|
||||
## Sponsors
|
||||
<table>
|
||||
<tr>
|
||||
<td width="229">
|
||||
<br/>
|
||||
<a href="https://www.testmu.ai/?utm_source=maxun&utm_medium=sponsor" target="_blank">
|
||||
<a href="https://www.testmuai.com/?utm_medium=sponsor&utm_source=Dorod Parser" target="_blank">
|
||||
<img src="https://github.com/user-attachments/assets/6c96005b-85df-43e0-9b63-96aaca676c11" /><br/><br/>
|
||||
<b>TestMu AI</b>
|
||||
</a>
|
||||
@@ -109,24 +109,24 @@ Maxun can run locally with or without Docker
|
||||
|
||||
## Features
|
||||
|
||||
- ✨ **Extract Data With No-Code** – Point and click interface
|
||||
- ✨ **LLM-Powered Extraction** – Describe what you want; use LLMs to scrape structured data
|
||||
- ✨ **Developer SDK** – Programmatic extraction, scheduling, and robot management
|
||||
- ✨ **Handle Pagination & Scrolling** – Automatic navigation
|
||||
- ✨ **Run Robots On Schedules** – Set it and forget it
|
||||
- ✨ **Turn Websites to APIs** – RESTful endpoints from any site
|
||||
- ✨ **Turn Websites to Spreadsheets** – Direct data export to Google Sheets & Airtable
|
||||
- ✨ **Adapt To Website Layout Changes** – Auto-recovery from site updates
|
||||
- ✨ **Extract Behind Login** – Handle authentication seamlessly
|
||||
- ✨ **Integrations** – Connect with your favorite tools
|
||||
- ✨ **MCP Support** – Model Context Protocol integration
|
||||
- ✨ **LLM-Ready Data** – Clean Markdown for AI applications
|
||||
- ✨ **Self-Hostable** – Full control over your infrastructure
|
||||
- ✨ **Open Source** – Transparent and community-driven
|
||||
- ✨ **Extract Data With No-Code** – Point and click interface
|
||||
- ✨ **LLM-Powered Extraction** – Describe what you want; use LLMs to scrape structured data
|
||||
- ✨ **Developer SDK** – Programmatic extraction, scheduling, and robot management
|
||||
- ✨ **Handle Pagination & Scrolling** – Automatic navigation
|
||||
- ✨ **Run Robots On Schedules** – Set it and forget it
|
||||
- ✨ **Turn Websites to APIs** – RESTful endpoints from any site
|
||||
- ✨ **Turn Websites to Spreadsheets** – Direct data export to Google Sheets & Airtable
|
||||
- ✨ **Adapt To Website Layout Changes** – Auto-recovery from site updates
|
||||
- ✨ **Extract Behind Login** – Handle authentication seamlessly
|
||||
- ✨ **Integrations** – Connect with your favorite tools
|
||||
- ✨ **MCP Support** – Model Context Protocol integration
|
||||
- ✨ **LLM-Ready Data** – Clean Markdown for AI applications
|
||||
- ✨ **Self-Hostable** – Full control over your infrastructure
|
||||
- ✨ **Open Source** – Transparent and community-driven
|
||||
|
||||
## Demos
|
||||
Maxun can be used for various use-cases, including lead generation, market research, content aggregation and more.
|
||||
View demos here: https://www.maxun.dev/usecases
|
||||
Dorod Parser can be used for various use-cases, including lead generation, market research, content aggregation and more.
|
||||
View demos here: https://www.Dorod Parser.dev/usecases
|
||||
|
||||
## Note
|
||||
This project is in early stages of development. Your feedback is very important for us - we're actively working on improvements. </a>
|
||||
@@ -143,11 +143,11 @@ If you rely on this project commercially, please consider contributing back
|
||||
or supporting its development.
|
||||
|
||||
## Support Us
|
||||
Star the repository, contribute if you love what we’re building, or [sponsor us](https://github.com/sponsors/amhsirak).
|
||||
Star the repository, contribute if you love what we’re building, or [sponsor us](https://github.com/sponsors/amhsirak).
|
||||
|
||||
## Contributors
|
||||
Thank you to the combined efforts of everyone who contributes!
|
||||
|
||||
<a href="https://github.com/getmaxun/maxun/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=getmaxun/maxun" />
|
||||
<a href="https://github.com/getDorod Parser/Dorod Parser/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=getDorod Parser/Dorod Parser" />
|
||||
</a>
|
||||
|
||||
25
README_RU.md
Normal file
25
README_RU.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Dorod Parser
|
||||
|
||||
**Визуальный веб-скрапер без кода с AI-интеграцией**
|
||||
|
||||
## Возможности
|
||||
|
||||
- **Recorder Mode** - записывай действия мышкой, создавай роботов
|
||||
- **AI Mode** - описывай что нужно извлечь естественным языком
|
||||
- **Динамические сайты** - адаптируется к изменениям структуры
|
||||
- **Экспорт** - Google Sheets, Airtable, JSON, CSV
|
||||
- **Self-hosted** - разворачивай на своих серверах через Docker
|
||||
- **REST API** - программное управление через SDK
|
||||
|
||||
## Технологии
|
||||
|
||||
- **Frontend:** React + Vite (порт 3030)
|
||||
- **Backend:** Node.js + Playwright
|
||||
- **Хранилище:** PostgreSQL + MinIO + Redis
|
||||
- **Лицензия:** AGPLv3
|
||||
|
||||
|
||||
docker-compose up
|
||||
\\\
|
||||
|
||||
Открой: **http://localhost:3030**
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
ports:
|
||||
- "${DB_PORT:-5432}:${DB_PORT:-5432}"
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
@@ -22,10 +22,10 @@ services:
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
|
||||
command: server /data --console-address :${MINIO_CONSOLE_PORT:-9001}
|
||||
command: server /data --console-address :9001
|
||||
ports:
|
||||
- "${MINIO_PORT:-9000}:${MINIO_PORT:-9000}" # API port
|
||||
- "${MINIO_CONSOLE_PORT:-9001}:${MINIO_CONSOLE_PORT:-9001}" # WebUI port
|
||||
- "9020:9000" # API port
|
||||
- "9021:9001" # WebUI port
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
||||
@@ -35,8 +35,7 @@ services:
|
||||
# dockerfile: Dockerfile.backend
|
||||
image: getmaxun/maxun-backend:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}"
|
||||
network_mode: "host"
|
||||
env_file: .env
|
||||
environment:
|
||||
BACKEND_URL: ${BACKEND_URL}
|
||||
@@ -65,12 +64,14 @@ services:
|
||||
# dockerfile: Dockerfile.frontend
|
||||
image: getmaxun/maxun-frontend:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}"
|
||||
network_mode: "host"
|
||||
env_file: .env
|
||||
environment:
|
||||
PUBLIC_URL: ${PUBLIC_URL}
|
||||
BACKEND_URL: ${BACKEND_URL}
|
||||
PUBLIC_URL: http://localhost:5174
|
||||
BACKEND_URL: http://localhost:8081
|
||||
VITE_BACKEND_URL: http://localhost:8081
|
||||
VITE_PUBLIC_URL: http://localhost:5174
|
||||
command: sh -c "npm run client"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
@@ -79,11 +80,11 @@ services:
|
||||
context: .
|
||||
dockerfile: browser/Dockerfile
|
||||
args:
|
||||
BROWSER_WS_PORT: ${BROWSER_WS_PORT:-3001}
|
||||
BROWSER_HEALTH_PORT: ${BROWSER_HEALTH_PORT:-3002}
|
||||
BROWSER_WS_PORT: 3001
|
||||
BROWSER_HEALTH_PORT: 3002
|
||||
ports:
|
||||
- "${BROWSER_WS_PORT:-3001}:${BROWSER_WS_PORT:-3001}"
|
||||
- "${BROWSER_HEALTH_PORT:-3002}:${BROWSER_HEALTH_PORT:-3002}"
|
||||
- "3011:3001"
|
||||
- "3012:3002"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DEBUG=pw:browser*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "maxun-core",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.31",
|
||||
"description": "Core package for Maxun, responsible for data extraction",
|
||||
"main": "build/index.js",
|
||||
"typings": "build/index.d.ts",
|
||||
|
||||
10
package.json
10
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "maxun",
|
||||
"version": "0.0.32",
|
||||
"author": "Maxun",
|
||||
{
|
||||
"name": "dorod-parser",
|
||||
"version": "0.0.33",
|
||||
"author": "Dorod Parser",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.71.2",
|
||||
@@ -54,7 +54,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.8.0",
|
||||
"loglevel-plugin-remote": "^0.6.8",
|
||||
"maxun-core": "^0.0.30",
|
||||
"maxun-core": "^0.0.31",
|
||||
"minio": "^8.0.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"node-cron": "^3.0.3",
|
||||
|
||||
1
postcss.config.cjs
Normal file
1
postcss.config.cjs
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
468
public/locales/ru.json
Normal file
468
public/locales/ru.json
Normal file
@@ -0,0 +1,468 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "С возвращением!",
|
||||
"email": "Введите рабочий email",
|
||||
"password": "Пароль",
|
||||
"button": "Войти",
|
||||
"loading": "Загрузка",
|
||||
"register_prompt": "Нет аккаунта?",
|
||||
"register_link": "Зарегистрироваться",
|
||||
"welcome_notification": "Добро пожаловать в Dorod Parser!",
|
||||
"validation": {
|
||||
"required_fields": "Email и пароль обязательны",
|
||||
"password_length": "Пароль должен быть не менее 6 символов"
|
||||
},
|
||||
"error": {
|
||||
"user_not_found": "Пользователь не существует",
|
||||
"invalid_credentials": "Неверный email или пароль",
|
||||
"server_error": "Ошибка входа. Попробуйте позже",
|
||||
"generic": "Произошла ошибка. Попробуйте снова"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"title": "Создать аккаунт",
|
||||
"email": "Введите рабочий email",
|
||||
"password": "Пароль",
|
||||
"button": "Зарегистрироваться",
|
||||
"loading": "Загрузка",
|
||||
"register_prompt": "Уже есть аккаунт?",
|
||||
"login_link": "Войти",
|
||||
"welcome_notification": "Добро пожаловать в Dorod Parser!",
|
||||
"validation": {
|
||||
"email_required": "Email обязателен",
|
||||
"password_requirements": "Пароль должен быть не менее 6 символов"
|
||||
},
|
||||
"error": {
|
||||
"user_exists": "Пользователь с таким email уже существует",
|
||||
"creation_failed": "Не удалось создать аккаунт",
|
||||
"server_error": "Ошибка сервера",
|
||||
"generic": "Регистрация не удалась. Попробуйте снова"
|
||||
}
|
||||
},
|
||||
"recordingtable":{
|
||||
"run": "Запустить",
|
||||
"name": "Название",
|
||||
"schedule": "Расписание",
|
||||
"integrate": "Интеграция",
|
||||
"settings": "Настройки",
|
||||
"options": "Опции",
|
||||
"heading":"Мои роботы",
|
||||
"new":"Создать робота",
|
||||
"deleteModalText": "Вы уверены, что хотите удалить этого робота?",
|
||||
"delete": "Удалить",
|
||||
"cancel": "Отмена",
|
||||
"deleteSuccess": "Робот успешно удален",
|
||||
"deleteFailed": "Не удалось удалить робота",
|
||||
"search": "Поиск роботов..."
|
||||
},
|
||||
"mainmenu": {
|
||||
"recordings": "Роботы",
|
||||
"runs": "Запуски",
|
||||
"proxy": "Прокси",
|
||||
"apikey": "API ключ",
|
||||
"feedback": "Присоединиться к Maxun Cloud",
|
||||
"apidocs": "Веб-сайт в API"
|
||||
},
|
||||
"recordingpage": {
|
||||
"stopRecording": "Остановить запись",
|
||||
"recording": "Запись...",
|
||||
"extract": "Извлечь",
|
||||
"actions": "Действия",
|
||||
"goto": "Перейти",
|
||||
"click": "Клик",
|
||||
"type": "Ввод",
|
||||
"scroll": "Прокрутка",
|
||||
"hover": "Наведение",
|
||||
"select": "Выбор",
|
||||
"clear": "Очистить",
|
||||
"press": "Нажать",
|
||||
"upload": "Загрузить",
|
||||
"download": "Скачать",
|
||||
"screenshot": "Скриншот",
|
||||
"wait": "Ожидание",
|
||||
"refresh": "Обновить",
|
||||
"back": "Назад",
|
||||
"forward": "Вперед",
|
||||
"close": "Закрыть",
|
||||
"newTab": "Новая вкладка",
|
||||
"switchTab": "Переключить вкладку",
|
||||
"pagination": "Пагинация",
|
||||
"captcha": "Капча",
|
||||
"saveRecording": "Сохранить запись",
|
||||
"discardRecording": "Отменить запись",
|
||||
"playRecording": "Воспроизвести",
|
||||
"editStep": "Редактировать шаг",
|
||||
"deleteStep": "Удалить шаг",
|
||||
"addStep": "Добавить шаг",
|
||||
"recordingName": "Название записи",
|
||||
"enterUrl": "Введите URL",
|
||||
"startRecording": "Начать запись",
|
||||
"noSteps": "Нет записанных действий",
|
||||
"error": {
|
||||
"recording_failed": "Ошибка записи",
|
||||
"save_failed": "Не удалось сохранить",
|
||||
"invalid_url": "Неверный URL"
|
||||
}
|
||||
},
|
||||
"robotSettings": {
|
||||
"title": "Настройки робота",
|
||||
"name": "Название",
|
||||
"description": "Описание",
|
||||
"url": "Начальный URL",
|
||||
"proxy": "Прокси",
|
||||
"useProxy": "Использовать прокси",
|
||||
"proxyUrl": "URL прокси",
|
||||
"userAgent": "User Agent",
|
||||
"customHeaders": "Дополнительные заголовки",
|
||||
"headerName": "Название",
|
||||
"headerValue": "Значение",
|
||||
"addHeader": "Добавить заголовок",
|
||||
"timeout": "Тайм-аут (мс)",
|
||||
"retries": "Повторные попытки",
|
||||
"screenshot": "Делать скриншоты",
|
||||
"notifications": "Уведомления",
|
||||
"emailNotifications": "Email уведомления",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"save": "Сохранить",
|
||||
"cancel": "Отмена",
|
||||
"advanced": "Расширенные настройки",
|
||||
"basic": "Основные настройки"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Настройка расписания",
|
||||
"enabled": "Включить расписание",
|
||||
"frequency": "Частота",
|
||||
"hourly": "Каждый час",
|
||||
"daily": "Ежедневно",
|
||||
"weekly": "Еженедельно",
|
||||
"monthly": "Ежемесячно",
|
||||
"custom": "Настроить",
|
||||
"time": "Время",
|
||||
"timezone": "Часовой пояс",
|
||||
"days": "Дни недели",
|
||||
"monday": "Понедельник",
|
||||
"tuesday": "Вторник",
|
||||
"wednesday": "Среда",
|
||||
"thursday": "Четверг",
|
||||
"friday": "Пятница",
|
||||
"saturday": "Суббота",
|
||||
"sunday": "Воскресенье",
|
||||
"nextRun": "Следующий запуск",
|
||||
"lastRun": "Последний запуск",
|
||||
"save": "Сохранить расписание",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"integrate": {
|
||||
"title": "Интеграции",
|
||||
"export": "Экспорт данных",
|
||||
"googleSheets": "Google Sheets",
|
||||
"airtable": "Airtable",
|
||||
"webhook": "Webhook",
|
||||
"api": "REST API",
|
||||
"connect": "Подключить",
|
||||
"disconnect": "Отключить",
|
||||
"connected": "Подключено",
|
||||
"notConnected": "Не подключено",
|
||||
"testConnection": "Проверить подключение",
|
||||
"exportFormat": "Формат экспорта",
|
||||
"json": "JSON",
|
||||
"csv": "CSV",
|
||||
"excel": "Excel",
|
||||
"save": "Сохранить настройки"
|
||||
},
|
||||
"runs": {
|
||||
"title": "История запусков",
|
||||
"status": "Статус",
|
||||
"success": "Успешно",
|
||||
"failed": "Ошибка",
|
||||
"running": "Выполняется",
|
||||
"pending": "Ожидание",
|
||||
"cancelled": "Отменено",
|
||||
"startTime": "Время начала",
|
||||
"endTime": "Время окончания",
|
||||
"duration": "Длительность",
|
||||
"recordsExtracted": "Извлечено записей",
|
||||
"viewDetails": "Подробности",
|
||||
"viewLogs": "Логи",
|
||||
"downloadData": "Скачать данные",
|
||||
"retry": "Повторить",
|
||||
"cancel": "Отменить",
|
||||
"noRuns": "Нет запусков"
|
||||
},
|
||||
"extraction": {
|
||||
"title": "Извлечение данных",
|
||||
"addField": "Добавить поле",
|
||||
"fieldName": "Название поля",
|
||||
"selector": "Селектор",
|
||||
"type": "Тип",
|
||||
"text": "Текст",
|
||||
"attribute": "Атрибут",
|
||||
"html": "HTML",
|
||||
"link": "Ссылка",
|
||||
"image": "Изображение",
|
||||
"list": "Список",
|
||||
"table": "Таблица",
|
||||
"required": "Обязательное",
|
||||
"optional": "Опциональное",
|
||||
"removeField": "Удалить поле",
|
||||
"testExtraction": "Тестировать",
|
||||
"preview": "Предпросмотр",
|
||||
"noData": "Нет данных"
|
||||
},
|
||||
"ai": {
|
||||
"title": "AI режим",
|
||||
"prompt": "Опишите, что нужно извлечь",
|
||||
"example": "Например: извлеките все названия товаров, цены и артикулы",
|
||||
"generate": "Сгенерировать",
|
||||
"generating": "Генерация...",
|
||||
"apply": "Применить",
|
||||
"edit": "Редактировать",
|
||||
"model": "Модель AI",
|
||||
"temperature": "Температура",
|
||||
"maxTokens": "Макс. токенов",
|
||||
"error": {
|
||||
"generation_failed": "Не удалось сгенерировать",
|
||||
"api_error": "Ошибка API",
|
||||
"invalid_prompt": "Неверный промпт"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"title": "Настройка пагинации",
|
||||
"enabled": "Включить пагинацию",
|
||||
"type": "Тип",
|
||||
"button": "Кнопка",
|
||||
"scroll": "Прокрутка",
|
||||
"url": "URL паттерн",
|
||||
"nextButton": "Селектор кнопки 'Далее'",
|
||||
"maxPages": "Макс. страниц",
|
||||
"waitTime": "Время ожидания (мс)",
|
||||
"stopCondition": "Условие остановки",
|
||||
"noMorePages": "Нет больше страниц",
|
||||
"maxPagesReached": "Достигнут лимит страниц",
|
||||
"custom": "Настроить"
|
||||
},
|
||||
"captcha": {
|
||||
"title": "Обработка капчи",
|
||||
"enabled": "Включить решение капчи",
|
||||
"service": "Сервис",
|
||||
"twoCaptcha": "2Captcha",
|
||||
"antiCaptcha": "AntiCaptcha",
|
||||
"manual": "Вручную",
|
||||
"apiKey": "API ключ",
|
||||
"timeout": "Тайм-аут (сек)",
|
||||
"test": "Тестировать",
|
||||
"save": "Сохранить"
|
||||
},
|
||||
"proxy": {
|
||||
"title": "Настройки прокси",
|
||||
"enabled": "Использовать прокси",
|
||||
"type": "Тип",
|
||||
"http": "HTTP",
|
||||
"https": "HTTPS",
|
||||
"socks4": "SOCKS4",
|
||||
"socks5": "SOCKS5",
|
||||
"host": "Хост",
|
||||
"port": "Порт",
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"testConnection": "Проверить подключение",
|
||||
"success": "Прокси работает",
|
||||
"failed": "Ошибка подключения"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
"account": "Аккаунт",
|
||||
"profile": "Профиль",
|
||||
"security": "Безопасность",
|
||||
"notifications": "Уведомления",
|
||||
"api": "API ключи",
|
||||
"billing": "Биллинг",
|
||||
"theme": "Тема",
|
||||
"language": "Язык",
|
||||
"light": "Светлая",
|
||||
"dark": "Темная",
|
||||
"auto": "Авто",
|
||||
"save": "Сохранить",
|
||||
"cancel": "Отмена",
|
||||
"logout": "Выйти"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Профиль",
|
||||
"name": "Имя",
|
||||
"email": "Email",
|
||||
"company": "Компания",
|
||||
"website": "Веб-сайт",
|
||||
"avatar": "Аватар",
|
||||
"changeAvatar": "Изменить аватар",
|
||||
"save": "Сохранить",
|
||||
"updated": "Профиль обновлен"
|
||||
},
|
||||
"security": {
|
||||
"title": "Безопасность",
|
||||
"changePassword": "Изменить пароль",
|
||||
"currentPassword": "Текущий пароль",
|
||||
"newPassword": "Новый пароль",
|
||||
"confirmPassword": "Подтвердите пароль",
|
||||
"twoFactor": "Двухфакторная аутентификация",
|
||||
"enable2FA": "Включить 2FA",
|
||||
"disable2FA": "Отключить 2FA",
|
||||
"apiKeys": "API ключи",
|
||||
"generateKey": "Сгенерировать ключ",
|
||||
"revokeKey": "Отозвать ключ",
|
||||
"save": "Сохранить"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Уведомления",
|
||||
"email": "Email уведомления",
|
||||
"runComplete": "Завершение запуска",
|
||||
"runFailed": "Ошибка запуска",
|
||||
"dailyReport": "Ежедневный отчет",
|
||||
"weeklyReport": "Еженедельный отчет",
|
||||
"webhook": "Webhook уведомления",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"testWebhook": "Тестировать",
|
||||
"save": "Сохранить"
|
||||
},
|
||||
"common": {
|
||||
"save": "Сохранить",
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить",
|
||||
"edit": "Редактировать",
|
||||
"create": "Создать",
|
||||
"update": "Обновить",
|
||||
"close": "Закрыть",
|
||||
"back": "Назад",
|
||||
"next": "Далее",
|
||||
"previous": "Назад",
|
||||
"finish": "Завершить",
|
||||
"loading": "Загрузка...",
|
||||
"saving": "Сохранение...",
|
||||
"deleting": "Удаление...",
|
||||
"success": "Успешно",
|
||||
"error": "Ошибка",
|
||||
"warning": "Предупреждение",
|
||||
"info": "Информация",
|
||||
"confirm": "Подтвердить",
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
"ok": "ОК",
|
||||
"search": "Поиск",
|
||||
"filter": "Фильтр",
|
||||
"sort": "Сортировка",
|
||||
"export": "Экспорт",
|
||||
"import": "Импорт",
|
||||
"download": "Скачать",
|
||||
"upload": "Загрузить",
|
||||
"copy": "Копировать",
|
||||
"paste": "Вставить",
|
||||
"cut": "Вырезать",
|
||||
"select_all": "Выбрать все",
|
||||
"clear_all": "Очистить все",
|
||||
"refresh": "Обновить",
|
||||
"settings": "Настройки",
|
||||
"help": "Помощь",
|
||||
"about": "О программе",
|
||||
"version": "Версия",
|
||||
"copyright": " 2026 Dorod Parser"
|
||||
},
|
||||
"errors": {
|
||||
"generic": "Произошла ошибка",
|
||||
"network": "Ошибка сети",
|
||||
"timeout": "Превышен тайм-аут",
|
||||
"not_found": "Не найдено",
|
||||
"unauthorized": "Не авторизован",
|
||||
"forbidden": "Доступ запрещен",
|
||||
"server_error": "Ошибка сервера",
|
||||
"validation": "Ошибка валидации",
|
||||
"try_again": "Попробуйте снова"
|
||||
},
|
||||
"success": {
|
||||
"saved": "Сохранено",
|
||||
"deleted": "Удалено",
|
||||
"updated": "Обновлено",
|
||||
"created": "Создано",
|
||||
"sent": "Отправлено",
|
||||
"copied": "Скопировано"
|
||||
},
|
||||
"robotCreate": {
|
||||
"title": "Создать нового робота",
|
||||
"tabs": {
|
||||
"extract": "Извлечь",
|
||||
"scrape": "Скрейпинг",
|
||||
"crawl": "Обход",
|
||||
"search": "Поиск"
|
||||
},
|
||||
"chooseMode": "Выберите способ создания",
|
||||
"modes": {
|
||||
"recorder": {
|
||||
"title": "Режим записи",
|
||||
"description": "Запишите свои действия в рабочий процесс"
|
||||
},
|
||||
"ai": {
|
||||
"title": "AI режим",
|
||||
"description": "Опишите задачу. Он создаст её для вас",
|
||||
"label": "Beta"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"description": "Извлекайте структурированные данные с веб-сайтов используя AI или записывайте свой собственный процесс извлечения",
|
||||
"websiteUrl": "URL веб-сайта",
|
||||
"websiteUrlOptional": "URL веб-сайта (необязательно)",
|
||||
"websiteUrlPlaceholder": "Например: https://www.ycombinator.com/companies/",
|
||||
"startRecording": "Начать запись",
|
||||
"starting": "Запуск...",
|
||||
"name": "Название",
|
||||
"namePlaceholder": "Название",
|
||||
"aiPrompt": "Промпт для извлечения",
|
||||
"aiPromptPlaceholder": "Например: Извлечь первые 15 названий компаний, описания и информацию о партиях",
|
||||
"aiExample": "Например: 'Извлечь названия продуктов, цены и рейтинги'",
|
||||
"llmProvider": "LLM Провайдер",
|
||||
"llmProviderOllama": "Ollama (Локально)",
|
||||
"llmProviderAnthropic": "Anthropic (Claude)",
|
||||
"llmProviderOpenAI": "OpenAI (GPT-4)",
|
||||
"model": "Модель",
|
||||
"modelDefault": "По умолчанию (llama3.2-vision)",
|
||||
"ollamaBaseUrl": "Ollama Base URL (необязательно)",
|
||||
"generate": "Создать робота",
|
||||
"generating": "Создание...",
|
||||
"createAndRun": "Создать и запустить робота",
|
||||
"creatingAndRunning": "Создание и запуск...",
|
||||
"apiKey": "API ключ (необязательно, если установлен в .env)",
|
||||
"apiKeyPlaceholder": "API ключ"
|
||||
},
|
||||
"scrape": {
|
||||
"description": "Скрейпить весь контент страницы в различных форматах",
|
||||
"websiteUrl": "URL веб-сайта",
|
||||
"robotName": "Название робота (необязательно)",
|
||||
"outputFormats": "Форматы вывода",
|
||||
"createRobot": "Создать робота",
|
||||
"creating": "Создание..."
|
||||
},
|
||||
"crawl": {
|
||||
"description": "Обходить веб-сайт и извлекать данные с нескольких страниц",
|
||||
"websiteUrl": "URL веб-сайта",
|
||||
"robotName": "Название робота (необязательно)",
|
||||
"maxPages": "Максимум страниц для обхода",
|
||||
"maxDepth": "Максимальная глубина обхода",
|
||||
"includePaths": "Включить пути (через запятую, необязательно)",
|
||||
"excludePaths": "Исключить пути (через запятую, необязательно)",
|
||||
"createRobot": "Создать робота",
|
||||
"creating": "Создание...",
|
||||
"advancedOptions": "Дополнительные опции"
|
||||
},
|
||||
"search": {
|
||||
"description": "Искать информацию в интернете используя AI",
|
||||
"query": "Поисковой запрос",
|
||||
"queryPlaceholder": "О чём вы хотите узнать?",
|
||||
"robotName": "Название робота (необязательно)",
|
||||
"createRobot": "Создать поискового робота",
|
||||
"creating": "Создание..."
|
||||
},
|
||||
"errors": {
|
||||
"urlRequired": "URL обязателен",
|
||||
"queryRequired": "Поисковой запрос обязателен",
|
||||
"failedToStart": "Не удалось начать запись. Попробуйте снова",
|
||||
"failedToCreate": "Не удалось создать робота"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,13 @@ export class RemoteBrowser {
|
||||
private lastScrollPosition = { x: 0, y: 0 };
|
||||
private scrollThreshold = 200;
|
||||
|
||||
/**
|
||||
* Flag to indicate if this is a recording session (requires rrweb for real-time DOM streaming)
|
||||
* When false (robot run mode), rrweb is skipped to improve performance
|
||||
* @private
|
||||
*/
|
||||
private isRecordingMode: boolean = false;
|
||||
|
||||
// private memoryCleanupInterval: NodeJS.Timeout | null = null;
|
||||
// private memoryManagementInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
@@ -110,11 +117,12 @@ export class RemoteBrowser {
|
||||
* @param socket socket.io socket instance used to communicate with the client side
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(socket: Socket, userId: string, poolId: string) {
|
||||
public constructor(socket: Socket, userId: string, poolId: string, isRecordingMode: boolean = false) {
|
||||
this.socket = socket;
|
||||
this.userId = userId;
|
||||
this.interpreter = new WorkflowInterpreter(socket);
|
||||
this.generator = new WorkflowGenerator(socket, poolId);
|
||||
this.isRecordingMode = isRecordingMode;
|
||||
}
|
||||
|
||||
// private initializeMemoryManagement(): void {
|
||||
@@ -264,11 +272,13 @@ export class RemoteBrowser {
|
||||
}
|
||||
});
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
||||
logger.warn('[rrweb] Network idle timeout on navigation, proceeding with rrweb initialization');
|
||||
});
|
||||
if (this.isRecordingMode) {
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
||||
logger.warn('[rrweb] Network idle timeout on navigation, proceeding with rrweb initialization');
|
||||
});
|
||||
|
||||
await this.initializeRRWebRecording(page);
|
||||
await this.initializeRRWebRecording(page);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -298,8 +308,14 @@ export class RemoteBrowser {
|
||||
/**
|
||||
* Initialize rrweb recording for real-time DOM streaming
|
||||
* This replaces the snapshot-based approach with live event streaming
|
||||
* Only runs in recording mode - skipped for robot runs to improve performance
|
||||
*/
|
||||
private async initializeRRWebRecording(page: Page): Promise<void> {
|
||||
if (!this.isRecordingMode) {
|
||||
logger.debug('[rrweb] Skipping initialization - not in recording mode (robot run)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rrwebJsPath = require.resolve('rrweb/dist/rrweb.min.js');
|
||||
const rrwebScriptContent = readFileSync(rrwebJsPath, 'utf8');
|
||||
@@ -529,11 +545,13 @@ export class RemoteBrowser {
|
||||
|
||||
await this.setupPageEventListeners(this.currentPage);
|
||||
|
||||
await this.currentPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
||||
logger.warn('[rrweb] Network idle timeout, proceeding with rrweb initialization');
|
||||
});
|
||||
if (this.isRecordingMode) {
|
||||
await this.currentPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
|
||||
logger.warn('[rrweb] Network idle timeout, proceeding with rrweb initialization');
|
||||
});
|
||||
|
||||
await this.initializeRRWebRecording(this.currentPage);
|
||||
await this.initializeRRWebRecording(this.currentPage);
|
||||
}
|
||||
|
||||
try {
|
||||
const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const initializeRemoteBrowserForRecording = (userId: string, mode: string
|
||||
const remoteBrowser = browserPool.getRemoteBrowser(activeId);
|
||||
remoteBrowser?.updateSocket(socket);
|
||||
} else {
|
||||
const browserSession = new RemoteBrowser(socket, userId, id);
|
||||
const browserSession = new RemoteBrowser(socket, userId, id, true);
|
||||
browserSession.interpreter.subscribeToPausing();
|
||||
|
||||
try {
|
||||
|
||||
@@ -452,9 +452,42 @@ const handleClickAction = async (
|
||||
}
|
||||
|
||||
const { selector, url, elementInfo, coordinates, isSPA = false } = data;
|
||||
|
||||
if (page.isClosed()) {
|
||||
logger.log("debug", "Page is closed, cannot remove target attribute");
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorInfo = await page.evaluate(({ sel }) => {
|
||||
try {
|
||||
const element = document.querySelector(sel);
|
||||
if (element) {
|
||||
if (element.getAttribute('target') === '_blank') {
|
||||
element.removeAttribute('target');
|
||||
}
|
||||
|
||||
const parentAnchor = element.closest('a[target="_blank"]') as HTMLAnchorElement;
|
||||
if (parentAnchor) {
|
||||
parentAnchor.removeAttribute('target');
|
||||
}
|
||||
|
||||
const anchor = element.tagName === 'A' ? element as HTMLAnchorElement : element.closest('a') as HTMLAnchorElement;
|
||||
if (anchor && anchor.href) {
|
||||
return { hasAnchor: true, href: anchor.href };
|
||||
}
|
||||
}
|
||||
return { hasAnchor: false, href: null };
|
||||
} catch (e) {
|
||||
console.error('Error removing target attribute:', e);
|
||||
return { hasAnchor: false, href: null };
|
||||
}
|
||||
}, { sel: selector });
|
||||
|
||||
const currentUrl = page.url();
|
||||
|
||||
if (elementInfo && coordinates && (elementInfo.tagName === 'INPUT' || elementInfo.tagName === 'TEXTAREA')) {
|
||||
const isInputElement = elementInfo && (elementInfo.tagName === 'INPUT' || elementInfo.tagName === 'TEXTAREA');
|
||||
|
||||
if (isInputElement && coordinates) {
|
||||
try {
|
||||
const elementHandle = await page.$(selector);
|
||||
if (elementHandle) {
|
||||
@@ -483,7 +516,7 @@ const handleClickAction = async (
|
||||
|
||||
logger.log("debug", `Click action processed: ${selector}`);
|
||||
|
||||
if (elementInfo && (elementInfo.tagName === 'INPUT' || elementInfo.tagName === 'TEXTAREA')) {
|
||||
if (isInputElement) {
|
||||
logger.log("debug", `Input field click - skipping DOM snapshot for smooth typing`);
|
||||
return;
|
||||
}
|
||||
@@ -493,10 +526,35 @@ const handleClickAction = async (
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
} else {
|
||||
const newUrl = page.url();
|
||||
const hasNavigated = newUrl !== currentUrl && !newUrl.endsWith("/#");
|
||||
try {
|
||||
await page.waitForNavigation({ timeout: 1500 });
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (hasNavigated) {
|
||||
let newUrl = page.url();
|
||||
|
||||
if (anchorInfo.hasAnchor && anchorInfo.href) {
|
||||
try {
|
||||
const expectedUrl = new URL(anchorInfo.href);
|
||||
const actualUrl = new URL(newUrl);
|
||||
|
||||
const navigatedToExpectedPage =
|
||||
expectedUrl.origin === actualUrl.origin &&
|
||||
expectedUrl.pathname === actualUrl.pathname;
|
||||
|
||||
if (!navigatedToExpectedPage) {
|
||||
logger.log("debug", `Click did not navigate to expected URL, using page.goto as fallback`);
|
||||
await page.goto(anchorInfo.href, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
newUrl = page.url();
|
||||
}
|
||||
} catch (urlError: any) {
|
||||
logger.log("debug", `Error comparing URLs: ${urlError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const finalNavigated = newUrl !== currentUrl && !newUrl.endsWith("/#");
|
||||
|
||||
if (finalNavigated) {
|
||||
logger.log("debug", `Navigation detected: ${currentUrl} -> ${newUrl}`);
|
||||
|
||||
await generator.onDOMNavigation(page, {
|
||||
|
||||
@@ -235,8 +235,8 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
padding: '8px',
|
||||
marginRight: '20px',
|
||||
'&:hover': {
|
||||
background: 'none',
|
||||
color: 'inherit'
|
||||
background: 'inherit',
|
||||
color: '#0000008A'
|
||||
}
|
||||
}}>
|
||||
<Typography variant="body1">Browse Auto Robots</Typography>
|
||||
@@ -364,6 +364,14 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
>
|
||||
Türkçe
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("ru");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Русский
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
window.open('https://docs.maxun.dev/development/i18n', '_blank');
|
||||
@@ -468,6 +476,14 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
>
|
||||
Türkçe
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("ru");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Русский
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
window.open('https://docs.maxun.dev/development/i18n', '_blank');
|
||||
|
||||
@@ -648,20 +648,18 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInCaptureMode) {
|
||||
const wheelEvent = e as WheelEvent;
|
||||
const deltaX = Math.round(wheelEvent.deltaX / 10) * 10;
|
||||
const deltaY = Math.round(wheelEvent.deltaY / 10) * 10;
|
||||
const wheelEvent = e as WheelEvent;
|
||||
const deltaX = Math.round(wheelEvent.deltaX / 10) * 10;
|
||||
const deltaY = Math.round(wheelEvent.deltaY / 10) * 10;
|
||||
|
||||
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
|
||||
if (socket) {
|
||||
socket.emit("dom:scroll", {
|
||||
deltaX,
|
||||
deltaY,
|
||||
});
|
||||
}
|
||||
notifyLastAction("scroll");
|
||||
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
|
||||
if (socket) {
|
||||
socket.emit("dom:scroll", {
|
||||
deltaX,
|
||||
deltaY,
|
||||
});
|
||||
}
|
||||
notifyLastAction("scroll");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) =
|
||||
throw new Error("No workflow found");
|
||||
}
|
||||
}
|
||||
).catch((error) => { console.log(`Failed to fetch workflow:`,error.message) })
|
||||
).catch((error) => { console.log(`Failed to fetch workflow:`, error.message) })
|
||||
};
|
||||
|
||||
interface RightSidePanelProps {
|
||||
@@ -48,22 +48,22 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
} | null>(null);
|
||||
const autoDetectionRunRef = useRef<string | null>(null);
|
||||
|
||||
const { notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog, currentListActionId, setCurrentListActionId, currentTextActionId, setCurrentTextActionId, currentScreenshotActionId, setCurrentScreenshotActionId, isDOMMode, updateDOMMode, currentTextGroupName } = useGlobalInfoStore();
|
||||
const {
|
||||
getText, startGetText, stopGetText,
|
||||
getList, startGetList, stopGetList,
|
||||
getScreenshot, startGetScreenshot, stopGetScreenshot,
|
||||
startPaginationMode, stopPaginationMode,
|
||||
paginationType, updatePaginationType,
|
||||
limitType, customLimit, updateLimitType, updateCustomLimit,
|
||||
stopLimitMode, startLimitMode,
|
||||
captureStage, setCaptureStage,
|
||||
showPaginationOptions, setShowPaginationOptions,
|
||||
showLimitOptions, setShowLimitOptions,
|
||||
workflow, setWorkflow,
|
||||
activeAction, setActiveAction, finishAction
|
||||
const { notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog, currentListActionId, setCurrentListActionId, currentTextActionId, setCurrentTextActionId, currentScreenshotActionId, setCurrentScreenshotActionId, isDOMMode, updateDOMMode, currentTextGroupName } = useGlobalInfoStore();
|
||||
const {
|
||||
getText, startGetText, stopGetText,
|
||||
getList, startGetList, stopGetList,
|
||||
getScreenshot, startGetScreenshot, stopGetScreenshot,
|
||||
startPaginationMode, stopPaginationMode,
|
||||
paginationType, updatePaginationType,
|
||||
limitType, customLimit, updateLimitType, updateCustomLimit,
|
||||
stopLimitMode, startLimitMode,
|
||||
captureStage, setCaptureStage,
|
||||
showPaginationOptions, setShowPaginationOptions,
|
||||
showLimitOptions, setShowLimitOptions,
|
||||
workflow, setWorkflow,
|
||||
activeAction, setActiveAction, finishAction
|
||||
} = useActionContext();
|
||||
|
||||
|
||||
const { browserSteps, addScreenshotStep, updateListStepLimit, updateListStepPagination, deleteStepsByActionId, updateListStepData, updateScreenshotStepData, emitActionForStep } = useBrowserSteps();
|
||||
const { id, socket } = useSocketStore();
|
||||
const { t } = useTranslation();
|
||||
@@ -214,7 +214,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return () => {
|
||||
socket?.off('listDataExtracted');
|
||||
};
|
||||
@@ -223,16 +223,16 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
const handleDirectScreenshot = (data: any) => {
|
||||
const screenshotSteps = browserSteps.filter(step =>
|
||||
const screenshotSteps = browserSteps.filter(step =>
|
||||
step.type === 'screenshot' && step.actionId === currentScreenshotActionId
|
||||
);
|
||||
|
||||
|
||||
if (screenshotSteps.length > 0) {
|
||||
const latestStep = screenshotSteps[screenshotSteps.length - 1];
|
||||
const latestStep = screenshotSteps[screenshotSteps.length - 1];
|
||||
updateScreenshotStepData(latestStep.id, data.screenshot);
|
||||
emitActionForStep(latestStep);
|
||||
}
|
||||
|
||||
|
||||
setCurrentScreenshotActionId('');
|
||||
};
|
||||
|
||||
@@ -288,11 +288,11 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
iframeDoc,
|
||||
listSelector,
|
||||
fields,
|
||||
5
|
||||
5
|
||||
);
|
||||
|
||||
updateListStepData(currentListId, extractedData);
|
||||
|
||||
|
||||
if (extractedData.length === 0) {
|
||||
console.warn("⚠️ No data extracted - this might indicate selector issues");
|
||||
notify("warning", "No data was extracted. Please verify your selections.");
|
||||
@@ -355,14 +355,14 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
notify('error', t('right_panel.errors.no_text_captured'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
stopGetText();
|
||||
if (currentTextActionStep) {
|
||||
emitActionForStep(currentTextActionStep);
|
||||
}
|
||||
setCurrentTextActionId('');
|
||||
resetInterpretationLog();
|
||||
finishAction('text');
|
||||
finishAction('text');
|
||||
onFinishCapture();
|
||||
clientSelectorGenerator.cleanup();
|
||||
}, [stopGetText, socket, browserSteps, resetInterpretationLog, finishAction, notify, onFinishCapture, t, currentTextActionId, currentTextGroupName, emitActionForStep]);
|
||||
@@ -453,7 +453,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
const getLatestListStep = (steps: BrowserStep[]) => {
|
||||
const listSteps = steps.filter(step => step.type === 'list');
|
||||
if (listSteps.length === 0) return null;
|
||||
|
||||
|
||||
return listSteps.sort((a, b) => b.id - a.id)[0];
|
||||
};
|
||||
|
||||
@@ -466,12 +466,12 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
step.listSelector &&
|
||||
Object.keys(step.fields).length > 0
|
||||
);
|
||||
|
||||
|
||||
if (!hasValidListSelectorForCurrentAction) {
|
||||
notify('error', t('right_panel.errors.capture_list_first'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const currentListStepForAutoDetect = browserSteps.find(
|
||||
step => step.type === 'list' && step.actionId === currentListActionId
|
||||
) as (BrowserStep & { type: 'list'; listSelector?: string }) | undefined;
|
||||
@@ -657,7 +657,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
notify('error', t('right_panel.errors.select_pagination'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const currentListStepForPagination = browserSteps.find(
|
||||
step => step.type === 'list' && step.actionId === currentListActionId
|
||||
) as (BrowserStep & { type: 'list' }) | undefined;
|
||||
@@ -728,15 +728,15 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
|
||||
const discardGetText = useCallback(() => {
|
||||
stopGetText();
|
||||
|
||||
if (currentTextActionId) {
|
||||
|
||||
if (currentTextActionId) {
|
||||
deleteStepsByActionId(currentTextActionId);
|
||||
|
||||
|
||||
if (socket) {
|
||||
socket.emit('removeAction', { actionId: currentTextActionId });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setCurrentTextActionId('');
|
||||
clientSelectorGenerator.cleanup();
|
||||
notify('error', t('right_panel.errors.capture_text_discarded'));
|
||||
@@ -744,10 +744,10 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
|
||||
const discardGetList = useCallback(() => {
|
||||
stopGetList();
|
||||
|
||||
|
||||
if (currentListActionId) {
|
||||
deleteStepsByActionId(currentListActionId);
|
||||
|
||||
|
||||
if (socket) {
|
||||
socket.emit('removeAction', { actionId: currentListActionId });
|
||||
}
|
||||
@@ -792,7 +792,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resetListState();
|
||||
stopPaginationMode();
|
||||
stopLimitMode();
|
||||
@@ -808,7 +808,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
const captureScreenshot = (fullPage: boolean) => {
|
||||
const screenshotCount = browserSteps.filter(s => s.type === 'screenshot').length + 1;
|
||||
const screenshotName = `Screenshot ${screenshotCount}`;
|
||||
|
||||
|
||||
const screenshotSettings = {
|
||||
fullPage,
|
||||
type: 'png' as const,
|
||||
@@ -819,7 +819,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
name: screenshotName,
|
||||
actionId: currentScreenshotActionId
|
||||
};
|
||||
socket?.emit('captureDirectScreenshot', screenshotSettings);
|
||||
socket?.emit('captureDirectScreenshot', screenshotSettings);
|
||||
addScreenshotStep(fullPage, currentScreenshotActionId);
|
||||
stopGetScreenshot();
|
||||
resetInterpretationLog();
|
||||
@@ -838,8 +838,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
{!isAnyActionActive && (
|
||||
<>
|
||||
{showCaptureList && (
|
||||
<Button
|
||||
variant="contained"
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleStartGetList}
|
||||
>
|
||||
{t('right_panel.buttons.capture_list')}
|
||||
@@ -847,8 +847,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
)}
|
||||
|
||||
{showCaptureText && (
|
||||
<Button
|
||||
variant="contained"
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleStartGetText}
|
||||
>
|
||||
{t('right_panel.buttons.capture_text')}
|
||||
@@ -856,8 +856,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
)}
|
||||
|
||||
{showCaptureScreenshot && (
|
||||
<Button
|
||||
variant="contained"
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleStartGetScreenshot}
|
||||
>
|
||||
{t('right_panel.buttons.capture_screenshot')}
|
||||
@@ -909,25 +909,25 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
{showPaginationOptions && (
|
||||
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
|
||||
<Typography>{t('right_panel.pagination.title')}</Typography>
|
||||
|
||||
|
||||
{autoDetectedPagination && autoDetectedPagination.type !== '' && (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 1,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: isDarkMode ? '#1a3a1a' : '#e8f5e9',
|
||||
border: `1px solid ${isDarkMode ? '#2e7d32' : '#4caf50'}`,
|
||||
color: '#1E2124',
|
||||
backgroundColor: isDarkMode ? '#f4f6f4' : '#f4f6f4',
|
||||
border: `1px solid ${isDarkMode ? '#f4f6f4' : '#f4f6f4'}`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: isDarkMode ? '#81c784' : '#2e7d32',
|
||||
fontWeight: 'bold',
|
||||
mb: 0.5
|
||||
}}
|
||||
@@ -943,7 +943,6 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: isDarkMode ? '#a5d6a7' : '#388e3c',
|
||||
display: 'block',
|
||||
mb: 1
|
||||
}}
|
||||
@@ -1008,11 +1007,11 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
color: isDarkMode ? '#81c784' : '#2e7d32',
|
||||
borderColor: isDarkMode ? '#81c784' : '#2e7d32',
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
'&:hover': {
|
||||
borderColor: isDarkMode ? '#a5d6a7' : '#4caf50',
|
||||
backgroundColor: isDarkMode ? '#1a3a1a' : '#f1f8f4',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: '#f4f6f4 !important',
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -1072,15 +1071,15 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
{t('right_panel.pagination.none')}</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{showLimitOptions && (
|
||||
<FormControl>
|
||||
<Typography variant="h6" sx={{
|
||||
fontSize: '16px',
|
||||
<Typography variant="h6" sx={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
mb: 1,
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word'
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word'
|
||||
}}>
|
||||
{t('right_panel.limit.title')}
|
||||
</Typography>
|
||||
@@ -1134,7 +1133,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{getText && (
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
|
||||
@@ -1164,7 +1163,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{getScreenshot && (
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Button variant="contained" onClick={() => captureScreenshot(true)}>
|
||||
|
||||
@@ -276,7 +276,7 @@ const RobotCreate: React.FC = () => {
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Typography variant="h5" component="h1">
|
||||
Create New Robot
|
||||
{t('robotCreate.title')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -299,10 +299,10 @@ const RobotCreate: React.FC = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab label="Extract" id="extract-robot" aria-controls="extract-robot" />
|
||||
<Tab label="Scrape" id="scrape-robot" aria-controls="scrape-robot" />
|
||||
<Tab label="Crawl" id="crawl-robot" aria-controls="crawl-robot" />
|
||||
<Tab label="Search" id="search-robot" aria-controls="search-robot" />
|
||||
<Tab label={t('robotCreate.tabs.extract')} id="extract-robot" aria-controls="extract-robot" />
|
||||
<Tab label={t('robotCreate.tabs.scrape')} id="scrape-robot" aria-controls="scrape-robot" />
|
||||
<Tab label={t('robotCreate.tabs.crawl')} id="crawl-robot" aria-controls="crawl-robot" />
|
||||
<Tab label={t('robotCreate.tabs.search')} id="search-robot" aria-controls="search-robot" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -321,11 +321,11 @@ const RobotCreate: React.FC = () => {
|
||||
/>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" mb={3}>
|
||||
Extract structured data from websites using AI or record your own extraction workflow.
|
||||
{t('robotCreate.extract.description')}
|
||||
</Typography>
|
||||
<Box sx={{ width: '100%', maxWidth: 700, mb: 3 }}>
|
||||
<Typography variant="subtitle1" gutterBottom sx={{ mb: 2 }} color="text.secondary">
|
||||
Choose How to Build
|
||||
{t('robotCreate.chooseMode')}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
@@ -345,10 +345,10 @@ const RobotCreate: React.FC = () => {
|
||||
<CardContent sx={{ textAlign: 'center', py: 3, color:"text.secondary" }}>
|
||||
<HighlightAlt sx={{ fontSize: 32, mb: 1 }} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Recorder Mode
|
||||
{t('robotCreate.modes.recorder.title')}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Record your actions into a workflow.
|
||||
{t('robotCreate.modes.recorder.description')}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -380,16 +380,16 @@ const RobotCreate: React.FC = () => {
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
Beta
|
||||
</Box>
|
||||
{t('robotCreate.modes.ai.label')}
|
||||
</Box>
|
||||
|
||||
<CardContent sx={{ textAlign: 'center', py: 3, color:"text.secondary" }}>
|
||||
<AutoAwesome sx={{ fontSize: 32, mb: 1 }} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
AI Mode
|
||||
{t('robotCreate.modes.ai.title')}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Describe the task. It builds it for you.
|
||||
{t('robotCreate.modes.ai.description')}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -399,45 +399,45 @@ const RobotCreate: React.FC = () => {
|
||||
<Box sx={{ width: '100%', maxWidth: 700 }}>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<TextField
|
||||
placeholder="Name"
|
||||
placeholder={t('robotCreate.extract.namePlaceholder')}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={extractRobotName}
|
||||
onChange={(e) => setExtractRobotName(e.target.value)}
|
||||
label="Name"
|
||||
label={t('robotCreate.extract.name')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<TextField
|
||||
placeholder="Example: Extract first 15 company names, descriptions, and batch information"
|
||||
placeholder={t('robotCreate.extract.aiPromptPlaceholder')}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
value={aiPrompt}
|
||||
onChange={(e) => setAiPrompt(e.target.value)}
|
||||
label="Extraction Prompt"
|
||||
label={t('robotCreate.extract.aiPrompt')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<TextField
|
||||
placeholder="Example: https://www.ycombinator.com/companies/"
|
||||
placeholder={t('robotCreate.extract.websiteUrlPlaceholder')}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
label="Website URL (Optional)"
|
||||
label={t('robotCreate.extract.websiteUrlOptional')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||
<FormControl sx={{ flex: 1 }}>
|
||||
<InputLabel>LLM Provider</InputLabel>
|
||||
<InputLabel>{t('robotCreate.extract.llmProvider')}</InputLabel>
|
||||
<Select
|
||||
value={llmProvider}
|
||||
label="LLM Provider"
|
||||
label={t('robotCreate.extract.llmProvider')}
|
||||
onChange={(e) => {
|
||||
const provider = e.target.value as 'anthropic' | 'openai' | 'ollama';
|
||||
setLlmProvider(provider);
|
||||
@@ -449,22 +449,22 @@ const RobotCreate: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="ollama">Ollama (Local)</MenuItem>
|
||||
<MenuItem value="anthropic">Anthropic (Claude)</MenuItem>
|
||||
<MenuItem value="openai">OpenAI (GPT-4)</MenuItem>
|
||||
<MenuItem value="ollama">{t('robotCreate.extract.llmProviderOllama')}</MenuItem>
|
||||
<MenuItem value="anthropic">{t('robotCreate.extract.llmProviderAnthropic')}</MenuItem>
|
||||
<MenuItem value="openai">{t('robotCreate.extract.llmProviderOpenAI')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl sx={{ flex: 1 }}>
|
||||
<InputLabel>Model</InputLabel>
|
||||
<InputLabel>{t('robotCreate.extract.model')}</InputLabel>
|
||||
<Select
|
||||
value={llmModel}
|
||||
label="Model"
|
||||
label={t('robotCreate.extract.model')}
|
||||
onChange={(e) => setLlmModel(e.target.value)}
|
||||
>
|
||||
{llmProvider === 'ollama' ? (
|
||||
[
|
||||
<MenuItem key="default" value="default">Default (llama3.2-vision)</MenuItem>,
|
||||
<MenuItem key="default" value="default">{t('robotCreate.extract.modelDefault')}</MenuItem>,
|
||||
<MenuItem key="llama3.2-vision" value="llama3.2-vision">llama3.2-vision</MenuItem>,
|
||||
<MenuItem key="llama3.2" value="llama3.2">llama3.2</MenuItem>
|
||||
]
|
||||
@@ -495,7 +495,7 @@ const RobotCreate: React.FC = () => {
|
||||
type="password"
|
||||
value={llmApiKey}
|
||||
onChange={(e) => setLlmApiKey(e.target.value)}
|
||||
label="API Key (Optional if set in .env)"
|
||||
label={t('robotCreate.extract.apiKey')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -508,7 +508,7 @@ const RobotCreate: React.FC = () => {
|
||||
fullWidth
|
||||
value={llmBaseUrl}
|
||||
onChange={(e) => setLlmBaseUrl(e.target.value)}
|
||||
label="Ollama Base URL (Optional)"
|
||||
label={t('robotCreate.extract.ollamaBaseUrl')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -626,7 +626,7 @@ const RobotCreate: React.FC = () => {
|
||||
}}
|
||||
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null}
|
||||
>
|
||||
{isLoading ? 'Creating & Running...' : 'Create & Run Robot'}
|
||||
{isLoading ? t('robotCreate.extract.creatingAndRunning') : t('robotCreate.extract.createAndRun')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
@@ -635,12 +635,12 @@ const RobotCreate: React.FC = () => {
|
||||
<>
|
||||
<Box sx={{ width: '100%', maxWidth: 700, mb: 3 }}>
|
||||
<TextField
|
||||
placeholder="Example: https://www.ycombinator.com/companies/"
|
||||
placeholder={t('robotCreate.extract.websiteUrlPlaceholder')}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
label="Website URL"
|
||||
label={t('robotCreate.extract.websiteUrl')}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ width: '100%', maxWidth: 700 }}>
|
||||
@@ -658,7 +658,7 @@ const RobotCreate: React.FC = () => {
|
||||
}}
|
||||
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null}
|
||||
>
|
||||
{isLoading ? 'Starting...' : 'Start Recording'}
|
||||
{isLoading ? t('robotCreate.extract.starting') : t('robotCreate.extract.startRecording')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -483,31 +483,51 @@ class ClientSelectorGenerator {
|
||||
let grouped = false;
|
||||
|
||||
for (let level = 1; level <= this.groupingConfig.maxParentLevels && !grouped; level++) {
|
||||
let ancestor: HTMLElement | null = currentGroup[0];
|
||||
for (let i = 0; i < level && ancestor; i++) {
|
||||
ancestor = ancestor.parentElement;
|
||||
}
|
||||
const ancestorBuckets = new Map<HTMLElement, HTMLElement[]>();
|
||||
|
||||
if (!ancestor) break;
|
||||
|
||||
const allShareAncestor = currentGroup.every(el => {
|
||||
for (const el of currentGroup) {
|
||||
let elAncestor: HTMLElement | null = el;
|
||||
for (let i = 0; i < level && elAncestor; i++) {
|
||||
elAncestor = elAncestor.parentElement;
|
||||
}
|
||||
return elAncestor === ancestor;
|
||||
});
|
||||
if (elAncestor) {
|
||||
const bucket = ancestorBuckets.get(elAncestor) || [];
|
||||
bucket.push(el);
|
||||
ancestorBuckets.set(elAncestor, bucket);
|
||||
}
|
||||
}
|
||||
|
||||
if (allShareAncestor) {
|
||||
let bestBucket: HTMLElement[] | null = null;
|
||||
for (const bucket of ancestorBuckets.values()) {
|
||||
if (bucket.length >= this.groupingConfig.minGroupSize) {
|
||||
const containsPivot = bucket.includes(element);
|
||||
const bestContainsPivot = bestBucket ? bestBucket.includes(element) : false;
|
||||
|
||||
if (!bestBucket) {
|
||||
bestBucket = bucket;
|
||||
} else if (containsPivot && !bestContainsPivot) {
|
||||
bestBucket = bucket;
|
||||
} else if (containsPivot === bestContainsPivot && bucket.length > bestBucket.length) {
|
||||
bestBucket = bucket;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestBucket) {
|
||||
const group: ElementGroup = {
|
||||
elements: currentGroup,
|
||||
elements: bestBucket,
|
||||
fingerprint,
|
||||
representative: element,
|
||||
};
|
||||
currentGroup.forEach((el) => {
|
||||
bestBucket.forEach((el) => {
|
||||
this.elementGroups.set(el, group);
|
||||
this.groupedElements.add(el);
|
||||
});
|
||||
for (const el of currentGroup) {
|
||||
if (!bestBucket.includes(el)) {
|
||||
processedElements.delete(el);
|
||||
}
|
||||
}
|
||||
grouped = true;
|
||||
}
|
||||
}
|
||||
@@ -1008,11 +1028,11 @@ class ClientSelectorGenerator {
|
||||
const result: HTMLElement[] = [];
|
||||
|
||||
for (const element of groupedElements) {
|
||||
const hasGroupedChild = groupedElements.some(
|
||||
const containsGroupedChild = groupedElements.some(
|
||||
(other) => other !== element && element.contains(other)
|
||||
);
|
||||
|
||||
if (hasGroupedChild) {
|
||||
if (!containsGroupedChild) {
|
||||
result.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ i18n
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
debug: import.meta.env.DEV,
|
||||
supportedLngs: ['en', 'es', 'ja', 'zh','de', 'tr'],
|
||||
supportedLngs: ['en', 'es', 'ja', 'zh','de', 'tr', 'ru'],
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes
|
||||
},
|
||||
|
||||
@@ -11,9 +11,14 @@ export default defineConfig(() => {
|
||||
'import.meta.env.VITE_BACKEND_URL': JSON.stringify(process.env.VITE_BACKEND_URL),
|
||||
'import.meta.env.VITE_PUBLIC_URL': JSON.stringify(publicUrl),
|
||||
},
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [], // Empty plugins array - NO PostCSS processing
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: new URL(publicUrl).hostname,
|
||||
port: parseInt(new URL(publicUrl).port),
|
||||
host: '0.0.0.0', // Listen on all interfaces for Docker
|
||||
port: 5174,
|
||||
},
|
||||
build: {
|
||||
outDir: 'build',
|
||||
|
||||
Reference in New Issue
Block a user