ncolex commited on
Commit
c1f04cf
·
verified ·
1 Parent(s): 8d99b85

Deploy BrowserPilot with NumPy fix (2.2.6)

Browse files
Files changed (46) hide show
  1. .dockerignore +27 -0
  2. .env.test +1 -0
  3. .github/workflows/README.md +58 -0
  4. .github/workflows/docker-build.yml +51 -0
  5. .github/workflows/keepalive.yml +50 -0
  6. .gitignore +31 -0
  7. DEPLOY_HUGGINGFACE.md +105 -0
  8. DEPLOY_RENDER.md +177 -0
  9. Dockerfile +80 -0
  10. KEEPALIVE.md +44 -0
  11. README.docker.md +201 -0
  12. README.md +233 -10
  13. README_HF.md +30 -0
  14. SETUP_COMPLETE.md +118 -0
  15. TELEGRAM_BOT.md +162 -0
  16. docker-compose.prod.yml +30 -0
  17. docker-compose.yml +35 -0
  18. entrypoint.sh +30 -0
  19. frontend/.gitignore +25 -0
  20. frontend/README.md +1 -0
  21. frontend/eslint.config.js +28 -0
  22. frontend/index.html +17 -0
  23. frontend/package-lock.json +0 -0
  24. frontend/package.json +33 -0
  25. frontend/postcss.config.js +6 -0
  26. frontend/src/App.tsx +8 -0
  27. frontend/src/components/BrowserPilotDashboard.tsx +189 -0
  28. frontend/src/components/DecisionLog.tsx +78 -0
  29. frontend/src/components/Header.tsx +96 -0
  30. frontend/src/components/JobForm.tsx +297 -0
  31. frontend/src/components/ProxyStats.tsx +67 -0
  32. frontend/src/components/ScreenshotGallery.tsx +105 -0
  33. frontend/src/components/StatusDisplay.tsx +93 -0
  34. frontend/src/components/StreamingViewer.tsx +232 -0
  35. frontend/src/components/TokenUsage.tsx +67 -0
  36. frontend/src/index.css +386 -0
  37. frontend/src/main.tsx +10 -0
  38. frontend/src/services/WebSocketManager.ts +178 -0
  39. frontend/src/vite-env.d.ts +1 -0
  40. frontend/tailwind.config.js +72 -0
  41. frontend/tsconfig.app.json +24 -0
  42. frontend/tsconfig.json +7 -0
  43. frontend/tsconfig.node.json +22 -0
  44. frontend/vite.config.ts +10 -0
  45. render.yaml +24 -0
  46. requirements.txt +18 -0
.dockerignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ *.md
4
+ !README.md
5
+ .env
6
+ .env.*
7
+ __pycache__
8
+ *.pyc
9
+ .pytest_cache
10
+ .coverage
11
+ *.egg-info
12
+ dist
13
+ build
14
+ node_modules
15
+ frontend/node_modules
16
+ frontend/dist
17
+ *.log
18
+ .DS_Store
19
+ Thumbs.db
20
+ .vscode
21
+ .idea
22
+ *.swp
23
+ *.swo
24
+ *~
25
+ .cache
26
+ outputs/*
27
+ !outputs/.gitkeep
.env.test ADDED
@@ -0,0 +1 @@
 
 
1
+ GOOGLE_API_KEY=dummy_key_for_test
.github/workflows/README.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GitHub Actions Workflows
2
+
3
+ ## Docker Build (`docker-build.yml`)
4
+
5
+ This workflow automatically builds and publishes the BrowserPilot Docker image when changes are merged to the main branch.
6
+
7
+ ### Triggers
8
+ - **Push to main branch**: When code is merged to main, specifically when these files change:
9
+ - `Dockerfile`
10
+ - `docker-compose*.yml`
11
+ - `backend/**`
12
+ - `frontend/**`
13
+ - `requirements.txt`
14
+ - **Pull Requests**: Builds (but doesn't push) images for PRs to test the build process
15
+ - **Manual trigger**: Can be run manually from the GitHub Actions tab
16
+
17
+ ### What it does
18
+ 1. **Builds the Docker image** using the multi-stage Dockerfile
19
+ 2. **Publishes to GitHub Container Registry** (`ghcr.io`) with multiple tags:
20
+ - `latest` (for main branch)
21
+ - `main` (branch name)
22
+ - `main-<sha>` (commit SHA)
23
+ 3. **Multi-architecture support**: Builds for both `linux/amd64` and `linux/arm64`
24
+ 4. **Caching**: Uses GitHub Actions cache for faster builds
25
+ 5. **Security**: Generates build attestations for supply chain security
26
+
27
+ ### Published Images
28
+ The Docker images are published to:
29
+ - `ghcr.io/veverkap/browserpilot:latest`
30
+ - `ghcr.io/veverkap/browserpilot:main`
31
+ - `ghcr.io/veverkap/browserpilot:main-<commit-sha>`
32
+
33
+ ### Usage
34
+ Users can pull and run the published images:
35
+
36
+ ```bash
37
+ # Pull the latest image
38
+ docker pull ghcr.io/veverkap/browserpilot:latest
39
+
40
+ # Run with Docker
41
+ docker run -p 8000:8000 -e GOOGLE_API_KEY=your_key ghcr.io/veverkap/browserpilot:latest
42
+
43
+ # Or use in docker-compose.yml
44
+ services:
45
+ browserpilot:
46
+ image: ghcr.io/veverkap/browserpilot:latest
47
+ # ... other config
48
+ ```
49
+
50
+ ### Permissions Required
51
+ The workflow needs the following permissions (automatically granted):
52
+ - `contents: read` - To checkout the repository
53
+ - `packages: write` - To publish to GitHub Container Registry
54
+
55
+ ### Optional: Docker Hub Integration
56
+ The workflow includes an optional step to update Docker Hub descriptions if you want to also publish to Docker Hub. To enable this:
57
+ 1. Add `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` secrets to your repository
58
+ 2. The workflow will automatically update the Docker Hub repository description using `README.docker.md`
.github/workflows/docker-build.yml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Build and Publish Docker Image
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ workflow_dispatch:
8
+
9
+ env:
10
+ REGISTRY: ghcr.io
11
+ IMAGE_NAME: ${{ github.repository }}
12
+
13
+ jobs:
14
+ build-and-publish:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+ packages: write
19
+
20
+ steps:
21
+ - name: Checkout repository
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Set up Docker Buildx
25
+ uses: docker/setup-buildx-action@v3
26
+
27
+ - name: Log in to the Container Registry
28
+ uses: docker/login-action@v3
29
+ with:
30
+ registry: ${{ env.REGISTRY }}
31
+ username: ${{ github.actor }}
32
+ password: ${{ secrets.GITHUB_TOKEN }}
33
+
34
+ - name: Extract metadata (tags, labels) for Docker
35
+ id: meta
36
+ uses: docker/metadata-action@v5
37
+ with:
38
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
39
+ tags: |
40
+ type=ref,event=branch
41
+ type=sha,prefix={{branch}}-
42
+ type=raw,value=latest,enable={{is_default_branch}}
43
+
44
+ - name: Build and push Docker image
45
+ uses: docker/build-push-action@v5
46
+ with:
47
+ context: .
48
+ platforms: linux/amd64,linux/arm64
49
+ push: true
50
+ tags: ${{ steps.meta.outputs.tags }}
51
+ labels: ${{ steps.meta.outputs.labels }}
.github/workflows/keepalive.yml ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Keep HF Space Awake
2
+
3
+ on:
4
+ schedule:
5
+ # Runs every 13 minutes
6
+ - cron: '*/13 * * * *'
7
+ workflow_dispatch: # Allow manual trigger
8
+
9
+ jobs:
10
+ keepalive:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Ping Hugging Face Space
14
+ id: ping
15
+ run: |
16
+ echo "🔍 Pinging Hugging Face Space..."
17
+ RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "${{ secrets.HF_SPACE_URL }}")
18
+ echo "✅ Response status: $RESPONSE"
19
+ echo "status_code=$RESPONSE" >> $GITHUB_OUTPUT
20
+
21
+ if [ "$RESPONSE" -ge 200 ] && [ "$RESPONSE" -lt 400 ]; then
22
+ echo "✅ Space is awake!"
23
+ echo "success=true" >> $GITHUB_OUTPUT
24
+ exit 0
25
+ else
26
+ echo "❌ Space returned status: $RESPONSE"
27
+ echo "success=false" >> $GITHUB_OUTPUT
28
+ exit 1
29
+ fi
30
+ env:
31
+ HF_SPACE_URL: ${{ secrets.HF_SPACE_URL }}
32
+
33
+ - name: Notify Telegram on failure
34
+ if: failure()
35
+ run: |
36
+ echo "📧 Sending Telegram alert..."
37
+ curl -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
38
+ -d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
39
+ -d "text=⚠️ <b>KeepAlive Alert</b>%0A%0A🔴 HF Space health check failed!%0A<b>Status:</b> ${{ steps.ping.outputs.status_code }}%0A%0AThe Space might be sleeping or down." \
40
+ -d "parse_mode=HTML"
41
+
42
+ - name: Notify Telegram on success (after previous failure)
43
+ if: success()
44
+ run: |
45
+ echo "📧 Sending Telegram success notification..."
46
+ curl -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
47
+ -d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
48
+ -d "text=✅ <b>KeepAlive OK</b>%0A%0A🟢 HF Space health check passed!%0A<b>Status:</b> ${{ steps.ping.outputs.status_code }}%0A%0AThe Space is awake and running." \
49
+ -d "parse_mode=HTML"
50
+
.gitignore ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ venv
2
+ .env
3
+ __pycache__
4
+ *.pyc
5
+ outputs
6
+
7
+ # Logs
8
+ logs
9
+ *.log
10
+ npm-debug.log*
11
+ yarn-debug.log*
12
+ yarn-error.log*
13
+ pnpm-debug.log*
14
+ lerna-debug.log*
15
+
16
+ node_modules
17
+ dist
18
+ dist-ssr
19
+ *.local
20
+
21
+ # Editor directories and files
22
+ .vscode/*
23
+ !.vscode/extensions.json
24
+ .idea
25
+ .DS_Store
26
+ *.suo
27
+ *.ntvs*
28
+ *.njsproj
29
+ *.sln
30
+ *.sw?
31
+ .env
DEPLOY_HUGGINGFACE.md ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces Deployment
2
+
3
+ ## Deploy en Hugging Face Spaces (100% GRATIS)
4
+
5
+ ### Paso 1: Crear cuenta en Hugging Face
6
+
7
+ 1. Ir a https://huggingface.co
8
+ 2. Click en "Sign Up" (usá GitHub para más facilidad)
9
+
10
+ ### Paso 2: Crear un nuevo Space
11
+
12
+ 1. Click en tu avatar → **"New Space"**
13
+ 2. Configurar el Space:
14
+ - **Space name**: `browserpilot` (o el nombre que quieras)
15
+ - **License**: MIT
16
+ - **SDK**: **Docker**
17
+ - **Visibility**: Public (o Private si querés)
18
+ 3. Click en **"Create Space"**
19
+
20
+ ### Paso 3: Conectar el repositorio de GitHub
21
+
22
+ 1. En el dashboard del Space, vas a ver **"Files"**
23
+ 2. Click en **"Import from GitHub"** (o **"Add file"** → **"Import from GitHub"**)
24
+ 3. Autorizá Hugging Face a acceder a tu GitHub
25
+ 4. Seleccioná el repositorio: `ncolex/33BrowserPilot`
26
+ 5. Click en **"Import"**
27
+
28
+ ### Paso 4: Configurar las Variables de Entorno
29
+
30
+ 1. En el Space, andá a **"Settings"** (pestaña de arriba)
31
+ 2. Bajá hasta **"Variables and secrets"**
32
+ 3. Agregá las siguientes variables:
33
+
34
+ | Type | Key | Value |
35
+ |------|-----|-------|
36
+ | Secret | `GOOGLE_API_KEY` | `AIzaSyBDl98MXre7ecN9jW16EVltBlaf38awkdo` |
37
+ | Secret | `DATABASE_URL` | `postgresql://neondb_owner:npg_obFVM76KLule@ep-twilight-field-aiwca4xu.c-4.us-east-1.aws.neon.tech/neondb?sslmode=require` |
38
+ | Variable | `PYTHONUNBUFFERED` | `1` |
39
+
40
+ **Para agregar cada una:**
41
+ - Click en **"New secret"** o **"New variable"**
42
+ - Completá el nombre y valor
43
+ - Click en **"Save"**
44
+
45
+ ### Paso 5: Esperar el deploy
46
+
47
+ 1. Hugging Face va a empezar a construir el Docker automáticamente
48
+ 2. El estado va a cambiar: `Building` → `Running`
49
+ 3. Puede tardar 5-10 minutos la primera vez
50
+
51
+ ### Paso 6: ¡Listo!
52
+
53
+ Una vez que diga **"Running"**, tu BrowserPilot está disponible en:
54
+
55
+ ```
56
+ https://huggingface.co/spaces/tu-usuario/browserpilot
57
+ ```
58
+
59
+ ## Notas Importantes
60
+
61
+ ⚠️ **Recursos limitados**: Los Spaces free tienen 2 vCPU y 16GB RAM, pero son compartidos.
62
+
63
+ ⚠️ **Se duerme**: Después de ~48 horas de inactividad, el Space se duerme. El primer request puede tardar 1-2 minutos.
64
+
65
+ ⚠️ **Sin GPU**: El plan free no incluye GPU, solo CPU.
66
+
67
+ ⚠️ **Storage temporal**: Los archivos en `/app/outputs` se pierden cuando el Space se reinicia. Para storage persistente, necesitás usar Hugging Face Datasets o un servicio externo.
68
+
69
+ ## Actualizar el Space
70
+
71
+ Cada vez que hagas push a GitHub:
72
+
73
+ ```bash
74
+ cd /home/ncx/BROPILOT/33BrowserPilot
75
+ git push origin main
76
+ ```
77
+
78
+ Hugging Face va a detectar los cambios y redeployar automáticamente.
79
+
80
+ ## Troubleshooting
81
+
82
+ ### El Space no arranca
83
+ - Revisá los logs en la pestaña **"Logs"**
84
+ - Verificá que la `GOOGLE_API_KEY` esté configurada
85
+ - Checkeá que el Dockerfile sea correcto
86
+
87
+ ### Error de memoria
88
+ - El Space free tiene límites de memoria
89
+ - Intentá reducir el uso de memoria en el código
90
+ - Considerá upgrade al plan Pro ($9/mes) si necesitás más recursos
91
+
92
+ ### Los archivos no persisten
93
+ - Los Spaces no tienen storage persistente en el plan free
94
+ - Para guardar archivos permanentemente, usá:
95
+ - Hugging Face Datasets
96
+ - Un servicio externo (S3, Google Drive, etc.)
97
+ - La base de datos Neon para metadata
98
+
99
+ ## Opcional: Agregar storage persistente
100
+
101
+ Para guardar los outputs permanentemente, podés usar Hugging Face Datasets:
102
+
103
+ 1. Crear un Dataset en Hugging Face
104
+ 2. Montarlo en el Space desde Settings → Datasets
105
+ 3. Guardar los archivos en `/datasets/tu-usuario/tu-dataset`
DEPLOY_RENDER.md ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Despliegue en Render.com + Neon Database
2
+
3
+ ## Arquitectura
4
+
5
+ - **Aplicación**: Render.com (Docker)
6
+ - **Base de Datos**: Neon PostgreSQL (gratis, 500MB)
7
+ - **Outputs**: Disco persistente en Render (5GB)
8
+
9
+ ## Paso 1: Crear la Base de Datos en Neon
10
+
11
+ 1. **Ir a Neon**
12
+ - Visitá https://neon.tech
13
+ - Click en "Sign Up" (usá GitHub para más facilidad)
14
+
15
+ 2. **Crear un nuevo proyecto**
16
+ - Click en "New Project"
17
+ - **Name**: `browserpilot-db`
18
+ - **Region**: Us-West (Oregon) - misma que Render
19
+ - Click en "Create Project"
20
+
21
+ 3. **Obtener la DATABASE_URL**
22
+ - En el dashboard del proyecto, vas a ver la "Connection String"
23
+ - Copiá el string que dice algo como:
24
+ ```
25
+ postgresql://user:password@ep-xxx.us-west-2.aws.neon.tech/dbname?sslmode=require
26
+ ```
27
+ - Guardala para el próximo paso
28
+
29
+ 4. **Configuración opcional**
30
+ - Podés ver la base de datos desde el dashboard de Neon
31
+ - Las tablas se crean automáticamente al iniciar la app
32
+
33
+ ## Paso 2: Desplegar en Render
34
+
35
+ ### Opción 1: Usando el Dashboard de Render (Recomendado)
36
+
37
+ 1. **Crear una cuenta en Render**
38
+ - Ir a https://render.com y registrarse
39
+
40
+ 2. **Crear un nuevo Web Service**
41
+ - Click en "New +" → "Web Service"
42
+ - Conectar tu repositorio de GitHub
43
+
44
+ 3. **Configurar el servicio**
45
+ - **Name**: `browserpilot` (o el nombre que quieras)
46
+ - **Region**: Oregon (us-west-2) - misma que Neon
47
+ - **Branch**: `main`
48
+ - **Root Directory**: (dejar vacío)
49
+ - **Runtime**: `Docker`
50
+ - **Docker Command**: (dejar vacío)
51
+
52
+ 4. **Configurar Variables de Entorno**
53
+ En la sección "Environment", agregar:
54
+
55
+ | Key | Value |
56
+ |-----|-------|
57
+ | `GOOGLE_API_KEY` | `AIzaSyBDl98MXre7ecN9jW16EVltBlaf38awkdo` |
58
+ | `DATABASE_URL` | (pegá el connection string de Neon) |
59
+ | `SCRAPER_PROXIES` | `[]` (o tu configuración de proxies si tenés) |
60
+
61
+ 5. **Configurar Disk (opcional pero recomendado)**
62
+ - Click en "Add Disk"
63
+ - **Name**: `outputs`
64
+ - **Mount Path**: `/app/outputs`
65
+ - **Size**: 5 GB (mínimo)
66
+
67
+ 6. **Seleccionar Plan**
68
+ - **Starter** ($7/mes) - recomendado para empezar
69
+ - El plan free no soporta Docker
70
+
71
+ 7. **Deploy**
72
+ - Click en "Create Web Service"
73
+ - Esperar a que se complete el despliegue (~5-10 minutos)
74
+
75
+ ### Opción 2: Usando Render CLI
76
+
77
+ ```bash
78
+ # Instalar Render CLI
79
+ npm install -g @render-cloud/renderctl
80
+
81
+ # Login
82
+ renderctl login
83
+
84
+ # Deploy usando el render.yaml
85
+ cd /home/ncx/BROPILOT/33BrowserPilot
86
+ renderctl up -f render.yaml
87
+ ```
88
+
89
+ ## Configuración del render.yaml
90
+
91
+ El archivo `render.yaml` ya está configurado con:
92
+ - Tipo: Web Service con Docker
93
+ - Región: Oregon
94
+ - Plan: Starter
95
+ - Health check: `/`
96
+ - Disco persistente para outputs
97
+
98
+ ## Después del despliegue
99
+
100
+ 1. **Verificar el deploy**
101
+ - Ir al dashboard de Render
102
+ - Ver los logs en tiempo real
103
+ - Verificar que el health check pase
104
+
105
+ 2. **Acceder a la aplicación**
106
+ - La URL será algo como: `https://browserpilot-xxxx.onrender.com`
107
+
108
+ 3. **Configurar dominio personalizado (opcional)**
109
+ - En el dashboard → Settings → Custom Domain
110
+
111
+ ## Variables de Entorno Requeridas
112
+
113
+ | Variable | Requerida | Descripción |
114
+ |----------|-----------|-------------|
115
+ | `GOOGLE_API_KEY` | ✅ Sí | API key de Google Gemini |
116
+ | `DATABASE_URL` | ✅ Sí | Connection string de Neon PostgreSQL |
117
+ | `SCRAPER_PROXIES` | ❌ No | Configuración de proxies (JSON array) |
118
+
119
+ ## Costos Estimados
120
+
121
+ | Servicio | Plan | Costo |
122
+ |----------|------|-------|
123
+ | **Render** | Starter | $7/mes |
124
+ | **Render Disk** | 5GB | ~$0.50/mes |
125
+ | **Neon** | Free | $0/mes (500MB) |
126
+ | **Total** | | ~$7.50/mes |
127
+
128
+ ## Endpoints de la API
129
+
130
+ ### Jobs
131
+ - `POST /job` - Crear un nuevo job
132
+ - `GET /jobs` - Listar todos los jobs (con paginación)
133
+ - `GET /job/{job_id}` - Obtener detalles de un job
134
+ - `DELETE /job/{job_id}` - Eliminar un job
135
+ - `GET /download/{job_id}` - Descargar el resultado
136
+
137
+ ### Streaming
138
+ - `GET /streaming/{job_id}` - Obtener info de streaming
139
+ - `POST /streaming/create/{job_id}` - Crear sesión de streaming
140
+ - `DELETE /streaming/{job_id}` - Limpiar sesión
141
+ - `WS /stream/{job_id}` - WebSocket para streaming
142
+
143
+ ### Sistema
144
+ - `GET /stats` - Estadísticas del sistema
145
+ - `GET /proxy/stats` - Estadísticas de proxies
146
+ - `POST /proxy/reload` - Recargar proxies
147
+
148
+ ## Notas Importantes
149
+
150
+ ⚠️ **Importante**: Render no soporta el plan free para servicios Docker.
151
+
152
+ ⚠️ **Importante**: Los servicios en el plan starter se duermen después de 15 minutos de inactividad. El primer request después de dormir puede tardar ~30 segundos.
153
+
154
+ ⚠️ **Importante**: Neon tiene un límite de 500MB en el plan free. Si necesitás más espacio, considerá upgrade a Neon Pro ($19/mes).
155
+
156
+ ## Troubleshooting
157
+
158
+ ### El deploy falla
159
+ - Verificar los logs en el dashboard de Render
160
+ - Asegurarse de que el Dockerfile sea correcto
161
+ - Verificar que las variables de entorno estén configuradas
162
+ - Checkear que la DATABASE_URL de Neon sea correcta
163
+
164
+ ### La aplicación no responde
165
+ - Verificar el health check en el dashboard
166
+ - Revisar los logs de errores
167
+ - Verificar que la GOOGLE_API_KEY sea válida
168
+ - Verificar que la conexión a Neon funcione
169
+
170
+ ### Problemas de memoria
171
+ - El plan starter tiene 512MB RAM
172
+ - Si necesitás más, upgrade al plan Standard ($15/mes)
173
+
174
+ ### La base de datos no conecta
175
+ - Verificar que la DATABASE_URL incluya `?sslmode=require`
176
+ - Checkear que la región de Neon sea la misma que Render (us-west-2)
177
+ - Verificar los logs de error de conexión
Dockerfile ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage Dockerfile for BrowserPilot
2
+ # Stage 1: Build the React frontend
3
+ FROM node:20-alpine AS frontend-builder
4
+
5
+ # Set working directory for frontend
6
+ WORKDIR /app/frontend
7
+
8
+ # Copy package files
9
+ COPY frontend/package*.json ./
10
+
11
+ # Install all dependencies (including dev dependencies needed for build)
12
+ RUN npm config set strict-ssl false && npm install
13
+
14
+ # Copy frontend source code
15
+ COPY frontend/ ./
16
+
17
+ # Build the frontend
18
+ RUN npm run build
19
+
20
+ # Stage 2: Use Playwright's official Docker image with Python (Ubuntu-based)
21
+ FROM mcr.microsoft.com/playwright/python:v1.53.0-jammy
22
+
23
+ # Set working directory
24
+ WORKDIR /app
25
+
26
+ # Copy Python requirements and install dependencies
27
+ COPY requirements.txt .
28
+ # Install compatible versions of numpy and pandas for Python 3.10
29
+ RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --no-cache-dir \
30
+ fastapi==0.111.0 \
31
+ uvicorn[standard]==0.29.0 \
32
+ playwright==1.53.0 \
33
+ google-generativeai==0.5.0 \
34
+ pydantic==2.7.1 \
35
+ bs4==0.0.2 \
36
+ lxml==5.2.1 \
37
+ markdownify==0.11.6 \
38
+ "numpy>=2.0.0,<2.3.0" \
39
+ "pandas>=2.0.0,<2.3.0" \
40
+ python-dateutil==2.9.0.post0 \
41
+ pytz==2025.2 \
42
+ tzdata==2025.2 \
43
+ reportlab==4.4.2 \
44
+ psycopg2-binary==2.9.9 \
45
+ asyncpg==0.29.0 \
46
+ python-telegram-bot==21.0 \
47
+ aiohttp==3.9.5
48
+
49
+ # Copy backend source code
50
+ COPY backend/ ./backend/
51
+
52
+ # Copy built frontend from the frontend-builder stage
53
+ COPY --from=frontend-builder /app/frontend/dist ./frontend/
54
+
55
+ # Create outputs directory
56
+ RUN mkdir -p outputs
57
+
58
+ # Copy entrypoint script
59
+ COPY entrypoint.sh /entrypoint.sh
60
+ RUN chmod +x /entrypoint.sh
61
+
62
+ # Set environment variables
63
+ ENV PYTHONPATH=/app
64
+ ENV PYTHONUNBUFFERED=1
65
+ ENV GRADIO_SERVER_NAME=0.0.0.0
66
+ ENV GRADIO_SERVER_PORT=8000
67
+
68
+ # Expose the port the app runs on
69
+ EXPOSE 8000
70
+
71
+ # Create a non-root user for security (the playwright image already has pwuser)
72
+ RUN chown -R pwuser:pwuser /app
73
+ USER pwuser
74
+
75
+ # Health check
76
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
77
+ CMD curl -f http://localhost:8000/ || exit 1
78
+
79
+ # Run the application using entrypoint
80
+ ENTRYPOINT ["/entrypoint.sh"]
KEEPALIVE.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # KeepAlive para Hugging Face Spaces - GitHub Actions
2
+
3
+ ## ¿Qué es esto?
4
+
5
+ GitHub Actions ejecuta un workflow cada **13 minutos** que hace ping a tu Hugging Face Space para evitar que se duerma.
6
+
7
+ ## Configuración
8
+
9
+ ### 1. Agregar el secreto en GitHub
10
+
11
+ 1. Andá a tu repo: https://github.com/ncolex/33BrowserPilot
12
+ 2. **Settings** → **Secrets and variables** → **Actions**
13
+ 3. **New repository secret**
14
+ 4. **Name**: `HF_SPACE_URL`
15
+ 5. **Value**: `https://huggingface.co/spaces/ncolex/browserpilot`
16
+ 6. **Add secret**
17
+
18
+ ### 2. ¡Listo!
19
+
20
+ El workflow se ejecuta automáticamente cada 13 minutos.
21
+
22
+ ## Verificación
23
+
24
+ - **Actions tab**: https://github.com/ncolex/33BrowserPilot/actions
25
+ - **Workflow**: "Keep HF Space Awake"
26
+ - **Runs**: Cada ~13 minutos
27
+
28
+ ## Trigger manual
29
+
30
+ Podés ejecutar manualmente el workflow:
31
+ 1. Andá a **Actions** → **Keep HF Space Awake**
32
+ 2. **Run workflow** → **Run workflow**
33
+
34
+ ## Costo: **$0/mes**
35
+
36
+ - **GitHub Actions**: 2000 minutos/mes gratis
37
+ - **Este workflow**: ~1 minuto/mes (segundos realmente)
38
+ - **Total usado**: <1% del límite gratis
39
+
40
+ ## Notas
41
+
42
+ - Los workflows se pueden ejecutar cada 5 minutos como mínimo
43
+ - 13 minutos es un buen balance (no muy frecuente, no muy raro)
44
+ - El workflow falla si el Space está caído (esperado)
README.docker.md ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🐳 Docker Setup for BrowserPilot
2
+
3
+ BrowserPilot can be easily deployed using Docker, which provides a consistent environment with all dependencies pre-installed.
4
+
5
+ ## Quick Start with Docker
6
+
7
+ ### 1. Using Docker Compose (Recommended)
8
+
9
+ The easiest way to run BrowserPilot is with Docker Compose:
10
+
11
+ ```bash
12
+ # Clone the repository
13
+ git clone https://github.com/ai-naymul/BrowserPilot.git
14
+ cd BrowserPilot
15
+
16
+ # Create environment file
17
+ echo 'GOOGLE_API_KEY=your_actual_api_key_here' > .env
18
+
19
+ # Run with Docker Compose
20
+ docker-compose up -d
21
+ ```
22
+
23
+ The application will be available at `http://localhost:8000`.
24
+
25
+ ### 2. Using Docker Build & Run
26
+
27
+ If you prefer to use Docker directly:
28
+
29
+ ```bash
30
+ # Build the image
31
+ docker build -t browserpilot .
32
+
33
+ # Run the container
34
+ docker run -d \
35
+ --name browserpilot \
36
+ -p 8000:8000 \
37
+ -e GOOGLE_API_KEY=your_actual_api_key_here \
38
+ -v $(pwd)/outputs:/app/outputs \
39
+ --shm-size=2g \
40
+ browserpilot
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ ### Environment Variables
46
+
47
+ Set these environment variables for the container:
48
+
49
+ ```bash
50
+ # Required
51
+ GOOGLE_API_KEY=your_gemini_api_key_here
52
+
53
+ # Optional - Proxy configuration
54
+ SCRAPER_PROXIES='[{"server": "http://proxy1:port", "username": "user", "password": "pass"}]'
55
+ ```
56
+
57
+ ### Volume Mounts
58
+
59
+ - `./outputs:/app/outputs` - Persist extracted data and downloads
60
+ - `./.env:/app/.env:ro` - Mount environment file (optional)
61
+
62
+ ## Production Deployment
63
+
64
+ For production use, use the production Docker Compose configuration:
65
+
66
+ ```bash
67
+ # Production deployment
68
+ docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
69
+ ```
70
+
71
+ This provides:
72
+ - Resource limits (4GB RAM, 2 CPU cores)
73
+ - Proper logging configuration
74
+ - Restart policies
75
+ - Optimized environment settings
76
+
77
+ ## Docker Image Details
78
+
79
+ ### Multi-Stage Build
80
+ The Dockerfile uses a multi-stage build process:
81
+ 1. **Frontend Builder**: Builds the React frontend using Node.js Alpine
82
+ 2. **Runtime**: Uses Microsoft's Playwright Python image with all browser dependencies
83
+
84
+ ### Features
85
+ - ✅ Pre-installed Playwright browsers (Chromium)
86
+ - ✅ All system dependencies for browser automation
87
+ - ✅ Non-root user for security
88
+ - ✅ Health checks included
89
+ - ✅ Optimized for container environments
90
+
91
+ ### Image Size
92
+ The final image is approximately 2.5GB due to:
93
+ - Playwright browsers and dependencies
94
+ - Python runtime and packages
95
+ - System libraries for browser automation
96
+
97
+ ## Troubleshooting
98
+
99
+ ### Common Issues
100
+
101
+ **Container exits immediately:**
102
+ ```bash
103
+ # Check logs
104
+ docker logs browserpilot
105
+
106
+ # Common issue: Missing GOOGLE_API_KEY
107
+ docker run -e GOOGLE_API_KEY=your_key_here browserpilot
108
+ ```
109
+
110
+ **Browser crashes or fails:**
111
+ ```bash
112
+ # Increase shared memory size
113
+ docker run --shm-size=2g browserpilot
114
+
115
+ # Or with Docker Compose (already configured)
116
+ docker-compose up
117
+ ```
118
+
119
+ **Permission issues with outputs:**
120
+ ```bash
121
+ # Fix output directory permissions
122
+ sudo chown -R $(id -u):$(id -g) outputs/
123
+ ```
124
+
125
+ ### Health Checks
126
+
127
+ The container includes health checks:
128
+ ```bash
129
+ # Check container health
130
+ docker inspect --format='{{.State.Health}}' browserpilot
131
+ ```
132
+
133
+ ### Performance Tuning
134
+
135
+ For better performance:
136
+ ```bash
137
+ # Increase resources
138
+ docker run \
139
+ --memory=4g \
140
+ --cpus=2 \
141
+ --shm-size=2g \
142
+ browserpilot
143
+ ```
144
+
145
+ ## Development with Docker
146
+
147
+ ### Live Development
148
+ For development with live reload:
149
+
150
+ ```bash
151
+ # Mount source code for development
152
+ docker run -it \
153
+ -p 8000:8000 \
154
+ -v $(pwd)/backend:/app/backend \
155
+ -v $(pwd)/outputs:/app/outputs \
156
+ -e GOOGLE_API_KEY=your_key_here \
157
+ browserpilot \
158
+ python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload
159
+ ```
160
+
161
+ ### Building Custom Images
162
+
163
+ To customize the Docker image:
164
+
165
+ ```dockerfile
166
+ # Extend the base image
167
+ FROM browserpilot:latest
168
+
169
+ # Add custom dependencies
170
+ RUN pip install your-custom-package
171
+
172
+ # Copy custom configurations
173
+ COPY custom-config.json /app/
174
+ ```
175
+
176
+ ## Security Considerations
177
+
178
+ - The container runs as a non-root user (`pwuser`)
179
+ - Uses security options for browser sandbox
180
+ - Environment variables are not exposed in the image
181
+ - Secrets should be mounted as files or environment variables
182
+
183
+ ## Monitoring and Logging
184
+
185
+ ### Logs
186
+ ```bash
187
+ # View logs
188
+ docker logs -f browserpilot
189
+
190
+ # With timestamps
191
+ docker logs -t browserpilot
192
+ ```
193
+
194
+ ### Monitoring
195
+ ```bash
196
+ # Resource usage
197
+ docker stats browserpilot
198
+
199
+ # Container inspection
200
+ docker inspect browserpilot
201
+ ```
README.md CHANGED
@@ -1,10 +1,233 @@
1
- ---
2
- title: Browserpilot
3
- emoji: 📉
4
- colorFrom: purple
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BrowserPilot
2
+
3
+ > Ever wished you could tell your browser "Hey, go grab all the product prices from that e-commerce site" and it would just... do it? That's exactly what this does, but smarter.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
7
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
8
+
9
+ ## What's This All About?
10
+
11
+ Tired of writing complex scrapers that break every time a website changes its layout? Yeah, me too.
12
+
13
+ This AI-powered browser actually *sees* web pages like you do. It doesn't care if Amazon redesigns their product pages or if LinkedIn adds new anti-bot measures. Just tell it what you want in plain English, and it figures out how to get it.
14
+
15
+ Think of it as having a really smart intern who never gets tired, never makes mistakes, and can handle any website you throw at them - even the ones with annoying CAPTCHAs.
16
+
17
+ ## See It In Action
18
+
19
+ Trust me, it's pretty cool watching an AI navigate websites like a human
20
+
21
+
22
+ https://github.com/user-attachments/assets/39d2ed68-e121-49b9-817e-2eb5edc25627
23
+
24
+
25
+ ## Why You'll Love This
26
+
27
+ ### It Actually "Sees" Websites
28
+ - Uses Google's Gemini AI to look at pages like you do
29
+ - Automatically figures out if it's looking at Amazon, LinkedIn, or your random blog
30
+ - Clicks the right buttons even when websites change their design
31
+ - Works on literally any website (yes, even the weird ones)
32
+
33
+ ### Handles the Annoying Stuff
34
+ - Gets blocked by Cloudflare? No problem, switches proxies automatically
35
+ - Encounters a CAPTCHA? Solves it with AI vision
36
+ - Website thinks it's a bot? Laughs in artificial intelligence
37
+ - Proxy goes down? Switches to a backup faster than you can blink
38
+
39
+ ### Gives You Data How You Want It
40
+ - Say "save as PDF" and boom, you get a PDF
41
+ - Ask for CSV and it structures everything perfectly
42
+ - Want JSON? It knows what you mean
43
+ - Organizes everything with timestamps and metadata (because details matter)
44
+
45
+ ### Watch It Work Live
46
+ - Stream the browser view in real-time (it's oddly satisfying)
47
+ - Click and type remotely if you need to step in
48
+ - Multiple people can watch the same session
49
+ - Perfect for debugging or just showing off
50
+
51
+ ## Getting Started (It's Actually Pretty Easy)
52
+
53
+ ### 🐳 Quick Start with Docker (Recommended)
54
+
55
+ The easiest way to run BrowserPilot is with Docker:
56
+
57
+ ```bash
58
+ # Clone and start with Docker Compose
59
+ git clone https://github.com/ai-naymul/BrowserPilot.git
60
+ cd BrowserPilot
61
+ echo 'GOOGLE_API_KEY=your_actual_api_key_here' > .env
62
+ docker-compose up -d
63
+ ```
64
+
65
+ Open `http://localhost:8000` and you're ready to go! 🚀
66
+
67
+ [📖 Full Docker Documentation](README.docker.md)
68
+
69
+ ### 💻 Manual Installation
70
+
71
+ ### What You'll Need
72
+ - Python 3.8 or newer (check with `python --version`)
73
+ - A Google AI API key (free to get, just sign up at ai.google.dev)
74
+ - Some proxies if you're planning to scrape heavily (optional but recommended)
75
+
76
+ ### Let's Get This Running
77
+
78
+ 1. **Grab the code**
79
+ ```bash
80
+ git clone https://github.com/ai-naymul/BrowserPilot.git
81
+ cd BrowserPilot
82
+ ```
83
+
84
+ 2. **Install the good stuff**
85
+ ```bash
86
+ curl -LsSf https://astral.sh/uv/install.sh | sh
87
+ uv pip install -r requirements.txt
88
+ ```
89
+
90
+ 3. **Add your secrets**
91
+ ```bash
92
+ # Create a .env file (don't worry, it's gitignored)
93
+ echo 'GOOGLE_API_KEY=your_actual_api_key_here' > .env
94
+ echo 'SCRAPER_PROXIES=[{"server": "http://proxy1:port", "username": "user", "password": "pass"}]' >> .env
95
+ ```
96
+
97
+ 4. **Fire it up**
98
+ ```bash
99
+ python -m uvicorn backend.main:app --reload
100
+ ```
101
+
102
+ 5. **See the magic**
103
+ Open `http://localhost:8000` and start telling it what to do
104
+
105
+ ## Real Examples (Because Everyone Loves Examples)
106
+
107
+ ### Just Getting Started
108
+ ```javascript
109
+ "Go to Hacker News and save the top stories as JSON"
110
+ ```
111
+ That's it. Seriously. It'll figure out the rest.
112
+
113
+ ### Shopping for Data
114
+ ```javascript
115
+ "Search Amazon for wireless headphones under $100 and export the results to CSV"
116
+ ```
117
+ It'll navigate, search, filter, and organize everything nicely for you.
118
+
119
+ ### Social Media Intel
120
+ ```javascript
121
+ "Go to LinkedIn, find AI engineers in San Francisco, and save their profiles"
122
+ ```
123
+ Don't worry, it handles all the login prompts and infinite scroll nonsense.
124
+
125
+ ### The Wild West
126
+ ```javascript
127
+ "Visit this random e-commerce site and grab all the product prices"
128
+ ```
129
+ Even works on sites you've never seen before. That's the beauty of AI vision.
130
+
131
+ ## Core Components
132
+
133
+ ### Smart Browser Controller
134
+ - Automatic anti-bot detection using AI vision
135
+ - Proxy rotation on detection/blocking
136
+ - CAPTCHA solving capabilities
137
+ - Browser restart with new proxies
138
+
139
+ ### Vision Model Integration
140
+ - Dynamic website analysis
141
+ - Anti-bot system detection
142
+ - Element interaction decisions
143
+ - CAPTCHA recognition and solving
144
+
145
+ ### Universal Extractor
146
+ - AI-powered content extraction
147
+ - Multiple output format support
148
+ - Structured data organization
149
+ - Metadata preservation
150
+
151
+ ### Proxy Management
152
+ - Health tracking and statistics
153
+ - Performance-based selection
154
+ - Site-specific blocking lists
155
+ - Automatic failure recovery
156
+
157
+ ## The Cool Technical Stuff
158
+
159
+ ### Smart Format Detection
160
+ Just talk to it naturally:
161
+ - "save as PDF" → Gets you a beautiful PDF
162
+ - "export to CSV" → Perfectly structured spreadsheet
163
+ - "give me JSON" → Clean, organized data structure
164
+
165
+ ### Anti-Bot Ninja Mode
166
+ - Spots Cloudflare challenges before they even load
167
+ - Solves CAPTCHAs like a human (but faster)
168
+ - Detects rate limits and backs off gracefully
169
+ - Switches identities when websites get suspicious
170
+
171
+ ### Dashboard That Actually Helps
172
+ - See which proxies are working (and which ones suck)
173
+ - Watch your browser sessions live
174
+ - Track how much you're spending on AI tokens
175
+ - Performance stats that make sense
176
+
177
+ ## Configuration
178
+
179
+ ### Proxy Configuration
180
+ ```json
181
+ {
182
+ "SCRAPER_PROXIES": [
183
+ {
184
+ "server": "http://proxy1.example.com:8080",
185
+ "username": "user1",
186
+ "password": "pass1",
187
+ "location": "US"
188
+ },
189
+ {
190
+ "server": "http://proxy2.example.com:8080",
191
+ "username": "user2",
192
+ "password": "pass2",
193
+ "location": "EU"
194
+ }
195
+ ]
196
+ }
197
+ ```
198
+
199
+ ### Environment Variables
200
+ ```bash
201
+ # Required
202
+ GOOGLE_API_KEY=your_gemini_api_key_here
203
+
204
+ # Optional
205
+ SCRAPER_PROXIES=your_proxy_configuration
206
+ ```
207
+
208
+ ## Contributors
209
+
210
+ <a href="https://github.com/your-username/your-repo/graphs/contributors">
211
+ <img src="https://contrib.rocks/image?repo=ai-naymul/BrowserPilot" />
212
+ </a>
213
+
214
+
215
+ ## 🤝 Want to Help Make This Better?
216
+
217
+ Found a bug? Have a crazy idea? Want to add support for your favorite website? I'd love the help!
218
+
219
+ Here's how to jump in:
220
+ 1. Fork this repo (there's a button for that)
221
+ 2. Create a branch with a name that makes sense (`git checkout -b fix-amazon-pagination`)
222
+ 3. Make your changes (and please test them!)
223
+ 4. Commit with a message that explains what you did
224
+ 5. Push it up and open a pull request
225
+
226
+ For big changes, maybe open an issue first so we can chat about it.
227
+
228
+ ## 🙏 Acknowledgments
229
+
230
+ - [Playwright](https://playwright.dev/) for browser automation
231
+ - [Google Gemini](https://ai.google.dev/) for vision AI capabilities
232
+ - [FastAPI](https://fastapi.tiangolo.com/) for the backend framework
233
+ - Open source community for inspiration and tools
README_HF.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: BrowserPilot
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # BrowserPilot - AI-Powered Browser Automation
12
+
13
+ Este espacio de Hugging Face ejecuta BrowserPilot, un navegador automatizado con IA que usa Google Gemini para navegar sitios web y extraer datos.
14
+
15
+ ## Cómo usar
16
+
17
+ 1. Agregá tu `GOOGLE_API_KEY` en los **Settings** de este Space (Variables and secrets)
18
+ 2. Opcional: agregá `DATABASE_URL` de Neon para guardar el historial de jobs
19
+ 3. El frontend estará disponible en la URL de este Space
20
+
21
+ ## Endpoints de la API
22
+
23
+ - `POST /job` - Crear un nuevo job
24
+ - `GET /jobs` - Listar todos los jobs
25
+ - `GET /download/{job_id}` - Descargar resultado
26
+ - `WS /stream/{job_id}` - Streaming del navegador
27
+
28
+ ## Ver código fuente
29
+
30
+ https://github.com/ncolex/33BrowserPilot
SETUP_COMPLETE.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 BrowserPilot - Configuración Lista
2
+
3
+ ## ✅ Lo que ya está hecho:
4
+
5
+ 1. **Base de datos Neon creada**
6
+ - Proyecto: `browserpilot-db`
7
+ - ID: `late-king-86238640`
8
+ - Región: `aws-us-east-1`
9
+ - ✅ Conectada y lista para usar
10
+
11
+ 2. **Código actualizado en GitHub**
12
+ - Repo: https://github.com/ncolex/33BrowserPilot
13
+ - ✅ Con integración a PostgreSQL
14
+ - ✅ Endpoints para listar jobs: `GET /jobs`
15
+
16
+ 3. **Variables configuradas localmente**
17
+ - `GOOGLE_API_KEY`: ✅ Configurada
18
+ - `DATABASE_URL`: ✅ Configurada
19
+
20
+ ---
21
+
22
+ ## 📋 Lo que tenés que hacer AHORA en Hugging Face:
23
+
24
+ ### Paso 1: Crear el Space
25
+ 1. Entrá a https://huggingface.co/spaces
26
+ 2. Click en **"Create new Space"**
27
+ 3. Completar:
28
+ - **Space name**: `browserpilot`
29
+ - **License**: MIT
30
+ - **SDK**: **Docker** ⚠️ (¡importante!)
31
+ - **Visibility**: Public
32
+ 4. **Create Space**
33
+
34
+ ### Paso 2: Importar desde GitHub
35
+ 1. En el Space, click en **"Import from GitHub repo"**
36
+ 2. Autorizá Hugging Face a acceder a tu GitHub
37
+ 3. Seleccioná: `ncolex/33BrowserPilot`
38
+ 4. **Import**
39
+
40
+ ### Paso 3: Configurar Variables (Settings → Variables and secrets)
41
+
42
+ **Secrets:**
43
+ ```
44
+ GOOGLE_API_KEY = AIzaSyBDl98MXre7ecN9jW16EVltBlaf38awkdo
45
+ DATABASE_URL = postgresql://neondb_owner:npg_obFVM76KLule@ep-twilight-field-aiwca4xu.c-4.us-east-1.aws.neon.tech/neondb?sslmode=require
46
+ ```
47
+
48
+ **Variables:**
49
+ ```
50
+ PYTHONUNBUFFERED = 1
51
+ ```
52
+
53
+ ### Paso 4: Esperar el deploy
54
+ - El build tarda **5-10 minutos**
55
+ - Cuando diga **"Running"**, ¡está listo!
56
+ - URL: `https://huggingface.co/spaces/tu-usuario/browserpilot`
57
+
58
+ ---
59
+
60
+ ## 🎯 URLs útiles:
61
+
62
+ | Servicio | URL |
63
+ |----------|-----|
64
+ | **GitHub Repo** | https://github.com/ncolex/33BrowserPilot |
65
+ | **Neon Dashboard** | https://console.neon.tech |
66
+ | **Hugging Face Spaces** | https://huggingface.co/spaces |
67
+
68
+ ---
69
+
70
+ ## 📊 Endpoints de la API (una vez desplegado):
71
+
72
+ ```bash
73
+ # Crear un job
74
+ curl -X POST https://huggingface.co/spaces/tu-usuario/browserpilot/job \
75
+ -H "Content-Type: application/json" \
76
+ -d '{"prompt": "Go to Hacker News and save top stories as JSON", "format": "json"}'
77
+
78
+ # Listar jobs
79
+ curl https://huggingface.co/spaces/tu-usuario/browserpilot/jobs
80
+
81
+ # Ver stats
82
+ curl https://huggingface.co/spaces/tu-usuario/browserpilot/stats
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 💰 Costo: $0/mes
88
+
89
+ - **Hugging Face Spaces**: 100% gratis
90
+ - **Neon Database**: 100% gratis (500MB)
91
+ - **Total**: $0/mes
92
+
93
+ ---
94
+
95
+ ## ⚠️ Importante:
96
+
97
+ 1. Los **Secrets** en HF no se ven en la UI, solo se usan internamente
98
+ 2. La **DATABASE_URL** guarda el historial de jobs permanentemente
99
+ 3. Los archivos en `/outputs` **NO persisten** si el Space se reinicia
100
+ 4. El Space se **duerme** después de ~48h de inactividad
101
+
102
+ ---
103
+
104
+ ## 🔧 Comandos útiles de Neon CLI:
105
+
106
+ ```bash
107
+ # Ver proyectos
108
+ neonctl projects list
109
+
110
+ # Ver connection string
111
+ neonctl connection-string --project-id late-king-86238640
112
+
113
+ # Ver branches
114
+ neonctl branches list --project-id late-king-86238640
115
+
116
+ # Ver queries (si está habilitado)
117
+ neonctl operations list --project-id late-king-86238640
118
+ ```
TELEGRAM_BOT.md ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🤖 Telegram Bot Integration
2
+
3
+ ## Bot: @Error33yobot
4
+
5
+ ### Configuración Requerida
6
+
7
+ **En Hugging Face Spaces** (Settings → Variables and secrets):
8
+
9
+ | Type | Key | Value |
10
+ |------|-----|-------|
11
+ | Secret | `TELEGRAM_BOT_TOKEN` | `8246977072:AAHItyl6wYcrkNwTcj5Teu0lsVwTeMKZZqU` |
12
+ | Secret | `TELEGRAM_CHAT_ID` | `5858991844` |
13
+
14
+ **En GitHub Secrets** (Settings → Secrets and variables → Actions):
15
+
16
+ | Secret | Value |
17
+ |--------|-------|
18
+ | `TELEGRAM_BOT_TOKEN` | `8246977072:AAHItyl6wYcrkNwTcj5Teu0lsVwTeMKZZqU` |
19
+ | `TELEGRAM_CHAT_ID` | `5858991844` |
20
+
21
+ ✅ **Configuración completa** - ¡El bot ya está funcionando!
22
+
23
+ ---
24
+
25
+ ## Cómo obtener tu Chat ID (ya configurado)
26
+
27
+ El Chat ID ya está configurado: `5858991844`
28
+
29
+ Si necesitás cambiarlo en el futuro, los pasos son:
30
+
31
+ 1. **Abrí Telegram**
32
+ 2. **Buscá**: `@Error33yobot`
33
+ 3. **Iniciá el bot**: Click en "Start" o mandale "hola"
34
+ 4. **Ejecutá este comando**:
35
+ ```bash
36
+ curl -s "https://api.telegram.org/bot8246977072:AAHItyl6wYcrkNwTcj5Teu0lsVwTeMKZZqU/getUpdates" | python3 -m json.tool
37
+ ```
38
+ 5. **Buscá** en el resultado: `"chat": {"id": 123456789, ...}`
39
+
40
+ ---
41
+
42
+ ## Comandos del Bot
43
+
44
+ | Comando | Descripción |
45
+ |---------|-------------|
46
+ | `/start` | Mostrar ayuda y comandos disponibles |
47
+ | `/ping` | Verificar que el bot está activo |
48
+ | `/status` | Ver estado del sistema (jobs activos, proxies, etc.) |
49
+ | `/jobs` | Listar los últimos 5 jobs |
50
+
51
+ ---
52
+
53
+ ## Crear Jobs desde Telegram
54
+
55
+ Simplemente **enviá un mensaje** al bot con tu tarea:
56
+
57
+ ```
58
+ Go to Hacker News and save the top 10 stories as JSON
59
+ ```
60
+
61
+ El bot va a:
62
+ 1. ✅ Crear el job
63
+ 2. 📋 Confirmarte con el Job ID
64
+ 3. 🔔 Avisarte cuando termine con el link de descarga
65
+
66
+ ---
67
+
68
+ ## Notificaciones Automáticas
69
+
70
+ ### Job Started
71
+ ```
72
+ 🚀 Job Started
73
+ ID: abc123...
74
+ Task: Go to Hacker News...
75
+ Format: json
76
+ ⏳ Processing...
77
+ ```
78
+
79
+ ### Job Completed
80
+ ```
81
+ ✅ Job Completed!
82
+ ID: abc123...
83
+ Format: json
84
+ 📥 Download Result
85
+ ```
86
+
87
+ ### Job Failed
88
+ ```
89
+ ❌ Job Failed
90
+ ID: abc123...
91
+ Error: [error message]
92
+ ```
93
+
94
+ ### KeepAlive Alert
95
+ ```
96
+ ⚠️ KeepAlive Alert
97
+ 🔴 HF Space health check failed!
98
+ Status: 503
99
+ The Space might be sleeping or down.
100
+ ```
101
+
102
+ ### KeepAlive Restored
103
+ ```
104
+ ✅ KeepAlive OK
105
+ 🟢 HF Space health check passed!
106
+ Status: 200
107
+ The Space is awake and running.
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Flujo de Uso
113
+
114
+ 1. **Configurá** las variables en HF Spaces y GitHub
115
+ 2. **Iniciá** el bot en Telegram (`/start`)
116
+ 3. **Enviá** tu tarea como mensaje
117
+ 4. **Esperá** la notificación de completado
118
+ 5. **Descargá** el resultado desde el link
119
+
120
+ ---
121
+
122
+ ## Ejemplos de Comandos
123
+
124
+ ### Buscar en Google
125
+ ```
126
+ Search for "AI engineers in San Francisco" on LinkedIn and save as CSV
127
+ ```
128
+
129
+ ### Extraer datos
130
+ ```
131
+ Go to amazon.com and get the top 5 wireless headphones under $100
132
+ ```
133
+
134
+ ### Navegación compleja
135
+ ```
136
+ Visit hackernews.com, click on "Ask HN", and save all posts from today
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Troubleshooting
142
+
143
+ ### El bot no responde
144
+ - Verificá que `TELEGRAM_BOT_TOKEN` esté configurado
145
+ - Verificá que `TELEGRAM_CHAT_ID` sea correcto
146
+ - Revisá los logs del Space
147
+
148
+ ### No llegan las notificaciones
149
+ - Verificá que el bot tenga permisos para enviarte mensajes
150
+ - Asegurate de haber iniciado el bot (`/start`)
151
+
152
+ ### El keepalive no notifica
153
+ - Verificá los secrets de GitHub Actions
154
+ - Revisá el workflow en: https://github.com/ncolex/33BrowserPilot/actions
155
+
156
+ ---
157
+
158
+ ## Costo: **$0/mes**
159
+
160
+ - Telegram Bot API: Gratis
161
+ - GitHub Actions: 2000 min/mes (usás ~1)
162
+ - Total: $0
docker-compose.prod.yml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ browserpilot:
5
+ restart: always
6
+ # Resource limits for production
7
+ deploy:
8
+ resources:
9
+ limits:
10
+ memory: 4G
11
+ cpus: '2.0'
12
+ reservations:
13
+ memory: 2G
14
+ cpus: '1.0'
15
+ # Production logging
16
+ logging:
17
+ driver: "json-file"
18
+ options:
19
+ max-size: "100m"
20
+ max-file: "10"
21
+ # Don't mount local .env in production
22
+ volumes:
23
+ - ./outputs:/app/outputs
24
+ # Production environment variables
25
+ environment:
26
+ - GOOGLE_API_KEY=${GOOGLE_API_KEY}
27
+ - SCRAPER_PROXIES=${SCRAPER_PROXIES:-[]}
28
+ # Production settings
29
+ - PYTHONUNBUFFERED=1
30
+ - PYTHONOPTIMIZE=1
docker-compose.yml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ browserpilot:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile
8
+ ports:
9
+ - "8000:8000"
10
+ environment:
11
+ # Required: Add your Google AI API key
12
+ - GOOGLE_API_KEY=${GOOGLE_API_KEY}
13
+ # Optional: Proxy configuration
14
+ - SCRAPER_PROXIES=${SCRAPER_PROXIES:-[]}
15
+ volumes:
16
+ # Persist outputs directory
17
+ - ./outputs:/app/outputs
18
+ # Optional: Mount .env file for local development
19
+ - ./.env:/app/.env:ro
20
+ restart: unless-stopped
21
+ # Use SHM size to prevent browser crashes
22
+ shm_size: '2gb'
23
+ # Security options for running browsers
24
+ security_opt:
25
+ - seccomp:unconfined
26
+ healthcheck:
27
+ test: ["CMD", "curl", "-f", "http://localhost:8000/"]
28
+ interval: 30s
29
+ timeout: 10s
30
+ retries: 3
31
+ start_period: 40s
32
+
33
+ networks:
34
+ default:
35
+ name: browserpilot-network
entrypoint.sh ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "🚀 Starting BrowserPilot on Hugging Face Spaces..."
5
+
6
+ # Create outputs directory if it doesn't exist
7
+ mkdir -p /app/outputs
8
+
9
+ # Install dependencies if needed
10
+ if [ -f requirements.txt ]; then
11
+ echo "📦 Installing Python dependencies..."
12
+ pip install -q --no-cache-dir -r requirements.txt
13
+ fi
14
+
15
+ # Check if GOOGLE_API_KEY is set
16
+ if [ -z "$GOOGLE_API_KEY" ]; then
17
+ echo "⚠️ WARNING: GOOGLE_API_KEY is not set!"
18
+ echo "Please add it in Settings → Variables and secrets"
19
+ fi
20
+
21
+ # Check if DATABASE_URL is set (optional)
22
+ if [ -n "$DATABASE_URL" ]; then
23
+ echo "✅ Database configured"
24
+ else
25
+ echo "ℹ️ DATABASE_URL not set - database features disabled"
26
+ fi
27
+
28
+ # Start the application
29
+ echo "🌐 Starting FastAPI server on port 8000..."
30
+ exec python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000
frontend/.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
25
+ .env
frontend/README.md ADDED
@@ -0,0 +1 @@
 
 
1
+ new-ui-browserpilot
frontend/eslint.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+ import reactHooks from 'eslint-plugin-react-hooks';
4
+ import reactRefresh from 'eslint-plugin-react-refresh';
5
+ import tseslint from 'typescript-eslint';
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ }
28
+ );
frontend/index.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>BrowserPilot - AI-Powered Web Automation</title>
8
+ <meta name="description" content="Professional AI browser automation dashboard for intelligent web scraping and data extraction">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ <script type="module" src="/src/main.tsx"></script>
16
+ </body>
17
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "vite-react-typescript-starter",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "lucide-react": "^0.344.0",
14
+ "react": "^18.3.1",
15
+ "react-dom": "^18.3.1"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^9.9.1",
19
+ "@types/react": "^18.3.5",
20
+ "@types/react-dom": "^18.3.0",
21
+ "@vitejs/plugin-react": "^4.3.1",
22
+ "autoprefixer": "^10.4.18",
23
+ "eslint": "^9.9.1",
24
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
25
+ "eslint-plugin-react-refresh": "^0.4.11",
26
+ "globals": "^15.9.0",
27
+ "postcss": "^8.4.35",
28
+ "tailwindcss": "^3.4.1",
29
+ "typescript": "^5.5.3",
30
+ "typescript-eslint": "^8.3.0",
31
+ "vite": "^5.4.2"
32
+ }
33
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
frontend/src/App.tsx ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { BrowserPilotDashboard } from './components/BrowserPilotDashboard';
3
+
4
+ function App() {
5
+ return <BrowserPilotDashboard />;
6
+ }
7
+
8
+ export default App;
frontend/src/components/BrowserPilotDashboard.tsx ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Header } from './Header';
3
+ import { JobForm } from './JobForm';
4
+ import { StatusDisplay } from './StatusDisplay';
5
+ import { TokenUsage } from './TokenUsage';
6
+ import { DecisionLog } from './DecisionLog';
7
+ import { ScreenshotGallery } from './ScreenshotGallery';
8
+ import { StreamingViewer } from './StreamingViewer';
9
+ import { ProxyStats } from './ProxyStats';
10
+ import { WebSocketManager } from '../services/WebSocketManager';
11
+
12
+ export const BrowserPilotDashboard: React.FC = () => {
13
+ const [wsManager] = useState(() => new WebSocketManager());
14
+ const [status, setStatus] = useState<{
15
+ message: string;
16
+ type: 'success' | 'error' | 'info';
17
+ } | null>(null);
18
+ const [tokenUsage, setTokenUsage] = useState({
19
+ prompt_tokens: 0,
20
+ response_tokens: 0,
21
+ total_tokens: 0,
22
+ api_calls: 0
23
+ });
24
+ const [proxyStats, setProxyStats] = useState({
25
+ available: 0,
26
+ healthy: 0,
27
+ blocked: 0,
28
+ retry_count: 0
29
+ });
30
+ const [decisions, setDecisions] = useState<any[]>([]);
31
+ const [screenshots, setScreenshots] = useState<string[]>([]);
32
+ const [currentJobId, setCurrentJobId] = useState<string | null>(null);
33
+ const [isLoading, setIsLoading] = useState(false);
34
+ const [streamingEnabled, setStreamingEnabled] = useState(false);
35
+
36
+ useEffect(() => {
37
+ // Set up WebSocket event listeners
38
+ wsManager.on('connected', () => {
39
+ setStatus({ message: 'Connected to BrowserPilot server', type: 'success' });
40
+ setIsLoading(false);
41
+ });
42
+
43
+ wsManager.on('decision', (data: any) => {
44
+ const decision = data.decision || data;
45
+ setDecisions(prev => [...prev, decision]);
46
+
47
+ if (decision.token_usage) {
48
+ updateTokenUsage(decision.token_usage);
49
+ }
50
+ });
51
+
52
+ wsManager.on('screenshot', (data: any) => {
53
+ const screenshot = data.screenshot || data;
54
+ if (typeof screenshot === 'string') {
55
+ setScreenshots(prev => [...prev, screenshot]);
56
+ }
57
+ });
58
+
59
+ wsManager.on('proxy_stats', (data: any) => {
60
+ setProxyStats(data.stats || data);
61
+ });
62
+
63
+ wsManager.on('token_usage', (data: any) => {
64
+ updateTokenUsage(data.token_usage || data);
65
+ });
66
+
67
+ wsManager.on('page_info', (data: any) => {
68
+ setStatus({
69
+ message: `Navigating: ${data.url} • Found ${data.interactive_elements} interactive elements`,
70
+ type: 'info'
71
+ });
72
+ });
73
+
74
+ wsManager.on('extraction', (data: any) => {
75
+ if (data.status === 'completed') {
76
+ setStatus({
77
+ message: `Extraction completed successfully in ${data.format?.toUpperCase()} format`,
78
+ type: 'success'
79
+ });
80
+ }
81
+ });
82
+
83
+ wsManager.on('error', (data: any) => {
84
+ setStatus({
85
+ message: data.message || data.error || 'An unexpected error occurred',
86
+ type: 'error'
87
+ });
88
+ setIsLoading(false);
89
+ });
90
+
91
+ return () => {
92
+ wsManager.disconnect();
93
+ wsManager.disconnectStream();
94
+ };
95
+ }, [wsManager]);
96
+
97
+ const updateTokenUsage = (usage: any) => {
98
+ setTokenUsage(prev => ({
99
+ prompt_tokens: prev.prompt_tokens + (usage.prompt_tokens || 0),
100
+ response_tokens: prev.response_tokens + (usage.response_tokens || 0),
101
+ total_tokens: prev.total_tokens + (usage.total_tokens || 0),
102
+ api_calls: prev.api_calls + 1
103
+ }));
104
+ };
105
+
106
+ const handleJobCreated = (jobData: { jobId: string; streaming: boolean; format: string }) => {
107
+ console.log('Job created:', jobData);
108
+ setCurrentJobId(jobData.jobId);
109
+ setIsLoading(true);
110
+ setStreamingEnabled(jobData.streaming);
111
+ wsManager.connect(jobData.jobId);
112
+ };
113
+
114
+ const clearDecisions = () => setDecisions([]);
115
+ const clearScreenshots = () => setScreenshots([]);
116
+
117
+ return (
118
+ <div className="min-h-screen bg-gradient-to-br from-stone-50 via-amber-50/30 to-orange-50/20 dark:from-stone-900 dark:via-stone-800 dark:to-stone-700 transition-all duration-1000">
119
+ <Header />
120
+
121
+ <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
122
+ {/* Welcome Animation */}
123
+ <div className="text-center mb-12 animate-in fade-in slide-in-from-top-1 duration-1000">
124
+ <h1 className="text-4xl font-light text-stone-800 dark:text-stone-200 mb-4 tracking-wide">
125
+ Welcome to <span className="font-medium text-stone-700 dark:text-stone-300">BrowserPilot</span>
126
+ </h1>
127
+ <p className="text-stone-600 dark:text-stone-400 text-lg font-light max-w-2xl mx-auto leading-relaxed">
128
+ Open-source alternative to Perplexity Comet and director.ai. Describe what you need, and watch as your browser comes to life.
129
+ </p>
130
+ </div>
131
+
132
+ {/* Control Panel */}
133
+ <div className="grid grid-cols-1 xl:grid-cols-3 gap-8 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-200">
134
+ <div className="xl:col-span-2">
135
+ <JobForm wsManager={wsManager} onJobCreated={handleJobCreated} />
136
+ </div>
137
+
138
+ <div className="space-y-6">
139
+ <TokenUsage usage={tokenUsage} />
140
+ <ProxyStats stats={proxyStats} />
141
+ </div>
142
+ </div>
143
+
144
+ {/* Status Display */}
145
+ {status && (
146
+ <div className="animate-in fade-in slide-in-from-top-2 duration-500">
147
+ <StatusDisplay status={status} onDismiss={() => setStatus(null)} />
148
+ </div>
149
+ )}
150
+
151
+ {/* Loading State */}
152
+ {isLoading && (
153
+ <div className="bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 p-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
154
+ <div className="flex items-center justify-center space-x-4">
155
+ <div className="w-8 h-8 border-4 border-amber-200 border-t-amber-600 rounded-full animate-spin"></div>
156
+ <p className="text-stone-700 font-medium">BrowserPilot is working...</p>
157
+ </div>
158
+ </div>
159
+ )}
160
+
161
+ {/* Browser Streaming */}
162
+ <div className="animate-in fade-in slide-in-from-left-4 duration-700 delay-400">
163
+ <StreamingViewer
164
+ wsManager={wsManager}
165
+ jobId={currentJobId}
166
+ autoConnect={streamingEnabled}
167
+ />
168
+ </div>
169
+
170
+ {/* Decision Log */}
171
+ <div className="animate-in fade-in slide-in-from-right-4 duration-700 delay-500">
172
+ <DecisionLog decisions={decisions} onClear={clearDecisions} />
173
+ </div>
174
+
175
+ {/* Screenshot Gallery */}
176
+ <div className="animate-in fade-in slide-in-from-bottom-4 duration-700 delay-600">
177
+ <ScreenshotGallery screenshots={screenshots} onClear={clearScreenshots} />
178
+ </div>
179
+ </main>
180
+
181
+ {/* Floating Action Button */}
182
+ <div className="fixed bottom-8 right-8 z-50">
183
+ <button className="w-14 h-14 bg-gradient-to-r from-amber-500 to-orange-500 text-white rounded-full shadow-lg hover:shadow-xl transform hover:scale-110 transition-all duration-300 flex items-center justify-center group">
184
+ <div className="w-6 h-6 border-2 border-white rounded-sm group-hover:rotate-12 transition-transform duration-300"></div>
185
+ </button>
186
+ </div>
187
+ </div>
188
+ );
189
+ };
frontend/src/components/DecisionLog.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Brain, Trash2, Clock, Target, Lightbulb } from 'lucide-react';
3
+
4
+ interface DecisionLogProps {
5
+ decisions: any[];
6
+ onClear: () => void;
7
+ }
8
+
9
+ export const DecisionLog: React.FC<DecisionLogProps> = ({ decisions, onClear }) => {
10
+ return (
11
+ <div className="bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 overflow-hidden transition-all duration-300 hover:shadow-md hover:bg-white/80">
12
+ <div className="p-6">
13
+ <div className="flex items-center justify-between mb-6">
14
+ <div className="flex items-center space-x-3">
15
+ <div className="w-10 h-10 bg-gradient-to-br from-stone-400 to-stone-500 dark:from-stone-500 dark:to-stone-600 rounded-xl flex items-center justify-center shadow-md">
16
+ <Brain className="w-5 h-5 text-white" />
17
+ </div>
18
+ <div>
19
+ <h2 className="text-lg font-medium text-stone-800 dark:text-stone-200">AI Decision Log</h2>
20
+ <p className="text-sm text-stone-600 dark:text-stone-400 font-light">Real-time reasoning & actions</p>
21
+ </div>
22
+ </div>
23
+
24
+ <button
25
+ onClick={onClear}
26
+ className="px-4 py-2 text-xs bg-stone-100/80 dark:bg-stone-700/80 text-stone-600 dark:text-stone-400 rounded-lg hover:bg-stone-200/80 dark:hover:bg-stone-600/80 transition-all duration-200 flex items-center space-x-2 group backdrop-blur-sm"
27
+ >
28
+ <Trash2 className="w-3 h-3 group-hover:scale-110 transition-transform duration-200" />
29
+ <span>Clear Log</span>
30
+ </button>
31
+ </div>
32
+
33
+ <div className="bg-stone-50/80 dark:bg-stone-800/50 backdrop-blur-sm rounded-xl border border-stone-200/60 dark:border-stone-600/60 h-80 overflow-y-auto">
34
+ {decisions.length === 0 ? (
35
+ <div className="flex items-center justify-center h-full text-stone-500 dark:text-stone-400">
36
+ <div className="text-center animate-in fade-in duration-500">
37
+ <Lightbulb className="w-12 h-12 text-stone-400 dark:text-stone-500 mx-auto mb-4" />
38
+ <p className="text-sm font-medium mb-2">No decisions yet</p>
39
+ <p className="text-xs text-stone-400 dark:text-stone-500 font-light">AI reasoning will appear here as tasks run</p>
40
+ </div>
41
+ </div>
42
+ ) : (
43
+ <div className="p-4 space-y-3">
44
+ {decisions.map((decision, index) => (
45
+ <div
46
+ key={index}
47
+ className="bg-white/80 dark:bg-stone-700/50 backdrop-blur-sm p-4 rounded-xl border-l-4 border-stone-400 dark:border-stone-500 shadow-sm hover:shadow-md transition-all duration-300 animate-in fade-in slide-in-from-bottom-2"
48
+ style={{ animationDelay: `${index * 100}ms` }}
49
+ >
50
+ <div className="flex items-center justify-between mb-3">
51
+ <div className="flex items-center space-x-2">
52
+ <Target className="w-4 h-4 text-stone-600 dark:text-stone-400" />
53
+ <span className="font-medium text-stone-600 dark:text-stone-400 text-sm tracking-wide">
54
+ {decision.action?.toUpperCase() || 'THINKING'}
55
+ </span>
56
+ </div>
57
+ <div className="flex items-center space-x-1 text-xs text-stone-500 dark:text-stone-400">
58
+ <Clock className="w-3 h-3" />
59
+ <span>{new Date().toLocaleTimeString()}</span>
60
+ </div>
61
+ </div>
62
+ <p className="text-stone-700 dark:text-stone-300 text-sm mb-2 leading-relaxed font-light">
63
+ {decision.reason || 'Processing...'}
64
+ </p>
65
+ {decision.text && (
66
+ <p className="text-xs text-stone-500 dark:text-stone-400 font-mono bg-stone-100/80 dark:bg-stone-600/50 px-3 py-2 rounded-lg backdrop-blur-sm">
67
+ "{decision.text}"
68
+ </p>
69
+ )}
70
+ </div>
71
+ ))}
72
+ </div>
73
+ )}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ );
78
+ };
frontend/src/components/Header.tsx ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Activity, Settings, User, Moon, Sun } from 'lucide-react';
3
+
4
+
5
+
6
+ const BrowserPilotLogo = ({ className = "" }) => (
7
+ <svg
8
+ viewBox="0 0 1024 1024"
9
+ className={`w-full h-full ${className}`}
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <path d="M0 0 C337.92 0 675.84 0 1024 0 C1024 337.92 1024 675.84 1024 1024 C686.08 1024 348.16 1024 0 1024 C0 686.08 0 348.16 0 0 Z " fill="#FEFEFE" transform="translate(0,0)"/>
13
+ <path d="M0 0 C16.75287235 2.39326748 30.16084399 9.09518011 43 20 C43.73605469 20.59167969 44.47210938 21.18335938 45.23046875 21.79296875 C51.60190269 27.11163563 60.6873028 35.38542461 62 44 C62.66 44 63.32 44 64 44 C64.23074219 44.53367187 64.46148437 45.06734375 64.69921875 45.6171875 C66.52384312 49.78569693 68.43494069 53.85197877 70.5625 57.875 C76.24387517 69.13286341 77.15074725 80.84522004 77.14526367 93.24267578 C77.14862228 94.34301651 77.1519809 95.44335724 77.15544128 96.57704163 C77.16490742 100.19981021 77.16689239 103.82253233 77.16796875 107.4453125 C77.17118495 109.9786684 77.1745493 112.51202412 77.17805481 115.04537964 C77.18403568 120.35284937 77.18593884 125.66030199 77.18530273 130.96777344 C77.18470347 137.06841877 77.1952304 143.16897524 77.2110464 149.26959813 C77.22579799 155.17383676 77.22928804 161.07804081 77.22869301 166.98229599 C77.2298664 169.4771244 77.23425185 171.9719536 77.24202538 174.46677017 C77.33350568 206.8656192 77.33350568 206.8656192 72 220 C71.72414063 220.68964844 71.44828125 221.37929688 71.1640625 222.08984375 C62.12427406 243.45310447 48.26244954 259.85812387 30.66748047 274.58789062 C27.45311344 277.30998417 24.34998247 280.15493026 21.23046875 282.984375 C18.52203531 285.43192606 15.77680541 287.83430427 13.01953125 290.2265625 C11.52433695 291.5395571 10.04488349 292.87052487 8.57421875 294.2109375 C7.76597656 294.94570313 6.95773437 295.68046875 6.125 296.4375 C5.38507813 297.11683594 4.64515625 297.79617188 3.8828125 298.49609375 C2 300 2 300 0 300 C0 201 0 102 0 0 Z " fill="#0F2F52" transform="translate(383,295)"/>
14
+ <path d="M0 0 C1.18980469 -0.01224609 2.37960937 -0.02449219 3.60546875 -0.03710938 C11.31114451 -0.04974163 18.57212145 0.6583843 26.125 2.1875 C26.125 2.8475 26.125 3.5075 26.125 4.1875 C26.99511719 4.29835938 27.86523438 4.40921875 28.76171875 4.5234375 C39.13498909 6.57158611 48.41756383 13.36524831 55.6875 20.8125 C57.48053543 23.77490636 57.85613932 24.78039077 57.125 28.1875 C54.74691804 30.58153675 51.98239531 32.37537509 49.1875 34.25 C48.3728125 34.81654297 47.558125 35.38308594 46.71875 35.96679688 C45.02303097 37.14488915 43.32258798 38.31620863 41.61767578 39.48095703 C37.9246459 42.00928993 34.27695515 44.60039306 30.625 47.1875 C29.21876709 48.17710762 27.81251696 49.16669077 26.40625 50.15625 C21.95413946 53.29891627 17.5377812 56.48994254 13.125 59.6875 C6.62441517 64.39790657 0.0863612 69.05047608 -6.48046875 73.66796875 C-11.6406672 77.30204566 -16.7642894 80.98421401 -21.875 84.6875 C-28.37558483 89.39790657 -34.9136388 94.05047608 -41.48046875 98.66796875 C-46.6406672 102.30204566 -51.7642894 105.98421401 -56.875 109.6875 C-63.372616 114.39575531 -69.90723693 119.04685192 -76.47167969 123.66137695 C-81.7204449 127.35704787 -86.92785214 131.10717754 -92.125 134.875 C-97.5209089 138.77947503 -102.92150187 142.67620283 -108.375 146.5 C-109.3959375 147.21800781 -110.416875 147.93601562 -111.46875 148.67578125 C-113.875 150.1875 -113.875 150.1875 -115.875 150.1875 C-115.92533329 144.96290409 -115.96114875 139.73845654 -115.98486328 134.51367188 C-115.99477398 132.74509211 -116.00830643 130.97652804 -116.02587891 129.20800781 C-116.11596352 119.89219849 -116.01276084 110.67978965 -115.00390625 101.41015625 C-114.92749908 100.69997345 -114.85109192 99.98979065 -114.77236938 99.25808716 C-111.70639993 74.07592452 -97.38310532 50.87093685 -79.875 33.1875 C-78.97201172 32.19363281 -78.97201172 32.19363281 -78.05078125 31.1796875 C-66.51677462 18.68693472 -50.02619612 9.65117207 -34.125 4.25 C-33.26189209 3.95520752 -32.39878418 3.66041504 -31.50952148 3.35668945 C-21.11573359 0.01650263 -10.84289408 0.00583579 0 0 Z " fill="#102F53" transform="translate(498.875,574.8125)"/>
15
+ <path d="M0 0 C0.94994742 -0.00170885 1.89989484 -0.0034177 2.87862855 -0.00517833 C6.07551821 -0.00848393 9.27219998 0.00254812 12.46907043 0.01345825 C14.75519811 0.01402213 17.04132615 0.01370626 19.32745361 0.01257324 C25.54611742 0.01199783 31.76470457 0.02378258 37.98335052 0.03772116 C44.47763544 0.05019719 50.97192198 0.05139654 57.46621704 0.05377197 C68.37050244 0.05928817 79.27475943 0.07179293 90.17903137 0.08963013 C101.41112629 0.10798449 112.64321355 0.12214316 123.87532043 0.13064575 C124.91361264 0.13143392 124.91361264 0.13143392 125.97288045 0.13223802 C129.44537302 0.13484807 132.91786566 0.13737549 136.39035833 0.13986182 C165.20413064 0.16059966 194.01787428 0.1959784 222.83161926 0.24050903 C222.83161926 1.56050903 222.83161926 2.88050903 222.83161926 4.24050903 C221.16941346 5.24790649 219.50141252 6.24573858 217.83161926 7.24050903 C216.15506823 8.9638911 214.63734271 10.79442599 213.08552551 12.63113403 C210.24993653 15.91393029 207.29812799 19.07617856 204.33161926 22.24050903 C203.73397766 22.87843384 203.13633606 23.51635864 202.52058411 24.1736145 C201.26870151 25.50910956 200.01641404 26.8442252 198.76374817 28.1789856 C196.80738954 30.26636141 194.85607132 32.35834787 192.90583801 34.45144653 C191.61031149 35.83951067 190.3147398 37.22753265 189.01911926 38.61550903 C188.43388489 39.24457153 187.84865051 39.87363403 187.24568176 40.52175903 C186.10679542 41.73962854 184.95567059 42.9462009 183.79099655 44.13943291 C181.79295443 46.21616339 181.79295443 46.21616339 180.05415726 48.54667854 C176.87732802 52.35217221 174.37178145 54.86006777 169.47389984 56.06991196 C165.35698172 56.38321585 161.32587844 56.41337311 157.20173645 56.34158325 C155.63216869 56.34723826 154.06261089 56.35678465 152.49308777 56.36990356 C148.25296424 56.39373816 144.01497207 56.36161102 139.77507687 56.31771231 C135.33050187 56.27990013 130.88599918 56.28944137 126.44129944 56.2925415 C118.9831574 56.29061857 111.5258405 56.25359574 104.06794739 56.19412231 C95.44877937 56.12568506 86.83060875 56.10646044 78.21119571 56.11489093 C69.90888518 56.1224174 61.60690894 56.10145438 53.30467987 56.06534195 C49.7752578 56.05011771 46.24598352 56.04373239 42.71652985 56.04424667 C38.56288216 56.04311567 34.41013393 56.01667327 30.2567482 55.97068787 C28.7334108 55.95769299 27.2099744 55.95313748 25.68658829 55.95766068 C23.60560981 55.96236383 21.52720472 55.9362782 19.44654846 55.90234375 C18.28295177 55.89526335 17.11935509 55.88818295 15.92049789 55.88088799 C11.21022401 54.90436524 9.02666672 51.98612036 6.39021301 48.13504028 C3.97239561 43.64480797 1.9587493 39.15619574 0.13630676 34.39675903 C-3.25606544 26.19006221 -7.08179858 18.00385367 -11.27531433 10.1736145 C-12.55220416 7.40969606 -12.4942862 5.23883925 -12.16838074 2.24050903 C-8.16063356 -0.43132242 -4.65265941 -0.03032001 0 0 Z " fill="#113054" transform="translate(504.1683807373047,413.7594909667969)"/>
16
+ <path d="M0 0 C1.22506288 0.00276538 1.22506288 0.00276538 2.4748745 0.00558662 C3.41159492 0.0028373 4.34831535 0.00008797 5.31342125 -0.00274467 C6.34416267 0.00444588 7.3749041 0.01163643 8.43688011 0.01904488 C9.5198185 0.01875282 10.60275688 0.01846077 11.71851158 0.01815987 C15.31140108 0.01955738 18.90408527 0.03513848 22.49693871 0.05078316 C24.98272653 0.05451179 27.46851583 0.05735947 29.95430565 0.0593586 C35.83686706 0.0662244 41.71935222 0.08200019 47.60188234 0.10205758 C54.95490879 0.12658579 62.30794434 0.13734311 69.66100121 0.14843941 C82.78083057 0.16991035 95.90059871 0.20367798 109.02037621 0.24609566 C107.22505512 4.12559282 104.91970566 6.5157086 101.83287621 9.49609566 C97.95694142 13.24735002 94.35376225 17.10489378 90.82579613 21.18457222 C87.8919369 24.53460701 84.8294053 27.76069229 81.77037621 30.99609566 C76.25656484 36.84912361 70.79324027 42.74233414 65.38756371 48.69531441 C64.60639183 49.53707222 63.82521996 50.37883003 63.02037621 51.24609566 C61.98324486 52.54196602 61.98324486 52.54196602 60.92516136 53.86401558 C56.97099627 57.10664365 54.14497315 56.97211761 49.17662621 56.91015816 C47.90655724 56.9173336 47.90655724 56.9173336 46.61083031 56.92465401 C44.8256144 56.92804741 43.04033589 56.91888406 41.25523949 56.89795113 C38.52840198 56.87117447 35.80568001 56.89771464 33.07896996 56.92968941 C31.34067965 56.92638521 29.60239191 56.91997875 27.86412621 56.91015816 C27.05254051 56.92027931 26.24095482 56.93040047 25.40477562 56.94082832 C21.19320196 56.85397974 19.50458267 56.62668178 16.08727074 53.94067574 C14.41550033 51.76121318 13.13666206 49.98637697 12.10631371 47.46875191 C11.82014183 46.7900605 11.53396996 46.11136909 11.23912621 45.41211128 C10.96068871 44.71795113 10.68225121 44.02379097 10.39537621 43.30859566 C9.82131787 41.92387387 9.24581725 40.53974904 8.66881371 39.15625191 C8.39649925 38.49383492 8.1241848 37.83141792 7.84361839 37.14892769 C7.07568897 35.37394492 6.19613562 33.64842383 5.30943871 31.92968941 C4.02037621 29.24609566 4.02037621 29.24609566 4.02037621 26.24609566 C3.36037621 26.24609566 2.70037621 26.24609566 2.02037621 26.24609566 C1.67104027 25.31925972 1.32170433 24.39242378 0.96178246 23.43750191 C0.50674339 22.23996284 0.05170433 21.04242378 -0.41712379 19.80859566 C-0.86958473 18.61363472 -1.32204567 17.41867378 -1.78821754 16.18750191 C-2.69410639 13.40163842 -2.69410639 13.40163842 -3.97962379 12.24609566 C-4.1041724 10.60379201 -4.15530904 8.9555729 -4.16712379 7.30859566 C-4.18130348 6.41269722 -4.19548317 5.51679878 -4.21009254 4.59375191 C-3.91152265 1.55238735 -3.10866164 0.38645127 0 0 Z " fill="#0F2F52" transform="translate(536.9796237945557,501.75390434265137)"/>
17
+ </svg>
18
+ );
19
+
20
+
21
+ export const Header: React.FC = () => {
22
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
23
+ const [isDarkMode, setIsDarkMode] = useState(false);
24
+
25
+ const toggleDarkMode = () => {
26
+ setIsDarkMode(!isDarkMode);
27
+ document.documentElement.classList.toggle('dark');
28
+ };
29
+
30
+ return (
31
+ <header className="bg-white/80 dark:bg-stone-900/80 backdrop-blur-md shadow-sm border-b border-stone-200/60 dark:border-stone-700/60 sticky top-0 z-50 transition-all duration-300">
32
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
33
+ <div className="flex items-center justify-between h-16">
34
+ <div className="flex items-center space-x-4 group">
35
+ {/* Logo Placeholder - You can replace this with your SVG */}
36
+ <div className="w-10 h-10 bg-gradient-to-br from-stone-100 to-stone-200 dark:from-stone-700 dark:to-stone-800 rounded-xl flex items-center justify-center shadow-md group-hover:shadow-lg transition-all duration-300 group-hover:scale-105 p-1">
37
+ <BrowserPilotLogo className="group-hover:scale-110 transition-transform duration-300" />
38
+ </div>
39
+ <div className="transform group-hover:translate-x-1 transition-transform duration-300">
40
+ <h1 className="text-xl font-medium text-stone-800 dark:text-stone-200 tracking-wide">BrowserPilot</h1>
41
+ <p className="text-xs text-stone-500 dark:text-stone-400 -mt-1 font-light">Open-source alternative to Perplexity Comet</p>
42
+ </div>
43
+ </div>
44
+
45
+ <div className="flex items-center space-x-6">
46
+ {/* Status Indicator */}
47
+ <div className="flex items-center space-x-3 px-4 py-2 bg-stone-50 dark:bg-stone-800 rounded-full border border-stone-200/60 dark:border-stone-700/60 hover:bg-stone-100 dark:hover:bg-stone-700 transition-colors duration-200">
48
+ <Activity className="w-4 h-4 text-emerald-500 animate-pulse" />
49
+ <span className="text-sm text-stone-600 dark:text-stone-300 font-medium">Active</span>
50
+ </div>
51
+
52
+ {/* Dark Mode Toggle */}
53
+ <button
54
+ onClick={toggleDarkMode}
55
+ className="w-10 h-10 bg-stone-100 dark:bg-stone-800 hover:bg-stone-200 dark:hover:bg-stone-700 rounded-full flex items-center justify-center transition-all duration-200 hover:scale-105"
56
+ >
57
+ {isDarkMode ? (
58
+ <Sun className="w-5 h-5 text-stone-600 dark:text-stone-300" />
59
+ ) : (
60
+ <Moon className="w-5 h-5 text-stone-600 dark:text-stone-300" />
61
+ )}
62
+ </button>
63
+
64
+ {/* User Menu */}
65
+ <div className="relative">
66
+ <button
67
+ onClick={() => setIsMenuOpen(!isMenuOpen)}
68
+ className="w-10 h-10 bg-stone-100 dark:bg-stone-800 hover:bg-stone-200 dark:hover:bg-stone-700 rounded-full flex items-center justify-center transition-all duration-200 hover:scale-105"
69
+ >
70
+ <User className="w-5 h-5 text-stone-600 dark:text-stone-300" />
71
+ </button>
72
+
73
+ {isMenuOpen && (
74
+ <div className="absolute right-0 mt-2 w-48 bg-white dark:bg-stone-800 rounded-xl shadow-lg border border-stone-200/60 dark:border-stone-700/60 py-2 animate-in fade-in slide-in-from-top-2 duration-200">
75
+ <button className="w-full px-4 py-2 text-left text-stone-700 dark:text-stone-300 hover:bg-stone-50 dark:hover:bg-stone-700 transition-colors duration-150 flex items-center space-x-3">
76
+ <Settings className="w-4 h-4" />
77
+ <span>Settings</span>
78
+ </button>
79
+ <a
80
+ href="https://browserpilot-alpha.vercel.app/"
81
+ target="_blank"
82
+ rel="noopener noreferrer"
83
+ className="w-full px-4 py-2 text-left text-stone-700 dark:text-stone-300 hover:bg-stone-50 dark:hover:bg-stone-700 transition-colors duration-150 flex items-center space-x-3"
84
+ >
85
+ <Activity className="w-4 h-4" />
86
+ <span>Visit Landing Page</span>
87
+ </a>
88
+ </div>
89
+ )}
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </header>
95
+ );
96
+ };
frontend/src/components/JobForm.tsx ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Play, Download, Monitor, Eye, EyeOff, Sparkles, ArrowRight } from 'lucide-react';
3
+ import { WebSocketManager } from '../services/WebSocketManager';
4
+ const API_BASE_URL = 'http://localhost:8000';
5
+
6
+ interface JobFormProps {
7
+ wsManager: WebSocketManager;
8
+ onJobCreated: (data: { jobId: string; streaming: boolean; format: string }) => void;
9
+ }
10
+
11
+ export const JobForm: React.FC<JobFormProps> = ({ wsManager, onJobCreated }) => {
12
+ const [prompt, setPrompt] = useState('Navigate to Hacker News and extract the top 10 stories as JSON with titles, URLs, and scores');
13
+ const [format, setFormat] = useState('json');
14
+ const [headless, setHeadless] = useState(false);
15
+ const [streaming, setStreaming] = useState(true);
16
+ const [currentJobId, setCurrentJobId] = useState<string | null>(null);
17
+ const [isSubmitting, setIsSubmitting] = useState(false);
18
+ const [detectedFormat, setDetectedFormat] = useState<string | null>(null);
19
+ const [isHovered, setIsHovered] = useState(false);
20
+
21
+ useEffect(() => {
22
+ const detected = detectFormatFromPrompt(prompt);
23
+ setDetectedFormat(detected);
24
+ }, [prompt]);
25
+
26
+ const detectFormatFromPrompt = (text: string): string | null => {
27
+ const lower = text.toLowerCase();
28
+ const patterns = {
29
+ pdf: [/\bpdf\b/, /pdf format/, /save.*pdf/, /as pdf/, /to pdf/],
30
+ csv: [/\bcsv\b/, /csv format/, /save.*csv/, /as csv/, /to csv/],
31
+ json: [/\bjson\b/, /json format/, /save.*json/, /as json/, /to json/],
32
+ html: [/\bhtml\b/, /html format/, /save.*html/, /as html/, /to html/],
33
+ md: [/\bmarkdown\b/, /md format/, /save.*markdown/, /as markdown/, /to md/],
34
+ txt: [/\btext\b/, /txt format/, /save.*text/, /as text/, /to txt/, /plain text/]
35
+ };
36
+
37
+ for (const [fmt, regexes] of Object.entries(patterns)) {
38
+ if (regexes.some(regex => regex.test(lower))) {
39
+ return fmt;
40
+ }
41
+ }
42
+ return null;
43
+ };
44
+
45
+ const handleSubmit = async (e: React.FormEvent) => {
46
+ e.preventDefault();
47
+ if (!prompt.trim() || isSubmitting) return;
48
+
49
+ setIsSubmitting(true);
50
+ const finalFormat = detectedFormat || format;
51
+
52
+ try {
53
+ const response = await fetch(`${API_BASE_URL}/job`, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({
57
+ prompt,
58
+ format: finalFormat,
59
+ headless,
60
+ enable_streaming: streaming
61
+ })
62
+ });
63
+
64
+ const data = await response.json();
65
+ setCurrentJobId(data.job_id);
66
+ onJobCreated({
67
+ jobId: data.job_id,
68
+ streaming,
69
+ format: finalFormat
70
+ });
71
+ } catch (error) {
72
+ console.error('Error creating job:', error);
73
+ } finally {
74
+ setIsSubmitting(false);
75
+ }
76
+ };
77
+
78
+ const handleDownload = async () => {
79
+ if (!currentJobId) return;
80
+
81
+ try {
82
+ const response = await fetch(`${API_BASE_URL}/download/${currentJobId}`);
83
+ if (response.ok) {
84
+ const blob = await response.blob();
85
+ const url = window.URL.createObjectURL(blob);
86
+ const a = document.createElement('a');
87
+ a.href = url;
88
+ a.download = `browserpilot_result_${currentJobId}.${format}`;
89
+ a.click();
90
+ window.URL.revokeObjectURL(url);
91
+ }
92
+ } catch (error) {
93
+ console.error('Download error:', error);
94
+ }
95
+ };
96
+
97
+ return (
98
+ <div
99
+ className="bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 overflow-hidden transition-all duration-500 hover:shadow-md hover:bg-white/80"
100
+ onMouseEnter={() => setIsHovered(true)}
101
+ onMouseLeave={() => setIsHovered(false)}
102
+ >
103
+ <div className="p-8">
104
+ <div className="flex items-center space-x-4 mb-8">
105
+ <div className={`w-12 h-12 bg-gradient-to-br from-amber-400 to-orange-500 rounded-xl flex items-center justify-center shadow-md transition-all duration-300 ${isHovered ? 'scale-110 rotate-3' : ''}`}>
106
+ <Sparkles className="w-6 h-6 text-white" />
107
+ </div>
108
+ <div>
109
+ <h2 className="text-xl font-medium text-stone-800 mb-1">Create Automation Task</h2>
110
+ <p className="text-stone-600 font-light">Describe what you want to accomplish, and BrowserPilot will handle the rest</p>
111
+ </div>
112
+ </div>
113
+
114
+ <form onSubmit={handleSubmit} className="space-y-8">
115
+ {/* Task Description */}
116
+ <div className="space-y-3">
117
+ <label htmlFor="prompt" className="block text-sm font-medium text-stone-700">
118
+ Task Description
119
+ <span className="text-stone-500 font-light ml-2">(natural language works best)</span>
120
+ </label>
121
+ <div className="relative">
122
+ <textarea
123
+ id="prompt"
124
+ value={prompt}
125
+ onChange={(e) => setPrompt(e.target.value)}
126
+ rows={4}
127
+ className="w-full px-4 py-4 border border-stone-300/60 dark:border-stone-600/60 rounded-xl focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500 transition-all duration-200 resize-none text-stone-800 dark:text-stone-200 placeholder-stone-400 dark:placeholder-stone-500 bg-white/50 dark:bg-stone-800/50 backdrop-blur-sm"
128
+ placeholder="Examples:&#10;• Navigate to Amazon and find wireless headphones under $100&#10;• Go to LinkedIn and extract AI engineer profiles in San Francisco&#10;• Visit news.ycombinator.com and save top stories as JSON"
129
+ />
130
+ <div className="absolute bottom-3 right-3">
131
+ <ArrowRight className="w-5 h-5 text-stone-400" />
132
+ </div>
133
+ </div>
134
+
135
+ {/* Example Prompts */}
136
+ <div className="mt-4">
137
+ <label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-3">
138
+ Quick Examples
139
+ <span className="text-stone-500 dark:text-stone-400 font-light ml-2">(click to use)</span>
140
+ </label>
141
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
142
+ <button
143
+ type="button"
144
+ onClick={() => setPrompt("Go to https://news.ycombinator.com and save top stories as JSON")}
145
+ className="text-left p-3 bg-stone-50/80 dark:bg-stone-800/50 hover:bg-stone-100/80 dark:hover:bg-stone-700/50 border border-stone-200/60 dark:border-stone-600/60 rounded-lg transition-all duration-200 group backdrop-blur-sm"
146
+ >
147
+ <div className="text-sm font-medium text-stone-700 dark:text-stone-300 group-hover:text-stone-900 dark:group-hover:text-stone-100">
148
+ Hacker News Stories
149
+ </div>
150
+ <div className="text-xs text-stone-500 dark:text-stone-400 mt-1">
151
+ Extract top stories as JSON
152
+ </div>
153
+ </button>
154
+
155
+ <button
156
+ type="button"
157
+ onClick={() => setPrompt("Visit firecrawl.dev pricing page and save to PDF format")}
158
+ className="text-left p-3 bg-stone-50/80 dark:bg-stone-800/50 hover:bg-stone-100/80 dark:hover:bg-stone-700/50 border border-stone-200/60 dark:border-stone-600/60 rounded-lg transition-all duration-200 group backdrop-blur-sm"
159
+ >
160
+ <div className="text-sm font-medium text-stone-700 dark:text-stone-300 group-hover:text-stone-900 dark:group-hover:text-stone-100">
161
+ Save Page as PDF
162
+ </div>
163
+ <div className="text-xs text-stone-500 dark:text-stone-400 mt-1">
164
+ Convert webpage to PDF
165
+ </div>
166
+ </button>
167
+
168
+ <button
169
+ type="button"
170
+ onClick={() => setPrompt("Search 'AI tools' and export results as CSV")}
171
+ className="text-left p-3 bg-stone-50/80 dark:bg-stone-800/50 hover:bg-stone-100/80 dark:hover:bg-stone-700/50 border border-stone-200/60 dark:border-stone-600/60 rounded-lg transition-all duration-200 group backdrop-blur-sm"
172
+ >
173
+ <div className="text-sm font-medium text-stone-700 dark:text-stone-300 group-hover:text-stone-900 dark:group-hover:text-stone-100">
174
+ Search & Export CSV
175
+ </div>
176
+ <div className="text-xs text-stone-500 dark:text-stone-400 mt-1">
177
+ Search results to spreadsheet
178
+ </div>
179
+ </button>
180
+
181
+ <button
182
+ type="button"
183
+ onClick={() => setPrompt("Find AI engineers in San Francisco on LinkedIn and save their profiles")}
184
+ className="text-left p-3 bg-stone-50/80 dark:bg-stone-800/50 hover:bg-stone-100/80 dark:hover:bg-stone-700/50 border border-stone-200/60 dark:border-stone-600/60 rounded-lg transition-all duration-200 group backdrop-blur-sm"
185
+ >
186
+ <div className="text-sm font-medium text-stone-700 dark:text-stone-300 group-hover:text-stone-900 dark:group-hover:text-stone-100">
187
+ LinkedIn Profiles
188
+ </div>
189
+ <div className="text-xs text-stone-500 dark:text-stone-400 mt-1">
190
+ Extract professional profiles
191
+ </div>
192
+ </button>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ {/* Format Detection Indicator */}
198
+ {detectedFormat && (
199
+ <div className="p-4 bg-amber-50/80 backdrop-blur-sm border border-amber-200/60 rounded-xl animate-in fade-in slide-in-from-top-1 duration-300">
200
+ <div className="flex items-center">
201
+ <Sparkles className="w-4 h-4 text-amber-600 mr-3" />
202
+ <span className="text-sm font-medium text-amber-800">Smart Detection:</span>
203
+ <span className="ml-2 text-sm text-amber-700 font-mono uppercase bg-amber-100 px-2 py-1 rounded-md">
204
+ {detectedFormat} format detected
205
+ </span>
206
+ </div>
207
+ </div>
208
+ )}
209
+
210
+ {/* Configuration Grid */}
211
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
212
+ {/* Format Selection */}
213
+ <div className="space-y-3">
214
+ <label htmlFor="format" className="block text-sm font-medium text-stone-700">
215
+ Output Format
216
+ </label>
217
+ <select
218
+ id="format"
219
+ value={format}
220
+ onChange={(e) => setFormat(e.target.value)}
221
+ className="w-full px-4 py-3 border border-stone-300/60 dark:border-stone-600/60 rounded-xl focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500 transition-all duration-200 text-stone-800 dark:text-stone-200 bg-white/50 dark:bg-stone-800/50 backdrop-blur-sm"
222
+ >
223
+ <option value="txt">Plain Text (TXT)</option>
224
+ <option value="md">Markdown (MD)</option>
225
+ <option value="json">JSON</option>
226
+ <option value="html">HTML</option>
227
+ <option value="csv">CSV</option>
228
+ <option value="pdf">PDF</option>
229
+ </select>
230
+ </div>
231
+
232
+ {/* Options */}
233
+ <div className="space-y-4">
234
+ <label className="block text-sm font-medium text-stone-700">Automation Options</label>
235
+ <div className="space-y-4">
236
+ <label className="flex items-center cursor-pointer group">
237
+ <input
238
+ type="checkbox"
239
+ checked={headless}
240
+ onChange={(e) => setHeadless(e.target.checked)}
241
+ className="w-4 h-4 text-amber-600 border-stone-300 dark:border-stone-600 rounded focus:ring-amber-500/20 focus:ring-2 bg-white dark:bg-stone-800"
242
+ />
243
+ <div className="ml-3 flex items-center">
244
+ {headless ? (
245
+ <EyeOff className="w-4 h-4 text-stone-600 dark:text-stone-400 mr-2" />
246
+ ) : (
247
+ <Eye className="w-4 h-4 text-stone-600 dark:text-stone-400 mr-2" />
248
+ )}
249
+ <span className="text-sm text-stone-700 dark:text-stone-300 group-hover:text-stone-900 dark:group-hover:text-stone-100 transition-colors">
250
+ Headless Mode
251
+ </span>
252
+ </div>
253
+ </label>
254
+ <label className="flex items-center cursor-pointer group">
255
+ <input
256
+ type="checkbox"
257
+ checked={streaming}
258
+ onChange={(e) => setStreaming(e.target.checked)}
259
+ className="w-4 h-4 text-amber-600 border-stone-300 dark:border-stone-600 rounded focus:ring-amber-500/20 focus:ring-2 bg-white dark:bg-stone-800"
260
+ />
261
+ <div className="ml-3 flex items-center">
262
+ <Monitor className="w-4 h-4 text-stone-600 dark:text-stone-400 mr-2" />
263
+ <span className="text-sm text-stone-700 dark:text-stone-300 group-hover:text-stone-900 dark:group-hover:text-stone-100 transition-colors">
264
+ Live Streaming
265
+ </span>
266
+ </div>
267
+ </label>
268
+ </div>
269
+ </div>
270
+ </div>
271
+
272
+ {/* Action Buttons */}
273
+ <div className="flex flex-wrap gap-4 pt-6 border-t border-stone-200/60">
274
+ <button
275
+ type="submit"
276
+ disabled={isSubmitting || !prompt.trim()}
277
+ className="flex-1 min-w-0 bg-gradient-to-r from-amber-500 to-orange-500 text-white px-8 py-4 rounded-xl font-medium hover:from-amber-600 hover:to-orange-600 focus:outline-none focus:ring-2 focus:ring-amber-500/20 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 flex items-center justify-center space-x-3 group shadow-md hover:shadow-lg"
278
+ >
279
+ <Play className={`w-5 h-5 transition-transform duration-300 ${isSubmitting ? 'animate-spin' : 'group-hover:scale-110'}`} />
280
+ <span className="text-lg">{isSubmitting ? 'Launching BrowserPilot...' : 'Start Automation'}</span>
281
+ </button>
282
+
283
+ <button
284
+ type="button"
285
+ onClick={handleDownload}
286
+ disabled={!currentJobId}
287
+ className="px-8 py-4 border border-stone-300/60 dark:border-stone-600/60 text-stone-700 dark:text-stone-300 rounded-xl font-medium hover:bg-stone-50 dark:hover:bg-stone-700/50 hover:border-stone-400 dark:hover:border-stone-500 focus:outline-none focus:ring-2 focus:ring-stone-500/20 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 flex items-center space-x-3 group bg-white/50 dark:bg-stone-800/50 backdrop-blur-sm"
288
+ >
289
+ <Download className="w-5 h-5 group-hover:scale-110 transition-transform duration-300" />
290
+ <span>Download Results</span>
291
+ </button>
292
+ </div>
293
+ </form>
294
+ </div>
295
+ </div>
296
+ );
297
+ };
frontend/src/components/ProxyStats.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Shield, CheckCircle, XCircle, RotateCcw } from 'lucide-react';
3
+
4
+ interface ProxyStatsProps {
5
+ stats: {
6
+ available: number;
7
+ healthy: number;
8
+ blocked: number;
9
+ retry_count: number;
10
+ };
11
+ }
12
+
13
+ export const ProxyStats: React.FC<ProxyStatsProps> = ({ stats }) => {
14
+ return (
15
+ <div className="bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 overflow-hidden transition-all duration-300 hover:shadow-md hover:bg-white/80">
16
+ <div className="p-6">
17
+ <div className="flex items-center space-x-3 mb-6">
18
+ <div className="w-10 h-10 bg-gradient-to-br from-stone-400 to-stone-500 dark:from-stone-500 dark:to-stone-600 rounded-xl flex items-center justify-center shadow-md">
19
+ <Shield className="w-5 h-5 text-white" />
20
+ </div>
21
+ <div>
22
+ <h2 className="text-lg font-medium text-stone-800 dark:text-stone-200">Proxy Network</h2>
23
+ <p className="text-sm text-stone-600 dark:text-stone-400 font-light">Smart rotation & health</p>
24
+ </div>
25
+ </div>
26
+
27
+ <div className="grid grid-cols-2 gap-4">
28
+ <div className="bg-gradient-to-br from-sage-50/80 to-sage-100/50 dark:from-stone-800/50 dark:to-stone-700/50 p-4 rounded-xl border border-sage-200/40 dark:border-stone-600/40 group hover:shadow-sm transition-all duration-300 backdrop-blur-sm">
29
+ <div className="flex items-center justify-between mb-2">
30
+ <CheckCircle className="w-4 h-4 text-sage-600 dark:text-sage-400" />
31
+ <span className="text-xs text-sage-600/70 dark:text-sage-400/70 font-medium tracking-wide">AVAILABLE</span>
32
+ </div>
33
+ <div className="text-2xl font-light text-sage-700 dark:text-sage-300 mb-1">{stats.available}</div>
34
+ <div className="text-sm text-sage-600/80 dark:text-sage-400/80 font-light">Proxies</div>
35
+ </div>
36
+
37
+ <div className="bg-gradient-to-br from-sky-50/80 to-sky-100/50 dark:from-stone-800/50 dark:to-stone-700/50 p-4 rounded-xl border border-sky-200/40 dark:border-stone-600/40 group hover:shadow-sm transition-all duration-300 backdrop-blur-sm">
38
+ <div className="flex items-center justify-between mb-2">
39
+ <CheckCircle className="w-4 h-4 text-sky-600 dark:text-sky-400 fill-current" />
40
+ <span className="text-xs text-sky-600/70 dark:text-sky-400/70 font-medium tracking-wide">HEALTHY</span>
41
+ </div>
42
+ <div className="text-2xl font-light text-sky-700 dark:text-sky-300 mb-1">{stats.healthy}</div>
43
+ <div className="text-sm text-sky-600/80 dark:text-sky-400/80 font-light">Active</div>
44
+ </div>
45
+
46
+ <div className="bg-gradient-to-br from-honey-50/80 to-honey-100/50 dark:from-stone-800/50 dark:to-stone-700/50 p-4 rounded-xl border border-honey-200/40 dark:border-stone-600/40 group hover:shadow-sm transition-all duration-300 backdrop-blur-sm">
47
+ <div className="flex items-center justify-between mb-2">
48
+ <XCircle className="w-4 h-4 text-honey-600 dark:text-honey-400" />
49
+ <span className="text-xs text-honey-600/70 dark:text-honey-400/70 font-medium tracking-wide">BLOCKED</span>
50
+ </div>
51
+ <div className="text-2xl font-light text-honey-700 dark:text-honey-300 mb-1">{stats.blocked}</div>
52
+ <div className="text-sm text-honey-600/80 dark:text-honey-400/80 font-light">Proxies</div>
53
+ </div>
54
+
55
+ <div className="bg-gradient-to-br from-blush-50/80 to-blush-100/50 dark:from-stone-800/50 dark:to-stone-700/50 p-4 rounded-xl border border-blush-200/40 dark:border-stone-600/40 group hover:shadow-sm transition-all duration-300 backdrop-blur-sm">
56
+ <div className="flex items-center justify-between mb-2">
57
+ <RotateCcw className="w-4 h-4 text-blush-600 dark:text-blush-400" />
58
+ <span className="text-xs text-blush-600/70 dark:text-blush-400/70 font-medium tracking-wide">RETRIES</span>
59
+ </div>
60
+ <div className="text-2xl font-light text-blush-700 dark:text-blush-300 mb-1">{stats.retry_count}</div>
61
+ <div className="text-sm text-blush-600/80 dark:text-blush-400/80 font-light">Count</div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ );
67
+ };
frontend/src/components/ScreenshotGallery.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react'
2
+ import { Camera, X, Maximize2 } from 'lucide-react'
3
+
4
+ interface ScreenshotGalleryProps {
5
+ screenshots: string[]
6
+ onClear: () => void
7
+ }
8
+
9
+ export const ScreenshotGallery: React.FC<ScreenshotGalleryProps> = ({ screenshots, onClear }) => {
10
+ const [selectedImage, setSelectedImage] = useState<string | null>(null)
11
+
12
+ const handleImageClick = (screenshot: string) => {
13
+ setSelectedImage(screenshot)
14
+ }
15
+
16
+ const closeModal = () => {
17
+ setSelectedImage(null)
18
+ }
19
+
20
+ return (
21
+ <div className="animate-fade-in-up" style={{ animationDelay: '0.6s' }}>
22
+ <div className="bg-white/80 dark:bg-stone-800/80 backdrop-blur-sm rounded-2xl border border-stone-200/50 dark:border-stone-700/50 shadow-lg hover:shadow-xl transition-all duration-300">
23
+ <div className="p-6">
24
+ <div className="flex items-center justify-between mb-6">
25
+ <div className="flex items-center space-x-3">
26
+ <div className="w-10 h-10 bg-gradient-to-br from-stone-400 to-stone-500 rounded-xl flex items-center justify-center shadow-sm">
27
+ <Camera className="w-5 h-5 text-white" />
28
+ </div>
29
+ <div>
30
+ <h2 className="text-lg font-semibold text-stone-900 dark:text-stone-100">Screenshots</h2>
31
+ <p className="text-sm text-stone-600 dark:text-stone-400">Captured browser states during automation</p>
32
+ </div>
33
+ </div>
34
+
35
+ <div className="flex items-center space-x-3">
36
+ <span className="text-sm text-stone-500 dark:text-stone-400">
37
+ {screenshots.length} screenshot{screenshots.length !== 1 ? 's' : ''}
38
+ </span>
39
+ <button
40
+ onClick={onClear}
41
+ className="px-3 py-1.5 text-xs bg-stone-100 dark:bg-stone-700 text-stone-600 dark:text-stone-300 rounded-lg hover:bg-stone-200 dark:hover:bg-stone-600 transition-colors duration-200"
42
+ >
43
+ Clear
44
+ </button>
45
+ </div>
46
+ </div>
47
+
48
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-96 overflow-y-auto">
49
+ {screenshots.length === 0 ? (
50
+ <div className="col-span-full text-stone-500 dark:text-stone-400 text-center py-12">
51
+ <Camera className="w-12 h-12 mx-auto mb-3 opacity-50" />
52
+ <p>No screenshots captured yet</p>
53
+ <p className="text-sm mt-1">Screenshots will appear here as the agent runs</p>
54
+ </div>
55
+ ) : (
56
+ screenshots.map((screenshot, index) => (
57
+ <div
58
+ key={index}
59
+ className="relative group cursor-pointer transform hover:scale-105 transition-all duration-300"
60
+ onClick={() => handleImageClick(screenshot)}
61
+ >
62
+ <img
63
+ src={`data:image/png;base64,${screenshot}`}
64
+ className="w-full h-32 object-cover rounded-xl border border-stone-200 dark:border-stone-700 shadow-sm group-hover:shadow-md transition-all duration-300"
65
+ alt={`Screenshot ${index + 1}`}
66
+ />
67
+ <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all duration-300 rounded-xl flex items-center justify-center">
68
+ <Maximize2 className="w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
69
+ </div>
70
+ <div className="absolute bottom-2 left-2 right-2">
71
+ <div className="bg-black/70 text-white text-xs px-2 py-1 rounded-lg backdrop-blur-sm">
72
+ Screenshot {index + 1} - {new Date().toLocaleTimeString()}
73
+ </div>
74
+ </div>
75
+ </div>
76
+ ))
77
+ )}
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ {/* Modal for full-size image */}
83
+ {selectedImage && (
84
+ <div
85
+ className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4 animate-fade-in"
86
+ onClick={closeModal}
87
+ >
88
+ <div className="relative max-w-4xl max-h-full">
89
+ <button
90
+ onClick={closeModal}
91
+ className="absolute -top-12 right-0 text-white hover:text-stone-300 transition-colors"
92
+ >
93
+ <X className="w-8 h-8" />
94
+ </button>
95
+ <img
96
+ src={`data:image/png;base64,${selectedImage}`}
97
+ className="max-w-full max-h-full object-contain rounded-xl shadow-2xl"
98
+ alt="Full size screenshot"
99
+ />
100
+ </div>
101
+ </div>
102
+ )}
103
+ </div>
104
+ )
105
+ }
frontend/src/components/StatusDisplay.tsx ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { CheckCircle, AlertCircle, Info, X, Sparkles } from 'lucide-react';
3
+
4
+ interface StatusDisplayProps {
5
+ status: {
6
+ message: string;
7
+ type: 'success' | 'error' | 'info';
8
+ };
9
+ onDismiss: () => void;
10
+ }
11
+
12
+ export const StatusDisplay: React.FC<StatusDisplayProps> = ({ status, onDismiss }) => {
13
+ const [isVisible, setIsVisible] = useState(true);
14
+
15
+ useEffect(() => {
16
+ setIsVisible(true);
17
+ const timer = setTimeout(() => {
18
+ setIsVisible(false);
19
+ setTimeout(onDismiss, 300);
20
+ }, 6000);
21
+
22
+ return () => clearTimeout(timer);
23
+ }, [status, onDismiss]);
24
+
25
+ const getStatusConfig = () => {
26
+ switch (status.type) {
27
+ case 'success':
28
+ return {
29
+ icon: CheckCircle,
30
+ bgColor: 'bg-emerald-50/90 border-emerald-200/60',
31
+ textColor: 'text-emerald-800',
32
+ iconColor: 'text-emerald-600',
33
+ accentColor: 'bg-emerald-500'
34
+ };
35
+ case 'error':
36
+ return {
37
+ icon: AlertCircle,
38
+ bgColor: 'bg-rose-50/90 border-rose-200/60',
39
+ textColor: 'text-rose-800',
40
+ iconColor: 'text-rose-600',
41
+ accentColor: 'bg-rose-500'
42
+ };
43
+ default:
44
+ return {
45
+ icon: Info,
46
+ bgColor: 'bg-blue-50/90 border-blue-200/60',
47
+ textColor: 'text-blue-800',
48
+ iconColor: 'text-blue-600',
49
+ accentColor: 'bg-blue-500'
50
+ };
51
+ }
52
+ };
53
+
54
+ const config = getStatusConfig();
55
+ const Icon = config.icon;
56
+
57
+ return (
58
+ <div
59
+ className={`
60
+ ${config.bgColor} border rounded-xl p-4 shadow-sm backdrop-blur-sm transition-all duration-500 relative overflow-hidden
61
+ ${isVisible ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-2 scale-95'}
62
+ `}
63
+ >
64
+ {/* Animated accent bar */}
65
+ <div className={`absolute top-0 left-0 h-1 ${config.accentColor} animate-pulse`} style={{ width: '100%' }}></div>
66
+
67
+ <div className="flex items-center justify-between">
68
+ <div className="flex items-center space-x-4">
69
+ <div className="relative">
70
+ <Icon className={`w-6 h-6 ${config.iconColor}`} />
71
+ {status.type === 'info' && (
72
+ <Sparkles className="w-3 h-3 text-blue-400 absolute -top-1 -right-1 animate-pulse" />
73
+ )}
74
+ </div>
75
+ <div>
76
+ <span className={`text-sm font-medium ${config.textColor} leading-relaxed`}>
77
+ {status.message}
78
+ </span>
79
+ </div>
80
+ </div>
81
+ <button
82
+ onClick={() => {
83
+ setIsVisible(false);
84
+ setTimeout(onDismiss, 300);
85
+ }}
86
+ className={`${config.textColor} hover:opacity-70 transition-all duration-200 p-1 rounded-lg hover:bg-white/20`}
87
+ >
88
+ <X className="w-4 h-4" />
89
+ </button>
90
+ </div>
91
+ </div>
92
+ );
93
+ };
frontend/src/components/StreamingViewer.tsx ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Monitor, Play, Square, Camera, Settings, Circle, Maximize2 } from 'lucide-react';
3
+ import { WebSocketManager } from '../services/WebSocketManager';
4
+
5
+ interface StreamingViewerProps {
6
+ wsManager: WebSocketManager;
7
+ jobId: string | null;
8
+ autoConnect?: boolean;
9
+ }
10
+
11
+ export const StreamingViewer: React.FC<StreamingViewerProps> = ({ wsManager, jobId, autoConnect = false }) => {
12
+ const [isConnected, setIsConnected] = useState(false);
13
+ const [currentFrame, setCurrentFrame] = useState<string | null>(null);
14
+ const [streamStats, setStreamStats] = useState({ frameCount: 0, fps: 0 });
15
+ const [showStream, setShowStream] = useState(false);
16
+ const [isFullscreen, setIsFullscreen] = useState(false);
17
+ const lastFrameTimeRef = useRef<number | null>(null);
18
+
19
+ useEffect(() => {
20
+ wsManager.on('stream_connected', () => {
21
+ setIsConnected(true);
22
+ });
23
+
24
+ wsManager.on('stream_disconnected', () => {
25
+ setIsConnected(false);
26
+ setStreamStats({ frameCount: 0, fps: 0 });
27
+ lastFrameTimeRef.current = null;
28
+ });
29
+
30
+ wsManager.on('stream_frame', (data: any) => {
31
+ setCurrentFrame(data.data);
32
+
33
+ const now = performance.now();
34
+ const lastFrame = lastFrameTimeRef.current;
35
+ const fps = lastFrame ? Math.round(1000 / Math.max(now - lastFrame, 1)) : 0;
36
+ lastFrameTimeRef.current = now;
37
+
38
+ setStreamStats(prev => ({
39
+ frameCount: prev.frameCount + 1,
40
+ fps: fps || prev.fps
41
+ }));
42
+ });
43
+
44
+ wsManager.on('streaming_info', (data: any) => {
45
+ if (data.streaming?.enabled) {
46
+ setShowStream(true);
47
+ }
48
+ });
49
+ }, [wsManager]);
50
+
51
+ useEffect(() => {
52
+ if (autoConnect && jobId && !wsManager.isStreamConnected()) {
53
+ setShowStream(true);
54
+ wsManager.connectStream(jobId);
55
+ } else {
56
+ console.log('Auto-connect conditions not met:', {
57
+ autoConnect: !!autoConnect,
58
+ jobId: !!jobId,
59
+ notConnected: !wsManager.isStreamConnected()
60
+ });
61
+ }
62
+
63
+ if (!jobId) {
64
+ setCurrentFrame(null);
65
+ setStreamStats({ frameCount: 0, fps: 0 });
66
+ lastFrameTimeRef.current = null;
67
+ setIsConnected(false);
68
+ }
69
+ }, [autoConnect, jobId, wsManager]);
70
+
71
+ const handleConnect = () => {
72
+ if (jobId) {
73
+ wsManager.connectStream(jobId);
74
+ }
75
+ };
76
+
77
+ const handleDisconnect = () => {
78
+ wsManager.disconnectStream();
79
+ };
80
+
81
+ const handleStreamClick = (e: React.MouseEvent<HTMLImageElement>) => {
82
+ if (!isConnected) return;
83
+
84
+ const rect = e.currentTarget.getBoundingClientRect();
85
+ const x = Math.round((e.clientX - rect.left) * (1280 / rect.width));
86
+ const y = Math.round((e.clientY - rect.top) * (800 / rect.height));
87
+
88
+ wsManager.sendStreamMessage({
89
+ type: 'mouse',
90
+ eventType: 'mousePressed',
91
+ x,
92
+ y,
93
+ button: 'left',
94
+ clickCount: 1
95
+ });
96
+
97
+ setTimeout(() => {
98
+ wsManager.sendStreamMessage({
99
+ type: 'mouse',
100
+ eventType: 'mouseReleased',
101
+ x,
102
+ y,
103
+ button: 'left'
104
+ });
105
+ }, 100);
106
+ };
107
+
108
+ if (!showStream) {
109
+ return (
110
+ <div className="bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 overflow-hidden transition-all duration-300 hover:shadow-md hover:bg-white/80">
111
+ <div className="p-8">
112
+ <div className="flex items-center space-x-3 mb-6">
113
+ <div className="w-10 h-10 bg-gradient-to-br from-purple-400 to-pink-500 rounded-xl flex items-center justify-center shadow-md">
114
+ <Monitor className="w-5 h-5 text-white" />
115
+ </div>
116
+ <div>
117
+ <h2 className="text-lg font-medium text-stone-800">Live Browser View</h2>
118
+ <p className="text-sm text-stone-600 font-light">Real-time browser streaming with interaction</p>
119
+ </div>
120
+ </div>
121
+
122
+ <div className="bg-stone-50/80 dark:bg-stone-800/50 backdrop-blur-sm rounded-xl border-2 border-dashed border-stone-300/60 dark:border-stone-600/60 p-16 text-center">
123
+ <div className="w-20 h-20 bg-stone-200/80 dark:bg-stone-700/80 rounded-2xl mx-auto mb-6 flex items-center justify-center">
124
+ <Monitor className="w-10 h-10 text-stone-400 dark:text-stone-500" />
125
+ </div>
126
+ <h3 className="text-xl font-medium text-stone-800 dark:text-stone-200 mb-3">Browser Streaming</h3>
127
+ <p className="text-stone-600 dark:text-stone-400 font-light mb-6 max-w-md mx-auto leading-relaxed">
128
+ Enable streaming to watch BrowserPilot navigate websites in real-time
129
+ </p>
130
+ <button
131
+ onClick={() => setShowStream(true)}
132
+ className="px-6 py-3 bg-gradient-to-r from-stone-500 to-stone-600 text-white rounded-xl hover:from-stone-600 hover:to-stone-700 transition-all duration-300 flex items-center space-x-3 mx-auto group shadow-md hover:shadow-lg"
133
+ >
134
+ <Play className="w-5 h-5 group-hover:scale-110 transition-transform duration-300" />
135
+ <span className="font-medium">Enable Live Streaming</span>
136
+ </button>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ return (
144
+ <div className="bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 overflow-hidden transition-all duration-300 hover:shadow-md hover:bg-white/80">
145
+ <div className="p-6">
146
+ <div className="flex items-center justify-between mb-6">
147
+ <div className="flex items-center space-x-3">
148
+ <div className="w-10 h-10 bg-gradient-to-br from-stone-400 to-stone-500 dark:from-stone-500 dark:to-stone-600 rounded-xl flex items-center justify-center shadow-md">
149
+ <Monitor className="w-5 h-5 text-white" />
150
+ </div>
151
+ <div>
152
+ <h2 className="text-lg font-medium text-stone-800 dark:text-stone-200">Live Browser View</h2>
153
+ <p className="text-sm text-stone-600 dark:text-stone-400 font-light">Real-time browser streaming with interaction</p>
154
+ </div>
155
+ </div>
156
+
157
+ <div className="flex items-center space-x-4">
158
+ <div className={`flex items-center space-x-2 px-3 py-1 rounded-full text-xs font-medium transition-all duration-300 ${
159
+ isConnected
160
+ ? 'bg-sage-100/80 dark:bg-sage-800/50 text-sage-800 dark:text-sage-200 backdrop-blur-sm'
161
+ : 'bg-blush-100/80 dark:bg-blush-800/50 text-blush-800 dark:text-blush-200 backdrop-blur-sm'
162
+ }`}>
163
+ <Circle className={`w-2 h-2 ${isConnected ? 'text-sage-600 dark:text-sage-400 fill-current animate-pulse' : 'text-blush-600 dark:text-blush-400 fill-current'}`} />
164
+ <span>{isConnected ? 'Live' : 'Offline'}</span>
165
+ </div>
166
+ <span className="text-xs text-stone-500 dark:text-stone-400 font-light">{streamStats.fps} FPS</span>
167
+ </div>
168
+ </div>
169
+
170
+ <div className="relative bg-stone-900 dark:bg-stone-950 rounded-xl overflow-hidden border border-stone-200/60 dark:border-stone-600/60 shadow-lg">
171
+ {currentFrame ? (
172
+ <img
173
+ src={`data:image/jpeg;base64,${currentFrame}`}
174
+ alt="Browser Stream"
175
+ className="w-full h-auto cursor-pointer hover:opacity-95 transition-opacity duration-200"
176
+ onClick={handleStreamClick}
177
+ />
178
+ ) : (
179
+ <div className="aspect-video flex items-center justify-center bg-stone-900 dark:bg-stone-950">
180
+ <div className="text-center animate-in fade-in duration-500">
181
+ <Monitor className="w-16 h-16 text-stone-400 dark:text-stone-500 mx-auto mb-4" />
182
+ <p className="text-stone-300 dark:text-stone-400 text-sm font-light">Waiting for stream data...</p>
183
+ </div>
184
+ </div>
185
+ )}
186
+
187
+ {/* Stream Controls Overlay */}
188
+ <div className="absolute bottom-4 left-4 right-4">
189
+ <div className="bg-black/70 backdrop-blur-md rounded-xl p-4 border border-white/10">
190
+ <div className="flex items-center justify-between">
191
+ <div className="flex items-center space-x-3">
192
+ <button
193
+ onClick={handleConnect}
194
+ disabled={isConnected || !jobId}
195
+ className="px-4 py-2 bg-emerald-500 text-white rounded-lg text-sm hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center space-x-2 group"
196
+ >
197
+ <Play className="w-4 h-4 group-hover:scale-110 transition-transform duration-200" />
198
+ <span>Connect</span>
199
+ </button>
200
+ <button
201
+ onClick={handleDisconnect}
202
+ disabled={!isConnected}
203
+ className="px-4 py-2 bg-rose-500 text-white rounded-lg text-sm hover:bg-rose-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center space-x-2 group"
204
+ >
205
+ <Square className="w-4 h-4 group-hover:scale-110 transition-transform duration-200" />
206
+ <span>Disconnect</span>
207
+ </button>
208
+ <button className="px-4 py-2 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600 transition-all duration-200 flex items-center space-x-2 group">
209
+ <Camera className="w-4 h-4 group-hover:scale-110 transition-transform duration-200" />
210
+ <span>Capture</span>
211
+ </button>
212
+ </div>
213
+
214
+ <div className="flex items-center space-x-3 text-white text-sm">
215
+ <button className="p-2 hover:bg-white/10 rounded-lg transition-colors duration-200">
216
+ <Maximize2 className="w-4 h-4" />
217
+ </button>
218
+ <Settings className="w-4 h-4" />
219
+ <select className="bg-stone-700/80 text-white rounded-lg px-3 py-1 text-xs border-0 focus:ring-2 focus:ring-purple-500 backdrop-blur-sm">
220
+ <option value="60">Low Quality</option>
221
+ <option value="80" selected>Medium Quality</option>
222
+ <option value="100">High Quality</option>
223
+ </select>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ );
232
+ };
frontend/src/components/TokenUsage.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { BarChart3, TrendingUp, Zap, Activity } from 'lucide-react';
3
+
4
+ interface TokenUsageProps {
5
+ usage: {
6
+ prompt_tokens: number;
7
+ response_tokens: number;
8
+ total_tokens: number;
9
+ api_calls: number;
10
+ };
11
+ }
12
+
13
+ export const TokenUsage: React.FC<TokenUsageProps> = ({ usage }) => {
14
+ return (
15
+ <div className="bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 overflow-hidden transition-all duration-300 hover:shadow-md hover:bg-white/80">
16
+ <div className="p-6">
17
+ <div className="flex items-center space-x-3 mb-6">
18
+ <div className="w-10 h-10 bg-gradient-to-br from-stone-400 to-stone-500 dark:from-stone-500 dark:to-stone-600 rounded-xl flex items-center justify-center shadow-md">
19
+ <BarChart3 className="w-5 h-5 text-white" />
20
+ </div>
21
+ <div>
22
+ <h2 className="text-lg font-medium text-stone-800 dark:text-stone-200">Token Usage</h2>
23
+ <p className="text-sm text-stone-600 dark:text-stone-400 font-light">AI model consumption</p>
24
+ </div>
25
+ </div>
26
+
27
+ <div className="grid grid-cols-2 gap-4">
28
+ <div className="bg-gradient-to-br from-stone-50/80 to-stone-100/50 dark:from-stone-800/50 dark:to-stone-700/50 p-4 rounded-xl border border-stone-200/40 dark:border-stone-600/40 group hover:shadow-sm transition-all duration-300 backdrop-blur-sm">
29
+ <div className="flex items-center justify-between mb-2">
30
+ <Activity className="w-4 h-4 text-stone-600 dark:text-stone-400" />
31
+ <span className="text-xs text-stone-600/70 dark:text-stone-400/70 font-medium tracking-wide">TOTAL</span>
32
+ </div>
33
+ <div className="text-2xl font-light text-stone-700 dark:text-stone-300 mb-1">{usage.total_tokens.toLocaleString()}</div>
34
+ <div className="text-sm text-stone-600/80 dark:text-stone-400/80 font-light">Tokens</div>
35
+ </div>
36
+
37
+ <div className="bg-gradient-to-br from-sage-50/80 to-sage-100/50 dark:from-stone-800/50 dark:to-stone-700/50 p-4 rounded-xl border border-sage-200/40 dark:border-stone-600/40 group hover:shadow-sm transition-all duration-300 backdrop-blur-sm">
38
+ <div className="flex items-center justify-between mb-2">
39
+ <TrendingUp className="w-4 h-4 text-sage-600 dark:text-sage-400" />
40
+ <span className="text-xs text-sage-600/70 dark:text-sage-400/70 font-medium tracking-wide">INPUT</span>
41
+ </div>
42
+ <div className="text-2xl font-light text-sage-700 dark:text-sage-300 mb-1">{usage.prompt_tokens.toLocaleString()}</div>
43
+ <div className="text-sm text-sage-600/80 dark:text-sage-400/80 font-light">Prompt</div>
44
+ </div>
45
+
46
+ <div className="bg-gradient-to-br from-lavender-50/80 to-lavender-100/50 dark:from-stone-800/50 dark:to-stone-700/50 p-4 rounded-xl border border-lavender-200/40 dark:border-stone-600/40 group hover:shadow-sm transition-all duration-300 backdrop-blur-sm">
47
+ <div className="flex items-center justify-between mb-2">
48
+ <Zap className="w-4 h-4 text-lavender-600 dark:text-lavender-400" />
49
+ <span className="text-xs text-lavender-600/70 dark:text-lavender-400/70 font-medium tracking-wide">OUTPUT</span>
50
+ </div>
51
+ <div className="text-2xl font-light text-lavender-700 dark:text-lavender-300 mb-1">{usage.response_tokens.toLocaleString()}</div>
52
+ <div className="text-sm text-lavender-600/80 dark:text-lavender-400/80 font-light">Response</div>
53
+ </div>
54
+
55
+ <div className="bg-gradient-to-br from-peach-50/80 to-peach-100/50 dark:from-stone-800/50 dark:to-stone-700/50 p-4 rounded-xl border border-peach-200/40 dark:border-stone-600/40 group hover:shadow-sm transition-all duration-300 backdrop-blur-sm">
56
+ <div className="flex items-center justify-between mb-2">
57
+ <Activity className="w-4 h-4 text-peach-600 dark:text-peach-400" />
58
+ <span className="text-xs text-peach-600/70 dark:text-peach-400/70 font-medium tracking-wide">CALLS</span>
59
+ </div>
60
+ <div className="text-2xl font-light text-peach-700 dark:text-peach-300 mb-1">{usage.api_calls}</div>
61
+ <div className="text-sm text-peach-600/80 dark:text-peach-400/80 font-light">API Requests</div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ );
67
+ };
frontend/src/index.css ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Custom color definitions for softer palette */
6
+ :root {
7
+ --sage-50: #f6f7f6;
8
+ --sage-100: #e8eae8;
9
+ --sage-200: #d1d5d1;
10
+ --sage-300: #adb5ad;
11
+ --sage-400: #8a948a;
12
+ --sage-500: #6b756b;
13
+ --sage-600: #565e56;
14
+ --sage-700: #484e48;
15
+ --sage-800: #3d423d;
16
+ --sage-900: #343834;
17
+
18
+ --lavender-50: #f7f6f9;
19
+ --lavender-100: #ede9f2;
20
+ --lavender-200: #ddd6e7;
21
+ --lavender-300: #c4b8d4;
22
+ --lavender-400: #a892bd;
23
+ --lavender-500: #9070a6;
24
+ --lavender-600: #7a5a8a;
25
+ --lavender-700: #654a71;
26
+ --lavender-800: #553f5e;
27
+ --lavender-900: #49374f;
28
+
29
+ --peach-50: #fef7f3;
30
+ --peach-100: #fdede5;
31
+ --peach-200: #fad8ca;
32
+ --peach-300: #f6baa4;
33
+ --peach-400: #f0926d;
34
+ --peach-500: #e97142;
35
+ --peach-600: #da5a28;
36
+ --peach-700: #b6481e;
37
+ --peach-800: #923d1d;
38
+ --peach-900: #76361c;
39
+
40
+ --honey-50: #fefbf3;
41
+ --honey-100: #fdf4e1;
42
+ --honey-200: #fae7c2;
43
+ --honey-300: #f6d498;
44
+ --honey-400: #f1bc6c;
45
+ --honey-500: #eca54a;
46
+ --honey-600: #dd8f39;
47
+ --honey-700: #b87532;
48
+ --honey-800: #945d30;
49
+ --honey-900: #784d2a;
50
+
51
+ --blush-50: #fdf4f3;
52
+ --blush-100: #fce7e6;
53
+ --blush-200: #f9d4d2;
54
+ --blush-300: #f4b5b1;
55
+ --blush-400: #ec8b85;
56
+ --blush-500: #e0675e;
57
+ --blush-600: #cd4f45;
58
+ --blush-700: #ab3f37;
59
+ --blush-800: #8d3832;
60
+ --blush-900: #753530;
61
+
62
+ --sky-50: #f0f9ff;
63
+ --sky-100: #e0f2fe;
64
+ --sky-200: #bae6fd;
65
+ --sky-300: #7dd3fc;
66
+ --sky-400: #38bdf8;
67
+ --sky-500: #0ea5e9;
68
+ --sky-600: #0284c7;
69
+ --sky-700: #0369a1;
70
+ --sky-800: #075985;
71
+ --sky-900: #0c4a6e;
72
+ }
73
+
74
+ /* Apply custom colors */
75
+ .bg-sage-50 { background-color: var(--sage-50); }
76
+ .bg-sage-100 { background-color: var(--sage-100); }
77
+ .text-sage-600 { color: var(--sage-600); }
78
+ .text-sage-700 { color: var(--sage-700); }
79
+ .text-sage-300 { color: var(--sage-300); }
80
+ .text-sage-400 { color: var(--sage-400); }
81
+ .border-sage-200 { border-color: var(--sage-200); }
82
+
83
+ .bg-lavender-50 { background-color: var(--lavender-50); }
84
+ .bg-lavender-100 { background-color: var(--lavender-100); }
85
+ .text-lavender-600 { color: var(--lavender-600); }
86
+ .text-lavender-700 { color: var(--lavender-700); }
87
+ .text-lavender-300 { color: var(--lavender-300); }
88
+ .text-lavender-400 { color: var(--lavender-400); }
89
+ .border-lavender-200 { border-color: var(--lavender-200); }
90
+
91
+ .bg-peach-50 { background-color: var(--peach-50); }
92
+ .bg-peach-100 { background-color: var(--peach-100); }
93
+ .text-peach-600 { color: var(--peach-600); }
94
+ .text-peach-700 { color: var(--peach-700); }
95
+ .text-peach-300 { color: var(--peach-300); }
96
+ .text-peach-400 { color: var(--peach-400); }
97
+ .border-peach-200 { border-color: var(--peach-200); }
98
+
99
+ .bg-honey-50 { background-color: var(--honey-50); }
100
+ .bg-honey-100 { background-color: var(--honey-100); }
101
+ .text-honey-600 { color: var(--honey-600); }
102
+ .text-honey-700 { color: var(--honey-700); }
103
+ .text-honey-300 { color: var(--honey-300); }
104
+ .text-honey-400 { color: var(--honey-400); }
105
+ .border-honey-200 { border-color: var(--honey-200); }
106
+
107
+ .bg-blush-50 { background-color: var(--blush-50); }
108
+ .bg-blush-100 { background-color: var(--blush-100); }
109
+ .text-blush-600 { color: var(--blush-600); }
110
+ .text-blush-700 { color: var(--blush-700); }
111
+ .text-blush-300 { color: var(--blush-300); }
112
+ .text-blush-400 { color: var(--blush-400); }
113
+ .border-blush-200 { border-color: var(--blush-200); }
114
+
115
+ .bg-sky-50 { background-color: var(--sky-50); }
116
+ .bg-sky-100 { background-color: var(--sky-100); }
117
+ .text-sky-600 { color: var(--sky-600); }
118
+ .text-sky-700 { color: var(--sky-700); }
119
+ .text-sky-300 { color: var(--sky-300); }
120
+ .text-sky-400 { color: var(--sky-400); }
121
+ .border-sky-200 { border-color: var(--sky-200); }
122
+
123
+ @layer base {
124
+ html {
125
+ scroll-behavior: smooth;
126
+ transition: background-color 0.3s ease, color 0.3s ease;
127
+ }
128
+
129
+ body {
130
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
131
+ margin: 0;
132
+ padding: 0;
133
+ -webkit-font-smoothing: antialiased;
134
+ -moz-osx-font-smoothing: grayscale;
135
+ background: linear-gradient(135deg, #fafaf9 0%, #f5f5f4 50%, #fef7ed 100%);
136
+ transition: background 0.3s ease;
137
+ }
138
+
139
+ .dark body {
140
+ background: linear-gradient(135deg, #1c1917 0%, #292524 50%, #44403c 100%);
141
+ }
142
+ }
143
+
144
+ @layer components {
145
+ .animate-in {
146
+ animation: animate-in 0.6s cubic-bezier(0.16, 1, 0.3, 1);
147
+ }
148
+
149
+ .fade-in {
150
+ animation: fade-in 0.6s cubic-bezier(0.16, 1, 0.3, 1);
151
+ }
152
+
153
+ .slide-in-from-top-1 {
154
+ animation: slide-in-from-top-1 0.6s cubic-bezier(0.16, 1, 0.3, 1);
155
+ }
156
+
157
+ .slide-in-from-top-2 {
158
+ animation: slide-in-from-top-2 0.6s cubic-bezier(0.16, 1, 0.3, 1);
159
+ }
160
+
161
+ .slide-in-from-bottom-2 {
162
+ animation: slide-in-from-bottom-2 0.6s cubic-bezier(0.16, 1, 0.3, 1);
163
+ }
164
+
165
+ .slide-in-from-bottom-4 {
166
+ animation: slide-in-from-bottom-4 0.6s cubic-bezier(0.16, 1, 0.3, 1);
167
+ }
168
+
169
+ .slide-in-from-left-4 {
170
+ animation: slide-in-from-left-4 0.6s cubic-bezier(0.16, 1, 0.3, 1);
171
+ }
172
+
173
+ .slide-in-from-right-4 {
174
+ animation: slide-in-from-right-4 0.6s cubic-bezier(0.16, 1, 0.3, 1);
175
+ }
176
+
177
+ .zoom-in-95 {
178
+ animation: zoom-in-95 0.3s cubic-bezier(0.16, 1, 0.3, 1);
179
+ }
180
+
181
+ .btn-primary {
182
+ @apply bg-gradient-to-r from-amber-500 to-orange-500 text-white px-8 py-4 rounded-xl font-medium hover:from-amber-600 hover:to-orange-600 focus:outline-none focus:ring-2 focus:ring-amber-500/20 focus:ring-offset-2 transition-all duration-300 shadow-md hover:shadow-lg;
183
+ }
184
+
185
+ .card {
186
+ @apply bg-white/70 backdrop-blur-sm rounded-2xl shadow-sm border border-stone-200/60 p-6;
187
+ }
188
+
189
+ .input-field {
190
+ @apply w-full px-4 py-3 border border-stone-300/60 rounded-xl focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500 transition-all duration-200 bg-white/50 backdrop-blur-sm;
191
+ }
192
+
193
+ .status-badge {
194
+ @apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium backdrop-blur-sm;
195
+ }
196
+ }
197
+
198
+ @keyframes animate-in {
199
+ 0% {
200
+ opacity: 0;
201
+ transform: translateY(-8px) scale(0.96);
202
+ }
203
+ 100% {
204
+ opacity: 1;
205
+ transform: translateY(0) scale(1);
206
+ }
207
+ }
208
+
209
+ @keyframes fade-in {
210
+ 0% {
211
+ opacity: 0;
212
+ }
213
+ 100% {
214
+ opacity: 1;
215
+ }
216
+ }
217
+
218
+ @keyframes slide-in-from-top-1 {
219
+ 0% {
220
+ opacity: 0;
221
+ transform: translateY(-4px);
222
+ }
223
+ 100% {
224
+ opacity: 1;
225
+ transform: translateY(0);
226
+ }
227
+ }
228
+
229
+ @keyframes slide-in-from-top-2 {
230
+ 0% {
231
+ opacity: 0;
232
+ transform: translateY(-8px);
233
+ }
234
+ 100% {
235
+ opacity: 1;
236
+ transform: translateY(0);
237
+ }
238
+ }
239
+
240
+ @keyframes slide-in-from-bottom-2 {
241
+ 0% {
242
+ opacity: 0;
243
+ transform: translateY(8px);
244
+ }
245
+ 100% {
246
+ opacity: 1;
247
+ transform: translateY(0);
248
+ }
249
+ }
250
+
251
+ @keyframes slide-in-from-bottom-4 {
252
+ 0% {
253
+ opacity: 0;
254
+ transform: translateY(16px);
255
+ }
256
+ 100% {
257
+ opacity: 1;
258
+ transform: translateY(0);
259
+ }
260
+ }
261
+
262
+ @keyframes slide-in-from-left-4 {
263
+ 0% {
264
+ opacity: 0;
265
+ transform: translateX(-16px);
266
+ }
267
+ 100% {
268
+ opacity: 1;
269
+ transform: translateX(0);
270
+ }
271
+ }
272
+
273
+ @keyframes slide-in-from-right-4 {
274
+ 0% {
275
+ opacity: 0;
276
+ transform: translateX(16px);
277
+ }
278
+ 100% {
279
+ opacity: 1;
280
+ transform: translateX(0);
281
+ }
282
+ }
283
+
284
+ @keyframes zoom-in-95 {
285
+ 0% {
286
+ opacity: 0;
287
+ transform: scale(0.95);
288
+ }
289
+ 100% {
290
+ opacity: 1;
291
+ transform: scale(1);
292
+ }
293
+ }
294
+
295
+ /* Scrollbar styling for webkit browsers */
296
+ ::-webkit-scrollbar {
297
+ width: 8px;
298
+ height: 8px;
299
+ }
300
+
301
+ ::-webkit-scrollbar-track {
302
+ background: #f8fafc;
303
+ border-radius: 4px;
304
+ }
305
+
306
+ ::-webkit-scrollbar-thumb {
307
+ background: linear-gradient(135deg, #d6d3d1, #a8a29e);
308
+ border-radius: 4px;
309
+ }
310
+
311
+ ::-webkit-scrollbar-thumb:hover {
312
+ background: linear-gradient(135deg, #a8a29e, #78716c);
313
+ }
314
+
315
+ /* Custom focus styles */
316
+ .focus\:ring-amber-500\/20:focus {
317
+ --tw-ring-color: rgb(245 158 11 / 0.2);
318
+ }
319
+
320
+ /* Gradient backgrounds */
321
+ .gradient-warm {
322
+ background: linear-gradient(135deg, #fef7ed 0%, #fed7aa 50%, #fdba74 100%);
323
+ }
324
+
325
+ .glass-effect {
326
+ @apply bg-white/70 backdrop-blur-md border border-stone-200/60;
327
+ }
328
+
329
+ /* Hover effects */
330
+ .hover-lift {
331
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1);
332
+ }
333
+
334
+ .hover-lift:hover {
335
+ transform: translateY(-2px) scale(1.02);
336
+ box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 10px -3px rgba(0, 0, 0, 0.05);
337
+ }
338
+
339
+ /* Loading animations */
340
+ @keyframes pulse-soft {
341
+ 0%, 100% {
342
+ opacity: 1;
343
+ }
344
+ 50% {
345
+ opacity: 0.7;
346
+ }
347
+ }
348
+
349
+ .animate-pulse-soft {
350
+ animation: pulse-soft 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
351
+ }
352
+
353
+ /* Interactive elements */
354
+ .interactive-scale {
355
+ transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
356
+ }
357
+
358
+ .interactive-scale:hover {
359
+ transform: scale(1.05);
360
+ }
361
+
362
+ .interactive-scale:active {
363
+ transform: scale(0.98);
364
+ }
365
+
366
+ /* Floating elements */
367
+ @keyframes float {
368
+ 0%, 100% {
369
+ transform: translateY(0px);
370
+ }
371
+ 50% {
372
+ transform: translateY(-4px);
373
+ }
374
+ }
375
+
376
+ .animate-float {
377
+ animation: float 3s ease-in-out infinite;
378
+ }
379
+
380
+ /* Staggered animations */
381
+ .stagger-1 { animation-delay: 0.1s; }
382
+ .stagger-2 { animation-delay: 0.2s; }
383
+ .stagger-3 { animation-delay: 0.3s; }
384
+ .stagger-4 { animation-delay: 0.4s; }
385
+ .stagger-5 { animation-delay: 0.5s; }
386
+ .stagger-6 { animation-delay: 0.6s; }
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App.tsx';
4
+ import './index.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
frontend/src/services/WebSocketManager.ts ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ type EventCallback = (data: any) => void;
2
+
3
+ export class WebSocketManager {
4
+ private websocket: WebSocket | null = null;
5
+ private streamWebSocket: WebSocket | null = null;
6
+ private eventListeners: Map<string, EventCallback[]> = new Map();
7
+ private reconnectAttempts = 0;
8
+ private maxReconnectAttempts = 5;
9
+ private reconnectDelay = 1000;
10
+
11
+ public connect(jobId: string): void {
12
+ if (this.websocket) {
13
+ this.websocket.close();
14
+ }
15
+
16
+ // Detect if we're in development mode (frontend on different port than backend)
17
+ const isDev = window.location.port === '5173';
18
+ const backendHost = isDev ? 'localhost:8000' : window.location.host;
19
+ const wsBase = `${window.location.protocol === "https:" ? "wss" : "ws"}://${backendHost}`;
20
+ console.log(`📡 Connecting to WebSocket: ${wsBase}/ws/${jobId}`);
21
+ this.websocket = new WebSocket(`${wsBase}/ws/${jobId}`);
22
+
23
+ this.websocket.onopen = () => {
24
+ console.log('📡 WebSocket connected successfully');
25
+ this.reconnectAttempts = 0;
26
+ this.emit('connected', { status: 'connected' });
27
+ };
28
+
29
+ this.websocket.onmessage = (event: MessageEvent) => {
30
+ try {
31
+ const data = JSON.parse(event.data);
32
+ console.log('📨 WebSocket message received:', data.type, data);
33
+
34
+ // Handle different message types
35
+ switch (data.type) {
36
+ case 'proxy_stats':
37
+ this.emit('proxy_stats', data.stats || data);
38
+ break;
39
+ case 'decision':
40
+ this.emit('decision', data.decision || data);
41
+ break;
42
+ case 'screenshot':
43
+ this.emit('screenshot', data.screenshot || data);
44
+ break;
45
+ case 'token_usage':
46
+ this.emit('token_usage', data.token_usage || data);
47
+ break;
48
+ case 'page_info':
49
+ this.emit('page_info', data);
50
+ break;
51
+ case 'extraction':
52
+ this.emit('extraction', data);
53
+ break;
54
+ case 'streaming_info':
55
+ this.emit('streaming_info', data);
56
+ break;
57
+ case 'error':
58
+ this.emit('error', data);
59
+ break;
60
+ default:
61
+ if (data.status) {
62
+ this.emit('status', data);
63
+ } else {
64
+ this.emit(data.type, data);
65
+ }
66
+ }
67
+ } catch (error) {
68
+ console.error('Error parsing WebSocket message:', error, event.data);
69
+ }
70
+ };
71
+
72
+ this.websocket.onclose = (event: CloseEvent) => {
73
+ console.log('📡 WebSocket disconnected:', event.code, event.reason);
74
+ this.websocket = null;
75
+
76
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
77
+ this.reconnectAttempts++;
78
+ console.log(`🔄 Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
79
+ setTimeout(() => this.connect(jobId), this.reconnectDelay);
80
+ this.reconnectDelay *= 2;
81
+ }
82
+ };
83
+
84
+ this.websocket.onerror = (error: Event) => {
85
+ console.error('📡 WebSocket error:', error);
86
+ this.emit('error', { error: 'WebSocket connection error' });
87
+ };
88
+ }
89
+
90
+ public connectStream(jobId: string): void {
91
+ if (this.streamWebSocket) {
92
+ this.streamWebSocket.close();
93
+ }
94
+
95
+ // Detect if we're in development mode (frontend on different port than backend)
96
+ const isDev = window.location.port === '5173';
97
+ const backendHost = isDev ? 'localhost:8000' : window.location.host;
98
+ const wsBase = `${window.location.protocol === "https:" ? "wss" : "ws"}://${backendHost}`;
99
+ console.log(`🎥 Connecting to Stream WebSocket: ${wsBase}/stream/${jobId}`);
100
+ this.streamWebSocket = new WebSocket(`${wsBase}/stream/${jobId}`);
101
+
102
+ this.streamWebSocket.onopen = () => {
103
+ console.log('🎥 Stream WebSocket connected successfully');
104
+ this.emit('stream_connected', { status: 'connected' });
105
+ };
106
+
107
+ this.streamWebSocket.onmessage = (event: MessageEvent) => {
108
+ try {
109
+ const data = JSON.parse(event.data);
110
+ console.log('🎥 Stream message received:', data.type);
111
+ this.emit('stream_' + data.type, data);
112
+ } catch (error) {
113
+ console.error('Error parsing stream message:', error);
114
+ }
115
+ };
116
+
117
+ this.streamWebSocket.onclose = () => {
118
+ console.log('🎥 Stream WebSocket disconnected');
119
+ this.streamWebSocket = null;
120
+ this.emit('stream_disconnected', { status: 'disconnected' });
121
+ };
122
+
123
+ this.streamWebSocket.onerror = (error: Event) => {
124
+ console.error('🎥 Stream WebSocket error:', error);
125
+ this.emit('stream_error', { error: 'Stream connection error' });
126
+ };
127
+ }
128
+
129
+ public sendStreamMessage(message: any): void {
130
+ if (this.streamWebSocket && this.streamWebSocket.readyState === WebSocket.OPEN) {
131
+ this.streamWebSocket.send(JSON.stringify(message));
132
+ } else {
133
+ console.warn('Stream WebSocket not connected');
134
+ }
135
+ }
136
+
137
+ public on(event: string, callback: EventCallback): void {
138
+ if (!this.eventListeners.has(event)) {
139
+ this.eventListeners.set(event, []);
140
+ }
141
+ this.eventListeners.get(event)?.push(callback);
142
+ console.log(`📝 Event listener added for: ${event}`);
143
+ }
144
+
145
+ private emit(event: string, data: any): void {
146
+ const listeners = this.eventListeners.get(event) || [];
147
+ console.log(`📤 Emitting event: ${event} to ${listeners.length} listeners`);
148
+ listeners.forEach(callback => {
149
+ try {
150
+ callback(data);
151
+ } catch (error) {
152
+ console.error(`Error in event listener for ${event}:`, error);
153
+ }
154
+ });
155
+ }
156
+
157
+ public disconnect(): void {
158
+ if (this.websocket) {
159
+ this.websocket.close();
160
+ this.websocket = null;
161
+ }
162
+ }
163
+
164
+ public disconnectStream(): void {
165
+ if (this.streamWebSocket) {
166
+ this.streamWebSocket.close();
167
+ this.streamWebSocket = null;
168
+ }
169
+ }
170
+
171
+ public isConnected(): boolean {
172
+ return this.websocket !== null && this.websocket.readyState === WebSocket.OPEN;
173
+ }
174
+
175
+ public isStreamConnected(): boolean {
176
+ return this.streamWebSocket !== null && this.streamWebSocket.readyState === WebSocket.OPEN;
177
+ }
178
+ }
frontend/src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
frontend/tailwind.config.js ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4
+ darkMode: 'class',
5
+ theme: {
6
+ extend: {
7
+ colors: {
8
+ sage: {
9
+ 50: '#f6f7f6',
10
+ 100: '#e8eae8',
11
+ 200: '#d1d5d1',
12
+ 300: '#adb5ad',
13
+ 400: '#8a948a',
14
+ 500: '#6b756b',
15
+ 600: '#565e56',
16
+ 700: '#484e48',
17
+ 800: '#3d423d',
18
+ 900: '#343834',
19
+ },
20
+ lavender: {
21
+ 50: '#f7f6f9',
22
+ 100: '#ede9f2',
23
+ 200: '#ddd6e7',
24
+ 300: '#c4b8d4',
25
+ 400: '#a892bd',
26
+ 500: '#9070a6',
27
+ 600: '#7a5a8a',
28
+ 700: '#654a71',
29
+ 800: '#553f5e',
30
+ 900: '#49374f',
31
+ },
32
+ peach: {
33
+ 50: '#fef7f3',
34
+ 100: '#fdede5',
35
+ 200: '#fad8ca',
36
+ 300: '#f6baa4',
37
+ 400: '#f0926d',
38
+ 500: '#e97142',
39
+ 600: '#da5a28',
40
+ 700: '#b6481e',
41
+ 800: '#923d1d',
42
+ 900: '#76361c',
43
+ },
44
+ honey: {
45
+ 50: '#fefbf3',
46
+ 100: '#fdf4e1',
47
+ 200: '#fae7c2',
48
+ 300: '#f6d498',
49
+ 400: '#f1bc6c',
50
+ 500: '#eca54a',
51
+ 600: '#dd8f39',
52
+ 700: '#b87532',
53
+ 800: '#945d30',
54
+ 900: '#784d2a',
55
+ },
56
+ blush: {
57
+ 50: '#fdf4f3',
58
+ 100: '#fce7e6',
59
+ 200: '#f9d4d2',
60
+ 300: '#f4b5b1',
61
+ 400: '#ec8b85',
62
+ 500: '#e0675e',
63
+ 600: '#cd4f45',
64
+ 700: '#ab3f37',
65
+ 800: '#8d3832',
66
+ 900: '#753530',
67
+ },
68
+ },
69
+ },
70
+ },
71
+ plugins: [],
72
+ };
frontend/tsconfig.app.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "isolatedModules": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+ "jsx": "react-jsx",
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true
22
+ },
23
+ "include": ["src"]
24
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+
8
+ /* Bundler mode */
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "isolatedModules": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+
15
+ /* Linting */
16
+ "strict": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "noFallthroughCasesInSwitch": true
20
+ },
21
+ "include": ["vite.config.ts"]
22
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ optimizeDeps: {
8
+ exclude: ['lucide-react'],
9
+ },
10
+ });
render.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: browserpilot
4
+ env: docker
5
+ region: oregon
6
+ plan: starter
7
+ dockerfilePath: ./Dockerfile
8
+ dockerContext: .
9
+ healthCheckPath: /
10
+ envVars:
11
+ - key: GOOGLE_API_KEY
12
+ sync: false
13
+ - key: DATABASE_URL
14
+ sync: false
15
+ - key: SCRAPER_PROXIES
16
+ sync: false
17
+ default: []
18
+ - key: PYTHONUNBUFFERED
19
+ sync: false
20
+ default: 1
21
+ disk:
22
+ name: outputs
23
+ mountPath: /app/outputs
24
+ sizeGB: 5
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn[standard]==0.29.0
3
+ playwright==1.53.0
4
+ google-generativeai==0.5.0
5
+ pydantic==2.7.1
6
+ bs4==0.0.2
7
+ lxml==5.2.1
8
+ markdownify==0.11.6
9
+ numpy==2.2.6
10
+ pandas==2.2.3
11
+ python-dateutil==2.9.0.post0
12
+ pytz==2025.2
13
+ tzdata==2025.2
14
+ reportlab==4.4.2
15
+ psycopg2-binary==2.9.9
16
+ asyncpg==0.29.0
17
+ python-telegram-bot==21.0
18
+ aiohttp==3.9.5