Compare commits

..

52 Commits

Author SHA1 Message Date
Simon 32be34bcb1 chore(version): bump version to 0.0.15 2025-12-22 17:02:14 +08:00
Simon 7063b8199d docs: update documentation to include llms package 2025-12-22 16:35:58 +08:00
Simon 64f1a8c443 chore(version): bump version to 0.0.14 2025-12-22 16:29:53 +08:00
Simon 635416f964 feat: implement @page-agent/llms 2025-12-22 16:29:19 +08:00
Simon 7c2d000e29 feat: create llms package and mv files 2025-12-22 16:12:34 +08:00
Simon b36a0c0261 chore(version): bump version to 0.0.13 2025-12-17 19:32:24 +08:00
Simon 339b066d2d fix(UMD): delay init 2025-12-17 19:31:28 +08:00
Simon c8efffa80a fix: update dotenv import and load config 2025-12-17 15:27:15 +08:00
Simon 59235263ad Merge pull request #80 from alibaba/fix/isPageDark
fix: isPageDark robust
2025-12-17 14:44:17 +08:00
Simon bc1e8fea3c fix: handle null body element 2025-12-17 14:40:11 +08:00
Simon 85a4096b2d chore(version): bump version to 0.0.12 2025-12-17 14:18:28 +08:00
Simon 0ef84bac13 chore: simplify Vite configuration 2025-12-16 20:38:15 +08:00
Simon a7834c77e1 docs: CDN and mirror 2025-12-16 20:23:17 +08:00
Simon 030e5cfc54 docs: stable CDN 2025-12-16 20:21:46 +08:00
Simon aa694e3b85 docs: stable CDN 2025-12-16 20:17:58 +08:00
Simon 2fee2d5ea3 chore: improve build scripts; change esm dir 2025-12-16 19:45:40 +08:00
Simon 3082843810 Merge pull request #77 from alibaba/dependabot/npm_and_yarn/development-dependencies-95987f47c2
chore(deps-dev): bump typescript-eslint from 8.49.0 to 8.50.0 in the development-dependencies group
2025-12-16 19:04:31 +08:00
Simon 34098e2f66 Merge pull request #76 from alibaba/dependabot/npm_and_yarn/production-dependencies-eefc12583a
chore(deps): bump zod from 4.1.13 to 4.2.0 in the production-dependencies group
2025-12-16 19:04:10 +08:00
dependabot[bot] 8461006c91 chore(deps-dev): bump typescript-eslint
Bumps the development-dependencies group with 1 update: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `typescript-eslint` from 8.49.0 to 8.50.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.50.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.50.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 19:30:44 +00:00
dependabot[bot] 2e76a35188 chore(deps): bump zod in the production-dependencies group
Bumps the production-dependencies group with 1 update: [zod](https://github.com/colinhacks/zod).


Updates `zod` from 4.1.13 to 4.2.0
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.1.13...v4.2.0)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 19:30:06 +00:00
Simon abc96b415f chore: clean up 2025-12-16 03:04:29 +08:00
Simon 77ce3b0b49 chore(version): bump version to 0.0.10 2025-12-16 02:27:53 +08:00
Simon 3742f75d14 chore: better UMD local dev serving 2025-12-16 02:22:52 +08:00
Simon c2e9e6a571 fix: monorepo build 2025-12-16 01:52:15 +08:00
Simon 54eb50f7be fix: monorepo typing 2025-12-16 01:30:40 +08:00
Simon 9351948794 docs: change banner 2025-12-15 20:52:55 +08:00
Simon 68d1a10dd0 docs: package structure changes 2025-12-15 19:08:53 +08:00
Simon b9fe8146b6 Merge pull request #75 from alibaba/dependabot/npm_and_yarn/development-dependencies-7f75046d18
chore(deps-dev): bump the development-dependencies group across 1 directory with 18 updates
2025-12-15 19:00:02 +08:00
dependabot[bot] 2a727226bc chore(deps-dev): bump the development-dependencies group across 1 directory with 18 updates
Bumps the development-dependencies group with 17 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.1.0` | `20.2.0` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.0.0` | `20.2.0` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.1` | `9.39.2` |
| [eslint](https://github.com/eslint/eslint) | `9.39.1` | `9.39.2` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.3.9` | `2.3.13` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.4.24` | `0.4.25` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.3.9` | `2.3.13` |
| [prettier](https://github.com/prettier/prettier) | `3.7.3` | `3.7.4` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.48.1` | `8.49.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.2.6` | `7.3.0` |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.55.1` | `7.55.2` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.17` | `4.1.18` |
| [i18next](https://github.com/i18next/i18next) | `25.7.1` | `25.7.3` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.0` | `19.2.3` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.0` | `19.2.3` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.3.5` | `16.5.0` |
| [wouter](https://github.com/molefrog/wouter) | `3.8.0` | `3.8.1` |



Updates `@commitlint/cli` from 20.1.0 to 20.2.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.2.0/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.0.0 to 20.2.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.2.0/@commitlint/config-conventional)

Updates `@eslint/js` from 9.39.1 to 9.39.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.2/packages/js)

Updates `eslint` from 9.39.1 to 9.39.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v9.39.2)

Updates `eslint-plugin-react-dom` from 2.3.9 to 2.3.13
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.3.13/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-refresh` from 0.4.24 to 0.4.25
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.24...v0.4.25)

Updates `eslint-plugin-react-x` from 2.3.9 to 2.3.13
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.3.13/packages/plugins/eslint-plugin-react-x)

Updates `prettier` from 3.7.3 to 3.7.4
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.7.3...3.7.4)

Updates `typescript-eslint` from 8.48.1 to 8.49.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.49.0/packages/typescript-eslint)

Updates `vite` from 7.2.6 to 7.3.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.0/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.0/packages/vite)

Updates `@microsoft/api-extractor` from 7.55.1 to 7.55.2
- [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md)
- [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.55.2/apps/api-extractor)

Updates `@tailwindcss/vite` from 4.1.17 to 4.1.18
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/@tailwindcss-vite)

Updates `i18next` from 25.7.1 to 25.7.3
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.7.1...v25.7.3)

Updates `react` from 19.2.0 to 19.2.3
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.3/packages/react)

Updates `react-dom` from 19.2.0 to 19.2.3
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.3/packages/react-dom)

Updates `react-i18next` from 16.3.5 to 16.5.0
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.3.5...v16.5.0)

Updates `tailwindcss` from 4.1.17 to 4.1.18
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/tailwindcss)

Updates `wouter` from 3.8.0 to 3.8.1
- [Release notes](https://github.com/molefrog/wouter/releases)
- [Commits](https://github.com/molefrog/wouter/compare/v3.8.0...v3.8.1)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@eslint/js"
  dependency-version: 9.39.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 9.39.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.3.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.4.25
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.3.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.7.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.49.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: vite
  dependency-version: 7.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.55.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: i18next
  dependency-version: 25.7.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react
  dependency-version: 19.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: wouter
  dependency-version: 3.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 10:56:56 +00:00
Simon 0167377b2f chore(version): bump version to 0.0.9 2025-12-15 18:54:18 +08:00
Simon f21914284c chore(version): bump version to 0.0.8 2025-12-15 18:46:43 +08:00
Simon 035091045a Merge pull request #72 from alibaba/refactor/package_ui
refactor(agent): move `ui` to a dedicated package.
2025-12-15 18:32:45 +08:00
Simon 1cd18b3ba9 Merge pull request #74 from alibaba/copilot/sub-pr-72
fix: clean up event listeners to prevent memory leaks in PageAgent
2025-12-15 17:56:37 +08:00
copilot-swe-agent[bot] 8ba04bf243 fix: also clean up beforeunload event listener
Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com>
2025-12-15 09:52:23 +00:00
copilot-swe-agent[bot] 67bf38c8d5 fix: clean up event listeners in PageAgent dispose method
Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com>
2025-12-15 09:50:28 +00:00
copilot-swe-agent[bot] 8885bde0ec Initial plan 2025-12-15 09:45:34 +00:00
Simon b33e361cee chore: clean up EventBus 2025-12-15 17:42:22 +08:00
Simon d1c8ca8197 refactor(ui): implement dedicated package @page-agent/ui 2025-12-15 17:34:12 +08:00
Simon 2b8b6ef86a refactor(ui): move ui and i18n to new package 2025-12-15 17:20:12 +08:00
Simon 0b46fd1d7a refactor(agent): decouple ui and agent; disable EventBus for now 2025-12-15 17:02:45 +08:00
Simon e2819714f1 chore: add Node and NPM version check in CI 2025-12-08 17:23:59 +08:00
Simon 3e6c431a0b chore: clean up unused param 2025-12-08 17:20:03 +08:00
Simon d0836fd788 chore: clean up; update CI node; update vscode ext; fix lint 2025-12-08 17:18:54 +08:00
Simon 32c17627e9 chore: rename UMD entry 2025-12-05 20:17:29 +08:00
Simon 13a6d2c164 docs: update contributing guidelines 2025-12-05 20:10:12 +08:00
Simon d756a2ef1c chore: improve monorepo 2025-12-05 20:00:31 +08:00
Simon b9b812cc25 docs: update roadmap 2025-12-05 19:41:50 +08:00
Simon 70caf6856b docs: update roadmap 2025-12-05 19:38:56 +08:00
Simon 34dad35698 chore: update lock 2025-12-05 19:32:05 +08:00
Simon 34e3a1a75c chore: update CI workflows 2025-12-05 19:30:38 +08:00
Simon cac86f2fbf Merge pull request #63 from alibaba/refactor/separate-dom-controller
refactor: Move DOM related logic to a new package(PageController)
2025-12-05 17:51:33 +08:00
Simon d1290c8070 chore: add repository directory 2025-12-05 17:41:03 +08:00
72 changed files with 1713 additions and 1066 deletions
+8 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
node-version: [20, 24, 25]
steps:
- uses: actions/checkout@v6
@@ -24,6 +24,13 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'npm'
# test on default version of npm
# - 9.6~10.8 on node@20
# - 11.3~11.6 on node@24
- name: Node and NPM version
run: node --version && npm --version
- name: Install dependencies
run: npm install
+2 -2
View File
@@ -18,14 +18,14 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 24
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build demo
run: npm run build
run: npm run build:website
- name: Setup Pages
uses: actions/configure-pages@v5
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 24
registry-url: 'https://registry.npmjs.org'
# Ensure npm 11.5.1 or later is installed
@@ -29,7 +29,7 @@ jobs:
run: npm ci
- name: Build
run: npm run build
run: npm run build:libs
- name: Publish all public packages
run: npm publish --workspaces --access public
+1
View File
@@ -23,6 +23,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.qoder
#
+1 -1
View File
@@ -1 +1 @@
npx lint-staged
npx lint-staged --allow-empty
+1 -1
View File
@@ -1,3 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
"recommendations": ["dbaeumer.vscode-eslint", "prettier.prettier-vscode"]
}
+8 -1
View File
@@ -1,6 +1,13 @@
{
"editor.fontLigatures": true,
"cSpell.words": ["HITL", "innerhtml", "opensource", "retryable", "wouter"],
"cSpell.words": [
"HITL",
"innerhtml",
"llms",
"opensource",
"retryable",
"wouter"
],
"markdownlint.config": {
// "comment": "Relaxed rules",
"default": true,
+40 -51
View File
@@ -7,9 +7,11 @@ This is a **monorepo** with npm workspaces containing **two main packages**:
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
And other internal packages. Such as:
And other internal packages:
- **Page Controller** (`packages/page-controller/`) - DOM operations and element interactions module. Independent of LLM, can be tested in unit tests.
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model.
- **Page Controller** (`packages/page-controller/`) - DOM operations and element interactions. Independent of LLM.
- **UI** (`packages/ui/`) - Panel, SimulatorMask, and i18n. Decoupled from PageAgent.
## Development Commands
@@ -17,24 +19,12 @@ And other internal packages. Such as:
```bash
npm start # Start website dev server
npm run dev # Same as start
npm run build # Build all packages
npm run build:lib # Build page-agent library only
npm run build:libs # Build all libraries
npm run build:website # Build the website
npm run lint # ESLint with TypeScript strict rules
```
### Package-specific Commands
```bash
# Core library
npm run build --workspace=page-agent
npm run build:watch --workspace=page-agent
# Website
npm run dev --workspace=@page-agent/website
npm run build --workspace=@page-agent/website
```
## Architecture & Critical Patterns
### Monorepo Structure
@@ -46,11 +36,9 @@ You must update tsconfig and vite config if you add/remove/rename a package.
```bash
packages/
├── page-agent/ # npm: "page-agent" ⭐ MAIN
│ ├── src/ # AI agent source
│ ├── src/
│ │ ├── PageAgent.ts # Main AI agent class
│ │ ── tools/ # LLM tool definitions
│ │ ├── llms/ # LLM integration
│ │ └── ui/ # UI components
│ │ ── tools/ # LLM tool definitions
│ ├── vite.config.js # Library build (ES + UMD)
│ └── package.json
├── website/ # npm: "@page-agent/website" (private) ⭐ MAIN
@@ -59,19 +47,31 @@ packages/
# ...internal packages below...
── page-controller/ # npm: "@page-agent/page-controller"
└── src/ # DOM operations source
├── PageController.ts # Main controller class
├── actions.ts # Element interaction actions
└── dom/ # DOM tree extraction
── llms/ # npm: "@page-agent/llms"
└── src/ # LLM client (reflection-before-action model)
├── index.ts
├── types.ts # MacroToolInput, AgentBrain, LLMConfig
└── OpenAI*.ts # OpenAI-compatible clients
├── page-controller/ # npm: "@page-agent/page-controller"
│ └── src/ # DOM operations
│ ├── PageController.ts
│ ├── actions.ts
│ └── dom/
└── ui/ # npm: "@page-agent/ui"
└── src/ # Panel and Mask Effects
├── Panel.ts
├── SimulatorMask.ts
└── i18n/
```
`workspaces` must be written in topological order to guarantee build order.
```json
"workspaces": [
// internal deps ...
// internal deps (topological order)
"packages/page-controller",
"packages/ui",
"packages/llms",
"packages/page-agent",
"packages/website"
],
@@ -80,8 +80,10 @@ packages/
### Module Boundaries (Critical)
- **Website** (`packages/website/`): CAN import from `page-agent` for demos. Alias `@/``website/src/`
- **Page Agent** (`packages/page-agent/`): The core lib. Imports from all internal packages. Never import from website.
- **Page Controller** (`packages/page-controller/`): Internal lib. Pure DOM operations, NO LLM dependency. Never import from page-agent.
- **Page Agent** (`packages/page-agent/`): The core lib. Imports from `@page-agent/llms`, `@page-agent/page-controller` and `@page-agent/ui`.
- **LLMs** (`packages/llms/`): LLM client with MacroToolInput contract. No dependency on page-agent.
- **UI** (`packages/ui/`): Panel, Mask, i18n. No dependency on page-agent.
- **Page Controller** (`packages/page-controller/`): Pure DOM operations. No LLM or UI dependency.
### PageController ↔ PageAgent Communication
@@ -108,19 +110,6 @@ DOM element references and internal state (selectorMap, elementTextMap) are enca
3. **LLM Processing**: AI model returns action plans (in page-agent)
4. **Indexed Operations**: PageAgent calls PageController methods by element index
### Event Bus Communication
Use `src/utils/bus.ts` for decoupled PageAgent ↔ UI communication:
```typescript
// Emit from PageAgent
getEventBus().emit('panel:show')
getEventBus().emit('panel:update', { status: 'thinking' })
// Listen in UI components
getEventBus().on('panel:show', () => panel.show())
```
### Hash Routing Requirement
Uses wouter with `useHashLocation` for static hosting:
@@ -146,13 +135,19 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
| File | Description |
|------|-------------|
| `src/PageAgent.ts` | ⭐ Main AI agent class orchestrating tools and LLM |
| `src/entry.ts` | CDN/UMD entry point with auto-initialization |
| `src/umd.ts` | CDN/UMD entry point with auto-initialization |
| `src/tools/` | Tool definitions that call PageController methods |
| `src/utils/bus.ts` | Type-safe event bus for decoupled communication |
| `src/ui/` | UI components (Panel, SimulatorMask) with CSS modules |
| `src/llms/` | LLM integration and communication layer |
| `vite.config.js` | Library build configuration (ES + UMD) |
### LLMs (`packages/llms/`)
| File | Description |
|------|-------------|
| `src/index.ts` | ⭐ LLM class with retry logic |
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig definitions |
| `src/OpenAILenientClient.ts` | OpenAI-compatible client with lenient parsing |
| `src/utils.ts` | Zod-to-OpenAI conversion, model patches |
### Page Controller (`packages/page-controller/`)
| File | Description |
@@ -195,11 +190,6 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
2. Expose via async method in `PageController.ts`
3. Export from `packages/page-controller/src/index.ts`
### New UI Component
1. Create in `packages/page-agent/src/ui/` with colocated CSS modules
2. Use event bus for PageAgent communication
## Code Standards
### TypeScript
@@ -234,5 +224,4 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
1. Check `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly
2. Test CDN injection with query params
3. Verify event bus communications are properly typed
4. Use `packages/website/src/test-pages/` for isolated testing
+16 -25
View File
@@ -7,11 +7,9 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
### Development Setup
1. **Prerequisites**
- Node.js 20+
- npm 10+
- typescript as required in package.json
- vscode or other editors that support ts/eslint/prettier
- make sure `eslint`, `auto-format` and `commit-lint` all work
- `node.js >= 20` with `npm >= 10`
- An editor that supports `ts/eslint/prettier`
- Make sure `eslint`, `prettier` and `commitlint` work well
2. **Setup**
@@ -27,7 +25,11 @@ This is a **monorepo** with npm workspaces containing **two main packages**:
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
We use a simplified monorepo solution with native npm-workspace. No fancy tooling. Hoisting is required.
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
- When developing. Use alias so that we don't have to pre-build.
- When bundling. Use external and disable ts `paths` alias to leave deps out.
- When bundling `UMD` and `Website`. Bundle everything including local packages.
## 🤝 How to Contribute
@@ -97,19 +99,6 @@ We use a simplified monorepo solution with native npm-workspace. No fancy toolin
- Github Copilot: `./.github/copilot-instructions.md`
- Claude Code: `./CLAUDE.md`
### Core Library (`src/`)
- **No React dependencies** - Pure JavaScript/TypeScript only
- Use event bus for component communication
- Follow browser-use patterns for DOM operations
### Doc&demo Site (`pages/`)
- Use React 19 + TypeScript
- Follow hash routing patterns (`useHashLocation`)
- Use Tailwind CSS for styling
- Add new docs pages to both router and sidebar
## 🔧 Development Workflows
### Website Development
@@ -120,24 +109,26 @@ npm start # React development server
### Core Lib Development and Testing
> @TODO this part is outdated. Update this.
- Config your LLM API
- Start a local script
- Start and serve a local umd script
```bash
npm run build:lib:watch # Auto-rebuild on changes
npm run dev:umd # Serving UMD with auto rebuild at http://localhost:5173/page-agent.umd.cjs
```
- Add a new bookmark enable it on other website
```
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/dist/lib/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
```
### Adding Documentation
1. Create `pages/docs/section/page-name/page.tsx`
2. Add route to `pages/router.tsx`
3. Add navigation link to `pages/components/DocsLayout.tsx`
1. Create `website/src/docs/section/page-name/page.tsx`
2. Add route to `website/src/router.tsx`
3. Add navigation link to `website/src/components/DocsLayout.tsx`
## 🎯 Contribution Areas
+9 -10
View File
@@ -31,10 +31,11 @@
### CDN 集成
```html
<!-- 临时 CDN URL. 未来会变更 -->
<script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
crossorigin="true"
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
<script
src="https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
```
@@ -72,12 +73,10 @@ PageAgent adopts a simplified monorepo structure:
```
packages/
├── page-agent/ # AI agent (npm: page-agent)
│ ├── PageAgent # Agent main loop
│ ├── tools/ # LLM tool definitions
├── ui/ # UI components & panels
│ └── llms/ # LLM integration layer
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
└── website/ # Documentation site
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller)
├── ui/ # 面板 & 蒙层 & 模拟鼠标 (npm: @page-agent/ui)
└── website/ # 文档站点
```
## 🤝 贡献
+8 -9
View File
@@ -1,6 +1,6 @@
# PageAgent 🤖🪄
![banner](https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg)
![banner](https://img.alicdn.com/imgextra/i3/O1CN01MyVCS21EoKkIHUT1s_!!6000000000398-49-tps-1280-353.webp)
[![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
@@ -31,10 +31,11 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
### CDN Integration
```html
<!-- temporary CDN URL. May change in the future -->
<script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
crossorigin="true"
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
<script
src="https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
```
@@ -72,11 +73,9 @@ PageAgent adopts a simplified monorepo structure:
```
packages/
├── page-agent/ # AI agent (npm: page-agent)
│ ├── PageAgent # Agent main loop
│ ├── tools/ # LLM tool definitions
│ ├── ui/ # UI components & panels
│ └── llms/ # LLM integration layer
├── llms/ # LLM client (npm: @page-agent/llms)
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
├── ui/ # Panel & Mask & Mouse Animation (npm: @page-agent/ui)
└── website/ # Demo & Documentation site
```
+13 -13
View File
@@ -24,33 +24,33 @@ The development progress and future plans for PageAgent.
- [ ] **Hooks and Events**
- [x] **lifecycle hooks**
- [ ] **lifecycle events**
- [ ] **❗Pause and intervene**
- Current pause does not allow user intervention.
- [ ] **User takeover**
- [#64](https://github.com/alibaba/page-agent/issues/64)
- [ ] **❗Hijack `page_open/page_change/page_unload` behavior**
- [ ] **Custom knowledge base and instructions**
- [#45](https://github.com/alibaba/page-agent/issues/45)
- [ ] **Black/white-list safeguard**
- [ ] **Data-masking**
- [#44](https://github.com/alibaba/page-agent/issues/44)
- [ ] **Improve Memory**
- Current phrasing can cause logic-loop for some models.
- Test adding `Action` to memory.
- [ ] **Tools for more complex tasks**
- todo list
- file sys
- [#66](https://github.com/alibaba/page-agent/issues/66)
- [#65](https://github.com/alibaba/page-agent/issues/65)
- [ ] **Optimize for popular UI frameworks**
- [x] **i18n of the website**
- [x] Chinese version
- [x] English version
- [ ] **Testing suits**
- [ ] **Support custom llm fetch**
- [ ] **Refactor: Separate `Agent` and `Page-Controller`**
- Agent should be able to run w/o dom.
- Actions should be able to be injected through iframe.
- [x] **Refactor: Separate `Agent` and `PageController`**
- [ ] **Chrome-ext wrapper for multi-page tasks**
♻️ Following browser-use's update and contribute back.
## 📋 Pending Features
- [ ] **Chrome-ext wrapper**
- [ ] **Tools for more complex tasks**
- todo list
- file sys
- [ ] **Support custom llm fetch**
- [ ] **Testing suits**
- [ ] **Same-origin multi-page-app rally**
- [ ] **Local MCP proxy**
+659 -452
View File
File diff suppressed because it is too large Load Diff
+19 -15
View File
@@ -1,10 +1,12 @@
{
"name": "root",
"private": true,
"version": "0.0.7",
"version": "0.0.15",
"type": "module",
"workspaces": [
"packages/page-controller",
"packages/ui",
"packages/llms",
"packages/page-agent",
"packages/website"
],
@@ -21,35 +23,37 @@
"npm": ">=10.0.0"
},
"scripts": {
"dev": "npm run dev --workspace=@page-agent/website",
"start": "npm run dev --workspace=@page-agent/website",
"build": "npm run build --workspaces --if-present",
"build:lib": "npm run build --workspace=page-agent",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:libs": "npm run build --workspaces --if-present",
"build": "npm run build:libs && npm run build:website",
"dev:umd": "npm run dev:umd --workspace=page-agent",
"version": "node scripts/sync-version.js",
"lint": "eslint .",
"prepare": "husky"
},
"devDependencies": {
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@eslint/js": "^9.39.1",
"@commitlint/cli": "^20.2.0",
"@commitlint/config-conventional": "^20.2.0",
"@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.55.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.3.9",
"eslint-plugin-react-dom": "^2.3.13",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-x": "^2.3.9",
"eslint-plugin-react-refresh": "^0.4.25",
"eslint-plugin-react-x": "^2.3.13",
"globals": "^16.4.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.4",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "^7.2.6",
"@microsoft/api-extractor": "^7.55.1",
"typescript-eslint": "^8.50.0",
"unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.0",
"vite-plugin-css-injected-by-js": "^3.5.2"
},
"lint-staged": {
+41
View File
@@ -0,0 +1,41 @@
# @page-agent/llms
LLM client with a **reflection-before-action** mental model for page-agent.
## Why This Package Exists
The LLM module and the agent logic are inherently coupled. This package exists not to decouple them, but to **define the interface contract** between the LLM and the agent.
The core abstraction is the `MacroToolInput` — a structured output format that **forces the model to reflect before acting**.
## The Reflection-Before-Action Model
Every tool call must first output its reasoning state before the actual action:
```typescript
interface MacroToolInput {
// Reflection (mandatory before any action)
evaluation_previous_goal?: string // How well did the previous action work?
memory?: string // Key information to remember
next_goal?: string // What to accomplish next
// Action (the actual operation)
action: Record<string, any>
}
```
This design ensures that:
1. **The model evaluates its previous action** before deciding the next step
2. **Working memory is explicitly maintained** across conversation turns
3. **Goals are clearly stated**, making the agent's reasoning transparent and debuggable
## Key Components
| Export | Description |
|--------|-------------|
| `LLM` | Main LLM client class with retry logic |
| `MacroToolInput` | The reflection-before-action input schema |
| `AgentBrain` | Agent's thinking state (eval, memory, goal) |
| `LLMConfig` | Configuration for LLM connection |
| `parseLLMConfig` | Parse and apply defaults to config |
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@page-agent/llms",
"version": "0.0.15",
"type": "module",
"main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-llms.js",
"default": "./dist/lib/page-agent-llms.js"
}
},
"files": [
"dist/"
],
"description": "LLM client with reflection-before-action mental model for page-agent",
"keywords": [
"page-agent",
"llm",
"openai",
"tool-calling",
"agent"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/llms"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"zod": "^4.2.0"
}
}
@@ -1,9 +1,15 @@
/**
* OpenAI Client implementation
*/
import type { MacroToolInput } from '../PageAgent'
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types'
import type {
InvokeResult,
LLMClient,
MacroToolInput,
Message,
OpenAIClientConfig,
Tool,
} from './types'
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
export class OpenAIClient implements LLMClient {
+21
View File
@@ -0,0 +1,21 @@
// Dev environment: use .env config if available, otherwise fallback to testing api
export const DEFAULT_MODEL_NAME: string =
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
? import.meta.env.LLM_MODEL_NAME
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_API_KEY: string =
import.meta.env.DEV && import.meta.env.LLM_API_KEY
? import.meta.env.LLM_API_KEY
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_BASE_URL: string =
import.meta.env.DEV && import.meta.env.LLM_BASE_URL
? import.meta.env.LLM_BASE_URL
: 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
// internal
export const LLM_MAX_RETRIES = 2
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
export const DEFAULT_MAX_TOKENS = 4096
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -31,26 +31,56 @@
* - 使 tool call
* - tool 使 tool call
*/
import type { LLMConfig } from '../config'
import { parseLLMConfig } from '../config'
import { EventBus, getEventBus } from '../utils/bus'
import { OpenAIClient } from './OpenAILenientClient'
import {
DEFAULT_API_KEY,
DEFAULT_BASE_URL,
DEFAULT_MAX_TOKENS,
DEFAULT_MODEL_NAME,
DEFAULT_TEMPERATURE,
LLM_MAX_RETRIES,
} from './constants'
import { InvokeError } from './errors'
import type { InvokeResult, LLMClient, Message, Tool } from './types'
import type {
AgentBrain,
InvokeResult,
LLMClient,
LLMConfig,
MacroToolInput,
MacroToolResult,
Message,
Tool,
} from './types'
export type { Message, Tool, InvokeResult, LLMClient }
export type {
AgentBrain,
InvokeResult,
LLMClient,
LLMConfig,
MacroToolInput,
MacroToolResult,
Message,
Tool,
}
export class LLM {
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
return {
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
apiKey: config.apiKey ?? DEFAULT_API_KEY,
model: config.model ?? DEFAULT_MODEL_NAME,
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
}
}
export class LLM extends EventTarget {
config: Required<LLMConfig>
id: string
client: LLMClient
#bus: EventBus
constructor(config: LLMConfig, id: string) {
constructor(config: LLMConfig) {
super()
this.config = parseLLMConfig(config)
this.id = id
this.#bus = getEventBus(id)
// Default to OpenAI client
this.client = new OpenAIClient({
@@ -81,17 +111,13 @@ export class LLM {
// retry settings
{
maxRetries: this.config.maxRetries,
onRetry: (retries: number) => {
this.#bus.emit('panel:update', {
type: 'retry',
displayText: `retry-ing (${retries} / ${this.config.maxRetries})`,
})
onRetry: (current: number) => {
this.dispatchEvent(
new CustomEvent('retry', { detail: { current, max: this.config.maxRetries } })
)
},
onError: (error: Error, withRetry: boolean) => {
this.#bus.emit('panel:update', {
type: 'error',
displayText: `step failed: ${(error as Error).message}`,
})
onError: (error: Error) => {
this.dispatchEvent(new CustomEvent('error', { detail: { error } }))
},
}
)
@@ -103,7 +129,7 @@ async function withRetry<T>(
settings: {
maxRetries: number
onRetry: (retries: number) => void
onError: (error: Error, withRetry: boolean) => void
onError: (error: Error) => void
}
): Promise<T> {
let retries = 0
@@ -118,7 +144,7 @@ async function withRetry<T>(
return await fn()
} catch (error: unknown) {
console.error(error)
settings.onError(error as Error, retries < settings.maxRetries)
settings.onError(error as Error)
// do not retry if aborted by user
if ((error as { name?: string })?.name === 'AbortError') throw error
@@ -75,3 +75,48 @@ export interface OpenAIClientConfig {
maxTokens?: number
maxRetries?: number
}
/**
* LLM configuration for PageAgent
*/
export interface LLMConfig {
baseURL?: string
apiKey?: string
model?: string
temperature?: number
maxTokens?: number
maxRetries?: number
}
/**
* Agent brain state - the reflection-before-action model
*
* Every tool call must first reflect on:
* - evaluation_previous_goal: How well did the previous action achieve its goal?
* - memory: Key information to remember for future steps
* - next_goal: What should be accomplished in the next action?
*/
export interface AgentBrain {
// thinking?: string
evaluation_previous_goal: string
memory: string
next_goal: string
}
/**
* MacroTool input structure
*
* This is the core abstraction that enforces the "reflection-before-action" mental model.
* Before executing any action, the LLM must output its reasoning state.
*/
export interface MacroToolInput extends AgentBrain {
action: Record<string, any>
}
/**
* MacroTool output structure
*/
export interface MacroToolResult {
input: MacroToolInput
output: string
}
@@ -4,9 +4,8 @@
import chalk from 'chalk'
import { z } from 'zod'
import type { MacroToolInput } from '../PageAgent'
import { InvokeError, InvokeErrorType } from './errors'
import type { Tool } from './types'
import type { MacroToolInput, Tool } from './types'
/**
* Convert Zod schema to OpenAI tool format
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"]
}
+37
View File
@@ -0,0 +1,37 @@
// @ts-check
import chalk from 'chalk'
import { dirname, resolve } from 'path'
import dts from 'unplugin-dts/vite'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
const __dirname = dirname(fileURLToPath(import.meta.url))
console.log(chalk.cyan(`📦 Building @page-agent/llms`))
export default defineConfig({
clearScreen: false,
plugins: [dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true })],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'PageAgentLLMs',
fileName: 'page-agent-llms',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: {
external: ['chalk', 'zod'],
},
minify: false,
sourcemap: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+15 -17
View File
@@ -1,22 +1,20 @@
{
"name": "page-agent",
"private": false,
"version": "0.0.7",
"version": "0.0.15",
"type": "module",
"main": "./dist/lib/page-agent.js",
"module": "./dist/lib/page-agent.js",
"types": "./dist/lib/PageAgent.d.ts",
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
"types": "./dist/esm/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageAgent.d.ts",
"import": "./dist/lib/page-agent.js",
"default": "./dist/lib/page-agent.js"
"types": "./dist/esm/PageAgent.d.ts",
"import": "./dist/esm/page-agent.js",
"default": "./dist/esm/page-agent.js"
}
},
"files": [
"dist/",
"README.md",
"LICENSE"
"dist/"
],
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
"keywords": [
@@ -39,17 +37,17 @@
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "npm run build:lib && npm run build:umd",
"build:lib": "MODE=lib vite build",
"build:umd": "MODE=umd vite build",
"build:watch": "MODE=umd vite build --watch",
"build": "vite build && vite build --config vite.umd.config.js",
"serve": "npx serve dist/umd -p 5173",
"dev:umd": "concurrently \"vite build --config vite.umd.config.js --watch\" \"npm run serve\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"ai-motion": "^0.4.7",
"chalk": "^5.6.2",
"zod": "^4.1.12",
"@page-agent/page-controller": "0.0.7"
"zod": "^4.2.0",
"@page-agent/llms": "0.0.15",
"@page-agent/page-controller": "0.0.15",
"@page-agent/ui": "0.0.15"
}
}
+73 -75
View File
@@ -2,49 +2,28 @@
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
import {
type AgentBrain,
LLM,
type MacroToolInput,
type MacroToolResult,
type Tool,
} from '@page-agent/llms'
import { PageController } from '@page-agent/page-controller'
import { Panel, SimulatorMask } from '@page-agent/ui'
import chalk from 'chalk'
import zod from 'zod'
import type { PageAgentConfig } from './config'
import { MAX_STEPS } from './config/constants'
import { I18n } from './i18n'
import { LLM, type Tool } from './llms'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools'
import { Panel, getToolCompletedText, getToolExecutingText } from './ui/Panel'
import { SimulatorMask } from './ui/SimulatorMask'
import { trimLines, uid, waitUntil } from './utils'
import { assert } from './utils/assert'
import { getEventBus } from './utils/bus'
export type { PageAgentConfig }
export { tool, type PageAgentTool } from './tools'
export interface AgentBrain {
// thinking?: string
evaluation_previous_goal: string
memory: string
next_goal: string
}
/**
* MacroTool input structure
*/
export interface MacroToolInput {
evaluation_previous_goal?: string
memory?: string
next_goal?: string
action: Record<string, any>
}
/**
* MacroTool output structure
*/
export interface MacroToolResult {
input: MacroToolInput
output: string
}
export type { AgentBrain, MacroToolInput, MacroToolResult }
export interface AgentHistory {
brain: AgentBrain
@@ -71,8 +50,6 @@ export interface ExecutionResult {
export class PageAgent extends EventTarget {
config: PageAgentConfig
id = uid()
bus = getEventBus(this.id)
i18n: I18n
panel: Panel
tools: typeof tools
paused = false
@@ -83,6 +60,9 @@ export class PageAgent extends EventTarget {
#llm: LLM
#totalWaitTime = 0
#abortController = new AbortController()
#llmRetryListener: ((e: Event) => void) | null = null
#llmErrorListener: ((e: Event) => void) | null = null
#beforeUnloadListener: ((e: Event) => void) | null = null
/** PageController for DOM operations */
pageController: PageController
@@ -96,14 +76,34 @@ export class PageAgent extends EventTarget {
super()
this.config = config
this.#llm = new LLM(this.config, this.id)
this.i18n = new I18n(this.config.language)
this.panel = new Panel(this)
this.#llm = new LLM(this.config)
this.panel = new Panel({
language: this.config.language,
onExecuteTask: (task) => this.execute(task),
onStop: () => this.dispose(),
onPauseToggle: () => {
this.paused = !this.paused
return this.paused
},
getPaused: () => this.paused,
})
this.tools = new Map(tools)
// Initialize PageController with config
this.pageController = new PageController(this.config)
// Listen to LLM events
this.#llmRetryListener = (e) => {
const { current, max } = (e as CustomEvent).detail
this.panel.update({ type: 'retry', current, max })
}
this.#llmErrorListener = (e) => {
const { error } = (e as CustomEvent).detail
this.panel.update({ type: 'error', message: `step failed: ${error.message}` })
}
this.#llm.addEventListener('retry', this.#llmRetryListener)
this.#llm.addEventListener('error', this.#llmErrorListener)
if (this.config.customTools) {
for (const [name, tool] of Object.entries(this.config.customTools)) {
if (tool === null) {
@@ -118,9 +118,10 @@ export class PageAgent extends EventTarget {
this.tools.delete('execute_javascript')
}
window.addEventListener('beforeunload', (e) => {
this.#beforeUnloadListener = (e) => {
if (!this.disposed) this.dispose('PAGE_UNLOADING')
})
}
window.addEventListener('beforeunload', this.#beforeUnloadListener)
}
/**
@@ -141,13 +142,10 @@ export class PageAgent extends EventTarget {
// Show mask and panel
this.mask.show()
this.bus.emit('panel:show')
this.bus.emit('panel:reset')
this.panel.show()
this.panel.reset()
this.bus.emit('panel:update', {
type: 'input',
displayText: this.task,
})
this.panel.update({ type: 'input', task: this.task })
if (this.#abortController) {
this.#abortController.abort()
@@ -171,10 +169,7 @@ export class PageAgent extends EventTarget {
// Update status to thinking
console.log(chalk.blue('Thinking...'))
this.bus.emit('panel:update', {
type: 'thinking',
displayText: this.i18n.t('ui.panel.thinking'),
})
this.panel.update({ type: 'thinking' })
const result = await this.#llm.invoke(
[
@@ -304,22 +299,14 @@ export class PageAgent extends EventTarget {
`)
console.log(brain)
this.bus.emit('panel:update', {
type: 'thinking',
displayText: brain,
})
this.panel.update({ type: 'thinking', text: brain })
// Find the corresponding tool
const tool = tools.get(toolName)
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
this.bus.emit('panel:update', {
type: 'tool_executing',
toolName,
toolArgs: toolInput,
displayText: getToolExecutingText(toolName, toolInput, this.i18n),
})
this.panel.update({ type: 'toolExecuting', toolName, args: toolInput })
const startTime = Date.now()
@@ -341,16 +328,13 @@ export class PageAgent extends EventTarget {
}
// Briefly display execution result
const displayResult = getToolCompletedText(toolName, toolInput, this.i18n)
if (displayResult)
this.bus.emit('panel:update', {
type: 'tool_executing',
toolName,
toolArgs: toolInput,
toolResult: result,
displayText: displayResult,
duration,
})
this.panel.update({
type: 'toolCompleted',
toolName,
args: toolInput,
result,
duration,
})
// Wait a moment to let user see the result
await new Promise((resolve) => setTimeout(resolve, 100))
@@ -426,16 +410,14 @@ export class PageAgent extends EventTarget {
this.pageController.cleanUpHighlights()
// Update panel status
this.bus.emit('panel:update', {
type: success ? 'output' : 'error',
displayText: text,
})
if (success) {
this.panel.update({ type: 'output', text })
} else {
this.panel.update({ type: 'error', message: text })
}
// Task completed
this.bus.emit('panel:update', {
type: 'completed',
displayText: this.i18n.t('ui.panel.taskCompleted'),
})
this.panel.update({ type: 'completed' })
this.mask.hide()
@@ -497,6 +479,22 @@ export class PageAgent extends EventTarget {
this.history = []
this.#abortController.abort(reason ?? 'PageAgent disposed')
// Clean up LLM event listeners
if (this.#llmRetryListener) {
this.#llm.removeEventListener('retry', this.#llmRetryListener)
this.#llmRetryListener = null
}
if (this.#llmErrorListener) {
this.#llm.removeEventListener('error', this.#llmErrorListener)
this.#llmErrorListener = null
}
// Clean up window event listeners
if (this.#beforeUnloadListener) {
window.removeEventListener('beforeunload', this.#beforeUnloadListener)
this.#beforeUnloadListener = null
}
this.config.onDispose?.call(this, reason)
}
}
+1 -21
View File
@@ -1,22 +1,2 @@
// Dev environment: use .env config if available, otherwise fallback to testing api
export const DEFAULT_MODEL_NAME: string =
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
? import.meta.env.LLM_MODEL_NAME
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_API_KEY: string =
import.meta.env.DEV && import.meta.env.LLM_API_KEY
? import.meta.env.LLM_API_KEY
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_BASE_URL: string =
import.meta.env.DEV && import.meta.env.LLM_BASE_URL
? import.meta.env.LLM_BASE_URL
: 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
// internal
export const LLM_MAX_RETRIES = 2
// Agent-specific constants (LLM constants moved to @page-agent/llms)
export const MAX_STEPS = 20
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
export const DEFAULT_MAX_TOKENS = 4096
+3 -28
View File
@@ -1,25 +1,11 @@
import type { LLMConfig } from '@page-agent/llms'
import type { PageControllerConfig } from '@page-agent/page-controller'
import type { SupportedLanguage } from '@page-agent/ui'
import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent'
import type { SupportedLanguage } from '../i18n'
import type { PageAgentTool } from '../tools'
import {
DEFAULT_API_KEY,
DEFAULT_BASE_URL,
DEFAULT_MAX_TOKENS,
DEFAULT_MODEL_NAME,
DEFAULT_TEMPERATURE,
LLM_MAX_RETRIES,
} from './constants'
export interface LLMConfig {
baseURL?: string
apiKey?: string
model?: string
temperature?: number
maxTokens?: number
maxRetries?: number
}
export type { LLMConfig }
export interface AgentConfig {
// theme?: 'light' | 'dark'
@@ -96,14 +82,3 @@ export interface AgentConfig {
}
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
return {
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
apiKey: config.apiKey ?? DEFAULT_API_KEY,
model: config.model ?? DEFAULT_MODEL_NAME,
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
}
}
-40
View File
@@ -1,40 +0,0 @@
/**
* Auto-run entry for page-agent.js. Insert this script into your page to get page-agent functionality.
*/
import { PageAgent, type PageAgentConfig } from './PageAgent'
// Clean up existing instances to prevent multiple injections from bookmarklet
if (window.pageAgent) {
window.pageAgent.dispose()
}
// Mount to global window object
window.PageAgent = PageAgent
// Export for ES module usage
// export { PageAgent }
console.log('🚀 page-agent.js loaded!')
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const currentScript = document.currentScript as HTMLScriptElement | null
if (currentScript) {
console.log('🚀 page-agent.js detected current script:', currentScript.src)
const url = new URL(currentScript.src)
const model = url.searchParams.get('model') || DEMO_MODEL
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
const config: PageAgentConfig = { model, baseURL, apiKey, language }
window.pageAgent = new PageAgent(config)
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
window.pageAgent = new PageAgent()
}
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
window.pageAgent.bus.emit('panel:show') // Show panel
+44
View File
@@ -0,0 +1,44 @@
/**
* Auto-run entry for page-agent.js. Insert this script into your page to get page-agent functionality.
*/
import { PageAgent, type PageAgentConfig } from './PageAgent'
// Clean up existing instances to prevent multiple injections from bookmarklet
if (window.pageAgent) {
window.pageAgent.dispose()
}
// Mount to global window object
window.PageAgent = PageAgent
// Export for ES module usage
// export { PageAgent }
console.log('🚀 page-agent.js loaded!')
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
// in case document.x is not ready yet
// @todo give a switch to disable auto-init
setTimeout(() => {
const currentScript = document.currentScript as HTMLScriptElement | null
if (currentScript) {
console.log('🚀 page-agent.js detected current script:', currentScript.src)
const url = new URL(currentScript.src)
const model = url.searchParams.get('model') || DEMO_MODEL
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
const config: PageAgentConfig = { model, baseURL, apiKey, language }
window.pageAgent = new PageAgent(config)
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
window.pageAgent = new PageAgent()
}
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
window.pageAgent.panel.show() // Show panel
})
+1 -21
View File
@@ -1,27 +1,8 @@
/**
* Type-safe event bus for decoupling PageAgent and Panel
*/
import type { Step } from '../ui/UIState'
/**
* Event mapping definitions
* @note Event bus callbacks must be repeatable without errors
*/
export interface PageAgentEventMap {
// Panel control events
// call panel.show()
'panel:show': { params: undefined }
// call panel.hide()
'panel:hide': { params: undefined }
// call panel.reset()
'panel:reset': { params: undefined }
// call panel.update()
'panel:update': { params: Omit<Step, 'id' | 'stepNumber' | 'timestamp'> }
// call panel.expand()
'panel:expand': { params: undefined }
// call panel.collapse()
'panel:collapse': { params: undefined }
// PageAgent status events
// 'agent:execute': { params: { task: string } }
// 'agent:done': { params: { text: string; success: boolean } }
@@ -31,8 +12,7 @@ export interface PageAgentEventMap {
// 'agent:error': { params: { error: string | Error } }
// Task status change events
// 'task:start': { params: { task: string } }
// 'task:step': { params: Omit<AgentStep, 'id' | 'stepNumber' | 'timestamp'> }
'task:start': { params: { task: string } }
// 'task:complete': { params: { text: string; success: boolean } }
// 'task:error': { params: { error: string | Error } }
+10 -2
View File
@@ -7,10 +7,18 @@
"baseUrl": ".",
"outDir": "dist",
"paths": {
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"@page-agent/ui": ["../ui/src/index.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [{ "path": "../page-controller" }]
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../ui" }
]
}
+10 -57
View File
@@ -1,7 +1,4 @@
// @ts-check
import chalk from 'chalk'
import 'dotenv/config'
import process from 'node:process'
import { dirname, resolve } from 'path'
import dts from 'unplugin-dts/vite'
import { fileURLToPath } from 'url'
@@ -10,15 +7,11 @@ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// ============================================================================
// Library Config (ES Module for NPM Package)
// ============================================================================
/** @type {import('vite').UserConfig} */
const libConfig = {
// ES Module for NPM Package
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
// dts({ tsconfigPath: './tsconfig.json', bundleTypes: true, compilerOptions: { paths: {} } }),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
@@ -32,9 +25,14 @@ const libConfig = {
fileName: 'page-agent',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'lib'),
outDir: resolve(__dirname, 'dist', 'esm'),
rollupOptions: {
external: ['ai', 'ai-motion', 'chalk', 'zod', '@page-agent/*'],
external: [
'chalk',
'zod',
// all the internal packages
/^@page-agent\//,
],
},
minify: false,
sourcemap: true,
@@ -43,49 +41,4 @@ const libConfig = {
define: {
'process.env.NODE_ENV': '"production"',
},
}
// ============================================================================
// UMD Config (Browser Bundle for CDN)
// ============================================================================
/** @type {import('vite').UserConfig} */
const umdConfig = {
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
publicDir: false,
esbuild: {
keepNames: true,
},
resolve: {
alias: {
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
},
},
build: {
lib: {
entry: resolve(__dirname, 'src/entry.ts'),
name: 'PageAgent',
fileName: 'page-agent',
formats: ['umd'],
},
outDir: resolve(__dirname, 'dist', 'umd'),
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
}
// ============================================================================
const MODE = process.env.MODE
console.log(chalk.cyan(`📦 Build mode: ${chalk.bold(MODE || 'lib')}`))
let config
if (MODE === 'umd') {
config = umdConfig
} else {
config = libConfig
}
export default defineConfig(config)
})
+45
View File
@@ -0,0 +1,45 @@
// @ts-check
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// UMD Bundle for CDN
// - alias all local packages so that they can be build in
// - no external
// - no d.ts. dts does not work with monorepo aliasing
export default defineConfig({
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
publicDir: false,
esbuild: {
keepNames: true,
},
resolve: {
alias: {
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
},
},
build: {
lib: {
entry: resolve(__dirname, 'src/umd.ts'),
name: 'PageAgent',
fileName: 'page-agent',
formats: ['umd'],
},
outDir: resolve(__dirname, 'dist', 'umd'),
rollupOptions: {
output: {
// force use .js as extension
entryFileNames: 'page-agent.js',
},
},
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+4 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/page-controller",
"version": "0.0.7",
"version": "0.0.15",
"type": "module",
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
@@ -13,9 +13,7 @@
}
},
"files": [
"dist/",
"README.md",
"LICENSE"
"dist/"
],
"description": "Page controller for page-agent - DOM operations and element interactions",
"keywords": [
@@ -28,12 +26,12 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/page-controller"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"build:watch": "vite build --watch",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
}
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ console.log(chalk.cyan(`📦 Building @page-agent/page-controller`))
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.json', bundleTypes: true }),
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
@@ -29,7 +29,7 @@ export default defineConfig({
},
outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: {
external: [],
external: ['@page-agent/*'],
},
minify: false,
sourcemap: true,
+41
View File
@@ -0,0 +1,41 @@
{
"name": "@page-agent/ui",
"version": "0.0.15",
"type": "module",
"main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-ui.js",
"default": "./dist/lib/page-agent-ui.js"
}
},
"files": [
"dist/"
],
"description": "UI components for page-agent - Panel, SimulatorMask, and i18n",
"keywords": [
"page-agent",
"ui",
"panel",
"i18n"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/ui"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"ai-motion": "^0.4.7"
}
}
@@ -1,11 +1,35 @@
import type { PageAgent } from '../PageAgent'
import type { I18n } from '../i18n'
import { truncate } from '../utils'
import type { EventBus } from '../utils/bus'
import { type Step, UIState } from './UIState'
import { I18n, type SupportedLanguage } from './i18n'
import { truncate } from './utils'
import styles from './Panel.module.css'
/**
* Panel configuration
*/
export interface PanelConfig {
language?: SupportedLanguage
onExecuteTask: (task: string) => void
onStop: () => void
onPauseToggle: () => boolean // returns new paused state
getPaused: () => boolean
}
/**
* Semantic update types - Panel handles i18n internally
*/
export type PanelUpdate =
| { type: 'thinking'; text?: string } // text is optional, defaults to i18n thinking text
| { type: 'input'; task: string }
| { type: 'question'; question: string }
| { type: 'userAnswer'; input: string }
| { type: 'retry'; current: number; max: number }
| { type: 'error'; message: string }
| { type: 'output'; text: string }
| { type: 'completed' }
| { type: 'toolExecuting'; toolName: string; args: any }
| { type: 'toolCompleted'; toolName: string; args: any; result?: string; duration?: number }
/**
* Agent control panel
*/
@@ -19,11 +43,11 @@ export class Panel {
#stopButton: HTMLElement
#inputSection: HTMLElement
#taskInput: HTMLInputElement
#bus: EventBus
#state = new UIState()
#isExpanded = false
#pageAgent: PageAgent
#config: PanelConfig
#i18n: I18n
#userAnswerResolver: ((input: string) => void) | null = null
#isWaitingForUserAnswer: boolean = false
#headerUpdateTimer: ReturnType<typeof setInterval> | null = null
@@ -34,9 +58,9 @@ export class Panel {
return this.#wrapper
}
constructor(pageAgent: PageAgent) {
this.#pageAgent = pageAgent
this.#bus = pageAgent.bus
constructor(config: PanelConfig) {
this.#config = config
this.#i18n = new I18n(config.language ?? 'en-US')
this.#wrapper = this.#createWrapper()
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
@@ -49,16 +73,8 @@ export class Panel {
this.#setupEventListeners()
this.#startHeaderUpdateLoop()
// this.#expand() // debug
this.#showInputArea()
this.#bus.on('panel:show', () => this.#show())
this.#bus.on('panel:hide', () => this.#hide())
this.#bus.on('panel:reset', () => this.#reset())
this.#bus.on('panel:update', (stepData) => this.#update(stepData))
this.#bus.on('panel:expand', () => this.#expand())
this.#bus.on('panel:collapse', () => this.#collapse())
}
/**
@@ -71,18 +87,67 @@ export class Panel {
this.#userAnswerResolver = resolve
// Update state to `running`
this.#update({
this.#updateInternal({
type: 'output',
displayText: this.#pageAgent.i18n.t('ui.panel.question', { question }),
displayText: this.#i18n.t('ui.panel.question', { question }),
}) // Expand history panel
if (!this.#isExpanded) {
this.#expand()
}
this.#showInputArea(this.#pageAgent.i18n.t('ui.panel.userAnswerPrompt'))
this.#showInputArea(this.#i18n.t('ui.panel.userAnswerPrompt'))
})
}
// ========== Public control methods ==========
show(): void {
this.wrapper.style.display = 'block'
void this.wrapper.offsetHeight
this.wrapper.style.opacity = '1'
this.wrapper.style.transform = 'translateX(-50%) translateY(0)'
}
hide(): void {
this.wrapper.style.opacity = '0'
this.wrapper.style.transform = 'translateX(-50%) translateY(20px)'
this.wrapper.style.display = 'none'
}
reset(): void {
this.#state.reset()
this.#statusText.textContent = this.#i18n.t('ui.panel.ready')
this.#updateStatusIndicator('thinking')
this.#updateHistory()
this.#collapse()
// Reset pause state via callback
if (this.#config.getPaused()) {
this.#config.onPauseToggle()
}
this.#updatePauseButton()
// Reset user input state
this.#isWaitingForUserAnswer = false
this.#userAnswerResolver = null
// Show input area
this.#showInputArea()
}
expand(): void {
this.#expand()
}
collapse(): void {
this.#collapse()
}
/**
* Update panel with semantic data - i18n handled internally
*/
update(data: PanelUpdate): void {
const stepData = this.#toStepData(data)
this.#updateInternal(stepData)
}
/**
* Dispose panel
*/
@@ -92,10 +157,102 @@ export class Panel {
this.wrapper.remove()
}
// ========== Private methods ==========
/**
* Update status
* Convert semantic update to step data with i18n
*/
#update(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): void {
#toStepData(data: PanelUpdate): Omit<Step, 'id' | 'stepNumber' | 'timestamp'> {
switch (data.type) {
case 'thinking':
return { type: 'thinking', displayText: data.text ?? this.#i18n.t('ui.panel.thinking') }
case 'input':
return { type: 'input', displayText: data.task }
case 'question':
return {
type: 'output',
displayText: this.#i18n.t('ui.panel.question', { question: data.question }),
}
case 'userAnswer':
return {
type: 'input',
displayText: this.#i18n.t('ui.panel.userAnswer', { input: data.input }),
}
case 'retry':
return { type: 'retry', displayText: `retry-ing (${data.current} / ${data.max})` }
case 'error':
return { type: 'error', displayText: data.message }
case 'output':
return { type: 'output', displayText: data.text }
case 'completed':
return { type: 'completed', displayText: this.#i18n.t('ui.panel.taskCompleted') }
case 'toolExecuting':
return {
type: 'tool_executing',
toolName: data.toolName,
toolArgs: data.args,
displayText: this.#getToolExecutingText(data.toolName, data.args),
}
case 'toolCompleted': {
const displayText = this.#getToolCompletedText(data.toolName, data.args)
if (!displayText) return { type: 'tool_executing', displayText: '' } // will be filtered
return {
type: 'tool_executing',
toolName: data.toolName,
toolArgs: data.args,
toolResult: data.result,
displayText,
duration: data.duration,
}
}
}
}
#getToolExecutingText(toolName: string, args: any): string {
switch (toolName) {
case 'click_element_by_index':
return this.#i18n.t('ui.tools.clicking', { index: args.index })
case 'input_text':
return this.#i18n.t('ui.tools.inputting', { index: args.index })
case 'select_dropdown_option':
return this.#i18n.t('ui.tools.selecting', { text: args.text })
case 'scroll':
return this.#i18n.t('ui.tools.scrolling')
case 'wait':
return this.#i18n.t('ui.tools.waiting', { seconds: args.seconds })
case 'done':
return this.#i18n.t('ui.tools.done')
default:
return this.#i18n.t('ui.tools.executing', { toolName })
}
}
#getToolCompletedText(toolName: string, args: any): string | null {
switch (toolName) {
case 'click_element_by_index':
return this.#i18n.t('ui.tools.clicked', { index: args.index })
case 'input_text':
return this.#i18n.t('ui.tools.inputted', { text: args.text })
case 'select_dropdown_option':
return this.#i18n.t('ui.tools.selected', { text: args.text })
case 'scroll':
return this.#i18n.t('ui.tools.scrolled')
case 'wait':
return this.#i18n.t('ui.tools.waited')
case 'done':
return null
default:
return null
}
}
/**
* Update status (internal)
*/
#updateInternal(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): void {
// Skip empty displayText (filtered toolCompleted for 'done')
if (!stepData.displayText) return
const step = this.#state.addStep(stepData)
// Queue header text update (will be processed by periodic check)
@@ -120,59 +277,20 @@ export class Panel {
}
}
/**
* Show panel
*/
#show(): void {
this.wrapper.style.display = 'block'
// Force reflow to trigger animation
void this.wrapper.offsetHeight
this.wrapper.style.opacity = '1'
this.wrapper.style.transform = 'translateX(-50%) translateY(0)'
}
/**
* Hide panel
*/
#hide(): void {
this.wrapper.style.opacity = '0'
this.wrapper.style.transform = 'translateX(-50%) translateY(20px)'
this.wrapper.style.display = 'none'
}
/**
* Reset state
*/
#reset(): void {
this.#state.reset()
this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.ready')
this.#updateStatusIndicator('thinking')
this.#updateHistory()
this.#collapse()
// Reset pause state
this.#pageAgent.paused = false
this.#updatePauseButton()
// Reset user input state
this.#isWaitingForUserAnswer = false
this.#userAnswerResolver = null
// Show input area
this.#showInputArea()
}
/**
* Toggle pause state
*/
#togglePause(): void {
this.#pageAgent.paused = !this.#pageAgent.paused
const paused = this.#config.onPauseToggle()
this.#updatePauseButton()
// Update status display
if (this.#pageAgent.paused) {
this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.paused')
this.#updateStatusIndicator('thinking') // Use existing thinking state
if (paused) {
this.#statusText.textContent = this.#i18n.t('ui.panel.paused')
this.#updateStatusIndicator('thinking')
} else {
this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.continueExecution')
this.#updateStatusIndicator('tool_executing') // Restore to execution state
this.#statusText.textContent = this.#i18n.t('ui.panel.continueExecution')
this.#updateStatusIndicator('tool_executing')
}
}
@@ -180,13 +298,14 @@ export class Panel {
* Update pause button state
*/
#updatePauseButton(): void {
if (this.#pageAgent.paused) {
const paused = this.#config.getPaused()
if (paused) {
this.#pauseButton.textContent = '▶'
this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.continue')
this.#pauseButton.title = this.#i18n.t('ui.panel.continue')
this.#pauseButton.classList.add(styles.paused)
} else {
this.#pauseButton.textContent = '⏸︎'
this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.pause')
this.#pauseButton.title = this.#i18n.t('ui.panel.pause')
this.#pauseButton.classList.remove(styles.paused)
}
}
@@ -196,12 +315,12 @@ export class Panel {
*/
#stopAgent(): void {
// Update status display
this.#update({
this.#updateInternal({
type: 'error',
displayText: this.#pageAgent.i18n.t('ui.panel.taskTerminated'),
displayText: this.#i18n.t('ui.panel.taskTerminated'),
})
this.#pageAgent.dispose()
this.#config.onStop()
}
/**
@@ -218,7 +337,7 @@ export class Panel {
// Handle user input mode
this.#handleUserAnswer(input)
} else {
this.#pageAgent.execute(input)
this.#config.onExecuteTask(input)
}
}
@@ -227,9 +346,9 @@ export class Panel {
*/
#handleUserAnswer(input: string): void {
// Add user input to history
this.#update({
this.#updateInternal({
type: 'input',
displayText: this.#pageAgent.i18n.t('ui.panel.userAnswer', { input }),
displayText: this.#i18n.t('ui.panel.userAnswer', { input }),
})
// Reset state
@@ -248,7 +367,7 @@ export class Panel {
#showInputArea(placeholder?: string): void {
// Clear input field
this.#taskInput.value = ''
this.#taskInput.placeholder = placeholder || this.#pageAgent.i18n.t('ui.panel.taskInput')
this.#taskInput.placeholder = placeholder || this.#i18n.t('ui.panel.taskInput')
this.#inputSection.classList.remove(styles.hidden)
// Focus on input field
setTimeout(() => {
@@ -294,23 +413,23 @@ export class Panel {
stepNumber: 0,
timestamp: new Date(),
type: 'thinking',
displayText: this.#pageAgent.i18n.t('ui.panel.waitingPlaceholder'),
displayText: this.#i18n.t('ui.panel.waitingPlaceholder'),
})}
</div>
</div>
<div class="${styles.header}">
<div class="${styles.statusSection}">
<div class="${styles.indicator} ${styles.thinking}"></div>
<div class="${styles.statusText}">${this.#pageAgent.i18n.t('ui.panel.ready')}</div>
<div class="${styles.statusText}">${this.#i18n.t('ui.panel.ready')}</div>
</div>
<div class="${styles.controls}">
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#pageAgent.i18n.t('ui.panel.expand')}">
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
</button>
<button class="${styles.controlButton} ${styles.pauseButton}" title="${this.#pageAgent.i18n.t('ui.panel.pause')}">
<button class="${styles.controlButton} ${styles.pauseButton}" title="${this.#i18n.t('ui.panel.pause')}">
</button>
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#pageAgent.i18n.t('ui.panel.stop')}">
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
X
</button>
</div>
@@ -501,8 +620,8 @@ export class Panel {
// Check if this is a result from done tool
if (step.toolName === 'done') {
// Judge success or failure based on result
const failureKeyword = this.#pageAgent.i18n.t('ui.tools.resultFailure')
const errorKeyword = this.#pageAgent.i18n.t('ui.tools.resultError')
const failureKeyword = this.#i18n.t('ui.tools.resultFailure')
const errorKeyword = this.#i18n.t('ui.tools.resultError')
const isSuccess =
!step.toolResult ||
(!step.toolResult.includes(failureKeyword) && !step.toolResult.includes(errorKeyword))
@@ -531,7 +650,7 @@ export class Panel {
}
const durationText = step.duration ? ` · ${step.duration}ms` : ''
const stepLabel = this.#pageAgent.i18n.t('ui.panel.step', {
const stepLabel = this.#i18n.t('ui.panel.step', {
number: step.stepNumber.toString(),
time,
duration: durationText || '', // Explicitly pass empty string to replace template
@@ -550,47 +669,3 @@ export class Panel {
`
}
}
/**
* Get display text for tool execution
*/
export function getToolExecutingText(toolName: string, args: any, i18n: I18n): string {
switch (toolName) {
case 'click_element_by_index':
return i18n.t('ui.tools.clicking', { index: args.index })
case 'input_text':
return i18n.t('ui.tools.inputting', { index: args.index })
case 'select_dropdown_option':
return i18n.t('ui.tools.selecting', { text: args.text })
case 'scroll':
return i18n.t('ui.tools.scrolling')
case 'wait':
return i18n.t('ui.tools.waiting', { seconds: args.seconds })
case 'done':
return i18n.t('ui.tools.done')
default:
return i18n.t('ui.tools.executing', { toolName })
}
}
/**
* Get display text for tool completion
*/
export function getToolCompletedText(toolName: string, args: any, i18n: I18n): string | null {
switch (toolName) {
case 'click_element_by_index':
return i18n.t('ui.tools.clicked', { index: args.index })
case 'input_text':
return i18n.t('ui.tools.inputted', { text: args.text })
case 'select_dropdown_option':
return i18n.t('ui.tools.selected', { text: args.text })
case 'scroll':
return i18n.t('ui.tools.scrolled')
case 'wait':
return i18n.t('ui.tools.waited')
case 'done':
return null
default:
return null
}
}
@@ -1,6 +1,6 @@
import { Motion } from 'ai-motion'
import { isPageDark } from '../utils/checkDarkMode'
import { isPageDark } from './checkDarkMode'
import styles from './SimulatorMask.module.css'
import cursorStyles from './cursor.module.css'
@@ -3,14 +3,14 @@
* @returns {boolean} - True if a common dark mode class is found.
*/
function hasDarkModeClass() {
const DFEAULT_DARK_MODE_CLASSES = ['dark', 'dark-mode', 'theme-dark', 'night', 'night-mode']
const DEFAULT_DARK_MODE_CLASSES = ['dark', 'dark-mode', 'theme-dark', 'night', 'night-mode']
const htmlElement = document.documentElement
const bodyElement = document.body
const bodyElement = document.body || document.documentElement // can be null in some cases
// Check class names on <html> and <body>
for (const className of DFEAULT_DARK_MODE_CLASSES) {
if (htmlElement.classList.contains(className) || bodyElement.classList.contains(className)) {
for (const className of DEFAULT_DARK_MODE_CLASSES) {
if (htmlElement.classList.contains(className) || bodyElement?.classList.contains(className)) {
return true
}
}
@@ -70,7 +70,7 @@ function isColorDark(colorString: string, threshold = 128) {
function isBackgroundDark() {
// We check both <html> and <body> because some pages set the color on <html>
const htmlStyle = window.getComputedStyle(document.documentElement)
const bodyStyle = window.getComputedStyle(document.body)
const bodyStyle = window.getComputedStyle(document.body || document.documentElement)
// Get background colors
const htmlBgColor = htmlStyle.backgroundColor
@@ -93,18 +93,23 @@ function isBackgroundDark() {
* @returns {boolean} - True if the page is likely dark.
*/
export function isPageDark() {
// Strategy 1: Check for common dark mode classes
if (hasDarkModeClass()) {
return true
try {
// Strategy 1: Check for common dark mode classes
if (hasDarkModeClass()) {
return true
}
// Strategy 2: Analyze the computed background color
if (isBackgroundDark()) {
return true
}
// @TODO add more checks here, e.g., analyzing text color,
// or checking the background of major layout elements like <main> or #app.
return false
} catch (error) {
console.warn('Error determining if page is dark:', error)
return false
}
// Strategy 2: Analyze the computed background color
if (isBackgroundDark()) {
return true
}
// @TODO add more checks here, e.g., analyzing text color,
// or checking the background of major layout elements like <main> or #app.
return false
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
+4
View File
@@ -0,0 +1,4 @@
export { Panel, type PanelConfig, type PanelUpdate } from './Panel'
export { SimulatorMask } from './SimulatorMask'
export { UIState, type Step, type AgentStatus } from './UIState'
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
+6
View File
@@ -0,0 +1,6 @@
export function truncate(text: string, maxLength: number): string {
if (text.length > maxLength) {
return text.substring(0, maxLength) + '...'
}
return text
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts", "**/*.js"],
"exclude": ["dist", "node_modules"]
}
+41
View File
@@ -0,0 +1,41 @@
// @ts-check
import chalk from 'chalk'
import { dirname, resolve } from 'path'
import dts from 'unplugin-dts/vite'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
console.log(chalk.cyan(`📦 Building @page-agent/ui`))
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'PageAgentUI',
fileName: 'page-agent-ui',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: {
external: ['ai-motion'],
},
minify: false,
sourcemap: true,
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+8 -9
View File
@@ -1,26 +1,25 @@
{
"name": "@page-agent/website",
"private": true,
"version": "0.0.7",
"version": "0.0.15",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:website": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"page-agent": "*",
"@tailwindcss/vite": "^4.1.14",
"@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"i18next": "^25.7.1",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^16.1.4",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-i18next": "^16.5.0",
"tailwindcss": "^4.1.14",
"wouter": "^3.7.1"
"wouter": "^3.8.1"
}
}
+3
View File
@@ -0,0 +1,3 @@
export const CDN_URL = 'https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js'
export const CDN_CN_URL =
'https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js'
@@ -1,5 +1,6 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
import { CDN_CN_URL, CDN_URL } from '@/constants'
export default function CdnSetup() {
return (
@@ -15,11 +16,16 @@ export default function CdnSetup() {
<CodeEditor
className="mb-8"
code={`
// 仅供测试使用,稳定 CDN 待定
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>
// CDN: \t${CDN_URL}
// Mirror: \t${CDN_CN_URL}
<script
src="${CDN_URL}"
crossorigin="true"
type="text/javascript"
></script>
<script>
window.pageAgent.panel.show()
// window.pageAgent
</script>`}
/>
@@ -8,7 +8,7 @@ export default function Overview() {
{/* 头图 */}
<figure className="mb-8 rounded-xl overflow-hidden">
<img
src="https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg"
src="https://img.alicdn.com/imgextra/i3/O1CN014xLeic1gnF2iWdzjJ_!!6000000004186-49-tps-1280-353.webp"
alt="page-agent"
className="w-full h-64 object-cover"
/>
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
import { CDN_CN_URL, CDN_URL } from '@/constants'
export default function QuickStart() {
const { t } = useTranslation('docs')
@@ -23,8 +24,9 @@ export default function QuickStart() {
<div>
<p className="text-sm font-medium mb-2">{t('quick_start.step1_cdn')}</p>
<CodeEditor
code={`// 仅供测试使用
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>`}
code={`// CDN: \t${CDN_URL}
// Mirror: \t${CDN_CN_URL}
<script src="${CDN_URL}" crossorigin="true" type="text/javascript"></script>`}
language="html"
/>
</div>
@@ -18,6 +18,9 @@ export default {
step1_content: 'Show your bookmarks bar',
step2_title: 'Step 2:',
step2_content: 'Drag this button to your bookmarks',
cdn_label: 'CDN Source',
cdn_international: 'International',
cdn_china: 'China Mirror',
step3_title: 'Step 3:',
step3_content: 'Click the bookmark on any site to activate',
notice_title: '⚠️ Heads Up',
@@ -17,6 +17,9 @@ export default {
step1_content: '显示收藏夹栏',
step2_title: '步骤 2:',
step2_content: '拖拽下面按钮到收藏夹栏',
cdn_label: 'CDN 源',
cdn_international: '国际',
cdn_china: '国内镜像',
step3_title: '步骤 3:',
step3_content: '在其他网站点击收藏夹中的按钮即可使用',
notice_title: '⚠️ 注意',
+2 -2
View File
@@ -4,8 +4,8 @@ import { useHashLocation } from 'wouter/use-hash-location'
import './i18n/config'
import './i18n/types'
import { default as PagesRouter } from './router.tsx'
import { default as TestPagesRouter } from './test-pages/router.tsx'
import { default as PagesRouter } from './router'
import { default as TestPagesRouter } from './test-pages/router'
import './index.css'
+38 -18
View File
@@ -6,23 +6,27 @@ import { Link, useSearchParams } from 'wouter'
import Footer from './components/Footer'
import Header from './components/Header'
import { CDN_CN_URL, CDN_URL } from './constants'
const injection = encodeURI(
"javascript:(function(){var s=document.createElement('script');s.src=`https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js?t=${Math.random()}`;s.setAttribute('crossorigin', true);s.type=`text/javascript`;s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();"
)
function getInjection(useCN?: boolean) {
const cdn = useCN ? CDN_CN_URL : CDN_URL
const injectionA = `
<a
href=${injection}
class="inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
draggable="true"
onclick="return false;"
title="Drag me to your bookmarks bar!"
>
PageAgent
</a>
const injection = encodeURI(
`javascript:(function(){var s=document.createElement('script');s.src=\`${cdn}?t=\${Math.random()}\`;s.setAttribute('crossorigin', true);s.type="text/javascript";s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();`
)
`
return `
<a
href=${injection}
class="inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
draggable="true"
onclick="return false;"
title="Drag me to your bookmarks bar!"
>
PageAgent
</a>
`
}
export default function HomePage() {
const { t, i18n } = useTranslation(['home', 'common'])
@@ -38,6 +42,7 @@ export default function HomePage() {
const isOther = params.has('try_other')
const [activeTab, setActiveTab] = useState<'try' | 'other'>(isOther ? 'other' : 'try')
const [cdnSource, setCdnSource] = useState<'international' | 'china'>('international')
const handleExecute = async () => {
if (!task.trim()) return
@@ -194,10 +199,25 @@ export default function HomePage() {
</span>{' '}
{t('home:try_other.step2_content')}
</p>
<div
className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400"
dangerouslySetInnerHTML={{ __html: injectionA }}
></div>
<div className="flex items-center justify-center gap-3">
<select
value={cdnSource}
onChange={(e) =>
setCdnSource(e.target.value as 'international' | 'china')
}
className="px-2 py-1.5 text-xs border border-gray-300 dark:border-gray-500 rounded bg-white dark:bg-gray-600 text-gray-700 dark:text-gray-200"
>
<option value="international">
{t('home:try_other.cdn_international')}
</option>
<option value="china">{t('home:try_other.cdn_china')}</option>
</select>
<div
dangerouslySetInnerHTML={{
__html: getInjection(cdnSource === 'china'),
}}
></div>
</div>
</div>
{/* Usage Instructions */}
+10 -2
View File
@@ -11,11 +11,19 @@
"@/*": ["src/*"],
// Simplified monorepo solution (raw npm workspace with hoisting)
"page-agent": ["../page-agent/src/PageAgent.ts"],
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"page-agent": ["../page-agent/src/PageAgent.ts"]
"@page-agent/ui": ["../ui/src/index.ts"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["dist", "node_modules"],
"references": [{ "path": "../page-controller" }, { "path": "../page-agent" }]
"references": [
//
{ "path": "../llms" },
{ "path": "../page-agent" },
{ "path": "../page-controller" },
{ "path": "../ui" }
]
}
+6 -1
View File
@@ -1,6 +1,6 @@
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react-swc'
import 'dotenv/config'
import { config as dotenvConfig } from 'dotenv'
import process from 'node:process'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
@@ -8,6 +8,9 @@ import { defineConfig } from 'vite'
const __dirname = dirname(fileURLToPath(import.meta.url))
// Load .env from repo root
dotenvConfig({ path: resolve(__dirname, '../../.env') })
// Website Config (React Documentation Site)
export default defineConfig({
base: './',
@@ -19,7 +22,9 @@ export default defineConfig({
'@': resolve(__dirname, 'src'),
// Monorepo packages (always bundle local code instead of npm versions)
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
'page-agent': resolve(__dirname, '../page-agent/src/PageAgent.ts'),
},
},
+1
View File
@@ -102,6 +102,7 @@ console.log(chalk.green.bold(`\n✓ Version synced: ${newVersion}\n`))
if (hasChanges) {
const tagName = `v${newVersion}`
console.log(chalk.cyan.bold('📋 Next steps:\n'))
console.log(chalk.blueBright(`npm i`))
console.log(
chalk.blueBright(`git add . && git commit -m "chore(version): bump version to ${newVersion}"`)
)
+2
View File
@@ -2,6 +2,8 @@
"extends": "./tsconfig.base.json",
"references": [
{ "path": "./packages/page-controller" },
{ "path": "./packages/ui" },
{ "path": "./packages/llms" },
{ "path": "./packages/page-agent" },
{ "path": "./packages/website" }
],