├── .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 |
6 |
--------------------------------------------------------------------------------
/apps/extension/public/icons/icon48.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
11 |
--------------------------------------------------------------------------------
/apps/web/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/apps/web/public/file-text.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
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 |
11 |
--------------------------------------------------------------------------------
/apps/web/public/globe.svg:
--------------------------------------------------------------------------------
1 |
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 | -
36 | Get started by editing
apps/docs/app/page.tsx
37 |
38 | - Save and see your changes instantly.
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 | -
36 | Get started by editing
apps/web/app/page.tsx
37 |
38 | - Save and see your changes instantly.
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 |
20 |
--------------------------------------------------------------------------------
/apps/docs/public/turborepo-dark.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/apps/docs/public/turborepo-light.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/apps/web/public/turborepo-light.svg:
--------------------------------------------------------------------------------
1 |
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 | /(