┌─────────────────────────────────────────────────────────────┐
│ React Frontend │
│ ┌────────────────────────┬──────────────────────────────┐ │
│ │ BrowserView (2/3) │ ChatPanel (1/3) │ │
│ │ - Live screenshots │ - Message history │ │
│ │ - URL bar │ - Input box │ │
│ │ - Bounding box │ - Model selector │ │
│ │ overlay │ - API key / system modals │ │
│ └────────────────────────┴──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
WebSocket (ws://localhost:8000/ws)
│
┌─────────────────────────────────────────────────────────────┐
│ FastAPI Backend │
│ │
│ main.py (WebSocket handler) │
│ ├── Receives user messages │
│ ├── Captures screenshot via BrowserManager │
│ ├── Sends screenshot + message to OpenRouterClient │
│ ├── Processes AI response through bounding box extractor │
│ ├── Generates JS click code from coordinates │
│ ├── Executes JS in browser via BrowserManager │
│ └── Sends results + updated screenshot to frontend │
│ │
│ browser_manager.py │
│ ├── Playwright headless Chromium lifecycle │
│ ├── Screenshot capture (base64 PNG) │
│ ├── Live screenshot streaming (2s interval) │
│ ├── JavaScript execution via page.evaluate() │
│ └── URL navigation │
│ │
│ openrouter_client.py │
│ ├── OpenAI SDK → OpenRouter API (base_url override) │
│ ├── Vision model: screenshot + user message → bbox JSON │
│ ├── Bbox processor model: raw response → clean coords │
│ └── Fallback chat method (no screenshot) │
└─────────────────────────────────────────────────────────────┘
- User types message in ChatPanel
- App.js sends
{ type: "chat_message", content: "..." }via WebSocket - Backend captures screenshot from Playwright
- Screenshot (base64) + user message sent to vision model via OpenRouter
- Vision model returns JSON with
textandbounding_box(x, y, width, height) - Response passed to bounding box processor model (
alibaba/tongyi-deepresearch-30b-a3b:free) for coordinate cleaning - If valid coordinates found: backend generates JS that calls
document.elementFromPoint(centerX, centerY).click() - JS executed in browser via
page.evaluate() - After 1s delay, new screenshot captured and sent to frontend
- AI text response + execution result sent to frontend
Client → Server:
chat_message— user's natural language requestnavigate— URL navigation (from address bar)screenshot_request— manual screenshot refreshset_api_key— set OpenRouter API key (saved to config.json)set_system_message— override default system promptset_model_name— switch AI model
Server → Client:
screenshot— base64 PNG screenshot (on-demand)live_screenshot— base64 PNG from streaming loopai_response— AI text response + optional screenshotexecution_result— JS execution success/failuremessage_received— acknowledgment of user messageapi_key_status— API key set confirmation + model nameerror— error messagesuccess— success message
The system uses a two-model pipeline:
Model 1 (Vision): Configurable via UI, default nvidia/nemotron-nano-12b-v2-vl:free
- Receives: screenshot (base64 image) + user message
- Returns: JSON with
textexplanation andbounding_boxcoordinates - System prompt instructs it to return coordinates, NOT JavaScript
Model 2 (Bbox Processor): Hardcoded alibaba/tongyi-deepresearch-30b-a3b:free
- Receives: raw text from Model 1
- Returns: cleaned
{ x, y, width, height }JSON - Handles format conversion (e.g.,
bbox_2d[x1,y1,x2,y2] → standard format)
JavaScript Generation: Done in main.py, not by the AI
- Calculates center of bounding box
- Generates
document.elementFromPoint()+.click()code - Includes fallback: expanding circle search for nearby clickable elements
- Includes fallback: traverse parent elements for clickable ancestors
- API key and model name saved to
backend/config.json(plaintext) - Loaded on startup, UI changes persist across restarts
.envfileOPENROUTER_API_KEYused as fallback if no saved config
- No authentication — single-user assumption
- API key stored in plaintext on disk
- CORS restricted to
http://localhost:3000 - AI-generated JavaScript executed without sandboxing
bypass_csp=Trueand--disable-web-securityon the browser
- Single browser instance shared across all WebSocket connections
- In-memory chat history (lost on restart)
- Synchronous AI calls per WebSocket message (no queuing)
- Live screenshot streaming to all connected clients