├── .npmrc ├── apps ├── server │ ├── README.md │ ├── app │ │ ├── __init__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── utils │ │ │ │ ├── __init__.py │ │ │ │ ├── prompts.py │ │ │ │ └── dom_parser │ │ │ │ │ ├── processor.py │ │ │ │ │ └── filters.py │ │ │ ├── endpoints │ │ │ │ ├── __init__.py │ │ │ │ ├── health.py │ │ │ │ ├── dom_parser.py │ │ │ │ └── tasks.py │ │ │ ├── services │ │ │ │ ├── __init__.py │ │ │ │ ├── task_service.py │ │ │ │ └── storage_service.py │ │ │ └── router.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── tasks.py │ │ │ └── dom.py │ │ ├── main.py │ │ └── config.py │ ├── .env.example │ ├── docker-compose.yml │ └── pyproject.toml ├── extension │ ├── src │ │ ├── vite-env.d.ts │ │ ├── dom │ │ │ ├── index.ts │ │ │ └── iframe.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── url.ts │ │ │ └── xpath.ts │ │ ├── main.tsx │ │ ├── constants │ │ │ └── AxiosInstance.ts │ │ ├── content.ts │ │ ├── App.css │ │ ├── App.tsx │ │ ├── types.ts │ │ ├── automation │ │ │ └── index.ts │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── highlight │ │ │ └── index.ts │ │ └── messaging │ │ │ └── index.ts │ ├── postcss.config.js │ ├── public │ │ ├── icons │ │ │ ├── logo.png │ │ │ ├── cursor.png │ │ │ ├── icon16.svg │ │ │ ├── icon48.svg │ │ │ └── icon128.svg │ │ ├── manifest.json │ │ └── vite.svg │ ├── tsconfig.json │ ├── .gitignore │ ├── tsconfig.node.json │ ├── vite.content.config.ts │ ├── tsconfig.app.json │ ├── vite.background.config.ts │ ├── eslint.config.js │ ├── vite.popup.config.ts │ ├── tailwind.config.js │ ├── icons.js │ ├── package.json │ ├── README.md │ ├── build.ts │ └── popup.html ├── web │ ├── app │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistVF.woff │ │ │ └── GeistMonoVF.woff │ │ ├── layout.tsx │ │ ├── globals.css │ │ ├── page.tsx │ │ └── page.module.css │ ├── next.config.js │ ├── eslint.config.js │ ├── tsconfig.json │ ├── public │ │ ├── vercel.svg │ │ ├── file-text.svg │ │ ├── window.svg │ │ ├── next.svg │ │ ├── globe.svg │ │ ├── turborepo-dark.svg │ │ └── turborepo-light.svg │ ├── .gitignore │ ├── package.json │ └── README.md └── docs │ ├── app │ ├── favicon.ico │ ├── fonts │ │ ├── GeistVF.woff │ │ └── GeistMonoVF.woff │ ├── layout.tsx │ ├── globals.css │ ├── page.tsx │ └── page.module.css │ ├── next.config.js │ ├── eslint.config.js │ ├── tsconfig.json │ ├── public │ ├── vercel.svg │ ├── file-text.svg │ ├── window.svg │ ├── next.svg │ ├── globe.svg │ ├── turborepo-dark.svg │ └── turborepo-light.svg │ ├── .gitignore │ ├── package.json │ └── README.md ├── pnpm-workspace.yaml ├── packages ├── eslint-config │ ├── README.md │ ├── package.json │ ├── base.js │ ├── react-internal.js │ └── next.js ├── ui │ ├── eslint.config.mjs │ ├── tsconfig.json │ ├── turbo │ │ └── generators │ │ │ ├── templates │ │ │ └── component.hbs │ │ │ └── config.ts │ ├── src │ │ ├── code.tsx │ │ ├── button.tsx │ │ └── card.tsx │ └── package.json ├── typescript-config │ ├── react-library.json │ ├── package.json │ ├── package-lock.json │ ├── nextjs.json │ └── base.json ├── components │ ├── react │ │ ├── package-lock.json │ │ └── package.json │ └── vanilla │ │ ├── package-lock.json │ │ └── package.json └── core │ ├── src │ ├── index.ts │ ├── types.ts │ └── utils │ │ └── dom-actions.ts │ └── package.json ├── turbo.json ├── package.json ├── .gitignore ├── LICENSE ├── temp.py ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/server/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/server/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/server/app/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/server/app/api/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/server/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/server/.env.example: -------------------------------------------------------------------------------- 1 | GEMINI_API_KEY="" -------------------------------------------------------------------------------- /apps/server/app/api/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/server/app/api/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /apps/extension/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/extension/src/dom/index.ts: -------------------------------------------------------------------------------- 1 | export * from './iframe'; 2 | export * from './processor'; -------------------------------------------------------------------------------- /apps/web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SohamRatnaparkhi/navigator-ai/HEAD/apps/web/app/favicon.ico -------------------------------------------------------------------------------- /apps/docs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SohamRatnaparkhi/navigator-ai/HEAD/apps/docs/app/favicon.ico -------------------------------------------------------------------------------- /apps/extension/postcss.config.js: -------------------------------------------------------------------------------- 1 | export const plugins = { 2 | tailwindcss: {}, 3 | autoprefixer: {}, 4 | }; -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /apps/extension/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // utils/index.ts 2 | // Re-export utility functions 3 | 4 | export * from './xpath'; 5 | -------------------------------------------------------------------------------- /apps/web/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SohamRatnaparkhi/navigator-ai/HEAD/apps/web/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /apps/docs/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SohamRatnaparkhi/navigator-ai/HEAD/apps/docs/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /apps/docs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /apps/docs/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SohamRatnaparkhi/navigator-ai/HEAD/apps/docs/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /apps/extension/public/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SohamRatnaparkhi/navigator-ai/HEAD/apps/extension/public/icons/logo.png -------------------------------------------------------------------------------- /apps/web/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SohamRatnaparkhi/navigator-ai/HEAD/apps/web/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /apps/extension/public/icons/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SohamRatnaparkhi/navigator-ai/HEAD/apps/extension/public/icons/cursor.png -------------------------------------------------------------------------------- /apps/docs/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { nextJsConfig } from "@repo/eslint-config/next-js"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default nextJsConfig; 5 | -------------------------------------------------------------------------------- /apps/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { nextJsConfig } from "@repo/eslint-config/next-js"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default nextJsConfig; 5 | -------------------------------------------------------------------------------- /packages/ui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { config } from "@repo/eslint-config/react-internal"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ui/turbo/generators/templates/component.hbs: -------------------------------------------------------------------------------- 1 | export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => { 2 | return ( 3 |
4 |

{{ pascalCase name }} Component

5 | {children} 6 |
7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /apps/server/app/api/endpoints/health.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter() 4 | 5 | 6 | @router.get("/") 7 | async def health_check(): 8 | """Health check endpoint""" 9 | return {"status": "ok", "message": "Navigator AI API is running"} 10 | -------------------------------------------------------------------------------- /packages/ui/src/code.tsx: -------------------------------------------------------------------------------- 1 | import { type JSX } from "react"; 2 | 3 | export function Code({ 4 | children, 5 | className, 6 | }: { 7 | children: React.ReactNode; 8 | className?: string; 9 | }): JSX.Element { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /packages/components/react/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "react", 9 | "version": "1.0.0", 10 | "license": "ISC" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions/dom-analyzer'; 2 | export * from './actions/automation'; 3 | export * from './types'; 4 | export * from './utils/filters'; 5 | export * from './utils/cursor'; 6 | export * from './utils/element-finder'; 7 | export * from './utils/dom-actions'; 8 | 9 | -------------------------------------------------------------------------------- /packages/components/vanilla/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "vanilla", 9 | "version": "1.0.0", 10 | "license": "ISC" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/components/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /packages/components/vanilla/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /apps/extension/public/icons/icon16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/extension/public/icons/icon48.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/extension/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import './index.css' 4 | import Sidebar from './components/Sidebar' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /apps/extension/public/icons/icon128.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/typescript-config/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/typescript-config", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@repo/typescript-config", 9 | "version": "0.0.0", 10 | "license": "MIT" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "plugins": [{ "name": "next" }], 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "allowJs": true, 9 | "jsx": "preserve", 10 | "noEmit": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/server/app/api/router.py: -------------------------------------------------------------------------------- 1 | from app.api.endpoints import dom_parser, health, tasks 2 | from fastapi import APIRouter 3 | 4 | api_router = APIRouter() 5 | 6 | api_router.include_router(health.router, tags=["health"]) 7 | api_router.include_router(dom_parser.router, prefix="/dom", tags=["dom"]) 8 | api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"]) 9 | -------------------------------------------------------------------------------- /apps/extension/.gitignore: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /apps/extension/src/utils/url.ts: -------------------------------------------------------------------------------- 1 | export function isValidUrl(url: string): boolean { 2 | return typeof url === 'string' && 3 | !url.startsWith('chrome://') && 4 | !url.startsWith('chrome-extension://') && 5 | !url.startsWith('chrome-search://') && 6 | !url.startsWith('about:') && 7 | !url.startsWith('edge://') && 8 | !url.startsWith('brave://'); 9 | } -------------------------------------------------------------------------------- /apps/extension/src/constants/AxiosInstance.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getAxiosInstance = async () => { 4 | const { serverURL } = await chrome.storage.local.get("serverURL"); 5 | return axios.create({ 6 | baseURL: serverURL || "http://localhost:8000", 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.ts", 12 | "**/*.tsx", 13 | "next-env.d.ts", 14 | "next.config.js", 15 | ".next/types/**/*.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.ts", 12 | "**/*.tsx", 13 | "next-env.d.ts", 14 | "next.config.js", 15 | ".next/types/**/*.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/docs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | redis: 5 | image: redis:7-alpine 6 | ports: 7 | - "6379:6379" 8 | volumes: 9 | - redis-data:/data 10 | command: redis-server --appendonly yes 11 | restart: unless-stopped 12 | healthcheck: 13 | test: ["CMD", "redis-cli", "ping"] 14 | interval: 5s 15 | timeout: 5s 16 | retries: 5 17 | 18 | volumes: 19 | redis-data: -------------------------------------------------------------------------------- /packages/ui/src/button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | 5 | interface ButtonProps { 6 | children: ReactNode; 7 | className?: string; 8 | appName: string; 9 | } 10 | 11 | export const Button = ({ children, className, appName }: ButtonProps) => { 12 | return ( 13 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /apps/server/app/api/endpoints/dom_parser.py: -------------------------------------------------------------------------------- 1 | from app.api.utils.dom_parser.processor import parse_dom 2 | from fastapi import APIRouter 3 | from pydantic import BaseModel 4 | 5 | router = APIRouter() 6 | 7 | 8 | class DOMParseRequest(BaseModel): 9 | html: str 10 | 11 | 12 | @router.post("/parse") 13 | async def dom_parse(request: DOMParseRequest): 14 | """Parse the DOM state and return the updated DOM state""" 15 | parsed_dom_state = parse_dom(request.html) 16 | return parsed_dom_state 17 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 8 | "outputs": [".next/**", "!.next/cache/**"] 9 | }, 10 | "lint": { 11 | "dependsOn": ["^lint"] 12 | }, 13 | "check-types": { 14 | "dependsOn": ["^check-types"] 15 | }, 16 | "dev": { 17 | "cache": false, 18 | "persistent": true 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/server/app/models/tasks.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class TaskCreate(BaseModel): 7 | task: str 8 | 9 | 10 | class TaskResponse(BaseModel): 11 | task_id: str 12 | status: str 13 | message: Optional[str] = None 14 | 15 | 16 | class TaskStatus(BaseModel): 17 | task_id: str 18 | status: str 19 | progress: Optional[float] = None 20 | created_at: Optional[str] = None 21 | updated_at: Optional[str] = None 22 | results: Optional[List[Dict]] = None 23 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@navigator-ai/core", 3 | "version": "0.0.1", 4 | "private": true, 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "build": "tsup src/index.ts --format esm,cjs --dts", 10 | "dev": "tsup src/index.ts --format esm,cjs --watch --dts", 11 | "lint": "eslint src/", 12 | "test": "jest" 13 | }, 14 | "devDependencies": { 15 | "typescript": "^5.0.0", 16 | "tsup": "^8.0.0", 17 | "@types/node": "^20.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # env files (can opt-in for commiting if needed) 29 | .env* 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # env files (can opt-in for commiting if needed) 29 | .env* 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/extension/tsconfig.node.json: -------------------------------------------------------------------------------- 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.popup.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "lib": ["es2022", "DOM", "DOM.Iterable"], 10 | "module": "NodeNext", 11 | "moduleDetection": "force", 12 | "moduleResolution": "NodeNext", 13 | "noUncheckedIndexedAccess": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "ES2022" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/extension/vite.content.config.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { defineConfig } from 'vite' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = dirname(__filename) 7 | 8 | export default defineConfig({ 9 | build: { 10 | outDir: 'dist', 11 | emptyOutDir: false, // Important: don't clear dist folder 12 | lib: { 13 | entry: resolve(__dirname, 'src/content.ts'), 14 | formats: ['iife'], 15 | name: 'content', 16 | fileName: () => 'content.js' 17 | } 18 | } 19 | }) -------------------------------------------------------------------------------- /packages/ui/src/card.tsx: -------------------------------------------------------------------------------- 1 | import { type JSX } from "react"; 2 | 3 | export function Card({ 4 | className, 5 | title, 6 | children, 7 | href, 8 | }: { 9 | className?: string; 10 | title: string; 11 | children: React.ReactNode; 12 | href: string; 13 | }): JSX.Element { 14 | return ( 15 | 21 |

22 | {title} -> 23 |

24 |

{children}

25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/docs/public/file-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/web/public/file-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/server/app/main.py: -------------------------------------------------------------------------------- 1 | from app.api.router import api_router 2 | from app.config import settings 3 | from fastapi import FastAPI 4 | from fastapi.middleware.cors import CORSMiddleware 5 | 6 | app = FastAPI( 7 | title=settings.APP_NAME, 8 | openapi_url=f"{settings.API_PREFIX}/openapi.json", 9 | debug=settings.DEBUG 10 | ) 11 | 12 | app.add_middleware( 13 | CORSMiddleware, 14 | allow_origins=settings.CORS_ORIGINS, 15 | allow_credentials=settings.CORS_ALLOW_CREDENTIALS, 16 | allow_methods=settings.CORS_ALLOW_METHODS, 17 | allow_headers=settings.CORS_ALLOW_HEADERS, 18 | ) 19 | 20 | app.include_router(api_router, prefix=settings.API_PREFIX) 21 | -------------------------------------------------------------------------------- /apps/extension/tsconfig.app.json: -------------------------------------------------------------------------------- 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 | } 25 | -------------------------------------------------------------------------------- /apps/server/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "app" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [ 6 | {name = "SohamRatnaparkhi",email = "soham.ratnaparkhi@gmail.com"} 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.11" 10 | dependencies = [ 11 | "fastapi[standard] (>=0.115.8,<0.116.0)", 12 | "bs4 (>=0.0.2,<0.0.3)", 13 | "google-generativeai (>=0.8.4,<0.9.0)", 14 | "google-genai (>=1.3.0,<2.0.0)", 15 | "python-dotenv (>=1.0.1,<2.0.0)", 16 | "redis (>=5.0.1,<6.0.0)", 17 | "openai (>=1.66.3,<2.0.0)" 18 | ] 19 | 20 | 21 | [build-system] 22 | requires = ["poetry-core>=2.0.0,<3.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /apps/extension/src/content.ts: -------------------------------------------------------------------------------- 1 | import { createSidebarContainer, isChromeSidePanelSupported } from './sidebar'; 2 | import { initializeMessageListener } from './messaging'; 3 | 4 | console.log('Content script loaded'); 5 | 6 | // Check if Chrome's sidePanel API is available 7 | const isChromeWithSidePanel = isChromeSidePanelSupported(); 8 | 9 | // Only create the custom sidebar container if Chrome's sidePanel API is not available 10 | if (!isChromeWithSidePanel) { 11 | console.log('Creating custom sidebar container (Chrome sidePanel API not available)'); 12 | createSidebarContainer(); 13 | } else { 14 | console.log('Using Chrome sidePanel API instead of custom sidebar'); 15 | } 16 | 17 | initializeMessageListener(); -------------------------------------------------------------------------------- /apps/web/public/window.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "exports": { 6 | "./*": "./src/*.tsx" 7 | }, 8 | "scripts": { 9 | "lint": "eslint . --max-warnings 0", 10 | "generate:component": "turbo gen react-component", 11 | "check-types": "tsc --noEmit" 12 | }, 13 | "devDependencies": { 14 | "@repo/eslint-config": "workspace:*", 15 | "@repo/typescript-config": "workspace:*", 16 | "@turbo/gen": "^2.4.0", 17 | "@types/node": "^22.13.0", 18 | "@types/react": "19.0.8", 19 | "@types/react-dom": "19.0.3", 20 | "eslint": "^9.20.0", 21 | "typescript": "5.7.3" 22 | }, 23 | "dependencies": { 24 | "react": "^19.0.0", 25 | "react-dom": "^19.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/docs/public/window.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "exports": { 7 | "./base": "./base.js", 8 | "./next-js": "./next.js", 9 | "./react-internal": "./react-internal.js" 10 | }, 11 | "devDependencies": { 12 | "@eslint/js": "^9.20.0", 13 | "@next/eslint-plugin-next": "^15.1.6", 14 | "eslint": "^9.20.0", 15 | "eslint-config-prettier": "^10.0.1", 16 | "eslint-plugin-only-warn": "^1.1.0", 17 | "eslint-plugin-react": "^7.37.4", 18 | "eslint-plugin-react-hooks": "^5.1.0", 19 | "eslint-plugin-turbo": "^2.4.0", 20 | "globals": "^15.15.0", 21 | "typescript": "^5.7.3", 22 | "typescript-eslint": "^8.24.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev --turbopack --port 3000", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint --max-warnings 0", 11 | "check-types": "tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "@repo/ui": "workspace:*", 15 | "next": "^15.1.6", 16 | "react": "^19.0.0", 17 | "react-dom": "^19.0.0" 18 | }, 19 | "devDependencies": { 20 | "@repo/eslint-config": "workspace:*", 21 | "@repo/typescript-config": "workspace:*", 22 | "@types/node": "^22", 23 | "@types/react": "19.0.8", 24 | "@types/react-dom": "19.0.3", 25 | "eslint": "^9.20.0", 26 | "typescript": "5.7.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev --turbopack --port 3001", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint --max-warnings 0", 11 | "check-types": "tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "@repo/ui": "workspace:*", 15 | "next": "^15.1.6", 16 | "react": "^19.0.0", 17 | "react-dom": "^19.0.0" 18 | }, 19 | "devDependencies": { 20 | "@repo/eslint-config": "workspace:*", 21 | "@repo/typescript-config": "workspace:*", 22 | "@types/node": "^22", 23 | "@types/react": "19.0.8", 24 | "@types/react-dom": "19.0.3", 25 | "eslint": "^9.20.0", 26 | "typescript": "5.7.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/extension/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /packages/eslint-config/base.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import turboPlugin from "eslint-plugin-turbo"; 4 | import tseslint from "typescript-eslint"; 5 | import onlyWarn from "eslint-plugin-only-warn"; 6 | 7 | /** 8 | * A shared ESLint configuration for the repository. 9 | * 10 | * @type {import("eslint").Linter.Config} 11 | * */ 12 | export const config = [ 13 | js.configs.recommended, 14 | eslintConfigPrettier, 15 | ...tseslint.configs.recommended, 16 | { 17 | plugins: { 18 | turbo: turboPlugin, 19 | }, 20 | rules: { 21 | "turbo/no-undeclared-env-vars": "warn", 22 | }, 23 | }, 24 | { 25 | plugins: { 26 | onlyWarn, 27 | }, 28 | }, 29 | { 30 | ignores: ["dist/**"], 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /apps/docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | }); 9 | const geistMono = localFont({ 10 | src: "./fonts/GeistMonoVF.woff", 11 | variable: "--font-geist-mono", 12 | }); 13 | 14 | export const metadata: Metadata = { 15 | title: "Create Next App", 16 | description: "Generated by create next app", 17 | }; 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: Readonly<{ 22 | children: React.ReactNode; 23 | }>) { 24 | return ( 25 | 26 | 27 | {children} 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | }); 9 | const geistMono = localFont({ 10 | src: "./fonts/GeistMonoVF.woff", 11 | variable: "--font-geist-mono", 12 | }); 13 | 14 | export const metadata: Metadata = { 15 | title: "Create Next App", 16 | description: "Generated by create next app", 17 | }; 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: Readonly<{ 22 | children: React.ReactNode; 23 | }>) { 24 | return ( 25 | 26 | 27 | {children} 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navigator-ai", 3 | "private": true, 4 | "workspaces": [ 5 | "apps/*", 6 | "packages/*", 7 | "packages/components/*" 8 | ], 9 | "scripts": { 10 | "build": "turbo run build", 11 | "dev": "turbo run dev", 12 | "lint": "turbo run lint", 13 | "test": "turbo run test", 14 | "dev:server": "cd apps/server && fastapi dev app/main.py --port=8000", 15 | "dev:all": "concurrently \"npm run dev\" \"npm run dev:server\"" 16 | }, 17 | "packageManager": "pnpm@10.4.1", 18 | "devDependencies": { 19 | "concurrently": "latest", 20 | "turbo": "latest" 21 | }, 22 | "pnpm": { 23 | "onlyBuiltDependencies": [ 24 | "core-js-pure", 25 | "dtrace-provider", 26 | "esbuild", 27 | "sharp", 28 | "spawn-sync" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/docs/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | } 23 | 24 | * { 25 | box-sizing: border-box; 26 | padding: 0; 27 | margin: 0; 28 | } 29 | 30 | a { 31 | color: inherit; 32 | text-decoration: none; 33 | } 34 | 35 | .imgDark { 36 | display: none; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | html { 41 | color-scheme: dark; 42 | } 43 | 44 | .imgLight { 45 | display: none; 46 | } 47 | .imgDark { 48 | display: unset; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | } 23 | 24 | * { 25 | box-sizing: border-box; 26 | padding: 0; 27 | margin: 0; 28 | } 29 | 30 | a { 31 | color: inherit; 32 | text-decoration: none; 33 | } 34 | 35 | .imgDark { 36 | display: none; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | html { 41 | color-scheme: dark; 42 | } 43 | 44 | .imgLight { 45 | display: none; 46 | } 47 | .imgDark { 48 | display: unset; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/extension/vite.background.config.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { defineConfig } from 'vite' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = dirname(__filename) 7 | 8 | export default defineConfig({ 9 | build: { 10 | outDir: 'dist', 11 | emptyOutDir: false, // Important: don't clear dist folder 12 | lib: { 13 | entry: resolve(__dirname, 'src/background.ts'), 14 | formats: ['iife'], 15 | name: 'background', 16 | fileName: () => 'background.js' 17 | }, 18 | rollupOptions: { 19 | output: { 20 | extend: true, 21 | dir: 'dist', 22 | entryFileNames: 'background.js' 23 | } 24 | } 25 | } 26 | }) -------------------------------------------------------------------------------- /apps/extension/eslint.config.js: -------------------------------------------------------------------------------- 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 | ) 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp 4 | .pnp.js 5 | __pycache__/ 6 | *.py[cod] 7 | venv/ 8 | .venv/ 9 | .env/ 10 | 11 | # Environment Variables 12 | .env 13 | .env.* 14 | !.env.example 15 | 16 | # Build Outputs 17 | .next/ 18 | out/ 19 | build/ 20 | dist/ 21 | *.egg-info/ 22 | 23 | # Cache 24 | .turbo/ 25 | .cache/ 26 | .eslintcache 27 | *.tsbuildinfo 28 | .pytest_cache/ 29 | 30 | # Testing 31 | coverage/ 32 | .coverage 33 | htmlcov/ 34 | 35 | # Development Tools 36 | .idea/ 37 | .vscode/ 38 | *.swp 39 | *.swo 40 | 41 | # Debug Logs 42 | npm-debug.log* 43 | yarn-debug.log* 44 | yarn-error.log* 45 | debug.log 46 | *.log 47 | 48 | # System Files 49 | .DS_Store 50 | Thumbs.db 51 | *.pem 52 | 53 | # Deployment 54 | .vercel 55 | .netlify 56 | 57 | # Python 58 | *.pyc 59 | *.pyo 60 | *.pyd 61 | .Python 62 | 63 | # Temp files 64 | *.tmp 65 | *~ 66 | extension_code.txt 67 | apps/server/dom_snapshots/ 68 | 69 | full_code.txt 70 | -------------------------------------------------------------------------------- /apps/server/app/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Settings(BaseModel): 5 | APP_NAME: str = "Navigator AI API" 6 | DEBUG: bool = True 7 | API_PREFIX: str = "" 8 | SNAPSHOTS_DIR: str = "dom_snapshots" 9 | 10 | # Redis settings 11 | REDIS_HOST: str = "localhost" 12 | REDIS_PORT: int = 6379 13 | REDIS_DB: int = 0 14 | REDIS_PASSWORD: str = "" 15 | REDIS_PREFIX: str = "navigator:" 16 | REDIS_TASK_PREFIX: str = "task:" 17 | REDIS_TASK_HISTORY_PREFIX: str = "task_history:" 18 | REDIS_PREV_STEP_ANS_PREFIX: str = "prev_step_ans:" 19 | REDIS_TASK_TTL: int = 60 * 60 * 24 # 24 hours 20 | 21 | # CORS settings 22 | CORS_ORIGINS: list[str] = ["*"] 23 | CORS_ALLOW_CREDENTIALS: bool = True 24 | CORS_ALLOW_METHODS: list[str] = ["*"] 25 | CORS_ALLOW_HEADERS: list[str] = ["*"] 26 | 27 | class Config: 28 | env_file = ".env" 29 | 30 | 31 | settings = Settings() 32 | -------------------------------------------------------------------------------- /apps/extension/vite.popup.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import autoprefixer from 'autoprefixer' 3 | import { dirname, resolve } from 'path' 4 | import tailwindcss from 'tailwindcss' 5 | import { fileURLToPath } from 'url' 6 | import { defineConfig } from 'vite' 7 | 8 | const __filename = fileURLToPath(import.meta.url) 9 | const __dirname = dirname(__filename) 10 | 11 | export default defineConfig({ 12 | plugins: [react()], 13 | css: { 14 | postcss: { 15 | plugins: [ 16 | tailwindcss('./tailwind.config.js'), 17 | autoprefixer() 18 | ] 19 | } 20 | }, 21 | build: { 22 | outDir: 'dist', 23 | emptyOutDir: false, 24 | rollupOptions: { 25 | input: { 26 | popup: resolve(__dirname, 'popup.html') 27 | }, 28 | output: { 29 | format: 'es', 30 | dir: 'dist', 31 | entryFileNames: '[name].js', 32 | assetFileNames: 'assets/[name].[ext]' 33 | } 34 | } 35 | } 36 | }) -------------------------------------------------------------------------------- /packages/ui/turbo/generators/config.ts: -------------------------------------------------------------------------------- 1 | import type { PlopTypes } from "@turbo/gen"; 2 | 3 | // Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation 4 | 5 | export default function generator(plop: PlopTypes.NodePlopAPI): void { 6 | // A simple generator to add a new React component to the internal UI library 7 | plop.setGenerator("react-component", { 8 | description: "Adds a new react component", 9 | prompts: [ 10 | { 11 | type: "input", 12 | name: "name", 13 | message: "What is the name of the component?", 14 | }, 15 | ], 16 | actions: [ 17 | { 18 | type: "add", 19 | path: "src/{{kebabCase name}}.tsx", 20 | templateFile: "templates/component.hbs", 21 | }, 22 | { 23 | type: "append", 24 | path: "package.json", 25 | pattern: /"exports": {(?)/g, 26 | template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",', 27 | }, 28 | ], 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /apps/extension/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import './App.css' 3 | import reactLogo from './assets/react.svg' 4 | import viteLogo from '/vite.svg' 5 | 6 | 7 | function App() { 8 | const [count, setCount] = useState(0) 9 | 10 | return ( 11 | <> 12 |
13 | 14 | Vite logo 15 | 16 | 17 | React logo 18 | 19 |
20 |

Vite + React

21 |
22 | 25 |

26 | Edit src/App.tsx and save to test HMR 27 |

28 |
29 |

30 | Click on the Vite and React logos to learn more 31 |

32 | 33 | ) 34 | } 35 | 36 | export default App 37 | -------------------------------------------------------------------------------- /apps/extension/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./popup.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | animation: { 10 | 'slide-up': 'slideUp 0.3s ease-out', 11 | 'slide-down': 'slideDown 0.3s ease-out', 12 | 'pulse': 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', 13 | }, 14 | keyframes: { 15 | slideUp: { 16 | '0%': { transform: 'translateY(100%)' }, 17 | '100%': { transform: 'translateY(0)' }, 18 | }, 19 | slideDown: { 20 | '0%': { transform: 'translateY(0)' }, 21 | '100%': { transform: 'translateY(100%)' }, 22 | }, 23 | pulse: { 24 | '0%, 100%': { opacity: 1 }, 25 | '50%': { opacity: 0.5 }, 26 | } 27 | } 28 | }, 29 | }, 30 | plugins: [], 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Soham Ratnaparkhi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/eslint-config/react-internal.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import tseslint from "typescript-eslint"; 4 | import pluginReactHooks from "eslint-plugin-react-hooks"; 5 | import pluginReact from "eslint-plugin-react"; 6 | import globals from "globals"; 7 | import { config as baseConfig } from "./base.js"; 8 | 9 | /** 10 | * A custom ESLint configuration for libraries that use React. 11 | * 12 | * @type {import("eslint").Linter.Config} */ 13 | export const config = [ 14 | ...baseConfig, 15 | js.configs.recommended, 16 | eslintConfigPrettier, 17 | ...tseslint.configs.recommended, 18 | pluginReact.configs.flat.recommended, 19 | { 20 | languageOptions: { 21 | ...pluginReact.configs.flat.recommended.languageOptions, 22 | globals: { 23 | ...globals.serviceworker, 24 | ...globals.browser, 25 | }, 26 | }, 27 | }, 28 | { 29 | plugins: { 30 | "react-hooks": pluginReactHooks, 31 | }, 32 | settings: { react: { version: "detect" } }, 33 | rules: { 34 | ...pluginReactHooks.configs.recommended.rules, 35 | // React scope no longer necessary with new JSX transform. 36 | "react/react-in-jsx-scope": "off", 37 | }, 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /apps/server/app/api/services/task_service.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import datetime 3 | 4 | from app.api.services.storage_service import StorageService 5 | from app.models.tasks import TaskCreate, TaskResponse 6 | 7 | 8 | class TaskService: 9 | """Service for handling tasks""" 10 | 11 | @staticmethod 12 | def create_task(task: TaskCreate) -> TaskResponse: 13 | """Create a new task""" 14 | task_id = f"task_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4()}" 15 | 16 | StorageService.store_task(task_id, task.task) 17 | 18 | return TaskResponse( 19 | task_id=task_id, 20 | status="created", 21 | message=f"Task created successfully: {task.task[:50]}" 22 | ) 23 | 24 | @staticmethod 25 | def get_task(task_id: str) -> str: 26 | """Get a task from storage""" 27 | return StorageService.get_task(task_id) 28 | 29 | @staticmethod 30 | def get_task_history(task_id: str) -> list: 31 | """Get task history from storage""" 32 | return StorageService.get_task_history(task_id) 33 | 34 | @staticmethod 35 | def get_prev_step_ans(task_id: str) -> str: 36 | """Get previous step answer for a task""" 37 | return StorageService.get_prev_step_ans(task_id) 38 | -------------------------------------------------------------------------------- /apps/extension/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Navigator AI", 4 | "version": "1.0.0", 5 | "description": "AI-powered website navigation and automation", 6 | "action": { 7 | "default_title": "Navigator AI", 8 | "default_icon": { 9 | "16": "icons/icon16.svg", 10 | "48": "icons/icon48.svg", 11 | "128": "icons/icon128.svg" 12 | } 13 | }, 14 | "icons": { 15 | "16": "icons/icon16.svg", 16 | "48": "icons/icon48.svg", 17 | "128": "icons/icon128.svg" 18 | }, 19 | "background": { 20 | "service_worker": "background.js", 21 | "type": "module" 22 | }, 23 | "permissions": ["storage", "activeTab", "scripting", "sidePanel", "windows", "clipboardWrite", "clipboardRead", "tabs"], 24 | "host_permissions": ["http://*/*", "https://*/*"], 25 | "content_scripts": [ 26 | { 27 | "matches": [""], 28 | "js": ["content.js"], 29 | "run_at": "document_idle", 30 | "all_frames": true 31 | } 32 | ], 33 | "web_accessible_resources": [ 34 | { 35 | "resources": ["popup.html", "assets/*"], 36 | "matches": [""] 37 | } 38 | ], 39 | "side_panel": { 40 | "default_path": "popup.html", 41 | "default_title": "Navigator AI", 42 | "enable_controls": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/docs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/extension/icons.js: -------------------------------------------------------------------------------- 1 | // This is a standalone script to create icons 2 | // Run with Node.js: node create-icons.js 3 | 4 | import { existsSync, mkdirSync, writeFileSync } from 'fs'; 5 | import path from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | const ICONS_DIR = path.join(__dirname, 'public', 'icons'); 10 | 11 | // Create basic SVG icon content 12 | const createIconSVG = (size) => ` 13 | 14 | 15 | 16 | 17 | `; 18 | 19 | // Ensure directory exists 20 | if (!existsSync(ICONS_DIR)) { 21 | mkdirSync(ICONS_DIR, { recursive: true }); 22 | console.log(`Created directory: ${ICONS_DIR}`); 23 | } 24 | 25 | // Create icons of different sizes 26 | const sizes = [16, 48, 128]; 27 | sizes.forEach(size => { 28 | const iconPath = path.join(ICONS_DIR, `icon${size}.svg`); 29 | writeFileSync(iconPath, createIconSVG(size)); 30 | console.log(`Created icon: ${iconPath}`); 31 | }); 32 | 33 | console.log('All icons created successfully!'); -------------------------------------------------------------------------------- /apps/extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --config vite.popup.config.ts", 8 | "build": "rimraf dist && tsx build.ts", 9 | "build:chrome": "rimraf dist && cross-env TARGET=chrome tsx build.ts", 10 | "build:firefox": "rimraf dist && cross-env TARGET=firefox tsx build.ts" 11 | }, 12 | "dependencies": { 13 | "@navigator-ai/core": "workspace:*", 14 | "axios": "^1.8.3", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0" 17 | }, 18 | "devDependencies": { 19 | "@eslint/js": "^9.19.0", 20 | "@tailwindcss": "link:types/@tailwindcss", 21 | "@types/chrome": "^0.0.306", 22 | "@types/fs-extra": "^11.0.4", 23 | "@types/node": "^20.17.19", 24 | "@types/react": "^19.0.8", 25 | "@types/react-dom": "^19.0.3", 26 | "@vitejs/plugin-react": "^4.3.4", 27 | "autoprefixer": "^10.4.20", 28 | "cross-env": "^7.0.3", 29 | "eslint": "^9.19.0", 30 | "eslint-plugin-react-hooks": "^5.0.0", 31 | "eslint-plugin-react-refresh": "^0.4.18", 32 | "fs-extra": "^11.3.0", 33 | "globals": "^15.14.0", 34 | "postcss": "^8.5.3", 35 | "rimraf": "^6.0.1", 36 | "tailwindcss": "^3.4.17", 37 | "tsx": "^4.19.3", 38 | "typescript": "~5.7.2", 39 | "typescript-eslint": "^8.22.0", 40 | "vite": "^6.1.0", 41 | "vite-plugin-web-extension": "^4.4.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/extension/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/eslint-config/next.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import tseslint from "typescript-eslint"; 4 | import pluginReactHooks from "eslint-plugin-react-hooks"; 5 | import pluginReact from "eslint-plugin-react"; 6 | import globals from "globals"; 7 | import pluginNext from "@next/eslint-plugin-next"; 8 | import { config as baseConfig } from "./base.js"; 9 | 10 | /** 11 | * A custom ESLint configuration for libraries that use Next.js. 12 | * 13 | * @type {import("eslint").Linter.Config} 14 | * */ 15 | export const nextJsConfig = [ 16 | ...baseConfig, 17 | js.configs.recommended, 18 | eslintConfigPrettier, 19 | ...tseslint.configs.recommended, 20 | { 21 | ...pluginReact.configs.flat.recommended, 22 | languageOptions: { 23 | ...pluginReact.configs.flat.recommended.languageOptions, 24 | globals: { 25 | ...globals.serviceworker, 26 | }, 27 | }, 28 | }, 29 | { 30 | plugins: { 31 | "@next/next": pluginNext, 32 | }, 33 | rules: { 34 | ...pluginNext.configs.recommended.rules, 35 | ...pluginNext.configs["core-web-vitals"].rules, 36 | }, 37 | }, 38 | { 39 | plugins: { 40 | "react-hooks": pluginReactHooks, 41 | }, 42 | settings: { react: { version: "detect" } }, 43 | rules: { 44 | ...pluginReactHooks.configs.recommended.rules, 45 | // React scope no longer necessary with new JSX transform. 46 | "react/react-in-jsx-scope": "off", 47 | }, 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /apps/extension/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface DOMCoordinates { 2 | x: number; 3 | y: number; 4 | } 5 | 6 | export interface CoordinateSet { 7 | topLeft: DOMCoordinates; 8 | topRight: DOMCoordinates; 9 | bottomLeft: DOMCoordinates; 10 | bottomRight: DOMCoordinates; 11 | center: DOMCoordinates; 12 | width: number; 13 | height: number; 14 | } 15 | 16 | export interface ViewportInfo { 17 | scrollX: number; 18 | scrollY: number; 19 | width: number; 20 | height: number; 21 | } 22 | 23 | export interface DOMElementNode { 24 | tagName: string; 25 | attributes: Record; 26 | xpath: string; 27 | children: number[]; // Array of IDs referencing other nodes in the map 28 | isInteractive: boolean; 29 | isVisible: boolean; 30 | isTopElement: boolean; 31 | highlightIndex?: number; // Optional, only present for interactive elements 32 | shadowRoot?: boolean; // Optional, only present if element has shadow DOM 33 | viewportCoordinates?: CoordinateSet; // Coordinates relative to viewport 34 | pageCoordinates?: CoordinateSet; // Coordinates relative to page 35 | viewport?: ViewportInfo; // Information about viewport and scroll position 36 | } 37 | 38 | // Text node representation 39 | export interface DOMTextNode { 40 | type: "TEXT_NODE"; 41 | text: string; 42 | isVisible: boolean; 43 | } 44 | 45 | export type DOMNode = DOMElementNode | DOMTextNode; 46 | 47 | export interface DOMHashMap { 48 | [id: string]: DOMNode; 49 | } 50 | 51 | export type NodeType = Element | Text; 52 | 53 | export interface Action { 54 | type: 'click' | 'scroll' | 'input' | 'navigate' | 'url' | 'copy' | 'switchToTab'; 55 | element_id?: string; 56 | xpath_ref?: string; 57 | selector?: string; 58 | text?: string; 59 | amount?: number; 60 | url?: string; 61 | tab_id?: number; 62 | } 63 | 64 | export interface AutomationOptions { 65 | debug?: boolean; 66 | cursorSize?: number; 67 | cursorUI?: string; 68 | } 69 | 70 | export interface ExecuteActionResult { 71 | success: boolean 72 | message: string 73 | } -------------------------------------------------------------------------------- /apps/server/app/models/dom.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, Dict, List, Optional, Union 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class DOMData(BaseModel): 8 | url: str 9 | html: str 10 | title: str 11 | timestamp: str 12 | 13 | 14 | class DOMCoordinates(BaseModel): 15 | x: float 16 | y: float 17 | 18 | 19 | class CoordinateSet(BaseModel): 20 | topLeft: DOMCoordinates 21 | topRight: DOMCoordinates 22 | bottomLeft: DOMCoordinates 23 | bottomRight: DOMCoordinates 24 | center: DOMCoordinates 25 | width: float 26 | height: float 27 | 28 | 29 | class ViewportInfo(BaseModel): 30 | scrollX: float 31 | scrollY: float 32 | width: float 33 | height: float 34 | 35 | 36 | class DOMElementNode(BaseModel): 37 | tagName: str 38 | attributes: Dict[str, str] 39 | xpath: str 40 | children: List[int] 41 | isInteractive: bool 42 | isVisible: bool 43 | isTopElement: bool 44 | highlightIndex: Optional[int] = None 45 | shadowRoot: Optional[bool] = None 46 | viewportCoordinates: Optional[CoordinateSet] = None 47 | pageCoordinates: Optional[CoordinateSet] = None 48 | viewport: Optional[ViewportInfo] = None 49 | 50 | 51 | class DOMTextNode(BaseModel): 52 | type: str = "TEXT_NODE" 53 | text: str 54 | isVisible: bool 55 | 56 | 57 | class DOMUpdate(BaseModel): 58 | task_id: str 59 | dom_data: DOMData 60 | result: List[Dict] = Field(default_factory=list) 61 | iterations: int = 0 62 | structure: Dict[str, Any] = Field(default_factory=dict) 63 | openTabsWithIds: List[Dict[str, Any]] = Field(default_factory=list) 64 | currentTab: Optional[Dict[str, Any]] = None 65 | 66 | 67 | class DOMUpdateResponse(BaseModel): 68 | status: str 69 | message: str 70 | result: Any 71 | 72 | class ExecuteActionResult(BaseModel): 73 | success: bool 74 | result: Any 75 | 76 | class DOMState(BaseModel): 77 | url: str 78 | element_tree: Dict[str, Union[DOMElementNode, DOMTextNode]] 79 | 80 | 81 | DOMNode = Union[DOMElementNode, DOMTextNode] 82 | DOMHashMap = Dict[str, DOMNode] 83 | -------------------------------------------------------------------------------- /temp.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def write_extension_code_to_file(extension_dir, output_file): 5 | """ 6 | Recursively reads all files in the extension directory and writes their content to a single text file. 7 | 8 | Args: 9 | extension_dir (str): Path to the extension directory 10 | output_file (str): Path to the output text file 11 | """ 12 | with open(output_file, 'w', encoding='utf-8') as outfile: 13 | for root, dirs, files in os.walk(extension_dir): 14 | for file in files: 15 | # Skip node_modules, dist, and other common directories to ignore 16 | if any(ignore in root for ignore in ['node_modules', 'dist', '.git']): 17 | continue 18 | 19 | # Get file extension 20 | _, file_ext = os.path.splitext(file) 21 | 22 | # List of file extensions to include 23 | valid_extensions = ['.ts', '.tsx', '.js', 24 | '.jsx', '.html', '.css', '.py'] 25 | 26 | invalid_files = ['package-lock.json'] 27 | 28 | if file_ext in valid_extensions and file not in invalid_files: 29 | file_path = os.path.join(root, file) 30 | relative_path = os.path.relpath(file_path, extension_dir) 31 | 32 | try: 33 | with open(file_path, 'r', encoding='utf-8') as infile: 34 | content = infile.read() 35 | 36 | # Write file header 37 | outfile.write(f"\n{'='*80}\n") 38 | outfile.write(f"File: {relative_path}\n") 39 | outfile.write(f"{'='*80}\n\n") 40 | 41 | # Write file content 42 | outfile.write(content) 43 | outfile.write("\n\n") 44 | except Exception as e: 45 | outfile.write( 46 | f"Error reading file {file_path}: {str(e)}\n") 47 | 48 | 49 | if __name__ == "__main__": 50 | # Specify the extension directory and output file 51 | # Adjust this path to match your project structure 52 | extension_dir = "./packages/core" 53 | output_file = "core.txt" 54 | 55 | write_extension_code_to_file(extension_dir, output_file) 56 | print(f"Code has been written to {output_file}") 57 | -------------------------------------------------------------------------------- /apps/extension/build.ts: -------------------------------------------------------------------------------- 1 | // build.ts 2 | import autoprefixer from 'autoprefixer' 3 | import fs from 'fs-extra' 4 | import { dirname, resolve } from 'path' 5 | import postcss from 'postcss' 6 | import tailwindcss from 'tailwindcss' 7 | import { fileURLToPath } from 'url' 8 | import { build } from 'vite' 9 | 10 | const __filename = fileURLToPath(import.meta.url) 11 | const __dirname = dirname(__filename) 12 | 13 | async function buildExtension() { 14 | // Clean dist folder first 15 | console.log('Cleaning dist folder...') 16 | await fs.remove('dist') 17 | await fs.ensureDir('dist') 18 | 19 | // Process CSS with Tailwind first 20 | console.log('Processing Tailwind CSS...') 21 | const cssContent = await fs.readFile('src/index.css', 'utf8') 22 | const result = await postcss([ 23 | tailwindcss('./tailwind.config.js'), 24 | autoprefixer 25 | ]).process(cssContent, { 26 | from: 'src/index.css', 27 | to: 'dist/assets/popup.css' 28 | }) 29 | 30 | // Make sure assets folder exists 31 | await fs.ensureDir('dist/assets') 32 | 33 | // Write processed CSS 34 | await fs.writeFile('dist/assets/popup.css', result.css) 35 | console.log('Tailwind CSS processed and saved') 36 | 37 | // Build in sequence 38 | console.log('Building popup...') 39 | await build({ 40 | configFile: resolve(__dirname, 'vite.popup.config.ts'), 41 | mode: 'production' 42 | }) 43 | 44 | console.log('Building background script...') 45 | await build({ 46 | configFile: resolve(__dirname, 'vite.background.config.ts'), 47 | mode: 'production' 48 | }) 49 | 50 | console.log('Building content script...') 51 | await build({ 52 | configFile: resolve(__dirname, 'vite.content.config.ts'), 53 | mode: 'production' 54 | }) 55 | 56 | // Copy manifest 57 | console.log('Copying manifest...') 58 | await fs.copy( 59 | resolve(__dirname, 'public/manifest.json'), 60 | resolve(__dirname, 'dist/manifest.json') 61 | ) 62 | 63 | // Update popup.html to correctly reference the CSS 64 | console.log('Updating popup.html...') 65 | let popupHtml = await fs.readFile('dist/popup.html', 'utf8') 66 | if (!popupHtml.includes('href="./assets/popup.css"')) { 67 | popupHtml = popupHtml.replace( 68 | '', 69 | '' 70 | ) 71 | await fs.writeFile('dist/popup.html', popupHtml) 72 | } 73 | 74 | console.log('Build complete!') 75 | } 76 | 77 | buildExtension().catch((err) => { 78 | console.error('Build failed:', err) 79 | process.exit(1) 80 | }) -------------------------------------------------------------------------------- /apps/docs/public/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/web/public/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/extension/src/types.ts: -------------------------------------------------------------------------------- 1 | // src/types.ts 2 | import { DOMHashMap } from "@navigator-ai/core"; 3 | import { Action, ExecuteActionResult } from "@navigator-ai/core"; 4 | 5 | export interface FrontendDOMState { 6 | url: string; 7 | html: string; 8 | title: string; 9 | timestamp: string; 10 | structure?: DOMHashMap; 11 | } 12 | 13 | export interface DOMUpdate { 14 | task_id: string; 15 | dom_data: FrontendDOMState; 16 | result: unknown[]; 17 | iterations: number; 18 | structure: DOMHashMap; 19 | openTabsWithIds: { id: number; url: string }[]; 20 | currentTab: { id: number; url: string }; 21 | } 22 | 23 | export interface Message { 24 | type: 'startTask' | 'startMonitoring' | 'stopMonitoring' | 'processDOM' | 'toggleSidebar' | 'toggleUI' | 'updateSidebarState' | 'dom_update' | 'pauseMonitoring' | 'resumeMonitoring' | 'executeActions' | 'startSequentialProcessing' | 'check_processing_status' | 'resetIterations' | 'singleDOMProcess' | 'ping' | 'resetWorkflow' | 'checkDomainChange' | 'updateProcessingStatus' | 'openSidePanel' | 'closeSidePanel' | 'toggleSidePanel' | 'switchTab' | 'processingStatusUpdate' | 'invalidURL' | 'stopAutomation'; 25 | task?: string; 26 | task_id?: string; 27 | dom_data?: FrontendDOMState; 28 | result?: unknown[]; 29 | iterations?: number; 30 | maxIterations?: number; 31 | isPaused?: boolean; 32 | isOpen?: boolean; // New property for sidebar state 33 | actions?: Action[]; 34 | status?: ProcessingStatus; 35 | isDone?: boolean; // Property for signaling if processing is complete 36 | currentUrl?: string; // Current URL for domain change detection 37 | iterationResults?: ExecuteActionResult[]; 38 | tabId?: number; 39 | } 40 | 41 | // Processing status for DOM operations 42 | export type ProcessingStatus = 43 | 'idle' | // Not processing anything 44 | 'parsing' | // Parsing DOM with server 45 | 'updating' | // Sending update to API 46 | 'executing_actions' | // Executing actions from API 47 | 'waiting_for_server' | // Waiting for server response 48 | 'completed' | // Task completed 49 | 'error' | // Error occurred 50 | 'paused' | // Processing paused 51 | 'stopping'; // Processing stopping 52 | 53 | export interface TaskState { 54 | taskId: string | null; 55 | status: 'idle' | 'running' | 'completed' | 'error' | 'paused' | 'stopping'; 56 | task: string; 57 | isRunning: boolean; 58 | iterations: number; 59 | isPaused: boolean; 60 | processingStatus?: ProcessingStatus; // Current processing step 61 | lastUpdateTimestamp?: string; // Timestamp of last successful update 62 | } 63 | 64 | // New type for sidebar settings 65 | export interface SidebarState { 66 | isOpen: boolean; 67 | activeTab: 'automation' | 'knowledge' | 'history' | 'settings'; 68 | } -------------------------------------------------------------------------------- /apps/extension/src/automation/index.ts: -------------------------------------------------------------------------------- 1 | import { Action, IAutomationHandler, initAutomationHandler, ExecuteActionResult } from '@navigator-ai/core'; 2 | 3 | let automationHandler: IAutomationHandler; 4 | try { 5 | automationHandler = initAutomationHandler(); 6 | 7 | if (typeof automationHandler.setDebugMode === 'function') { 8 | automationHandler.setDebugMode(true); 9 | } 10 | } catch (error) { 11 | console.error('Error creating AutomationHandler:', error); 12 | // automationHandler = { 13 | // setDebugMode: () => {}, 14 | // setCursorSize: () => {}, 15 | // ensureCursorVisible: () => {}, 16 | // executeAction: async () => {}, 17 | // executeActions: async () => [] 18 | // }; 19 | } 20 | 21 | export async function handleAutomationActions(actions: Action[]): Promise { 22 | try { 23 | console.log('=== STARTING ACTION EXECUTION ==='); 24 | console.log('Received actions:', JSON.stringify(actions, null, 2)); 25 | 26 | if (!Array.isArray(actions) || actions.length === 0) { 27 | console.error('Invalid actions array:', actions); 28 | throw new Error('Invalid actions array'); 29 | } 30 | 31 | if (!automationHandler) { 32 | console.error('AutomationHandler is not initialized'); 33 | throw new Error('AutomationHandler not available'); 34 | } 35 | 36 | try { 37 | if (typeof automationHandler.ensureCursorVisible === 'function') { 38 | automationHandler.ensureCursorVisible(); 39 | console.log('Cursor visibility ensured'); 40 | } 41 | } catch (initError) { 42 | console.error('Error initializing cursor:', initError); 43 | } 44 | 45 | console.log('About to execute actions with automationHandler...'); 46 | 47 | // Create a timeout promise that rejects after 120 seconds 48 | const timeoutPromise = new Promise((_, reject) => { 49 | setTimeout(() => { 50 | reject(new Error('Action execution timed out after 120 seconds')); 51 | }, 120000); 52 | }); 53 | 54 | // Execute the actions with a timeout 55 | const results = await Promise.race([ 56 | automationHandler.executeActions(actions), 57 | timeoutPromise 58 | ]); 59 | 60 | console.log('=== ACTION EXECUTION COMPLETE ==='); 61 | console.log('Results:', JSON.stringify(results, null, 2)); 62 | 63 | if (results.some(result => !result.success)) { 64 | const failedIndex = results.findIndex(result => !result.success); 65 | console.warn(`Action at index ${failedIndex} failed:`, actions[failedIndex]); 66 | console.warn(`Failure reason:`, results[failedIndex].message); 67 | } 68 | 69 | return results; 70 | } catch (error) { 71 | console.error('=== ACTION EXECUTION ERROR ==='); 72 | console.error('Error in handleAutomationActions:', error); 73 | throw error; 74 | } 75 | } -------------------------------------------------------------------------------- /apps/extension/src/utils/xpath.ts: -------------------------------------------------------------------------------- 1 | // utils/xpath.ts 2 | // Utility functions for working with XPath 3 | 4 | /** 5 | * Gets an element by XPath, including searching in iframes 6 | * @param xpath The XPath expression 7 | * @returns The element if found, or null 8 | */ 9 | export function getElementByXPathIncludingIframes(xpath: string): HTMLElement | null { 10 | try { 11 | // First try to find the element in the main document 12 | const result = document.evaluate( 13 | xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null 14 | ); 15 | const element = result.singleNodeValue as HTMLElement; 16 | 17 | if (element) { 18 | return element; 19 | } 20 | 21 | // If not found, check if it's in an iframe 22 | // Look for navigator-iframe-data tags in the DOM xpath which might indicate 23 | // the element is inside an iframe 24 | const iframePathMatch = xpath.match(/\/navigator-iframe-data\[@data-iframe-id="([^"]+)"\]/); 25 | 26 | if (iframePathMatch) { 27 | const iframeId = iframePathMatch[1]; 28 | const iframeElement = document.querySelector(`iframe[data-navigator-iframe-id="${iframeId}"]`); 29 | 30 | if (iframeElement && iframeElement instanceof HTMLIFrameElement) { 31 | try { 32 | // Extract the part of xpath after the navigator-iframe-data part 33 | const remainingXpath = xpath.substring(xpath.indexOf(iframePathMatch[0]) + iframePathMatch[0].length); 34 | 35 | // Try to evaluate this xpath in the iframe content document 36 | if (iframeElement.contentDocument) { 37 | const iframeResult = iframeElement.contentDocument.evaluate( 38 | remainingXpath, iframeElement.contentDocument, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null 39 | ); 40 | return iframeResult.singleNodeValue as HTMLElement; 41 | } 42 | } catch (error) { 43 | console.error('Error evaluating XPath in iframe:', error); 44 | } 45 | } 46 | } 47 | 48 | // If we still haven't found it, try searching in all accessible iframes 49 | const iframes = document.querySelectorAll('iframe'); 50 | for (let i = 0; i < iframes.length; i++) { 51 | const iframe = iframes[i]; 52 | if (iframe.contentDocument) { 53 | try { 54 | const iframeResult = iframe.contentDocument.evaluate( 55 | xpath, iframe.contentDocument, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null 56 | ); 57 | const iframeElement = iframeResult.singleNodeValue as HTMLElement; 58 | if (iframeElement) { 59 | return iframeElement; 60 | } 61 | } catch { 62 | // Silently continue to the next iframe 63 | } 64 | } 65 | } 66 | 67 | return null; 68 | } catch (error) { 69 | console.error('Error finding element by XPath:', error); 70 | return null; 71 | } 72 | } -------------------------------------------------------------------------------- /apps/docs/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image, { type ImageProps } from "next/image"; 2 | import { Button } from "@repo/ui/button"; 3 | import styles from "./page.module.css"; 4 | 5 | type Props = Omit & { 6 | srcLight: string; 7 | srcDark: string; 8 | }; 9 | 10 | const ThemeImage = (props: Props) => { 11 | const { srcLight, srcDark, ...rest } = props; 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default function Home() { 22 | return ( 23 |
24 |
25 | 34 |
    35 |
  1. 36 | Get started by editing apps/docs/app/page.tsx 37 |
  2. 38 |
  3. Save and see your changes instantly.
  4. 39 |
40 | 41 | 66 | 69 |
70 | 100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /apps/web/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image, { type ImageProps } from "next/image"; 2 | import { Button } from "@repo/ui/button"; 3 | import styles from "./page.module.css"; 4 | 5 | type Props = Omit & { 6 | srcLight: string; 7 | srcDark: string; 8 | }; 9 | 10 | const ThemeImage = (props: Props) => { 11 | const { srcLight, srcDark, ...rest } = props; 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default function Home() { 22 | return ( 23 |
24 |
25 | 34 |
    35 |
  1. 36 | Get started by editing apps/web/app/page.tsx 37 |
  2. 38 |
  3. Save and see your changes instantly.
  4. 39 |
40 | 41 | 66 | 69 |
70 | 100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /apps/server/app/api/endpoints/tasks.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | 5 | from app.api.services.storage_service import StorageService 6 | from app.api.services.task_service import TaskService 7 | from app.api.utils.prompts import build_system_prompt, build_user_message 8 | from app.api.utils.dom_parser.dom_optimizer import process_element_references 9 | from app.config import settings 10 | from app.models.dom import DOMState, DOMUpdate, DOMUpdateResponse 11 | from app.models.tasks import TaskCreate, TaskResponse 12 | from fastapi import APIRouter, HTTPException 13 | from datetime import datetime 14 | 15 | from app.api.utils.llm import generate 16 | 17 | router = APIRouter() 18 | 19 | logger = logging.getLogger("task_logger") 20 | 21 | 22 | @router.post("/create", response_model=TaskResponse) 23 | async def create_task(task: TaskCreate): 24 | """Create a new navigation task""" 25 | try: 26 | return TaskService.create_task(task) 27 | except Exception as e: 28 | raise HTTPException(status_code=500, detail=str(e)) 29 | 30 | 31 | @router.post("/update", response_model=DOMUpdateResponse) 32 | async def update_task(update: DOMUpdate): 33 | """Update a task with DOM data""" 34 | try: 35 | files = StorageService.save_dom_snapshot(update) 36 | dom_state = DOMState( 37 | url=update.dom_data.url, 38 | element_tree=update.structure 39 | ) 40 | 41 | task_text = TaskService.get_task(update.task_id) 42 | 43 | task_history = TaskService.get_task_history(update.task_id) 44 | prev_step_ans = TaskService.get_prev_step_ans(update.task_id) 45 | logger.info(f"Retrieved history for task {update.task_id}: {len(task_history)} entries with {update.result} results") 46 | 47 | user_message, xpath_map, selector_map = build_user_message( 48 | dom_state=dom_state, 49 | task=task_text, 50 | result=update.result, 51 | history=task_history 52 | ) 53 | 54 | system_message = build_system_prompt() + f"\n\nOpen tabs: {update.openTabsWithIds}" + f"\n\nPrevious step answer: {prev_step_ans}\n\nCurrent tab: {update.currentTab}" 55 | 56 | result = generate(user_message, system_message) 57 | processed_result = process_element_references(result, xpath_map, selector_map) 58 | 59 | if processed_result and hasattr(processed_result, "actions") and processed_result.actions: 60 | logger.info(f"Storing AI-generated actions for task {update.task_id}") 61 | StorageService.append_task_history(update.task_id, { 62 | "url": update.dom_data.url, 63 | "timestamp": update.dom_data.timestamp, 64 | "actions": [action.model_dump() for action in processed_result.actions] 65 | }, (prev_step_ans if prev_step_ans is not None else "") + "\n\n" + (result.current_state.data_useful_for_next_step if result.current_state.data_useful_for_next_step is not None else "")) 66 | 67 | try: 68 | os.makedirs(settings.SNAPSHOTS_DIR, exist_ok=True) 69 | snapshot_file = os.path.join( 70 | settings.SNAPSHOTS_DIR, 71 | f"task_{update.task_id}_dom_snapshot_prompt_{datetime.now().strftime('%Y%m%d%H%M%S')}.txt" 72 | ) 73 | 74 | with open(snapshot_file, "w", encoding='utf-8') as f: 75 | content = f"{system_message}\n\n{user_message}\n\n{processed_result.model_dump_json()}" 76 | f.write(content) 77 | f.flush() 78 | 79 | except Exception as e: 80 | logger.error(f"Error saving snapshot: {str(e)}") 81 | 82 | return DOMUpdateResponse( 83 | status="success", 84 | message="DOM update received and stored", 85 | result=processed_result 86 | ) 87 | except Exception as e: 88 | logger.error(f"Error processing DOM update: {str(e)}") 89 | raise HTTPException(status_code=500, detail=str(e)) 90 | -------------------------------------------------------------------------------- /apps/extension/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/docs/app/page.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | --gray-rgb: 0, 0, 0; 3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08); 4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05); 5 | 6 | --button-primary-hover: #383838; 7 | --button-secondary-hover: #f2f2f2; 8 | 9 | display: grid; 10 | grid-template-rows: 20px 1fr 20px; 11 | align-items: center; 12 | justify-items: center; 13 | min-height: 100svh; 14 | padding: 80px; 15 | gap: 64px; 16 | font-synthesis: none; 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | .page { 21 | --gray-rgb: 255, 255, 255; 22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145); 23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06); 24 | 25 | --button-primary-hover: #ccc; 26 | --button-secondary-hover: #1a1a1a; 27 | } 28 | } 29 | 30 | .main { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 32px; 34 | grid-row-start: 2; 35 | } 36 | 37 | .main ol { 38 | font-family: var(--font-geist-mono); 39 | padding-left: 0; 40 | margin: 0; 41 | font-size: 14px; 42 | line-height: 24px; 43 | letter-spacing: -0.01em; 44 | list-style-position: inside; 45 | } 46 | 47 | .main li:not(:last-of-type) { 48 | margin-bottom: 8px; 49 | } 50 | 51 | .main code { 52 | font-family: inherit; 53 | background: var(--gray-alpha-100); 54 | padding: 2px 4px; 55 | border-radius: 4px; 56 | font-weight: 600; 57 | } 58 | 59 | .ctas { 60 | display: flex; 61 | gap: 16px; 62 | } 63 | 64 | .ctas a { 65 | appearance: none; 66 | border-radius: 128px; 67 | height: 48px; 68 | padding: 0 20px; 69 | border: none; 70 | font-family: var(--font-geist-sans); 71 | border: 1px solid transparent; 72 | transition: background 0.2s, color 0.2s, border-color 0.2s; 73 | cursor: pointer; 74 | display: flex; 75 | align-items: center; 76 | justify-content: center; 77 | font-size: 16px; 78 | line-height: 20px; 79 | font-weight: 500; 80 | } 81 | 82 | a.primary { 83 | background: var(--foreground); 84 | color: var(--background); 85 | gap: 8px; 86 | } 87 | 88 | a.secondary { 89 | border-color: var(--gray-alpha-200); 90 | min-width: 180px; 91 | } 92 | 93 | button.secondary { 94 | appearance: none; 95 | border-radius: 128px; 96 | height: 48px; 97 | padding: 0 20px; 98 | border: none; 99 | font-family: var(--font-geist-sans); 100 | border: 1px solid transparent; 101 | transition: background 0.2s, color 0.2s, border-color 0.2s; 102 | cursor: pointer; 103 | display: flex; 104 | align-items: center; 105 | justify-content: center; 106 | font-size: 16px; 107 | line-height: 20px; 108 | font-weight: 500; 109 | background: transparent; 110 | border-color: var(--gray-alpha-200); 111 | min-width: 180px; 112 | } 113 | 114 | .footer { 115 | font-family: var(--font-geist-sans); 116 | grid-row-start: 3; 117 | display: flex; 118 | gap: 24px; 119 | } 120 | 121 | .footer a { 122 | display: flex; 123 | align-items: center; 124 | gap: 8px; 125 | } 126 | 127 | .footer img { 128 | flex-shrink: 0; 129 | } 130 | 131 | /* Enable hover only on non-touch devices */ 132 | @media (hover: hover) and (pointer: fine) { 133 | a.primary:hover { 134 | background: var(--button-primary-hover); 135 | border-color: transparent; 136 | } 137 | 138 | a.secondary:hover { 139 | background: var(--button-secondary-hover); 140 | border-color: transparent; 141 | } 142 | 143 | .footer a:hover { 144 | text-decoration: underline; 145 | text-underline-offset: 4px; 146 | } 147 | } 148 | 149 | @media (max-width: 600px) { 150 | .page { 151 | padding: 32px; 152 | padding-bottom: 80px; 153 | } 154 | 155 | .main { 156 | align-items: center; 157 | } 158 | 159 | .main ol { 160 | text-align: center; 161 | } 162 | 163 | .ctas { 164 | flex-direction: column; 165 | } 166 | 167 | .ctas a { 168 | font-size: 14px; 169 | height: 40px; 170 | padding: 0 16px; 171 | } 172 | 173 | a.secondary { 174 | min-width: auto; 175 | } 176 | 177 | .footer { 178 | flex-wrap: wrap; 179 | align-items: center; 180 | justify-content: center; 181 | } 182 | } 183 | 184 | @media (prefers-color-scheme: dark) { 185 | .logo { 186 | filter: invert(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /apps/web/app/page.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | --gray-rgb: 0, 0, 0; 3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08); 4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05); 5 | 6 | --button-primary-hover: #383838; 7 | --button-secondary-hover: #f2f2f2; 8 | 9 | display: grid; 10 | grid-template-rows: 20px 1fr 20px; 11 | align-items: center; 12 | justify-items: center; 13 | min-height: 100svh; 14 | padding: 80px; 15 | gap: 64px; 16 | font-synthesis: none; 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | .page { 21 | --gray-rgb: 255, 255, 255; 22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145); 23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06); 24 | 25 | --button-primary-hover: #ccc; 26 | --button-secondary-hover: #1a1a1a; 27 | } 28 | } 29 | 30 | .main { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 32px; 34 | grid-row-start: 2; 35 | } 36 | 37 | .main ol { 38 | font-family: var(--font-geist-mono); 39 | padding-left: 0; 40 | margin: 0; 41 | font-size: 14px; 42 | line-height: 24px; 43 | letter-spacing: -0.01em; 44 | list-style-position: inside; 45 | } 46 | 47 | .main li:not(:last-of-type) { 48 | margin-bottom: 8px; 49 | } 50 | 51 | .main code { 52 | font-family: inherit; 53 | background: var(--gray-alpha-100); 54 | padding: 2px 4px; 55 | border-radius: 4px; 56 | font-weight: 600; 57 | } 58 | 59 | .ctas { 60 | display: flex; 61 | gap: 16px; 62 | } 63 | 64 | .ctas a { 65 | appearance: none; 66 | border-radius: 128px; 67 | height: 48px; 68 | padding: 0 20px; 69 | border: none; 70 | font-family: var(--font-geist-sans); 71 | border: 1px solid transparent; 72 | transition: background 0.2s, color 0.2s, border-color 0.2s; 73 | cursor: pointer; 74 | display: flex; 75 | align-items: center; 76 | justify-content: center; 77 | font-size: 16px; 78 | line-height: 20px; 79 | font-weight: 500; 80 | } 81 | 82 | a.primary { 83 | background: var(--foreground); 84 | color: var(--background); 85 | gap: 8px; 86 | } 87 | 88 | a.secondary { 89 | border-color: var(--gray-alpha-200); 90 | min-width: 180px; 91 | } 92 | 93 | button.secondary { 94 | appearance: none; 95 | border-radius: 128px; 96 | height: 48px; 97 | padding: 0 20px; 98 | border: none; 99 | font-family: var(--font-geist-sans); 100 | border: 1px solid transparent; 101 | transition: background 0.2s, color 0.2s, border-color 0.2s; 102 | cursor: pointer; 103 | display: flex; 104 | align-items: center; 105 | justify-content: center; 106 | font-size: 16px; 107 | line-height: 20px; 108 | font-weight: 500; 109 | background: transparent; 110 | border-color: var(--gray-alpha-200); 111 | min-width: 180px; 112 | } 113 | 114 | .footer { 115 | font-family: var(--font-geist-sans); 116 | grid-row-start: 3; 117 | display: flex; 118 | gap: 24px; 119 | } 120 | 121 | .footer a { 122 | display: flex; 123 | align-items: center; 124 | gap: 8px; 125 | } 126 | 127 | .footer img { 128 | flex-shrink: 0; 129 | } 130 | 131 | /* Enable hover only on non-touch devices */ 132 | @media (hover: hover) and (pointer: fine) { 133 | a.primary:hover { 134 | background: var(--button-primary-hover); 135 | border-color: transparent; 136 | } 137 | 138 | a.secondary:hover { 139 | background: var(--button-secondary-hover); 140 | border-color: transparent; 141 | } 142 | 143 | .footer a:hover { 144 | text-decoration: underline; 145 | text-underline-offset: 4px; 146 | } 147 | } 148 | 149 | @media (max-width: 600px) { 150 | .page { 151 | padding: 32px; 152 | padding-bottom: 80px; 153 | } 154 | 155 | .main { 156 | align-items: center; 157 | } 158 | 159 | .main ol { 160 | text-align: center; 161 | } 162 | 163 | .ctas { 164 | flex-direction: column; 165 | } 166 | 167 | .ctas a { 168 | font-size: 14px; 169 | height: 40px; 170 | padding: 0 16px; 171 | } 172 | 173 | a.secondary { 174 | min-width: auto; 175 | } 176 | 177 | .footer { 178 | flex-wrap: wrap; 179 | align-items: center; 180 | justify-content: center; 181 | } 182 | } 183 | 184 | @media (prefers-color-scheme: dark) { 185 | .logo { 186 | filter: invert(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /apps/web/public/turborepo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/docs/public/turborepo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/docs/public/turborepo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/web/public/turborepo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/extension/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* All styles will be scoped within shadow DOM, so they won't leak */ 6 | :host { 7 | color-scheme: light dark; 8 | } 9 | 10 | /* These styles will only apply within the extension's shadow DOM */ 11 | .browser-automation-container { 12 | position: fixed; 13 | bottom: 20px; 14 | right: 20px; 15 | z-index: 2147483647; 16 | width: 400px; 17 | min-width: 400px; 18 | background-color: transparent; 19 | border-radius: 8px; 20 | overflow: visible; 21 | box-shadow: none; 22 | will-change: transform, left, top; 23 | transform: translate3d(0, 0, 0); 24 | transition: box-shadow 0.3s ease; 25 | } 26 | 27 | /* Add strong visual feedback when dragging */ 28 | .browser-automation-container.dragging { 29 | box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25); 30 | transition: none; 31 | cursor: grabbing; 32 | } 33 | 34 | /* Minimized popup styling */ 35 | .popup-minimized { 36 | @apply w-14 h-14 rounded-full bg-blue-600 cursor-pointer 37 | flex items-center justify-center shadow-lg 38 | hover:bg-blue-700 transition-all duration-200; 39 | } 40 | 41 | /* Animation classes */ 42 | .animate-slide-up { 43 | animation: slideUp 0.3s ease-out forwards; 44 | } 45 | 46 | .animate-slide-down { 47 | animation: slideDown 0.3s ease-out forwards; 48 | } 49 | 50 | @keyframes slideUp { 51 | 0% { transform: translateY(100%); } 52 | 100% { transform: translateY(0); } 53 | } 54 | 55 | @keyframes slideDown { 56 | 0% { transform: translateY(0); } 57 | 100% { transform: translateY(100%); } 58 | } 59 | 60 | /* Glass morphism effect */ 61 | .bg-glass { 62 | @apply bg-slate-800/90 backdrop-blur-sm border border-slate-700/50; 63 | } 64 | 65 | /* Remove all white background colors */ 66 | .bg-white { 67 | background-color: transparent !important; 68 | } 69 | 70 | /* Ensure popup container is transparent */ 71 | .popup-container { 72 | background-color: transparent !important; 73 | } 74 | 75 | /* Enhance drag handle styling for better user experience */ 76 | .drag-handle { 77 | cursor: grab; 78 | user-select: none; 79 | position: relative; 80 | touch-action: none; 81 | } 82 | 83 | /* Show an active state when the handle is being dragged */ 84 | .drag-handle:active, 85 | .dragging .drag-handle { 86 | cursor: grabbing; 87 | } 88 | 89 | /* Enhance the hover indicator for minimized state only */ 90 | .drag-handle:hover::before { 91 | content: ""; 92 | position: absolute; 93 | top: 0; 94 | left: 0; 95 | right: 0; 96 | height: 3px; 97 | background: rgba(100, 149, 237, 0.8); 98 | border-radius: 3px 3px 0 0; 99 | } 100 | 101 | /* Add these new styles for minimized state */ 102 | .fixed.bottom-4.right-4 .drag-handle { 103 | cursor: grab; 104 | } 105 | 106 | .fixed.bottom-4.right-4 .drag-handle:hover { 107 | cursor: grab; 108 | box-shadow: 0 0 0 2px rgba(100, 149, 237, 0.5); 109 | } 110 | 111 | .fixed.bottom-4.right-4 .drag-handle:active { 112 | cursor: grabbing; 113 | } 114 | 115 | /* Fix for popup.tsx container */ 116 | .w-96.bg-slate-800 { 117 | background-color: rgba(30, 41, 59, 0.5) !important; /* Much more transparent */ 118 | backdrop-filter: blur(5px); 119 | width: 400px !important; 120 | } 121 | 122 | /* Add transparent background to any containers */ 123 | .min-w-96 { 124 | background-color: transparent !important; 125 | } 126 | 127 | /* Force transparent backgrounds in light mode */ 128 | @media (prefers-color-scheme: light) { 129 | /* Target the container and all its children */ 130 | #browser-automation-extension, 131 | #browser-automation-extension * { 132 | background-color: transparent !important; 133 | } 134 | 135 | /* Fix common background classes */ 136 | .bg-white, 137 | .bg-slate-800, 138 | .bg-slate-700, 139 | .bg-gray-50, 140 | .bg-gray-100, 141 | .bg-gray-200 { 142 | background-color: transparent !important; 143 | } 144 | 145 | /* Handle glass backgrounds specially in light mode */ 146 | .bg-slate-800\/90, 147 | .bg-slate-700\/90 { 148 | background-color: rgba(51, 65, 85, 0.75) !important; /* Darker in light mode */ 149 | backdrop-filter: blur(10px); 150 | } 151 | } 152 | 153 | /* Improve container visibility with better contrast */ 154 | .min-w-96 .h-full.w-full.flex.flex-col { 155 | background-color: rgba(30, 41, 59, 0.9) !important; 156 | backdrop-filter: blur(10px); 157 | } 158 | 159 | /* Remove any box-shadow that might show in light mode */ 160 | iframe, 161 | .popup-container, 162 | .browser-automation-container, 163 | .extension-wrapper { 164 | box-shadow: none !important; 165 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | soham.ratnaparkhi@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /apps/extension/src/highlight/index.ts: -------------------------------------------------------------------------------- 1 | import { DOMElementNode, DOMHashMap, DOMNode } from '@navigator-ai/core'; 2 | import { getElementByXPathIncludingIframes } from '../utils'; 3 | 4 | const colors = [ 5 | "rgba(66, 133, 244, 0.7)", 6 | "rgba(234, 67, 53, 0.7)", 7 | "rgba(52, 168, 83, 0.7)", 8 | "rgba(251, 188, 5, 0.7)", 9 | "rgba(149, 117, 205, 0.7)", 10 | "rgba(59, 178, 208, 0.7)", 11 | "rgba(240, 98, 146, 0.7)", 12 | "rgba(255, 145, 0, 0.7)", 13 | ]; 14 | 15 | export function highlightInteractiveElements(domStructure: DOMHashMap): void { 16 | clearAllHighlights(); 17 | 18 | const interactiveElements = Object.values(domStructure).filter((node) => { 19 | if (!node.isVisible) { 20 | return false; 21 | } 22 | if (!('type' in node)) { 23 | const element = node as DOMElementNode; 24 | return element.isInteractive; 25 | } 26 | return false; 27 | }); 28 | 29 | interactiveElements.forEach((element: DOMNode, index: number) => { 30 | const xpath = (element as DOMElementNode).xpath; 31 | try { 32 | let highlightedElement = getElementByXPathIncludingIframes(xpath); 33 | 34 | if (!highlightedElement) { 35 | highlightedElement = document.evaluate( 36 | xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null 37 | ).singleNodeValue as HTMLElement; 38 | } 39 | 40 | if (highlightedElement && highlightedElement instanceof HTMLElement) { 41 | let parentDocument = document; 42 | let isInIframe = false; 43 | 44 | try { 45 | const iframes = document.querySelectorAll('iframe'); 46 | for (let i = 0; i < iframes.length; i++) { 47 | const iframe = iframes[i]; 48 | if (iframe.contentDocument && iframe.contentDocument.contains(highlightedElement)) { 49 | parentDocument = iframe.contentDocument; 50 | isInIframe = true; 51 | break; 52 | } 53 | } 54 | } catch (frameError) { 55 | console.error('Error finding parent frame:', frameError); 56 | } 57 | 58 | if (isInIframe) { 59 | try { 60 | let styleEl = parentDocument.getElementById('navigator-ai-highlight-style'); 61 | if (!styleEl) { 62 | styleEl = parentDocument.createElement('style'); 63 | styleEl.id = 'navigator-ai-highlight-style'; 64 | parentDocument.head.appendChild(styleEl); 65 | } 66 | 67 | const color = colors[index % colors.length]; 68 | styleEl.textContent += ` 69 | .navigator-ai-highlight-${index} { 70 | outline: 2px solid ${color} !important; 71 | outline-offset: 2px !important; 72 | } 73 | `; 74 | 75 | highlightedElement.classList.add(`navigator-ai-highlight-${index}`); 76 | highlightedElement.classList.add('navigator-ai-highlight'); 77 | } catch (styleError) { 78 | console.error('Error applying iframe styles:', styleError); 79 | } 80 | } else { 81 | highlightedElement.style.outline = `2px solid ${colors[index % colors.length]}`; 82 | highlightedElement.style.outlineOffset = '2px'; 83 | highlightedElement.classList.add('navigator-ai-highlight'); 84 | } 85 | } 86 | } catch (error) { 87 | console.error('Error highlighting element:', error); 88 | } 89 | }); 90 | } 91 | 92 | export function clearAllHighlights(): void { 93 | const highlightedElements = document.querySelectorAll('.navigator-ai-highlight'); 94 | highlightedElements.forEach((el) => { 95 | if (el instanceof HTMLElement) { 96 | el.style.outline = ''; 97 | el.style.outlineOffset = ''; 98 | el.className = el.className 99 | .split(' ') 100 | .filter(c => !c.startsWith('navigator-ai-highlight')) 101 | .join(' '); 102 | } 103 | }); 104 | 105 | try { 106 | const iframes = document.querySelectorAll('iframe'); 107 | for (let i = 0; i < iframes.length; i++) { 108 | const iframe = iframes[i]; 109 | 110 | if (!iframe.contentDocument || iframe.src.startsWith('chrome-extension://')) { 111 | continue; 112 | } 113 | 114 | try { 115 | const iframeHighlights = iframe.contentDocument.querySelectorAll('.navigator-ai-highlight'); 116 | iframeHighlights.forEach((el) => { 117 | if (el instanceof HTMLElement) { 118 | el.style.outline = ''; 119 | el.style.outlineOffset = ''; 120 | el.className = el.className 121 | .split(' ') 122 | .filter(c => !c.startsWith('navigator-ai-highlight')) 123 | .join(' '); 124 | } 125 | }); 126 | 127 | const styleEl = iframe.contentDocument.getElementById('navigator-ai-highlight-style'); 128 | if (styleEl) { 129 | styleEl.parentNode?.removeChild(styleEl); 130 | } 131 | } catch (iframeError) { 132 | console.error(`Error clearing highlights in iframe ${i}:`, iframeError); 133 | } 134 | } 135 | } catch (error) { 136 | console.error('Error clearing highlights in iframes:', error); 137 | } 138 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Navigator AI 2 | 3 | Thank you for your interest in contributing to Navigator AI! This document provides guidelines and instructions for contributing to this project. 4 | 5 | ## Table of Contents 6 | 7 | - [Code of Conduct](#code-of-conduct) 8 | - [Getting Started](#getting-started) 9 | - [Development Workflow](#development-workflow) 10 | - [Pull Request Process](#pull-request-process) 11 | - [Coding Standards](#coding-standards) 12 | - [Testing](#testing) 13 | - [Documentation](#documentation) 14 | - [Communication](#communication) 15 | 16 | ## Code of Conduct 17 | 18 | We expect all contributors to follow our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before participating. 19 | 20 | ## Getting Started 21 | 22 | ### Prerequisites 23 | 24 | Make sure you have the following installed: 25 | 26 | - Node.js (v16+) - [Install Guide](https://nodejs.org/en/download/) 27 | - pnpm - [Install Guide](https://pnpm.io/installation) (`npm install -g pnpm`) 28 | - Python 3.9+ - [Install Guide](https://www.python.org/downloads/) 29 | - Poetry - [Install Guide](https://python-poetry.org/docs/#installation) 30 | - Docker and Docker Compose - [Install Guide](https://docs.docker.com/get-docker/) 31 | 32 | ### Setup 33 | 34 | 1. Fork the repository 35 | 2. Clone your fork: 36 | ```bash 37 | git clone https://github.com/your-username/navigator-ai.git 38 | cd navigator-ai 39 | ``` 40 | 3. Add the original repository as upstream: 41 | ```bash 42 | git remote add upstream https://github.com/original-owner/navigator-ai.git 43 | ``` 44 | 4. Install dependencies : 45 | ```bash 46 | cd apps/server 47 | # Install Python dependencies 48 | poetry install 49 | 50 | cd apps/extension 51 | # Install Node.js dependencies 52 | pnpm install 53 | ``` 54 | 5. Start Redis: 55 | ```bash 56 | cd apps/server 57 | docker compose up -d 58 | cd ../.. 59 | ``` 60 | 7. Run the development server: 61 | ```bash 62 | pnpm run dev:server 63 | ``` 64 | 65 | ## Development Workflow 66 | 67 | ### Project Structure 68 | 69 | Navigator AI is organized as a monorepo using Turborepo. Here's an overview of the directory structure: 70 | 71 | ``` 72 | navigator-ai/ 73 | ├── apps/ 74 | │ ├── extension/ # Chrome extension 75 | │ ├── server/ # Backend API server 76 | │ └── web/ # Web application 77 | ├── packages/ 78 | │ ├── ui/ # Shared UI components 79 | │ ├── core/ # Core functionality 80 | │ └── utils/ # Shared utilities 81 | └── scripts/ # Development and build scripts 82 | ``` 83 | 84 | ### Branch Naming Convention 85 | 86 | - `feature/your-feature-name` - For new features 87 | - `fix/issue-description` - For bug fixes 88 | - `docs/what-you-documented` - For documentation changes 89 | - `refactor/what-you-refactored` - For code refactoring 90 | 91 | ### Development Process 92 | 93 | 1. Sync with the upstream repository: 94 | ```bash 95 | git checkout main 96 | git pull upstream main 97 | ``` 98 | 99 | 2. Create a new branch for your work: 100 | ```bash 101 | git checkout -b feature/your-feature-name 102 | ``` 103 | 104 | 3. Make your changes, commit them, and push to your fork: 105 | ```bash 106 | git add . 107 | git commit -m "feat: add your feature description" 108 | git push origin feature/your-feature-name 109 | ``` 110 | 111 | 4. Create a Pull Request from your branch to the main repository. 112 | 113 | ## Pull Request Process 114 | 115 | 1. Ensure all tests pass and your code meets our coding standards. 116 | 2. Update documentation if necessary. 117 | 3. Fill out the pull request template completely. 118 | 4. Request a review from maintainers. 119 | 5. Address any feedback provided during the review. 120 | 121 | PRs need at least one approval from a maintainer before they can be merged. 122 | 123 | ## Coding Standards 124 | 125 | ### TypeScript/JavaScript 126 | 127 | - We follow the [ESLint](https://eslint.org/) configuration in the repository. 128 | - Use TypeScript for all new code. 129 | - Format your code using [Prettier](https://prettier.io/). 130 | 131 | ### Python 132 | 133 | - Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. 134 | - Use type hints for all function parameters and return values. 135 | - Format your code using [Black](https://github.com/psf/black). 136 | - Sort imports using [isort](https://pycqa.github.io/isort/). 137 | 138 | ### Commit Messages 139 | 140 | We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: 141 | 142 | ``` 143 | [optional scope]: 144 | 145 | [optional body] 146 | 147 | [optional footer(s)] 148 | ``` 149 | 150 | Types include: 151 | - `feat`: A new feature 152 | - `fix`: A bug fix 153 | - `docs`: Documentation changes 154 | - `style`: Changes that do not affect the meaning of the code 155 | - `refactor`: Code changes that neither fix a bug nor add a feature 156 | - `perf`: Performance improvements 157 | - `test`: Adding or fixing tests 158 | - `chore`: Changes to the build process or auxiliary tools 159 | 160 | ## Testing 161 | 162 | ### Frontend Tests 163 | 164 | - Write tests for all new components using [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/). 165 | - Run tests with: 166 | ```bash 167 | pnpm test 168 | ``` 169 | 170 | ### Backend Tests 171 | 172 | - Write tests for all new endpoints using [pytest](https://docs.pytest.org/). 173 | - Run tests with: 174 | ```bash 175 | cd apps/server 176 | poetry run pytest 177 | ``` 178 | 179 | ## Documentation 180 | 181 | - Document all public API endpoints. 182 | - Add JSDoc comments to all TypeScript/JavaScript functions. 183 | - Update the README.md if you add or change functionality. 184 | - For major changes, update or add to the project documentation. 185 | 186 | ## Communication 187 | 188 | - For bug reports and feature requests, please open an issue. 189 | 190 | ## License 191 | 192 | By contributing to Navigator AI, you agree that your contributions will be licensed under the project's [MIT License](LICENSE). 193 | 194 | --- 195 | 196 | Thank you for contributing to Navigator AI! Your efforts help make this project better for everyone. 197 | -------------------------------------------------------------------------------- /apps/extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Navigator AI 8 | 9 | 81 | 183 | 184 | 185 | 186 |
187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /apps/server/app/api/utils/prompts.py: -------------------------------------------------------------------------------- 1 | from app.api.utils.dom_parser.optimizer3 import generate_enhanced_highlight_dom 2 | 3 | def build_system_prompt(): 4 | prompt = """You are an AI browser named Navigator AI. You are an automation assistant designed to help users accomplish tasks on websites. Your goal is to accurately interact with web elements to complete the user's ultimate task. 5 | 6 | # INPUT INFORMATION 7 | You will receive: 8 | 1. The user's task description 9 | 2. The current URL of the web page 10 | 3. Interactive elements on the page with unique element IDs (E1, E2, etc.) 11 | 4. History of previous actions (if any) 12 | 5. Results of the last action (if any) 13 | 6. Open tabs with their IDs 14 | 15 | Only use the `switchToTab` action to switch to a different tab. Always check if we are already on that tab using current tab details. 16 | 17 | Use the `data_useful_for_next_step` field to provide any additional data that might be useful for future steps. This should ideally be textual data. Usually you can store any useful text on the page in this field as you will not be able to access it again for next step. 18 | 19 | # ELEMENT INTERACTION RULES 20 | - Interactive elements are marked with IDs like [E1], [E2], etc. 21 | - ONLY elements with these IDs can be interacted with 22 | - The element description includes: tag type, key attributes, and visible text 23 | - Example: [E5] 24 | 25 | # ONLY RETURN is_done=true WHEN THE END GOAL IS COMPLETED and not for intermediate steps/goals. 26 | 27 | # If you are provided the open tabs and a target url tab is already open then use the `switchToTab` action to switch to the target tab. 28 | 29 | # ACTIONS cannot be empty if is_done is false. You have to give actions in such cases. 30 | 31 | # RESPONSE FORMAT 32 | You MUST ALWAYS respond with valid JSON in this exact format: 33 | ```json 34 | { 35 | "current_state": { 36 | "page_summary": "Detailed summary of the current page focused on information relevant to the task. Be specific and factual.", 37 | "evaluation_previous_goal": "Success|Failed|Unknown - Analyze if previous actions succeeded based on the current page state. Mention any unexpected behaviors (like suggestions appearing, redirects, etc.).", 38 | "next_goal": "Specific immediate goal for the next action(s)", 39 | "data_useful_for_next_step": "Any additional data that might be useful for future steps. This should ideally be textual data" 40 | }, 41 | "actions": [ 42 | { 43 | "type": "ACTION_TYPE (click|input|scroll|url|switchToTab)", 44 | "element_id": "E5", // Use EXACT element ID as shown in the page description 45 | "text": "TEXT_TO_INPUT", // Only for 'input' actions 46 | "amount": NUMBER, // Only for 'scroll' actions (pixels) 47 | "url": "URL" // Only for 'url' actions 48 | "tab_id": "TAB_ID" // Only for 'switchToTab' actions 49 | } 50 | ], 51 | "is_done": true/false // Only true when the entire task is complete 52 | }""" 53 | return prompt 54 | 55 | def build_user_message(dom_state, task=None, history=None, result=None): 56 | """ 57 | Build an optimized user message for LLM with highlight-style DOM representation. 58 | 59 | Args: 60 | dom_state: The DOM state object 61 | task: The user's task (optional) 62 | history: Previous action history (optional) 63 | result: Result of the last action (optional) 64 | 65 | Returns: 66 | Tuple of (content, xpath_map, selector_map) 67 | """ 68 | key_attributes = ['id', 'name', 'type', 'value', 'placeholder', 'href'] 69 | 70 | dom_content, xpath_map, selector_map = generate_enhanced_highlight_dom( 71 | dom_state, include_attributes=key_attributes) 72 | 73 | content = "" 74 | 75 | if task: 76 | content += f"MAIN TASK (END GOAL): {task}\n\n" 77 | 78 | content += f"CURRENT URL: {dom_state.url}\n\n" 79 | 80 | content += "INTERACTIVE ELEMENTS:\n" 81 | content += "(Only elements with [E#] IDs can be interacted with)\n" 82 | content += f"{dom_content}\n" 83 | 84 | if history and len(history) > 0: 85 | content += "\nACTION HISTORY:\n" 86 | 87 | for i, step in enumerate(history): 88 | if not isinstance(step, dict): 89 | print(f"Warning: Invalid history step format: {type(step)}") 90 | continue 91 | 92 | content += f"Step {i+1}: URL: {step.get('url', 'unknown')}\n" 93 | actions = step.get('actions', []) 94 | 95 | if not actions: 96 | print(f"Warning: No actions in history step {i+1}") 97 | continue 98 | 99 | if not isinstance(actions, list): 100 | print(f"Warning: Actions not a list in step {i+1}: {type(actions)}") 101 | if isinstance(actions, dict): 102 | actions = [actions] 103 | else: 104 | continue 105 | 106 | for action in actions: 107 | if not isinstance(action, dict): 108 | print(f"Warning: Invalid action format in step {i+1}: {type(action)}") 109 | continue 110 | 111 | action_str = f" - {action.get('type', '').upper()}" 112 | 113 | if 'element_id' in action: 114 | action_str += f" element [{action['element_id']}]" 115 | elif 'xpath_ref' in action and 'selector' in action: 116 | action_str += f" element with selector: {action['selector']}" 117 | 118 | if 'text' in action and action['text']: 119 | action_str += f" with text: '{action['text']}'" 120 | if 'url' in action and action['url']: 121 | action_str += f" to URL: {action['url']}" 122 | if 'amount' in action: 123 | action_str += f" by {action['amount']} pixels" 124 | if 'tab_id' in action: 125 | action_str += f" to tab: {action['tab_id']}" 126 | 127 | content += action_str + "\n" 128 | content += "\n" 129 | 130 | if result: 131 | content += f"RESULT OF LAST ACTION:\n{result}\n" 132 | 133 | content += "\nREMINDERS:\n" 134 | content += "- Use EXACT element IDs (E1, E2, etc.) as shown above\n" 135 | content += "- For input actions, include both element_id and text\n" 136 | content += "- Only set is_done:true when the entire task is complete\n" 137 | 138 | return content, xpath_map, selector_map -------------------------------------------------------------------------------- /apps/extension/src/messaging/index.ts: -------------------------------------------------------------------------------- 1 | // messaging/index.ts 2 | // Functions for handling messaging between content script and background script 3 | 4 | import { Message } from '../types'; 5 | import { createSidebarContainer, toggleSidebar, updateSidebarState, isChromeSidePanelSupported } from '../sidebar'; 6 | import { singleDOMProcessIteration } from '../dom/processor'; 7 | import { clearAllHighlights } from '../highlight'; 8 | import { handleAutomationActions } from '../automation'; 9 | 10 | /** 11 | * Initialize message listener for content script 12 | */ 13 | export function initializeMessageListener(): void { 14 | // Check if Chrome's sidePanel API is available 15 | const isChromeWithSidePanel = isChromeSidePanelSupported(); 16 | 17 | chrome.runtime.onMessage.addListener((message: Message, sender, sendResponse) => { 18 | console.log('Content script received message:', message.type); 19 | 20 | if (message.type === 'ping') { 21 | sendResponse({ success: true }); 22 | return true; 23 | } 24 | 25 | if (message.type === 'singleDOMProcess' && message.task_id) { 26 | // Only create the custom container for non-Chrome browsers 27 | if (!isChromeWithSidePanel) { 28 | createSidebarContainer(); 29 | } 30 | 31 | singleDOMProcessIteration(message.task_id) 32 | .then((result) => { 33 | console.log('Single DOM process complete:', result); 34 | sendResponse(result); 35 | }) 36 | .catch(error => { 37 | console.error('Error in singleDOMProcess:', error); 38 | sendResponse({ 39 | success: false, 40 | error: error instanceof Error ? error.message : String(error) 41 | }); 42 | }); 43 | 44 | return true; 45 | } 46 | 47 | if (message.type === 'executeActions' && Array.isArray(message.actions)) { 48 | handleAutomationActions(message.actions) 49 | .then(results => { 50 | sendResponse({ success: true, results }); 51 | }) 52 | .catch(error => { 53 | console.error('Error executing actions:', error); 54 | sendResponse({ success: false, error: error.message }); 55 | }); 56 | return true; // Keep channel open for async response 57 | } 58 | 59 | // if (message.type === 'processDOM' && message.task_id) { 60 | // // Only create the custom container for non-Chrome browsers 61 | // if (!isChromeWithSidePanel) { 62 | // createSidebarContainer(); 63 | // } 64 | 65 | // processDOM(message.task_id) 66 | // .then((domData) => { 67 | // sendResponse({ success: true, domData }); 68 | // }) 69 | // .catch(error => { 70 | // console.error('Error in processDOM:', error); 71 | // sendResponse({ 72 | // success: false, 73 | // error: error instanceof Error ? error.message : String(error) 74 | // }); 75 | // }); 76 | // return true; 77 | // } 78 | // else if (message.type === 'startSequentialProcessing' && message.task_id) { 79 | // // Only create the custom container for non-Chrome browsers 80 | // if (!isChromeWithSidePanel) { 81 | // createSidebarContainer(); 82 | // } 83 | 84 | // sequentialDOMProcessing(message.task_id, message.maxIterations || 10) 85 | // .then((result) => { 86 | // sendResponse({ success: true, result }); 87 | // }) 88 | // .catch(error => { 89 | // console.error('Error in sequential processing:', error); 90 | // sendResponse({ 91 | // success: false, 92 | // error: error instanceof Error ? error.message : String(error) 93 | // }); 94 | // }); 95 | // return true; // Keep channel open for async response 96 | // } 97 | else if (message.type === 'toggleUI' || message.type === 'toggleSidebar') { 98 | // Use Chrome's sidePanel API if available, otherwise fall back to custom sidebar 99 | if (isChromeWithSidePanel) { 100 | // Forward the request to the background script which will handle the Chrome sidePanel API 101 | chrome.runtime.sendMessage({ type: 'toggleSidePanel' }, (response) => { 102 | sendResponse(response); 103 | }); 104 | } else { 105 | const isVisible = toggleSidebar(); 106 | sendResponse({ success: true, isVisible }); 107 | } 108 | return true; 109 | } 110 | else if (message.type === 'updateSidebarState') { 111 | // Use Chrome's sidePanel API if available, otherwise fall back to custom sidebar 112 | if (isChromeWithSidePanel) { 113 | // Forward to background script 114 | if (message.isOpen) { 115 | chrome.runtime.sendMessage({ type: 'openSidePanel' }, (response) => { 116 | sendResponse(response); 117 | }); 118 | } else { 119 | chrome.runtime.sendMessage({ type: 'closeSidePanel' }, (response) => { 120 | sendResponse(response); 121 | }); 122 | } 123 | } else { 124 | updateSidebarState(message.isOpen || false); 125 | sendResponse({ success: true }); 126 | } 127 | return true; 128 | } 129 | else if (message.type === 'resetWorkflow') { 130 | // Clear all highlights and reset UI state 131 | clearAllHighlights(); 132 | console.log('Workflow reset received, clearing DOM highlights'); 133 | sendResponse({ success: true }); 134 | return true; 135 | } 136 | else if (message.type === 'stopAutomation') { 137 | clearAllHighlights(); 138 | // Any other cleanup needed in content script 139 | sendResponse({ success: true }); 140 | return true; 141 | } 142 | 143 | // If we reach here, it was an unknown message type 144 | return false; 145 | }); 146 | } -------------------------------------------------------------------------------- /packages/core/src/utils/dom-actions.ts: -------------------------------------------------------------------------------- 1 | export class DomActions { 2 | private debugMode = false; 3 | 4 | constructor(options?: { debug?: boolean }) { 5 | this.debugMode = options?.debug ?? false; 6 | } 7 | 8 | public setDebugMode(enable: boolean): void { 9 | this.debugMode = enable; 10 | } 11 | 12 | public async simulateHumanClick(element: Element): Promise { 13 | const rect = element.getBoundingClientRect(); 14 | const centerX = Math.floor(rect.left + rect.width / 2); 15 | const centerY = Math.floor(rect.top + rect.height / 2); 16 | 17 | element.dispatchEvent(new MouseEvent('mouseover', { 18 | view: window, 19 | bubbles: true, 20 | cancelable: true, 21 | clientX: centerX, 22 | clientY: centerY 23 | })); 24 | 25 | element.dispatchEvent(new MouseEvent('mousedown', { 26 | view: window, 27 | bubbles: true, 28 | cancelable: true, 29 | clientX: centerX, 30 | clientY: centerY 31 | })); 32 | 33 | await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 100)); 34 | 35 | element.dispatchEvent(new MouseEvent('mouseup', { 36 | view: window, 37 | bubbles: true, 38 | cancelable: true, 39 | clientX: centerX, 40 | clientY: centerY 41 | })); 42 | 43 | element.dispatchEvent(new MouseEvent('click', { 44 | view: window, 45 | bubbles: true, 46 | cancelable: true, 47 | clientX: centerX, 48 | clientY: centerY 49 | })); 50 | 51 | await new Promise(resolve => setTimeout(resolve, 300)); 52 | } 53 | 54 | public async simulateHumanInput(element: HTMLInputElement, text: string, shouldPressEnter = true): Promise { 55 | element.focus(); 56 | element.value = ''; 57 | 58 | for (let i = 0; i < text.length; i++) { 59 | const char = text.charAt(i); 60 | 61 | element.value += char; 62 | 63 | element.dispatchEvent(new Event('input', { bubbles: true })); 64 | 65 | const keyCode = char.charCodeAt(0); 66 | 67 | element.dispatchEvent(new KeyboardEvent('keydown', { 68 | key: char, 69 | code: `Key${char.toUpperCase()}`, 70 | keyCode: keyCode, 71 | which: keyCode, 72 | bubbles: true, 73 | cancelable: true 74 | })); 75 | 76 | element.dispatchEvent(new KeyboardEvent('keypress', { 77 | key: char, 78 | code: `Key${char.toUpperCase()}`, 79 | keyCode: keyCode, 80 | which: keyCode, 81 | bubbles: true, 82 | cancelable: true 83 | })); 84 | 85 | element.dispatchEvent(new KeyboardEvent('keyup', { 86 | key: char, 87 | code: `Key${char.toUpperCase()}`, 88 | keyCode: keyCode, 89 | which: keyCode, 90 | bubbles: true, 91 | cancelable: true 92 | })); 93 | 94 | const typingDelay = Math.floor(Math.random() * 70) + 30; 95 | await new Promise(resolve => setTimeout(resolve, typingDelay)); 96 | 97 | if (Math.random() < 0.125 && i < text.length - 1) { 98 | await new Promise(resolve => setTimeout(resolve, 150 + Math.random() * 200)); 99 | } 100 | } 101 | 102 | element.dispatchEvent(new Event('change', { bubbles: true })); 103 | 104 | if (shouldPressEnter) { 105 | await this.simulateEnterKey(element); 106 | } 107 | 108 | await new Promise(resolve => setTimeout(resolve, 300)); 109 | } 110 | 111 | public async simulateEnterKey(element: Element): Promise { 112 | element.dispatchEvent(new KeyboardEvent('keydown', { 113 | key: 'Enter', 114 | code: 'Enter', 115 | keyCode: 13, 116 | which: 13, 117 | bubbles: true, 118 | cancelable: true 119 | })); 120 | 121 | element.dispatchEvent(new KeyboardEvent('keypress', { 122 | key: 'Enter', 123 | code: 'Enter', 124 | keyCode: 13, 125 | which: 13, 126 | bubbles: true, 127 | cancelable: true 128 | })); 129 | 130 | let formToSubmit: HTMLFormElement | null = null; 131 | if (element instanceof HTMLInputElement && element.form) { 132 | formToSubmit = element.form; 133 | } else if (element.closest('form')) { 134 | formToSubmit = element.closest('form') as HTMLFormElement; 135 | } 136 | 137 | if (formToSubmit) { 138 | formToSubmit.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); 139 | 140 | const submitButton = formToSubmit.querySelector('input[type="submit"], button[type="submit"]'); 141 | if (submitButton) { 142 | submitButton.dispatchEvent(new MouseEvent('click', { 143 | bubbles: true, 144 | cancelable: true, 145 | view: window 146 | })); 147 | } 148 | } 149 | 150 | element.dispatchEvent(new KeyboardEvent('keyup', { 151 | key: 'Enter', 152 | code: 'Enter', 153 | keyCode: 13, 154 | which: 13, 155 | bubbles: true, 156 | cancelable: true 157 | })); 158 | 159 | await new Promise(resolve => setTimeout(resolve, 200)); 160 | } 161 | 162 | public async copyElementToClipboard(element: Element): Promise { 163 | try { 164 | // copy element html to clipboard 165 | const html = element.outerHTML; 166 | await navigator.clipboard.writeText(html); 167 | } catch (error) { 168 | console.error('Failed to copy to clipboard:', error); 169 | } 170 | } 171 | 172 | public async switchToTab(tabId: number): Promise { 173 | try { 174 | // Send a message to the background script to switch tabs 175 | // @ts-ignore 176 | chrome.runtime.sendMessage({ 177 | type: 'switchTab', 178 | tabId: tabId 179 | }, (response) => { 180 | if (response && response.success) { 181 | console.log(`Successfully switched to tab ${tabId}`); 182 | } else { 183 | console.error('Failed to switch tab:', response?.error || 'Unknown error'); 184 | } 185 | }); 186 | } catch (error) { 187 | console.error('Failed to switch to tab:', error); 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /apps/server/app/api/services/storage_service.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from datetime import datetime 4 | 5 | import redis 6 | 7 | from app.config import settings 8 | from app.models.dom import DOMUpdate 9 | import logging 10 | 11 | logger = logging.getLogger("redis_logger") 12 | 13 | class StorageService: 14 | """Service for storing DOM snapshots and metadata""" 15 | 16 | _redis_client = None 17 | 18 | @classmethod 19 | def get_redis(cls): 20 | """Get or create Redis connection""" 21 | if cls._redis_client is None: 22 | cls._redis_client = redis.Redis( 23 | host=settings.REDIS_HOST, 24 | port=settings.REDIS_PORT, 25 | db=settings.REDIS_DB, 26 | password=settings.REDIS_PASSWORD or None, 27 | decode_responses=True 28 | ) 29 | return cls._redis_client 30 | 31 | @staticmethod 32 | def ensure_snapshots_directory(): 33 | """Ensure the snapshots directory exists""" 34 | os.makedirs(settings.SNAPSHOTS_DIR, exist_ok=True) 35 | 36 | @classmethod 37 | def save_dom_snapshot(cls, update: DOMUpdate) -> dict: 38 | """Save DOM snapshot and metadata to disk""" 39 | cls.ensure_snapshots_directory() 40 | 41 | timestamp = datetime.fromisoformat( 42 | update.dom_data.timestamp.replace('Z', '+00:00')) 43 | 44 | base_filename = f"task_{update.task_id}_{timestamp.strftime('%Y%m%d_%H%M%S')}" 45 | 46 | html_filename = f"{settings.SNAPSHOTS_DIR}/{base_filename}.html" 47 | 48 | with open(html_filename, "w", encoding="utf-8") as f: 49 | f.write(update.dom_data.html) 50 | 51 | metadata = { 52 | "task_id": update.task_id, 53 | "url": update.dom_data.url, 54 | "title": update.dom_data.title, 55 | "timestamp": update.dom_data.timestamp, 56 | "results": update.result, 57 | "iterations": update.iterations 58 | } 59 | 60 | metadata_filename = f"{settings.SNAPSHOTS_DIR}/{base_filename}_metadata.json" 61 | 62 | with open(metadata_filename, "w", encoding="utf-8") as f: 63 | json.dump(metadata, f, indent=2) 64 | 65 | if update.structure: 66 | structure_filename = f"{settings.SNAPSHOTS_DIR}/{base_filename}_structure.json" 67 | with open(structure_filename, "w", encoding="utf-8") as f: 68 | json.dump(update.structure, f, indent=2) 69 | 70 | return { 71 | "html": html_filename, 72 | "metadata": metadata_filename, 73 | "structure": f"{settings.SNAPSHOTS_DIR}/{base_filename}_structure.json" if update.structure else None 74 | } 75 | 76 | @classmethod 77 | def normalize_task_id(cls, task_id: str) -> str: 78 | if task_id.startswith(settings.REDIS_TASK_PREFIX): 79 | return task_id 80 | return f"{settings.REDIS_TASK_PREFIX}{task_id}" 81 | 82 | @classmethod 83 | def store_task(cls, task_id: str, task_text: str) -> bool: 84 | normalized_id = cls.normalize_task_id(task_id) 85 | redis_key = f"{settings.REDIS_PREFIX}{normalized_id}" 86 | logger.info(f"Storing task with Redis key: {redis_key}") 87 | return cls.get_redis().set( 88 | redis_key, 89 | task_text, 90 | ex=settings.REDIS_TASK_TTL 91 | ) 92 | 93 | @classmethod 94 | def get_task(cls, task_id: str) -> str: 95 | normalized_id = cls.normalize_task_id(task_id) 96 | redis_key = f"{settings.REDIS_PREFIX}{normalized_id}" 97 | logger.info(f"Getting task with Redis key: {redis_key}") 98 | return cls.get_redis().get(redis_key) 99 | 100 | @classmethod 101 | def append_task_history(cls, task_id: str, action_data: dict, prev_step_ans: str) -> bool: 102 | """Append action history for a task""" 103 | normalized_id = cls.normalize_task_id(task_id) 104 | redis_key = f"{settings.REDIS_PREFIX}{settings.REDIS_TASK_HISTORY_PREFIX}{normalized_id}" 105 | redis_prev_step_ans_key = f"{settings.REDIS_PREFIX}{settings.REDIS_PREV_STEP_ANS_PREFIX}{normalized_id}" 106 | redis_client = cls.get_redis() 107 | 108 | logger.info(f"Appending task history with Redis key: {redis_key}") 109 | 110 | try: 111 | redis_client.rpush(redis_key, json.dumps(action_data)) 112 | redis_client.set(redis_prev_step_ans_key, prev_step_ans) 113 | 114 | history_list = redis_client.lrange(redis_key, 0, -1) 115 | logger.info(f"Added history item to Redis. Current history length: {len(history_list)}") 116 | 117 | redis_client.expire(redis_key, settings.REDIS_TASK_TTL) 118 | redis_client.expire(redis_prev_step_ans_key, settings.REDIS_TASK_TTL) 119 | return True 120 | except Exception as e: 121 | logger.error(f"Error appending task history: {str(e)}") 122 | return False 123 | 124 | 125 | @classmethod 126 | def get_task_history(cls, task_id: str) -> list: 127 | """Get action history for a task""" 128 | normalized_id = cls.normalize_task_id(task_id) 129 | redis_key = f"{settings.REDIS_PREFIX}{settings.REDIS_TASK_HISTORY_PREFIX}{normalized_id}" 130 | 131 | logger.info(f"Getting task history with Redis key: {redis_key}") 132 | 133 | try: 134 | redis_client = cls.get_redis() 135 | history_list = redis_client.lrange(redis_key, 0, -1) 136 | 137 | logger.info(f"Found {len(history_list)} history entries for task {task_id}") 138 | 139 | history = [] 140 | for item in history_list: 141 | try: 142 | history.append(json.loads(item)) 143 | except json.JSONDecodeError: 144 | logger.error(f"Error decoding history item for task {task_id}") 145 | continue 146 | 147 | return history 148 | except Exception as e: 149 | logger.error(f"Error retrieving task history: {str(e)}") 150 | return [] 151 | 152 | @classmethod 153 | def get_prev_step_ans(cls, task_id: str) -> str: 154 | """Get previous step answer for a task""" 155 | normalized_id = cls.normalize_task_id(task_id) 156 | redis_key = f"{settings.REDIS_PREFIX}{settings.REDIS_PREV_STEP_ANS_PREFIX}{normalized_id}" 157 | 158 | logger.info(f"Getting previous step answer with Redis key: {redis_key}") 159 | 160 | try: 161 | redis_client = cls.get_redis() 162 | return redis_client.get(redis_key) 163 | except Exception as e: 164 | logger.error(f"Error retrieving previous step answer: {str(e)}") 165 | return None 166 | -------------------------------------------------------------------------------- /apps/extension/src/dom/iframe.ts: -------------------------------------------------------------------------------- 1 | export async function captureIframeContents(originalHtml: string): Promise { 2 | try { 3 | console.log('Capturing iframe contents...'); 4 | 5 | const iframes = document.querySelectorAll('iframe'); 6 | 7 | if (iframes.length === 0) { 8 | console.log('No iframes found on the page'); 9 | return originalHtml; 10 | } 11 | 12 | console.log(`Found ${iframes.length} iframes on the page`); 13 | let processedHtml = originalHtml; 14 | 15 | const iframeContents: string[] = []; 16 | 17 | for (let i = 0; i < iframes.length; i++) { 18 | try { 19 | const iframe = iframes[i]; 20 | 21 | if (!iframe.contentDocument || !iframe.contentWindow || 22 | iframe.src.startsWith('chrome-extension://')) { 23 | console.log(`Skipping iframe ${i} - cannot access content or is extension iframe`); 24 | continue; 25 | } 26 | 27 | let iframeContent: string; 28 | try { 29 | const iframeDoc = iframe.contentDocument; 30 | 31 | const baseContent = iframeDoc.documentElement.outerHTML; 32 | 33 | const nestedIframes = iframeDoc.querySelectorAll('iframe'); 34 | if (nestedIframes.length > 0) { 35 | console.log(`Found ${nestedIframes.length} nested iframes in iframe ${i}`); 36 | iframeContent = await captureIframeContents(baseContent); 37 | } else { 38 | iframeContent = baseContent; 39 | } 40 | } catch (err) { 41 | console.log(`Cannot access iframe ${i} content due to cross-origin restrictions:`, err); 42 | continue; 43 | } 44 | 45 | const iframeId = `iframe-content-${i}`; 46 | 47 | const iframeAttrs: string[] = []; 48 | if (iframe.id) iframeAttrs.push(`id="${iframe.id}"`); 49 | if (iframe.className) iframeAttrs.push(`class="${iframe.className}"`); 50 | if (iframe.src) iframeAttrs.push(`src="${iframe.src}"`); 51 | if (iframe.name) iframeAttrs.push(`name="${iframe.name}"`); 52 | 53 | const iframeXPath = getXPathForElement(iframe); 54 | if (iframeXPath) iframeAttrs.push(`xpath="${iframeXPath}"`); 55 | 56 | const iframeDataTag = `\n${iframeContent}\n`; 57 | iframeContents.push(iframeDataTag); 58 | 59 | try { 60 | const domPosition = findIframePositionInHTML(processedHtml, iframe); 61 | if (domPosition > -1) { 62 | const beforeIframe = processedHtml.substring(0, domPosition); 63 | const afterIframe = processedHtml.substring(domPosition); 64 | 65 | const newAfterIframe = afterIframe.replace( 66 | /( 0) { 81 | processedHtml += `\n\n\n${iframeContents.join('\n')}\n`; 82 | console.log(`Added content from ${iframeContents.length} iframes to the DOM`); 83 | } 84 | 85 | return processedHtml; 86 | } catch (error) { 87 | console.error('Error capturing iframe contents:', error); 88 | return originalHtml; 89 | } 90 | } 91 | 92 | export function getXPathForElement(element: Element): string | null { 93 | try { 94 | if (element === document.documentElement) { 95 | return '/html'; 96 | } 97 | 98 | if (element === document.body) { 99 | return '/html/body'; 100 | } 101 | 102 | let xpath = ''; 103 | let current = element; 104 | 105 | while (current && current !== document.documentElement) { 106 | const nodeName = current.nodeName.toLowerCase(); 107 | let position = 1; 108 | let sibling = current.previousSibling; 109 | 110 | while (sibling) { 111 | if (sibling.nodeType === Node.ELEMENT_NODE && 112 | sibling.nodeName.toLowerCase() === nodeName) { 113 | position++; 114 | } 115 | sibling = sibling.previousSibling; 116 | } 117 | 118 | xpath = `/${nodeName}[${position}]${xpath}`; 119 | 120 | if (current.parentNode) { 121 | current = current.parentNode as Element; 122 | } else { 123 | break; 124 | } 125 | } 126 | 127 | return `/html${xpath}`; 128 | } catch (error) { 129 | console.error('Error generating XPath:', error); 130 | return null; 131 | } 132 | } 133 | 134 | 135 | export function findIframePositionInHTML(html: string, iframe: HTMLIFrameElement): number { 136 | try { 137 | const attributes: [string, string][] = []; 138 | 139 | if (iframe.id) attributes.push(['id', iframe.id]); 140 | if (iframe.className) attributes.push(['class', iframe.className]); 141 | if (iframe.src) attributes.push(['src', iframe.src]); 142 | if (iframe.name) attributes.push(['name', iframe.name]); 143 | 144 | for (const [attr, value] of attributes) { 145 | const searchStr = `