Compare commits

..

254 Commits

Author SHA1 Message Date
Simon 4b69a7ee00 chore(version): bump version to 0.3.0-beta.1 2026-01-19 19:35:27 +08:00
Simon 25e4d07fc5 chore(demo): clean up 2026-01-19 19:33:15 +08:00
Simon 6a381dede1 chore(version): bump version to 0.3.0-beta 2026-01-19 19:31:45 +08:00
Simon b3bfbb96bb docs: update cdn urls 2026-01-19 19:31:07 +08:00
Simon 3d10953324 feat: remove iife-full build 2026-01-19 19:18:19 +08:00
Simon 8dd4c5d78e feat: remove @page-agent/cdn 2026-01-19 19:10:00 +08:00
Simon 8bde218f28 Merge pull request #119 from alibaba/feat/decouple-dom
feat!: decouple `DOM` from `Agent`
2026-01-19 17:22:57 +08:00
Simon 0643cc4ff9 feat: enhance headless mode documentation and usage 2026-01-19 17:14:36 +08:00
Simon 4c5dd23fcd feat: decouple PageController from PageAgent 2026-01-19 17:14:09 +08:00
Simon 3ce4d8e3fe docs: update instructions. add advanced section 2026-01-19 16:47:48 +08:00
Simon b217e6a2ca feat!: PageAgent with UI by default 2026-01-19 16:07:17 +08:00
Simon c9f049a733 refactor: rename page-agent to page-agent-core 2026-01-19 16:06:07 +08:00
Simon 09c3084629 chore: typo 2026-01-19 15:21:43 +08:00
Simon 38c8020e4a chore: typo 2026-01-19 15:15:23 +08:00
Simon 5057f7472b feat: pass language to formatTime for localization 2026-01-19 14:25:25 +08:00
Simon 6820eec676 feat(PageController): make showMask and hideMask async 2026-01-19 12:54:33 +08:00
Simon ef2777b521 feat: custom PageController; remove beforeunload; node-safe PageController 2026-01-18 00:07:23 +08:00
Simon 8bba3a63f5 chore: ui folder structure 2026-01-17 23:35:07 +08:00
Simon 722eb10286 feat: change onAfterStep params 2026-01-17 23:31:02 +08:00
Simon 15ffc27059 chore: agent event docs 2026-01-17 23:30:37 +08:00
Simon 5f162b1a1c chore: rm event bus; separate agent types 2026-01-17 23:30:12 +08:00
Simon 083c002ce0 fix: type 2026-01-17 23:20:09 +08:00
Simon 1ede1d9911 feat!: redo Panel; decouple Panel from Agent
BREAKING CHANGES: Agent and Panel API Changes
2026-01-17 23:14:26 +08:00
Simon 8abe3afed9 fix: type 2026-01-17 18:07:59 +08:00
Simon af36f43104 chore(version): bump version to 0.2.5 2026-01-17 17:47:50 +08:00
Simon b5f260207c chore: wording 2026-01-17 17:47:04 +08:00
Simon 9075204c29 Merge pull request #118 from alibaba/feat/make-panel-optional
feat: make all ui optional
2026-01-17 17:43:40 +08:00
Simon ce3a80bc53 docs: typo 2026-01-17 17:30:16 +08:00
Simon 64c36f67bb docs: update config; instructions for programmatic usage 2026-01-17 17:28:28 +08:00
Simon 78d4919289 feat: make panel optional 2026-01-17 17:24:17 +08:00
Simon 1af2607cca feat: remove ask_user tool from sys_prompt 2026-01-17 17:18:31 +08:00
Simon ab713f3d3a chore(version): bump version to 0.2.4 2026-01-17 16:22:45 +08:00
Simon 05cc4f36c8 chore: add .prettierignore for system_prompt.md 2026-01-17 16:22:28 +08:00
Simon 54e9bdad7d chore: prettier json and markdowns 2026-01-17 16:15:36 +08:00
Simon 0fc88c0080 chore(version): bump version to 0.2.3 2026-01-17 16:03:11 +08:00
Simon 02f97ef2fa chore: banner 2026-01-17 15:57:22 +08:00
Simon 0853ffbbc6 chore: banner 2026-01-17 15:48:30 +08:00
Simon 60ac6da09f chore: new demo video 2026-01-17 15:17:14 +08:00
Simon 11a9fb1181 chore: new demo video 2026-01-17 15:15:48 +08:00
Simon 3762d0f1e0 chore: refine docs 2026-01-17 14:59:09 +08:00
Simon ebbe92f7ca fix(website): update links to use relative paths 2026-01-17 02:13:19 +08:00
Simon 9f239b2de3 chore: comment out unused config 2026-01-17 02:05:30 +08:00
Simon 2410c27b55 chore(website): update config docs 2026-01-17 02:05:09 +08:00
Simon 6d6c82648c chore: cleanup 2026-01-17 01:46:35 +08:00
Simon f41119c1ad fix(website): fix router 2026-01-17 01:36:15 +08:00
Simon 9e6e1b996c chore(website): simplify chunks 2026-01-17 01:27:19 +08:00
Simon 1ab23e181f chore: clean up 2026-01-17 01:23:23 +08:00
Simon a1b485691f chore(version): bump version to 0.2.2 2026-01-17 00:59:55 +08:00
Simon 694e05b800 chore: npm audit fix 2026-01-17 00:54:20 +08:00
Simon b761795c1a feat!: local iife testing
BREAKING CHANGES: remove umd build from npm package and cdn
2026-01-16 19:04:26 +08:00
Simon 8cc54983aa chore(version): bump version to 0.2.1 2026-01-16 16:16:18 +08:00
Simon 59a599285e Merge pull request #115 from alibaba/fix/phantom-panel
fix(panel): hide panel by default
2026-01-16 16:15:43 +08:00
Simon 2f8c2b395c fix(panel): hide panel by default 2026-01-16 16:06:57 +08:00
Simon 28f836674c feat: edge feedback for scroll actions 2026-01-16 15:53:47 +08:00
Simon 6f01632231 fix(website): build error 2026-01-16 15:09:30 +08:00
Simon 41123ac561 chore(website): optimize bundle size 2026-01-16 15:05:09 +08:00
Simon c8bf0dd20f chore: get rid of build warnings 2026-01-16 14:50:56 +08:00
Simon ec2b744bab Merge pull request #114 from alibaba/feat/user-takeover
feat: remove `pause`
2026-01-15 21:51:01 +08:00
Simon eab9cf64e8 feat: includes history events in panel 2026-01-15 21:48:14 +08:00
Simon 42130d6f1d feat: remove pause 2026-01-15 21:37:03 +08:00
Simon c0ce6e7d14 chore(version): bump version to 0.2.0 2026-01-15 20:47:39 +08:00
Simon 8f5f98f889 feat(agent)!: rename brain to reflection; fix undefined lines in panel 2026-01-15 20:45:40 +08:00
Simon ee4719cdcf feat: observation 2026-01-15 20:28:39 +08:00
Simon b4c82b7833 refactor: mv getBrowserState to Controller; simplify Agent 2026-01-15 20:17:50 +08:00
Simon aefc3cfb89 Merge pull request #111 from alibaba/feat/extend-agent-history-for-observation
feat: extend history type
2026-01-15 19:57:41 +08:00
Simon 7f2b8c2766 feat: extend history type 2026-01-15 19:18:25 +08:00
Simon 62e24a3050 chore(version): bump version to 0.1.0 2026-01-14 18:50:06 +08:00
Simon 4f6249a252 feat!: require user to provide LLM api. do not fallback to demo LLM
BREAKING CHANGE: LLM API will be required for agent constructor
2026-01-14 18:49:23 +08:00
Simon 62519416bb chore(version): bump version to 0.0.24 2026-01-14 18:34:55 +08:00
Simon a1e70e663d feat(PageController): exclude aria-hidden elements 2026-01-14 18:04:31 +08:00
Simon 338e317be1 chore(website): fix homepage overflow 2026-01-14 17:45:49 +08:00
Simon 9a66d5205f feat(website): display version in header and load from page-agent package 2026-01-14 17:42:18 +08:00
Simon 7d45bf6598 feat(website): update CDN links and usage 2026-01-14 17:27:35 +08:00
Simon 78ee72eea4 chore(version): bump version to 0.0.23 2026-01-14 15:46:10 +08:00
Simon 3b71c535b1 fix: clean up debug code 2026-01-14 15:23:02 +08:00
Simon 6615f7b300 Merge pull request #110 from alibaba/fix/panel-text-uncompleted
feat: add HTML escaping to Panel
2026-01-14 15:17:43 +08:00
Simon 38caae5e23 feat: add HTML escaping to Panel 2026-01-14 15:15:51 +08:00
Simon 1751ba325d chore(version): bump version to 0.0.22 2026-01-13 21:01:16 +08:00
Simon 066b045822 Merge pull request #108 from alibaba/feat/dual-umd
feat: add `cdn` packages for iife builds
2026-01-13 20:58:37 +08:00
Simon 3b21c42cdb feat: add cdn packages for iife builds 2026-01-13 20:52:41 +08:00
Simon 8b1e91d665 Merge pull request #107 from alibaba/refactor/mv-simulator-mask-to-page-controller
Refactor: move simulator mask to page controller
2026-01-13 17:38:37 +08:00
Simon aa5909264a chore: mv mask code to folder 2026-01-13 17:35:24 +08:00
Simon b6232d4e21 refactor: mv SimulatorMask from ui to page-controller 2026-01-13 17:33:38 +08:00
Simon 0d3cef5578 chore(version): bump version to 0.0.21 2026-01-13 15:18:50 +08:00
Simon 1e737f3a71 docs: models recommendation 2026-01-13 15:17:43 +08:00
Simon 2f2bdb4697 fix: update Qwen patch condition for model name 2026-01-13 15:07:01 +08:00
Simon 3fa7c8b85a feat: improve qwen3 compatibility (#106)
* feat: improve qwen3 compatibility

* fix: qwen

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 15:04:03 +08:00
dependabot[bot] 7efce91a91 chore(deps-dev): bump the development-dependencies group with 11 updates (#101)
Bumps the development-dependencies group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.3.0` | `20.3.1` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.3.0` | `20.3.1` |
| [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) | `6.0.1` | `6.0.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.3` | `25.0.6` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.5.1` | `2.5.5` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.5.1` | `2.5.5` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.52.0` | `8.53.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.0` | `7.3.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.7` | `19.2.8` |
| [i18next](https://github.com/i18next/i18next) | `25.7.3` | `25.7.4` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.1` | `16.5.2` |


Updates `@commitlint/cli` from 20.3.0 to 20.3.1
- [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.3.1/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.3.0 to 20.3.1
- [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.3.1/@commitlint/config-conventional)

Updates `@trivago/prettier-plugin-sort-imports` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v6.0.1...v6.0.2)

Updates `@types/node` from 25.0.3 to 25.0.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint-plugin-react-dom` from 2.5.1 to 2.5.5
- [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.5.5/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.5.1 to 2.5.5
- [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.5.5/packages/plugins/eslint-plugin-react-x)

Updates `typescript-eslint` from 8.52.0 to 8.53.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.53.0/packages/typescript-eslint)

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

Updates `@types/react` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `i18next` from 25.7.3 to 25.7.4
- [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.3...v25.7.4)

Updates `react-i18next` from 16.5.1 to 16.5.2
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.5.1...v16.5.2)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.53.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: i18next
  dependency-version: 25.7.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:30:02 +08:00
dependabot[bot] a11d370f2b chore(deps): bump the production-dependencies group with 3 updates (#100)
Bumps the production-dependencies group with 3 updates: [ai-motion](https://github.com/gaomeng1900/ai-motion), [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `ai-motion` from 0.4.7 to 0.4.8
- [Release notes](https://github.com/gaomeng1900/ai-motion/releases)
- [Commits](https://github.com/gaomeng1900/ai-motion/compare/v0.4.7...v0.4.8)

Updates `motion` from 12.24.0 to 12.26.1
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.24.0...v12.26.1)

Updates `simple-icons` from 16.4.0 to 16.5.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.4.0...16.5.0)

---
updated-dependencies:
- dependency-name: ai-motion
  dependency-version: 0.4.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: motion
  dependency-version: 12.26.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:27:23 +08:00
dependabot[bot] d7bfa42f85 chore(deps-dev): bump globals from 16.5.0 to 17.0.0 (#102)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.5.0 to 17.0.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.5.0...v17.0.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:26:37 +08:00
Simon 551ba49280 Merge pull request #104 from alibaba/refactor/mv-brain-outof-llms
refactor: move agent logic out of `llms`
2026-01-13 14:24:29 +08:00
Simon adab24bb22 Merge branch 'refactor/mv-brain-outof-llms' of https://github.com/alibaba/page-agent into refactor/mv-brain-outof-llms 2026-01-13 14:22:48 +08:00
Simon 64aea7e84c chore: typing 2026-01-13 14:21:57 +08:00
Simon 67f6bd619e chore: rename autoFixer for better understanding 2026-01-13 14:20:52 +08:00
Simon 35d1fd1166 chore: better erroring
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 14:16:51 +08:00
Simon d5b8019fb1 chore: better docs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 14:14:56 +08:00
Simon 526cb4dbb3 chore: rename OpenAIClient; clean up old notes 2026-01-13 13:58:21 +08:00
Simon 6dc56c57c6 chore: rm legacy llm client 2026-01-13 13:56:30 +08:00
Simon a3263fdd3a chore: rename OpenAICompatibleClient 2026-01-13 13:51:09 +08:00
Simon 14974c0257 feat!: mv brain from llms to agent; redo toolCall auto fixer 2026-01-13 13:49:19 +08:00
Simon e70ae40096 chore: guardrail 2026-01-12 13:55:39 +08:00
Simon 3e04a131f1 chore(version): bump version to 0.0.20 2026-01-11 01:40:54 +08:00
Simon b487a7900b feat(website): layout change 2026-01-11 01:40:21 +08:00
Simon 78b49c8b7a Merge pull request #99 from alibaba/refactor/website-structure
refactor: website structure
2026-01-11 01:21:55 +08:00
Simon 949e511dcf Merge pull request #98 from alibaba/feat/data-masking
feat: data masking
2026-01-11 01:17:08 +08:00
Simon d9faca5d01 refactor: normalize website structure 2026-01-11 01:16:57 +08:00
Simon 398c6d0869 chore: rm test-pages 2026-01-11 01:04:43 +08:00
Simon 54d478df1e docs: update data masking docs 2026-01-11 00:58:42 +08:00
Simon 43c80f5418 feat: transformPageContent hook for data-masking 2026-01-11 00:54:59 +08:00
Simon b186ffcaa6 chore: info about CDN 2026-01-11 00:05:21 +08:00
Simon 778bbda4b8 chore: remove roadmap 2026-01-10 23:53:19 +08:00
Simon f5900bd096 chore(version): bump version to 0.0.19 2026-01-10 23:35:02 +08:00
Simon 09772d79bd fix: panel language not found 2026-01-10 23:33:36 +08:00
Simon bfa6a5347e fix: rm maxTokens from api 2026-01-10 23:27:08 +08:00
Simon da5cf44012 Merge pull request #97 from alibaba/feat/instructions
feat: custom instructions
2026-01-10 19:27:27 +08:00
Simon 1666c08e58 refactor: rename instructions to custom-instructions 2026-01-10 19:25:56 +08:00
Simon 0c0c76fdd8 feat: wrap user callback in try catch
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-10 19:23:02 +08:00
Simon 6b0b1218ac chore: cleanup 2026-01-10 19:23:01 +08:00
Simon d70f652285 chore(website): add instructions for PageAgent demo page 2026-01-10 19:08:50 +08:00
Simon c3c01b63fb docs: upgrade docs for custom instructions 2026-01-10 19:02:18 +08:00
Simon 463d010c24 feat(agent): add instructions api 2026-01-10 18:44:09 +08:00
Simon 4e52f17b9f feat(agent): mv <agent_state> in front of <agent_history> 2026-01-10 17:44:08 +08:00
Simon 5237edfcca Merge branch 'main' of https://github.com/alibaba/page-agent 2026-01-07 19:58:56 +08:00
Simon 79dcf9d9e4 docs: fix api doc, should be model instead of modelName 2026-01-07 19:58:43 +08:00
Simon 09544205ed Merge pull request #91 from alibaba/dependabot/npm_and_yarn/production-dependencies-d50d67c463
chore(deps): bump the production-dependencies group across 1 directory with 3 updates
2026-01-07 19:28:54 +08:00
Simon 2c74c3170a Merge pull request #92 from alibaba/dependabot/npm_and_yarn/development-dependencies-f82d5f2ef0
chore(deps-dev): bump the development-dependencies group across 1 directory with 7 updates
2026-01-07 19:28:40 +08:00
dependabot[bot] 9cee8f4174 chore(deps-dev): bump the development-dependencies group across 1 directory with 7 updates
Bumps the development-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.2.0` | `20.3.0` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.2.0` | `20.3.0` |
| [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) | `6.0.0` | `6.0.1` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.3.13` | `2.5.1` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.3.13` | `2.5.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.50.1` | `8.52.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.0` | `16.5.1` |



Updates `@commitlint/cli` from 20.2.0 to 20.3.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.3.0/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.2.0 to 20.3.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.3.0/@commitlint/config-conventional)

Updates `@trivago/prettier-plugin-sort-imports` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v6.0.0...v6.0.1)

Updates `eslint-plugin-react-dom` from 2.3.13 to 2.5.1
- [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.5.1/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.3.13 to 2.5.1
- [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.5.1/packages/plugins/eslint-plugin-react-x)

Updates `typescript-eslint` from 8.50.1 to 8.52.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.52.0/packages/typescript-eslint)

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

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-version: 6.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.52.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 19:38:18 +00:00
dependabot[bot] d05b43cf50 chore(deps): bump the production-dependencies group across 1 directory with 3 updates
Bumps the production-dependencies group with 3 updates in the / directory: [zod](https://github.com/colinhacks/zod), [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `zod` from 4.2.1 to 4.3.5
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.2.1...v4.3.5)

Updates `motion` from 12.23.26 to 12.24.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.23.26...v12.24.0)

Updates `simple-icons` from 16.2.0 to 16.4.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.2.0...16.4.0)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.3.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: motion
  dependency-version: 12.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 19:37:33 +00:00
Simon 3fe6ae6862 chore(version): bump version to 0.0.18 2025-12-24 19:32:39 +08:00
Simon 66be1e56e3 fix: tool description lost 2025-12-24 19:28:59 +08:00
Simon 0fd4eaf9af chore: log network error for better debug 2025-12-24 19:06:35 +08:00
Simon a0712baa91 chore(version): bump version to 0.0.17 2025-12-24 19:02:57 +08:00
Simon 49af3a9c58 fix: illegal custom fetch 2025-12-24 19:00:43 +08:00
Simon 20ad3d04d2 docs(ROADMAP): check custom LLM fetch 2025-12-24 16:54:18 +08:00
Simon c7e0538836 chore(version): bump version to 0.0.16 2025-12-24 16:43:48 +08:00
Simon 59fcfaa503 feat: add customFetch config 2025-12-24 16:42:31 +08:00
Simon 17befe4bab refactor: unify LLM configuration types 2025-12-23 17:37:09 +08:00
Simon 7911bac052 Merge pull request #86 from alibaba/dependabot/npm_and_yarn/development-dependencies-402857438c
chore(deps-dev): bump the development-dependencies group with 3 updates
2025-12-23 16:09:21 +08:00
Simon 87a8da5783 Merge pull request #85 from alibaba/dependabot/npm_and_yarn/production-dependencies-5419ff3310
chore(deps): bump zod from 4.2.0 to 4.2.1 in the production-dependencies group
2025-12-23 16:07:51 +08:00
dependabot[bot] 0aec35aee3 chore(deps-dev): bump the development-dependencies group with 3 updates
Bumps the development-dependencies group with 3 updates: [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh), [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) and [wouter](https://github.com/molefrog/wouter).


Updates `eslint-plugin-react-refresh` from 0.4.25 to 0.4.26
- [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.25...v0.4.26)

Updates `typescript-eslint` from 8.50.0 to 8.50.1
- [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.1/packages/typescript-eslint)

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

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.4.26
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.50.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: wouter
  dependency-version: 3.9.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-23 08:06:54 +00:00
Simon 35fe51427c feat(website): homepage makeover 2025-12-23 16:04:44 +08:00
dependabot[bot] 9e0b5bec8b 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.2.0 to 4.2.1
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.2.0...v4.2.1)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 19:20:12 +00:00
Simon 70d33a9756 feat: update AGENTS.md; add instructions for website 2025-12-22 21:42:05 +08:00
Simon 1c99d21c19 feat(website): add shadcn and magicUI components 2025-12-22 21:39:27 +08:00
Simon e3c612c338 feat(website): add shadcn 2025-12-22 20:49:32 +08:00
Simon 87596469b6 docs: cleanup 2025-12-22 19:56:53 +08:00
Simon 566b2d7022 docs: update model recommendations 2025-12-22 19:55:37 +08:00
Simon f521bd9beb docs: update model integration doc 2025-12-22 19:41:47 +08:00
Simon 35db15460e feat: improve perf on newer models 2025-12-22 19:10:13 +08:00
Simon a34d86ba75 chore: add cleanup script 2025-12-22 17:10:52 +08:00
Simon 3144e8d44d Merge branch 'refactor/sep-llms' 2025-12-22 17:05:33 +08:00
Simon 32be34bcb1 chore(version): bump version to 0.0.15 2025-12-22 17:02:14 +08:00
Simon a774bd94dc Merge pull request #84 from alibaba/refactor/sep-llms
refactor: separate `@page-agent/llms`
2025-12-22 16:38:46 +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
Simon 49d6163a70 fix: release action; bump version to 0.0.7 2025-12-05 17:33:41 +08:00
Simon 20f33ac478 fix: monorepo build order 2025-12-05 17:02:30 +08:00
Simon 683602bb6b refactor(PageController): implement PageController 2025-12-05 16:18:01 +08:00
Simon ad19a26a57 refactor(PageController): mv dom and actions 2025-12-02 21:30:39 +08:00
dependabot[bot] 51f81fc44c chore(deps): bump actions/checkout in the github-actions group (#56)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:20:08 +08:00
dependabot[bot] 71905a6ca0 chore(deps-dev): bump the development-dependencies group with 2 updates (#57)
Bumps the development-dependencies group with 2 updates: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [i18next](https://github.com/i18next/i18next).


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

Updates `i18next` from 25.6.3 to 25.7.1
- [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.6.3...v25.7.1)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: i18next
  dependency-version: 25.7.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:19:30 +08:00
dependabot[bot] 9ada66bb43 chore(deps-dev): bump @trivago/prettier-plugin-sort-imports (#58)
Bumps [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) from 5.2.2 to 6.0.0.
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v5.2.2...v6.0.0)

---
updated-dependencies:
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-version: 6.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:18:46 +08:00
Simon 2d66915570 chore: simplify published pkg 2025-12-01 21:05:10 +08:00
Simon 25098f8772 chore(version): bump version to 0.0.5 2025-12-01 20:53:56 +08:00
Simon 8a67b96fdb chore: add version sync script 2025-12-01 20:52:26 +08:00
Simon 682a08e0dc chore: automatically copy readme when publish 2025-12-01 20:33:10 +08:00
Simon b53d4dd8a5 chore: clean up 2025-12-01 20:26:52 +08:00
Simon a4e48d32a4 chore: fix github actions 2025-12-01 20:20:27 +08:00
Simon cdd1602f79 chore: issue template wording 2025-12-01 20:16:05 +08:00
Simon adec9d8197 refactor: monorepo 2025-12-01 20:11:12 +08:00
Simon 1b9970da14 chore: lint warnings 2025-11-26 20:41:55 +08:00
Simon aa8f5ae235 chore: lint 2025-11-26 20:39:37 +08:00
Simon 60a6b5e5b6 chore: wording; tailwind lint 2025-11-26 19:56:47 +08:00
Simon 7ae8fe753a feat: update logo 2025-11-25 21:00:41 +08:00
Simon a1dfe879c1 chore: update issue templates 2025-11-25 19:46:18 +08:00
Simon 6e7619760f chore: update lock 2025-11-24 20:33:57 +08:00
Simon efc85da91c Merge pull request #41 from alibaba/dependabot/npm_and_yarn/development-dependencies-8c725c2457
chore(deps-dev): bump the development-dependencies group across 1 directory with 17 updates
2025-11-24 20:31:21 +08:00
Simon 8e11fcb918 chore: i18n improve 2025-11-24 20:30:12 +08:00
dependabot[bot] 4a9bd9aed0 chore(deps-dev): bump the development-dependencies group across 1 directory with 17 updates
Bumps the development-dependencies group with 16 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.38.0` | `9.39.1` |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.53.1` | `7.55.0` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.15` | `4.1.17` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.2` | `19.2.5` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.2` | `19.2.3` |
| [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc) | `4.1.0` | `4.2.2` |
| [eslint](https://github.com/eslint/eslint) | `9.38.0` | `9.39.1` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.2.2` | `2.3.5` |
| [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) | `7.0.0` | `7.0.1` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.2.2` | `2.3.5` |
| [globals](https://github.com/sindresorhus/globals) | `16.4.0` | `16.5.0` |
| [i18next](https://github.com/i18next/i18next) | `25.6.0` | `25.6.2` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.2.5` | `16.2.6` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.1.4` | `16.3.3` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.46.2` | `8.47.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.11` | `7.2.2` |



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

Updates `@microsoft/api-extractor` from 7.53.1 to 7.55.0
- [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.0/apps/api-extractor)

Updates `@tailwindcss/vite` from 4.1.15 to 4.1.17
- [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.17/packages/@tailwindcss-vite)

Updates `@types/react` from 19.2.2 to 19.2.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 19.2.2 to 19.2.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `@vitejs/plugin-react-swc` from 4.1.0 to 4.2.2
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react-swc@4.2.2/packages/plugin-react-swc)

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

Updates `eslint-plugin-react-dom` from 2.2.2 to 2.3.5
- [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.5/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-hooks` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

Updates `eslint-plugin-react-x` from 2.2.2 to 2.3.5
- [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.5/packages/plugins/eslint-plugin-react-x)

Updates `globals` from 16.4.0 to 16.5.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.4.0...v16.5.0)

Updates `i18next` from 25.6.0 to 25.6.2
- [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.6.0...v25.6.2)

Updates `lint-staged` from 16.2.5 to 16.2.6
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.2.5...v16.2.6)

Updates `react-i18next` from 16.1.4 to 16.3.3
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.1.4...v16.3.3)

Updates `tailwindcss` from 4.1.15 to 4.1.17
- [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.17/packages/tailwindcss)

Updates `typescript-eslint` from 8.46.2 to 8.47.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.47.0/packages/typescript-eslint)

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

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.55.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.1.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@vitejs/plugin-react-swc"
  dependency-version: 4.2.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.3.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-hooks
  dependency-version: 7.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.3.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: globals
  dependency-version: 16.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: i18next
  dependency-version: 25.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: lint-staged
  dependency-version: 16.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.1.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.47.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: vite
  dependency-version: 7.2.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 19:50:43 +00:00
Simon 0f697c21bd chore: update lock 2025-10-24 21:47:25 +08:00
Simon e0d6d0d51b docs: plan 2025-10-24 17:50:57 +08:00
Simon d92ba7d882 fix: update aria-label for theme switcher button 2025-10-24 17:21:39 +08:00
Simon 3e8e584065 fix: demo video cn 2025-10-24 17:06:32 +08:00
Simon 9bdd565feb fix: demo video cn 2025-10-24 17:05:32 +08:00
Simon b0a31e777b fix: demo video
Updated demo video source in README.
2025-10-24 17:03:56 +08:00
Simon 78a54d0d3d docs: 演示视频 2025-10-24 16:56:26 +08:00
Simon 7f04f3564a fix: gemini response format 2025-10-24 16:54:05 +08:00
Simon 22a25b17f5 Merge pull request #36 from gaomeng1900/main
docs update
2025-10-24 01:21:01 +08:00
Simon 8a63c1b716 feat: Enhance code highlighting 2025-10-24 00:50:51 +08:00
Simon ab710bcaf5 stye: optimize mobile header with icon navigation 2025-10-24 00:07:15 +08:00
Simon df449166d5 docs: wording 2025-10-23 21:20:48 +08:00
Simon da822c5fe1 docs: wording 2025-10-23 20:48:57 +08:00
Simon 78412323a7 docs: wording 2025-10-23 20:45:03 +08:00
Simon ad06cd1869 docs: wording 2025-10-23 20:42:27 +08:00
Simon 86682c3a1e docs: wording 2025-10-23 20:41:12 +08:00
Simon fee3bbdf85 docs: Authentication suggestion 2025-10-23 20:25:55 +08:00
Simon 4d3c878809 feat: add experimentalScriptExecutionTool 2025-10-23 19:59:17 +08:00
Simon e704398c43 feat: add Google Analytics tracking script 2025-10-23 16:16:20 +08:00
Simon 56d20904b9 docs: demo 2025-10-23 16:07:59 +08:00
Simon bdddcb860e chore: add Open Graph meta tags for better sharing 2025-10-23 15:43:12 +08:00
Simon e6e8ebf4e3 docs: wording 2025-10-23 15:37:22 +08:00
Simon 9b1131ce13 docs: wording 2025-10-23 15:32:56 +08:00
Simon 462d76dbb6 docs: update CDN links to version 0.0.4 2025-10-23 15:27:31 +08:00
Simon 3791d1e157 chore: wording 2025-10-23 15:17:05 +08:00
Simon 2e3355f52c fix: update language setting to use i18n configuration 2025-10-22 23:07:23 +08:00
198 changed files with 11731 additions and 9022 deletions
+21 -27
View File
@@ -6,32 +6,11 @@ body:
- type: markdown
attributes:
value: |
Please follow our community communication guidelines when filing issues.
- Be polite and respectful.
- Prefer constructive, actionable feedback over venting.
- Include clear reproduction steps and environment details.
- Consider contributing a fix if possible — PRs are welcome.
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
提交 Issue 时请遵守社区沟通规范:
- 保持礼貌与尊重。
- 提供建设性、可行动的反馈,避免情绪化表达。
- 请包含清晰的复现步骤与环境信息。
- 如果可以,欢迎直接提交修复 PR。
See: CODE_OF_CONDUCT.md / CODE_OF_CONDUCT_zh.md, CONTRIBUTING.md
- type: checkboxes
id: community
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true
- label: I will share constructive, actionable details. / 我会提供建设性、可行动的细节。
required: true
- label: I have read the Code of Conduct. / 我已阅读行为准则。
required: true
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
- type: textarea
id: description
@@ -48,7 +27,7 @@ body:
render: typescript
placeholder: Minimal reproduction code
validations:
required: true
required: false
- type: input
id: browser
@@ -56,12 +35,27 @@ body:
label: Browser
placeholder: 'Chrome 120, Firefox 119, etc.'
validations:
required: true
required: false
- type: input
id: version
attributes:
label: version
placeholder: '0.0.0'
validations:
required: false
- type: checkboxes
id: community
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
required: true
- label: I have read the Code of Conduct. / 我已阅读行为准则。
required: true
validations:
required: true
-3
View File
@@ -12,6 +12,3 @@ contact_links:
- name: Code of Conduct / 行为准则
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md
about: Community expectations and standards. 社区行为期望与标准。
- name: 行为准则(中文)
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT_zh.md
about: 中文版社区行为准则。
+22 -33
View File
@@ -6,19 +6,30 @@ body:
- type: markdown
attributes:
value: |
Please follow our community communication guidelines when proposing features.
- Be polite and respectful.
- Explain the problem first, then propose solutions.
- Prefer constructive, actionable suggestions.
- Contributions are encouraged — consider opening a PR.
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
提交功能建议时请遵守社区沟通规范:
- 保持礼貌与尊重。
- 先阐述问题,再提出解决方案。
- 提供建设性、可行动的建议。
- 鼓励代码贡献,欢迎直接提交 PR。
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
See: CODE_OF_CONDUCT.md / CODE_OF_CONDUCT_zh.md, CONTRIBUTING.md
- type: textarea
id: description
attributes:
label: Feature Description / 功能描述
description: Describe the problem, solution, and any API changes. / 描述问题、解决方案以及 API 变更。
placeholder: |
**Problem**:
What problem does this solve?
**Solution**:
How should this work?
**Proposed API**:
```typescript
// code here
```
validations:
required: true
- type: checkboxes
id: community
@@ -32,27 +43,5 @@ body:
required: true
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
required: true
- type: textarea
id: problem
attributes:
label: Problem
placeholder: What problem does this solve?
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution
placeholder: How should this work?
validations:
required: true
- type: textarea
id: api
attributes:
label: Proposed API
render: typescript
validations:
required: false
+5 -8
View File
@@ -5,9 +5,12 @@ Brief description of changes.
## Type
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Feature / Improvement
- [ ] Refactor
- [ ] Documentation
- [ ] Website
- [ ] Demo / Testing
- [ ] Breaking change
## Testing
@@ -19,10 +22,4 @@ Closes #(issue)
## Requirements / 要求
- [ ] I will be polite and respectful. / 我会保持礼貌与尊重。
- [ ] My comments and replies are constructive and actionable. / 我的评论与回复具有建设性。
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
## Contributing / 贡献
Constructive suggestions and code contributions are encouraged. If this PR originated from a discussion or issue, please link it above. 欢迎建设性意见与代码贡献;如源自讨论或 Issue,请在上方关联链接。
+9 -2
View File
@@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
node-version: [20, 24, 25]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@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
+4 -4
View File
@@ -13,19 +13,19 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- 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
@@ -33,7 +33,7 @@ jobs:
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: './dist'
path: './packages/website/dist'
- name: Deploy to GitHub Pages
id: deployment
+7 -7
View File
@@ -6,30 +6,30 @@ on:
- 'v*'
permissions:
id-token: write # Required for OIDC
id-token: write # Required for OIDC
contents: read
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- 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
- name: Update npm
run: npm install -g npm@latest
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build:lib
run: npm run build:libs
- name: Publish to npm
run: npm publish
- name: Publish all public packages
run: npm publish --workspaces --access public
+7 -1
View File
@@ -23,7 +23,13 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.qoder
#
.env
.env
# Copied files for npm publish (generated by prepublishOnly)
packages/page-agent/README.md
packages/page-agent/README-zh.md
packages/page-agent/LICENSE
+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"]
}
+29 -16
View File
@@ -1,18 +1,31 @@
{
"editor.fontLigatures": true,
"cSpell.words": ["HITL", "opensource", "retryable"],
"markdownlint.config": {
// "comment": "Relaxed rules",
"default": true,
"whitespace": false,
"line_length": false,
"ul-indent": false,
"no-inline-html": false,
"no-bare-urls": false,
"fenced-code-language": false,
"first-line-h1": false,
"block-spacing": false,
"blanks-around-lists": false,
"ol-prefix": false
}
"editor.fontLigatures": true,
"cSpell.words": [
"deepseek",
"historychange",
"HITL",
"innerhtml",
"llms",
"onwarn",
"opensource",
"qwen",
"retryable",
"shadcn",
"statuschange",
"wouter"
],
"markdownlint.config": {
// "comment": "Relaxed rules",
"default": true,
"whitespace": false,
"line_length": false,
"ul-indent": false,
"no-inline-html": false,
"no-bare-urls": false,
"fenced-code-language": false,
"first-line-h1": false,
"block-spacing": false,
"blanks-around-lists": false,
"ol-prefix": false
}
}
+99 -117
View File
@@ -1,161 +1,143 @@
# Instructions for coding assistants
# Instructions for Coding Assistants
## Project Overview
This is a dual-architecture project with **two separate parts**:
This is a **monorepo** with npm workspaces:
1. **Core Library** (`src/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation
2. **Demo&docs Website** (`pages/`) - React documentation and landing page
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
Internal packages:
- **Core** (`packages/core/`) - PageAgentCore without UI (npm: `@page-agent/core`)
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
- **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback (SimulatorMask), independent of LLM
- **UI** (`packages/ui/`) - Panel and i18n. Decoupled from PageAgent
## Development Commands
### Core Commands
```bash
npm start # Start React website development server
npm run build # Build both library AND website
npm run build:lib # Build pure JS library only (src/ → dist/lib/)
npm run build:lib:watch # Library development with auto-rebuild
npm start # Start website dev server
npm run build # Build all packages
npm run build:libs # Build all libraries
npm run lint # ESLint with TypeScript strict rules
```
## Architecture & Critical Patterns
## Architecture
### Dual Build System
### Monorepo Structure
- **Website build**: `vite.config.js` → React SPA with hash routing → `dist/`
- **Library build**: `vite.lib.config.js` → UMD/ES modules → `dist/lib/`
- **Entry points**: `src/entry.ts` (library), `pages/main.tsx` (website)
Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig and vite config when adding/removing packages.
### Module Boundaries (Critical)
```
packages/
├── page-agent/ # npm: "page-agent" ⭐ MAIN (with Panel UI + IIFE builds)
├── core/ # npm: "@page-agent/core" (headless, no UI)
├── website/ # @page-agent/website (private)
├── llms/ # @page-agent/llms
├── page-controller/ # @page-agent/page-controller
└── ui/ # @page-agent/ui
```
- **Core library** (`src/`): NEVER import from `pages/` - must remain pure JavaScript
- **Website** (`pages/`): CAN import from `src/` via `@/` alias for demos
- **Import aliases**: `@/``src/`, `@pages/``pages/`
`workspaces` in `package.json` must be in topological order.
### Module Boundaries
- **Page Agent**: Main entry with UI. Extends PageAgentCore and adds Panel. Imports from `@page-agent/core`, `@page-agent/ui`
- **Core**: PageAgentCore without UI. Imports from `@page-agent/llms`, `@page-agent/page-controller`
- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent
- **UI**: Panel and i18n. Decoupled from PageAgent via PanelAgentAdapter interface
- **Page Controller**: DOM operations with optional visual feedback (SimulatorMask). No LLM dependency. Enable mask via `enableMask: true` config
### PageController ↔ PageAgent Communication
All communication is async and isolated:
```typescript
// PageAgent delegates DOM operations to PageController
await this.pageController.updateTree()
await this.pageController.clickElement(index)
await this.pageController.inputText(index, text)
await this.pageController.scroll({ down: true, numPages: 1 })
// PageController exposes state via async methods
const simplifiedHTML = await this.pageController.getSimplifiedHTML()
const pageInfo = await this.pageController.getPageInfo()
```
### DOM Pipeline
1. **DOM Extraction**: Convert live DOM to `FlatDomTree` via `src/dom/dom_tree/`
2. **Dehydration**: DOM tree → simplified text for LLM processing
3. **LLM Processing**: AI model returns action plans
4. **Indexed Operations**: Map LLM responses back to specific DOM elements
1. **DOM Extraction**: Live DOM `FlatDomTree` via `page-controller/src/dom/dom_tree/`
2. **Dehydration**: DOM tree → simplified text for LLM
3. **LLM Processing**: AI returns action plans (page-agent)
4. **Indexed Operations**: PageAgent calls PageController by element index
### Event Bus Communication
### IIFE Builds (`packages/page-agent/dist/iife/`)
Use `src/utils/bus.ts` for decoupled PageAgent ↔ UI communication:
Two IIFE builds for script tag usage:
```typescript
// Emit from PageAgent
getEventBus().emit('panel:show')
getEventBus().emit('panel:update', { status: 'thinking' })
| Build | File | Description |
| ----- | -------------------- | -------------------------------- |
| Demo | `page-agent.demo.js` | Auto-init with built-in test API |
| Full | `page-agent.js` | Exposes `PageAgent` class only |
// Listen in UI components
getEventBus().on('panel:show', () => panel.show())
```
Demo build supports query params (e.g., `?model=gpt-4&lang=en-US`).
### Hash Routing Requirement
Build with `npm run build:iife --workspace=page-agent`.
Uses wouter with `useHashLocation` for static hosting:
## Key Files Reference
```tsx
<Router hook={useHashLocation}> // Always hash-based routes
```
### Page Agent (`packages/page-agent/`)
### CDN Auto-Injection Pattern
| File | Description |
| ----------------------- | -------------------------------------------- |
| `src/PageAgent.ts` | ⭐ Main class with UI, extends PageAgentCore |
| `src/entry-iife.ts` | IIFE entry (exposes PageAgent class) |
| `src/entry-iife-demo.ts`| IIFE demo entry (auto-init with demo API) |
Library auto-initializes when injected via script tag:
### Core (`packages/core/`)
```html
<script src="page-agent.js?model=gpt-4"></script>
```
| File | Description |
| ----------------------- | ------------------------------------------- |
| `src/PageAgentCore.ts` | ⭐ Core agent class without UI |
| `src/tools/` | Tool definitions calling PageController |
| `src/config/` | Configuration types and constants |
| `src/prompts/` | System prompt templates |
Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
### LLMs (`packages/llms/`)
## File Organization
| File | Description |
| ---------------------------- | ------------------------------------- |
| `src/index.ts` | ⭐ LLM class with retry logic |
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig |
| `src/OpenAILenientClient.ts` | OpenAI-compatible client |
### Core Library (`src/`)
### Page Controller (`packages/page-controller/`)
- `entry.ts` - CDN/UMD entry point with auto-initialization
- `PageAgent.ts` - **Main AI agent class** orchestrating DOM operations
- `tools/` - Agent tool implementations for web actions
- `ui/` - UI components (Panel, SimulatorMask) with CSS modules
- `utils/bus.ts` - **Type-safe event bus** for decoupled communication
- `patches/` - Framework-specific optimizations (React, Antd compatibility)
- `llms/` - LLM integration and communication layer
- `dom/` - HTML serialization and page analysis utilities
- `config/` - Configuration constants and settings
### Website (`pages/`)
- `main.tsx` - Site entry with hash routing setup
- `router.tsx` - **Manual route definitions** (requires explicit registration)
- `components/DocsLayout.tsx` - Navigation structure (hardcoded nav items)
- `docs/[section]/[topic]/page.tsx` - Documentation pages
- `test-pages/` - Library integration test pages
| File | Description |
| --------------------------- | ---------------------------------------------------------- |
| `src/PageController.ts` | ⭐ Main controller class with optional mask support |
| `src/SimulatorMask.ts` | Visual overlay blocking user interaction during automation |
| `src/actions.ts` | Element interactions (click, input, scroll) |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine |
## Adding New Features
### New Documentation Page
1. Create `pages/docs/<section>/<slug>/page.tsx`
2. Add route to `pages/router.tsx` with `<Header /> + <DocsLayout>` wrapper
3. Add navigation item to `DocsLayout.tsx`
### New Agent Tool
1. Implement under `src/tools/`
2. Export via `src/tools/index.ts`
3. Wire into `PageAgent.ts` if needed
1. Implement in `packages/core/src/tools/index.ts`
2. If tool needs DOM ops, add method to PageController first
3. Tool calls `this.pageController.methodName()` for DOM interactions
### New UI Component
### New PageController Action
1. Create in `src/ui/` with colocated CSS modules
2. Use event bus for PageAgent communication
3. Test via `pages/test-pages/`
1. Add implementation in `packages/page-controller/src/actions.ts`
2. Expose via async method in `PageController.ts`
3. Export from `packages/page-controller/src/index.ts`
## Code Standards
### TypeScript
- Explicit typing for exported/public APIs
- ESLint relaxes some unsafe rules for rapid iteration
### CSS & Styling
- **Prefer Tailwind CSS over custom CSS**
- Custom CSS variables for theme gradients in `src/index.css`
- Dark mode support via `dark:` classes
- CSS modules for component-specific styles
### Import Organization
- External libraries first
- Internal modules (`@/`, `@pages/`)
- Relative imports last
- Blank lines between groups
## Critical Files to Understand
- `pages/router.tsx` - Central routing definition (manual registration required)
- `pages/components/DocsLayout.tsx` - Navigation structure
- `src/PageAgent.ts` - Core AI agent class with DOM manipulation
- `src/dom/dom_tree/index.js` - DOM extraction engine
- `src/utils/bus.ts` - Type-safe event bus system
- `src/entry.ts` - Library entry point for CDN usage
- `vite.config.js` / `vite.lib.config.js` - Dual build configuration
## Debugging Common Issues
### Blank Documentation Pages
1. Verify route exists in `pages/router.tsx`
2. Check component import path
3. Verify CSS isn't hiding content (check dark mode classes)
4. Test with minimal component first
### Library Integration Issues
1. Check `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 `pages/test-pages/` for isolated testing
- Every change you make should not only implement the desired functionality but also improve the quality of the codebase
- All code and comments must be in English.
+70 -66
View File
@@ -7,25 +7,30 @@ 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**
```bash
npm install
npm start # Start demo and documentation site
```
```bash
npm ci
npm start # Start demo and documentation site
```
### Project Structure
This project has **two separate parts**:
This is a **monorepo** with npm workspaces containing **3 main packages**:
- **Core Library** (`src/`) - Pure JavaScript AI agent library
- **Documentation Website** (`pages/`) - React web app for landing page and docs
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
- **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 + 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 `IIFE` and `Website`. Bundle everything including local packages.
## 🤝 How to Contribute
@@ -40,40 +45,39 @@ This project has **two separate parts**:
1. **Fork and Clone**
```bash
git clone https://github.com/your-username/page-agent.git
cd page-agent
```
```bash
git clone https://github.com/your-username/page-agent.git
cd page-agent
```
2. **Create Feature Branch**
```bash
git checkout -b feat/your-feature-name
```
```bash
git checkout -b feat/your-feature-name
```
3. **Make Changes**
- Follow existing code style and patterns
- Add tests for new functionality
- Update documentation as needed
- Follow existing code style and patterns
- Add tests for new functionality
- Update documentation as needed
4. **Test Your Changes**
```bash
# TODO
```
- Test in our demo website
- Test it on other websites if applicable
- `@TODO: test suite`
5. **Commit and Push**
```bash
git add .
git commit -m "feat: add awesome feature"
git push origin feat/your-feature-name
```
```bash
git add .
git commit -m "feat: add awesome feature"
git push origin feat/your-feature-name
```
6. **Create Pull Request**
- Provide clear description of changes
- Link related issues
- Include screenshots for UI changes
- Provide clear description of changes
- Link related issues
- Include screenshots for UI changes
## 📝 Code Style
@@ -90,52 +94,52 @@ This project has **two separate parts**:
- It's **recommended** to heavily rely on AI (aka "vibe coding") when maintaining **demo pages and tests**.
- BUT **NOT the core lib!!!** Be very careful if AI ever touched the core lib!!!
- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.
- Update the AI instructions when structure changed.
- Cursor and Cline: `./.cursor/rules`
- 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
- Update the [AI instructions](AGENTS.md) when structure changed.
## 🔧 Development Workflows
### Test With Your Own LLM API
- Create a `.env` file in the repo root with your LLM API config
```env
LLM_MODEL_NAME=gpt-5.2
LLM_API_KEY=your-api-key
LLM_BASE_URL=https://api.your-llm-provider.com/v1
```
- Restart the dev server to load new env vars
- If not provided, the demo will the free testing proxy by default
### Website Development
```bash
npm start # React development server
npm start
```
### Core Lib Development and Testing
### Testing on Other Websites
- Config your LLM API
- Start a local script
- Start and serve a local `iife` script
```bash
npm run build:lib:watch # Auto-rebuild on changes
```
```bash
npm run dev:iife # Serving IIFE with auto rebuild at http://localhost:5174/page-agent.js
```
- Add a new bookmark enable it on other website
- Add a new bookmark
```
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
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5174/page-agent.js?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
```
- Click the bookmark on any page to load Page-Agent
> Warning: AK in your local `.env` will be inlined in the iife script.
### 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`
Ask an AI to help you add documentation to the `website/` package. Follow the existing style.
> Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated.
## 🎯 Contribution Areas
@@ -150,7 +154,7 @@ We especially welcome contributions in:
## 🚫 What We Don't Accept
- Changes that break existing API compatibility
- Changes that break existing API compatibility (Discuss first)
- Heavy dependencies to core library
- Contributions without proper testing
- Code that doesn't follow project conventions
+2 -1
View File
@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2025 Alibaba
Copyright (c) 2026 Alibaba
Copyright (c) 2026 Simon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+48 -47
View File
@@ -1,16 +1,19 @@
# PageAgent 🤖🪄
![banner](https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[![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)
[![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)
**让你的网页支持 AI 自动化。**
运行在页面内的 UI agent. 使用自然语言操作 Web 应用。
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
🌐 [English](./README.md) | **中文**
👉 [🚀 **Demo**](https://alibaba.github.io/page-agent/) | [📖 **Documentation**](https://alibaba.github.io/page-agent/#/docs/introduction/overview)
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
---
@@ -24,21 +27,28 @@
## 🗺️ Roadmap
👉 [**Roadmap**](./ROADMAP.md)
👉 [**Roadmap**](https://github.com/alibaba/page-agent/issues/96)
## 🚀 快速开始
### CDN 集成
### 快速体验 (With Testing LLM)
最快的体验方式:
```html
<!-- 临时 CDN URL. 未来会变更 -->
<script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js"
crossorigin="true"
type="text/javascript"
src="https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js"
crossorigin="true"
></script>
```
> ⚠️ **仅用于技术评估。** Demo 模型有速率限制和使用限制,生产环境请使用 NPM 方式。
| Mirrors | URL |
| ------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js |
| China | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js |
### NPM 安装
```bash
@@ -48,63 +58,50 @@ npm install page-agent
```javascript
import { PageAgent } from 'page-agent'
// 测试接口
// @note: 限流,限制 prompt 内容,限制来源,随时变更,请替换成你自己的
// @note: 使用 DeepSeek-chat(3.2) 官方版本,使用协议和隐私策略见 DeepSeek 网站
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 agent = new PageAgent({
modelName: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'zh-CN',
model: 'deepseek-chat',
baseURL: 'https://api.deepseek.com',
apiKey: 'YOUR_API_KEY',
language: 'zh-CN',
})
await agent.execute('点击登录按钮')
```
适用于无法使用 NPM 的环境,我们也提供了 IIFE 构建的 CDN 方式。[@see CDN Usage](https://alibaba.github.io/page-agent/#/docs/integration/cdn-setup)
## 🏗️ 架构设计
PageAgent 采用清晰的模块化架构:
PageAgent adopts a simplified monorepo structure:
```
src/
├── PageAgent.ts # Agent 主流程
├── dom/ # DOM 理解
├── tools/ # 代理交互工具
├── ui/ # UI 组件和面板
├── llms/ # LLM 集成层
── utils/ # 事件总线和工具
packages/
├── page-agent/ # AI agent with UI Panel(npm: page-agent)
├── core/ # Agent core logic without UI(npm: @page-agent/core)
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
├── ui/ # 面板 & i18n (npm: @page-agent/ui)
── cdn/ # CDN IIFE builds (npm: @page-agent/cdn)
└── website/ # 文档站点
```
## 🤝 贡献
欢迎社区贡献!以下是参与方式:
### 开发环境
1. Fork 并 clone。`git clone https://github.com/alibaba/page-agent.git && cd page-agent`
2. 安装依赖: `npm install`
3. 启动开发: `npm start`
1. Fork 项目仓库
2. Clone or fork: `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
3. 安装依赖: `npm install`
4. 启动开发: `npm start`
更多详情请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
### 贡献指南
请在贡献前阅读我们的[行为准则](CODE_OF_CONDUCT.md)和[贡献指南](CONTRIBUTING.md)。
请在贡献前阅读我们的[行为准则](CODE_OF_CONDUCT.md)。
## 👏 致谢
本项目基于以下优秀项目构建
本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建
- **[browser-use](https://github.com/browser-use/browser-use)**
PageAgent 专为**客户端网页增强**设计,不是服务端自动化工具。
## 📄 许可证
MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。
`PageAgent` 专为**客户端网页增强**设计,不是服务端自动化工具。
```
DOM processing components and prompt are derived from browser-use:
@@ -123,6 +120,10 @@ Third-party dependencies and their licenses can be found in the package.json
file and in the node_modules directory after installation.
```
## 📄 许可证
[MIT License](LICENSE)
---
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
+47 -48
View File
@@ -1,22 +1,25 @@
# PageAgent 🤖🪄
![banner](https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[![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)
[![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)
**Transform your webpage into an AI-powered application with a single script tag.**
An in-page UI agent in javascript. Control web interfaces with natural language.
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
🌐 **English** | [中文](./README-zh.md)
👉 [🚀 **Demo**](https://alibaba.github.io/page-agent/) | [📖 **Documentation**](https://alibaba.github.io/page-agent/#/docs/introduction/overview)
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
---
## ✨ Features
- **🎯 Easy Integration**
- **🎯 Easy Integration** - Transform your webpage into an agent with a single script tag.
- **🔐 Client-Side Processing**
- **🧠 DOM Extraction**
- **💬 Natural Language Interface**
@@ -24,21 +27,28 @@ An in-page UI agent in javascript. Control web interfaces with natural language.
## 🗺️ Roadmap
👉 [**Roadmap**](./ROADMAP.md)
👉 [**Roadmap**](https://github.com/alibaba/page-agent/issues/96)
## 🚀 Quick Start
### CDN Integration
### Quick Try (With Testing LLM)
Fastest way to try PageAgent:
```html
<!-- temporary CDN URL. May change in the future -->
<script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js"
crossorigin="true"
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/page-agent/dist/iife/page-agent.demo.js"
crossorigin="true"
></script>
```
> ⚠️ **For technical evaluation only.** Demo model has rate limits and usage restrictions. Use NPM for production.
| Mirrors | URL |
| ------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/latest/files/dist/iife/page-agent.demo.js |
### NPM Installation
```bash
@@ -48,18 +58,11 @@ npm install page-agent
```javascript
import { PageAgent } from 'page-agent'
// test server
// @note: rate limit. prompt limit. Origin limit. May change anytime. Use your own llm!
// @note Using official DeepSeek-chat(3.2). Go to DeepSeek website for privacy policy.
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 agent = new PageAgent({
modelName: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'en-US',
model: 'deepseek-chat',
baseURL: 'https://api.deepseek.com',
apiKey: 'YOUR_API_KEY',
language: 'en-US',
})
await agent.execute('Click the login button')
@@ -67,44 +70,36 @@ await agent.execute('Click the login button')
## 🏗️ Structure
PageAgent follows a clean, modular architecture:
PageAgent adopts a simplified monorepo structure:
```
src/
├── PageAgent.ts # Agent main loop
├── dom/ # DOM processing
├── tools/ # Agent tools
├── ui/ # UI components & panels
├── llms/ # LLM integration layer
── utils/ # Event bus & utilities
packages/
├── page-agent/ # AI agent with UI Panel(npm: page-agent)
├── core/ # Agent core logic without UI(npm: @page-agent/core)
├── llms/ # LLM client (npm: @page-agent/llms)
├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
├── ui/ # Panel & i18n (npm: @page-agent/ui)
── cdn/ # CDN IIFE builds (npm: @page-agent/cdn)
└── website/ # Demo & Documentation site
```
## 🤝 Contributing
We welcome contributions from the community! Here's how to get started:
### Setup
1. Fork and clone. `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
2. Install dependencies: `npm install`
3. Start development: `npm start`
1. Fork the repository
2. Clone your fork: `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
3. Install dependencies: `npm install`
4. Start development: `npm start`
More details in [CONTRIBUTING.md](CONTRIBUTING.md).
### Contributing Guidelines
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) before contributing.
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing.
## 👏 Acknowledgments
This project builds upon the excellent work of:
This project builds upon the excellent work of **[`browser-use`](https://github.com/browser-use/browser-use)**.
- **[browser-use](https://github.com/browser-use/browser-use)**
PageAgent is designed for **client-side web enhancement**, not server-side automation.
## 📄 License
MIT License - see the [LICENSE](LICENSE) file for details.
`PageAgent` is designed for **client-side web enhancement**, not server-side automation.
```
DOM processing components and prompt are derived from browser-use:
@@ -123,6 +118,10 @@ Third-party dependencies and their licenses can be found in the package.json
file and in the node_modules directory after installation.
```
## 📄 License
[MIT License](LICENSE)
---
**⭐ Star this repo if you find PageAgent helpful!**
-55
View File
@@ -1,55 +0,0 @@
# 🗺️ PageAgent Roadmap
The development progress and future plans for PageAgent.
## 🚀 Current Works
- [x] **MVP**
- Core functionality implemented.
- [x] **SPA interaction**
- [x] **Reasoning and (short) memory**
- [x] **Multi model provider integration and testing**
- [x] **UI with HITL**
- Human-in-the-loop user interface. Agent can ask user questions.
- [x] **Landing and doc pages**
- [x] **Remove `ai-sdk`**
- Only one function of AI-ADK is being used.
- Our agent memory and thinking mechanism does not suite ai-sdk.
- [x] **Robust LLM output**
- Auto-fix incomplete output format of DeepSeek and QWen.
- [x] **Working homepage with live LLM API**
- [x] **~~free~~ CDN**
- [x] **Free evaluation plan**
- [x] **Custom actions and HITL**
- [ ] **Hooks and Events**
- [x] **lifecycle hooks**
- [ ] **lifecycle events**
- [ ] **❗Pause and intervene**
- Current pause does not allow user intervention.
- [ ] **❗Hijack `page_open/page_change/page_unload` behavior**
- [ ] **Custom knowledge base and instructions**
- [ ] **Black/white-list safeguard**
- [ ] **Data-masking**
- [ ] **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.
♻️ Following browser-use's update and contribute back.
## 📋 Pending Features
- [ ] **Chrome-ext wrapper**
- [ ] **Same-origin multi-page-app rally**
- [ ] **Local MCP proxy**
## 🤔 To Be Decided
- [ ] **Cross-origin multi-page?**
- Tricky
- Need some kind of "memory rally"
Vendored
-25
View File
@@ -1,25 +0,0 @@
/// <reference types="vite/client" />
import type { PageAgent } from './src/PageAgent'
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
declare module '*.md?raw' {
const content: string
export default content
}
/**
* for local dev and umd demo
*/
declare global {
interface Window {
pageAgent?: PageAgent
PageAgent: typeof PageAgent
__PAGE_AGENT_IDS__: string[]
}
}
+8 -3
View File
@@ -8,7 +8,7 @@ import globals from 'globals'
import tseslint from 'typescript-eslint'
export default defineConfig([
globalIgnores(['dist', 'test-pages']),
globalIgnores(['**/dist', '**/node_modules', 'packages/website/src/components/ui']),
{
plugins: {
'react-hooks': reactHooks,
@@ -37,8 +37,10 @@ export default defineConfig([
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname,
// project: ['./tsconfig.json'],
// project: ['./packages/*/tsconfig.json'],
// tsconfigRootDir: import.meta.dirname,
projectService: true,
},
ecmaVersion: 2020,
globals: globals.browser,
@@ -67,6 +69,9 @@ export default defineConfig([
'react-dom/no-missing-button-type': 'off',
'react-x/no-nested-component-definitions': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
// 'require-await': 'off',
'@typescript-eslint/require-await': 'off',
},
},
])
-45
View File
@@ -1,45 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="https://img.alicdn.com/imgextra/i3/O1CN01uo9VaC20mamWNM2Zn_!!6000000006892-55-tps-64-64.svg"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PageAgent - AI-powered UI Agent in Your Webpage</title>
<meta
name="description"
content="PageAgent.js: One-line CDN import, add intelligent UI Agent to any website. Modern web AI automation with minimal integration."
/>
<meta
name="keywords"
content="PageAgent, AI Agent, Web Automation, UI Automation, Frontend, CDN, JavaScript, React, Vite, LLM"
/>
<meta name="theme-color" content="#58c0fc" />
<meta name="color-scheme" content="light dark" />
<meta name="author" content="PageAgent.js Team" />
<meta property="og:title" content="PageAgent.js - AI-powered UI Agent" />
<meta
property="og:description"
content="One-line CDN import, add intelligent UI Agent to any website."
/>
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="zh_CN" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./pages/main.tsx"></script>
<script>
// Dynamically update html lang attribute based on i18n detection
const updateHtmlLang = () => {
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
document.documentElement.lang = lang
}
updateHtmlLang()
window.addEventListener('storage', updateHtmlLang)
</script>
</body>
</html>
+2170 -1024
View File
File diff suppressed because it is too large Load Diff
+57 -68
View File
@@ -1,35 +1,17 @@
{
"name": "page-agent",
"private": false,
"version": "0.0.4",
"name": "root",
"private": true,
"version": "0.3.0-beta.1",
"type": "module",
"main": "./dist/lib/page-agent.js",
"module": "./dist/lib/page-agent.js",
"types": "./dist/lib/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageAgent.d.ts",
"import": "./dist/lib/page-agent.js",
"default": "./dist/lib/page-agent.js"
}
},
"files": [
"dist/lib/",
"README.md",
"LICENSE",
"NOTICE"
],
"description": "AI-powered UI agent for web applications - add intelligent automation to any webpage with a single script tag",
"keywords": [
"ai",
"automation",
"ui-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"intelligent-ui"
"workspaces": [
"packages/page-controller",
"packages/ui",
"packages/llms",
"packages/core",
"packages/page-agent",
"packages/website"
],
"description": "AI-powered UI agent for web applications",
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
@@ -38,56 +20,47 @@
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": ">=20.0.0"
"node": ">=20.0.0",
"npm": ">=10.0.0"
},
"scripts": {
"dev": "vite",
"start": "vite",
"build": "tsc -b && vite build && npm run build:lib && npm run build:umd",
"build:lib": "MODE=lib vite build",
"build:lib:watch": "MODE=lib vite build --watch",
"build:umd": "MODE=umd vite build",
"cleanup": "rm -rf packages/*/dist",
"start": "npm run dev --workspace=@page-agent/website",
"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:iife": "npm run dev:iife --workspace=page-agent",
"version": "node scripts/sync-version.js",
"lint": "eslint .",
"prepare": "husky"
},
"dependencies": {
"ai-motion": "^0.4.7",
"chalk": "^5.6.2",
"zod": "^4.1.12"
},
"devDependencies": {
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@eslint/js": "^9.37.0",
"@microsoft/api-extractor": "^7.53.1",
"@tailwindcss/vite": "^4.1.14",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"@commitlint/cli": "^20.3.1",
"@commitlint/config-conventional": "^20.3.1",
"@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.55.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.0.7",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"eslint": "^9.37.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.0.6",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.23",
"eslint-plugin-react-x": "^2.0.6",
"globals": "^16.4.0",
"eslint-plugin-react-dom": "^2.5.7",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react-x": "^2.5.7",
"globals": "^17.0.0",
"husky": "^9.1.7",
"i18next": "^25.6.0",
"i18next-browser-languagedetector": "^8.2.0",
"lint-staged": "^16.2.4",
"prettier": "^3.6.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^16.1.4",
"tailwindcss": "^4.1.14",
"prettier": "^3.7.4",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.0",
"typescript-eslint": "^8.53.0",
"unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.1.9",
"vite-plugin-css-injected-by-js": "^3.5.2",
"wouter": "^3.7.1"
"vite": "^7.3.1",
"vite-plugin-css-injected-by-js": "^3.5.2"
},
"overrides": {
"typescript": "^5.9.3"
},
"lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [
@@ -129,6 +102,22 @@
".css$"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
"importOrderSortSpecifiers": true,
"overrides": [
{
"files": "*.md",
"options": {
"useTabs": false,
"tabWidth": 4
}
},
{
"files": "*.json",
"options": {
"useTabs": false,
"tabWidth": 4
}
}
]
}
}
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@page-agent/core",
"private": false,
"version": "0.3.0-beta.1",
"type": "module",
"main": "./dist/esm/page-agent-core.js",
"module": "./dist/esm/page-agent-core.js",
"types": "./dist/esm/PageAgentCore.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgentCore.d.ts",
"import": "./dist/esm/page-agent-core.js",
"default": "./dist/esm/page-agent-core.js"
}
},
"files": [
"dist/"
],
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
"keywords": [
"ai",
"automation",
"ui-agent",
"GUI-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"web-automation",
"GUI-simulation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
"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": {
"chalk": "^5.6.2",
"zod": "^4.3.5",
"@page-agent/llms": "0.3.0-beta.1",
"@page-agent/page-controller": "0.3.0-beta.1"
}
}
+588
View File
@@ -0,0 +1,588 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
import { LLM, type Tool } from '@page-agent/llms'
import type { PageController } from '@page-agent/page-controller'
import chalk from 'chalk'
import zod from 'zod'
import { type PageAgentConfig } from './config'
import { MAX_STEPS } from './config/constants'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools'
import {
AgentActivity,
AgentReflection,
AgentStatus,
AgentStep,
ExecutionResult,
HistoricalEvent,
MacroToolInput,
MacroToolResult,
} from './types'
import { normalizeResponse, trimLines, uid } from './utils'
import { assert } from './utils/assert'
export { type PageAgentConfig }
export { tool, type PageAgentTool } from './tools'
/**
* AI agent for browser DOM automation.
*
* @remarks
* ## Event System
* - `statuschange` - Agent status transitions (idle → running → completed/error)
* - `historychange` - History events updated (persistent, part of agent memory)
* - `activity` - Real-time activity feedback (transient, for UI only)
* - `dispose` - Agent cleanup triggered
*
* ## Information Streams
* 1. **History Events** (`history` array)
* - Persistent event stream that forms agent's memory
* - Included in LLM context across steps
* - Types: steps, observations, user takeovers, llm errors
*
* 2. **Activity Events** (via `activity` event)
* - Transient UI feedback during task execution
* - NOT included in LLM context
* - Types: thinking, executing, executed, retrying, error
*/
export class PageAgentCore extends EventTarget {
config: PageAgentConfig
id = uid()
tools: typeof tools
disposed = false
task = ''
taskId = ''
/** Agent execution status */
#status: AgentStatus = 'idle'
/**
* Callback for when agent needs user input (ask_user tool)
* If not set, ask_user tool will be disabled
* @example onAskUser: (q) => window.prompt(q) || ''
*/
onAskUser?: (question: string) => Promise<string>
#llm: LLM
#abortController = new AbortController()
/** PageController for DOM operations */
pageController: PageController
/** Runtime states for tracking across steps */
states = {
/** Accumulated wait time in seconds, used by wait tool */
totalWaitTime: 0,
/** Last known URL for detecting navigation */
lastURL: '',
}
/** History events */
history: HistoricalEvent[] = []
constructor(config: PageAgentConfig & { pageController: PageController }) {
super()
this.config = config
this.#llm = new LLM(this.config)
this.tools = new Map(tools)
this.pageController = config.pageController
// Listen to LLM retry events
this.#llm.addEventListener('retry', (e) => {
const { attempt, maxAttempts } = (e as CustomEvent).detail
this.emitActivity({ type: 'retrying', attempt, maxAttempts })
// Also push to history for panel rendering
this.history.push({
type: 'error',
errorType: 'retry',
message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
attempt,
maxAttempts,
})
this.#emitHistoryChange()
})
this.#llm.addEventListener('error', (e) => {
const { error } = (e as CustomEvent).detail
const message = String(error)
this.emitActivity({ type: 'error', message })
// Also push to history for panel rendering
this.history.push({
type: 'error',
errorType: 'error',
message,
})
this.#emitHistoryChange()
})
if (this.config.customTools) {
for (const [name, tool] of Object.entries(this.config.customTools)) {
if (tool === null) {
this.tools.delete(name)
continue
}
this.tools.set(name, tool)
}
}
if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript')
}
}
/** Get current agent status */
get status(): AgentStatus {
return this.#status
}
/** Emit statuschange event */
#emitStatusChange(): void {
this.dispatchEvent(new Event('statuschange'))
}
/** Emit historychange event */
#emitHistoryChange(): void {
this.dispatchEvent(new Event('historychange'))
}
/**
* Emit activity event - for transient UI feedback
* @param activity - Current agent activity
*/
emitActivity(activity: AgentActivity): void {
this.dispatchEvent(new CustomEvent('activity', { detail: activity }))
}
/** Update status and emit event */
#setStatus(status: AgentStatus): void {
if (this.#status !== status) {
this.#status = status
this.#emitStatusChange()
}
}
/**
* Push a persistent observation to the history event stream.
* This will be visible in <agent_history> and remain in memory across steps.
*/
pushObservation(content: string): void {
this.history.push({ type: 'observation', content })
this.#emitHistoryChange()
}
async execute(task: string): Promise<ExecutionResult> {
if (!task) throw new Error('Task is required')
this.task = task
this.taskId = uid()
// Disable ask_user tool if onAskUser is not set
if (!this.onAskUser) {
this.tools.delete('ask_user')
}
const onBeforeStep = this.config.onBeforeStep || (() => void 0)
const onAfterStep = this.config.onAfterStep || (() => void 0)
const onBeforeTask = this.config.onBeforeTask || (() => void 0)
const onAfterTask = this.config.onAfterTask || (() => void 0)
await onBeforeTask.call(this)
// Show mask
await this.pageController.showMask()
if (this.#abortController) {
this.#abortController.abort()
this.#abortController = new AbortController()
}
this.history = []
this.#setStatus('running')
this.#emitHistoryChange()
// Reset states
this.states = {
totalWaitTime: 0,
lastURL: '',
}
try {
let step = 0
while (true) {
await this.#generateObservations(step)
await onBeforeStep.call(this, step)
console.group(`step: ${step}`)
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
// Thinking
console.log(chalk.blue('Thinking...'))
this.emitActivity({ type: 'thinking' })
const result = await this.#llm.invoke(
[
{
role: 'system',
content: this.#getSystemPrompt(),
},
{
role: 'user',
content: await this.#assembleUserPrompt(),
},
],
{ AgentOutput: this.#packMacroTool() },
this.#abortController.signal,
{
toolChoiceName: 'AgentOutput',
normalizeResponse,
}
)
const macroResult = result.toolResult as MacroToolResult
const input = macroResult.input
const output = macroResult.output
const reflection: Partial<AgentReflection> = {
evaluation_previous_goal: input.evaluation_previous_goal,
memory: input.memory,
next_goal: input.next_goal,
}
const actionName = Object.keys(input.action)[0]
const action: AgentStep['action'] = {
name: actionName,
input: input.action[actionName],
output: output,
}
this.history.push({
type: 'step',
reflection,
action,
usage: result.usage,
} as AgentStep)
this.#emitHistoryChange()
console.log(chalk.green('Step finished:'), actionName)
console.groupEnd()
await onAfterStep.call(this, this.history)
step++
if (step > MAX_STEPS) {
this.#onDone('Step count exceeded maximum limit', false)
const result: ExecutionResult = {
success: false,
data: 'Step count exceeded maximum limit',
history: this.history,
}
await onAfterTask.call(this, result)
return result
}
if (actionName === 'done') {
const success = action.input?.success ?? false
const text = action.input?.text || 'no text provided'
console.log(chalk.green.bold('Task completed'), success, text)
this.#onDone(text, success)
const result: ExecutionResult = {
success,
data: text,
history: this.history,
}
await onAfterTask.call(this, result)
return result
}
}
} catch (error: unknown) {
console.error('Task failed', error)
const errorMessage = String(error)
this.emitActivity({ type: 'error', message: errorMessage })
this.#onDone(errorMessage, false)
const result: ExecutionResult = {
success: false,
data: errorMessage,
history: this.history,
}
await onAfterTask.call(this, result)
return result
}
}
/**
* Merge all tools into a single MacroTool with the following input:
* - thinking: string
* - evaluation_previous_goal: string
* - memory: string
* - next_goal: string
* - action: { toolName: toolInput }
* where action must be selected from tools defined in this.tools
*/
#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
const tools = this.tools
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
})
const actionSchema = zod.union(
actionSchemas as unknown as [zod.ZodType, zod.ZodType, ...zod.ZodType[]]
)
const macroToolSchema = zod.object({
// thinking: zod.string().optional(),
evaluation_previous_goal: zod.string().optional(),
memory: zod.string().optional(),
next_goal: zod.string().optional(),
action: actionSchema,
})
return {
description: 'You MUST call this tool every step. Outputs your reflections and next action.',
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
console.log(chalk.blue.bold('MacroTool execute'), input)
const action = input.action
const toolName = Object.keys(action)[0]
const toolInput = action[toolName]
// Build reflection text, only include non-empty fields
const reflectionLines: string[] = []
if (input.evaluation_previous_goal)
reflectionLines.push(`✅: ${input.evaluation_previous_goal}`)
if (input.memory) reflectionLines.push(`💾: ${input.memory}`)
if (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`)
const reflectionText = reflectionLines.length > 0 ? reflectionLines.join('\n') : ''
if (reflectionText) {
console.log(reflectionText)
}
// 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)
// Emit executing activity
this.emitActivity({ type: 'executing', tool: toolName, input: toolInput })
const startTime = Date.now()
// Execute tool, bind `this` to PageAgent
const result = await tool.execute.bind(this)(toolInput)
const duration = Date.now() - startTime
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
// Emit executed activity
this.emitActivity({
type: 'executed',
tool: toolName,
input: toolInput,
output: result,
duration,
})
// Reset wait time for non-wait tools
if (toolName !== 'wait') {
this.states.totalWaitTime = 0
}
// Return structured result
return {
input,
output: result,
}
},
}
}
/**
* Get system prompt, dynamically replace language settings based on configured language
*/
#getSystemPrompt(): string {
let systemPrompt = SYSTEM_PROMPT
const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
systemPrompt = systemPrompt.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
)
return systemPrompt
}
/**
* Get instructions from config and format as XML block
*/
async #getInstructions(): Promise<string> {
const { instructions } = this.config
if (!instructions) return ''
const systemInstructions = instructions.system?.trim()
const url = await this.pageController.getCurrentUrl()
let pageInstructions: string | undefined
if (instructions.getPageInstructions) {
try {
pageInstructions = instructions.getPageInstructions(url)?.trim()
} catch (error) {
console.error(
chalk.red('[PageAgent] Failed to execute getPageInstructions callback:'),
error
)
}
}
if (!systemInstructions && !pageInstructions) return ''
let result = '<instructions>\n'
if (systemInstructions) {
result += `<system_instructions>\n${systemInstructions}\n</system_instructions>\n`
}
if (pageInstructions) {
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
}
result += '</instructions>\n\n'
return result
}
/**
* Generate observations before each step
* - URL change detection
* - Too many steps warning
* @todo loop detection
* @todo console error
*/
async #generateObservations(stepCount: number): Promise<void> {
// Detect URL change
const currentURL = await this.pageController.getCurrentUrl()
if (currentURL !== this.states.lastURL) {
this.pushObservation(`Page navigated to → ${currentURL}`)
this.states.lastURL = currentURL
}
// Warn about remaining steps
const remaining = MAX_STEPS - stepCount
if (remaining === 5) {
this.pushObservation(
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
)
} else if (remaining === 2) {
this.pushObservation(
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
)
}
}
async #assembleUserPrompt(): Promise<string> {
let prompt = ''
// <instructions> (optional)
prompt += await this.#getInstructions()
// <agent_state>
// - <user_request>
// - <step_info>
// <agent_state>
const stepCount = this.history.filter((e) => e.type === 'step').length
prompt += `<agent_state>
<user_request>
${this.task}
</user_request>
<step_info>
Step ${stepCount + 1} of ${MAX_STEPS} max possible steps
Current date and time: ${new Date().toISOString()}
</step_info>
</agent_state>
`
// <agent_history>
// - <step_N> for steps
// - <sys> for observations and system messages
prompt += '\n<agent_history>\n'
let stepIndex = 0
for (const event of this.history) {
if (event.type === 'step') {
stepIndex++
prompt += `<step_${stepIndex}>
Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
Memory: ${event.reflection.memory}
Next Goal: ${event.reflection.next_goal}
Action Results: ${event.action.output}
</step_${stepIndex}>
`
} else if (event.type === 'observation') {
prompt += `<sys>${event.content}</sys>\n`
} else if (event.type === 'user_takeover') {
prompt += `<sys>User took over control and made changes to the page.</sys>\n`
} else if (event.type === 'error') {
// Error events are mainly for panel rendering, not included in LLM context
// to avoid polluting the agent's reasoning with transient errors
}
}
prompt += '</agent_history>\n\n'
// <browser_state>
prompt += await this.#getBrowserState()
return trimLines(prompt)
}
#onDone(text: string, success = true) {
this.pageController.cleanUpHighlights()
this.pageController.hideMask() // No await - fire and forget
this.#setStatus(success ? 'completed' : 'error')
this.#abortController.abort()
}
async #getBrowserState(): Promise<string> {
const state = await this.pageController.getBrowserState()
let content = state.content
if (this.config.transformPageContent) {
content = await this.config.transformPageContent(content)
}
return trimLines(`<browser_state>
Current Page: [${state.title}](${state.url})
${state.header}
${content}
${state.footer}
</browser_state>
`)
}
dispose(reason?: string) {
console.log('Disposing PageAgent...')
this.disposed = true
this.pageController.dispose()
this.history = []
this.#abortController.abort(reason ?? 'PageAgent disposed')
// Emit dispose event for UI cleanup
this.dispatchEvent(new Event('dispose'))
this.config.onDispose?.call(this, reason)
}
}
+2
View File
@@ -0,0 +1,2 @@
// Agent-specific constants (LLM constants moved to @page-agent/llms)
export const MAX_STEPS = 20
+123
View File
@@ -0,0 +1,123 @@
import type { LLMConfig } from '@page-agent/llms'
import type { PageController, PageControllerConfig } from '@page-agent/page-controller'
import type { PageAgentCore } from '../PageAgentCore'
import type { PageAgentTool } from '../tools'
import type { ExecutionResult, HistoricalEvent } from '../types'
export type { LLMConfig }
/** Supported UI languages */
export type SupportedLanguage = 'en-US' | 'zh-CN'
export interface AgentConfig {
// theme?: 'light' | 'dark'
language?: SupportedLanguage
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see PageAgentTool
*
* @example
* // override internal tool
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: zod.object({
* question: zod.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
/**
* Instructions to guide the agent's behavior
*/
instructions?: {
/**
* Global system-level instructions, applied to all tasks
*/
system?: string
/**
* Dynamic page-level instructions callback
* Called before each step to get instructions for the current page
* @param url - Current page URL (window.location.href)
* @returns Instructions string, or undefined/null to skip
*/
getPageInstructions?: (url: string) => string | undefined | null
}
// lifecycle hooks
// @todo: use event instead of hooks
// @todo: remove `this` binding, pass agent as explicit parameter instead
onBeforeStep?: (this: PageAgentCore, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void
onBeforeTask?: (this: PageAgentCore) => Promise<void> | void
onAfterTask?: (this: PageAgentCore, result: ExecutionResult) => Promise<void> | void
/**
* @note this hook can block the disposal process
* @todo remove `this` binding, pass agent as explicit parameter instead
*/
onDispose?: (this: PageAgentCore, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* Transform page content before sending to LLM.
* Called after DOM extraction and simplification, before LLM invocation.
* Use cases: inspect extraction results, modify page info, mask sensitive data.
*
* @param content - Simplified page content that will be sent to LLM
* @returns Transformed content
*
* @example
* // Mask phone numbers
* transformPageContent: async (content) => {
* return content.replace(/1[3-9]\d{9}/g, '***********')
* }
*/
transformPageContent?: (content: string) => Promise<string> | string
/**
* TODO: @unimplemented
* hook when action causes a new page to be opened
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
* @todo remove `this` binding, pass agent as explicit parameter instead
*/
// onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
/**
* TODO: @unimplemented
* try to navigate to a new page instead of opening a new tab/window.
* @note will unload the current page when a action tries to open a new page. so that things keep in the same tab/window.
*/
// experimentalPreventNewPage?: boolean
}
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.md?raw' {
const content: string
export default content
}
@@ -0,0 +1 @@
system_prompt.md
@@ -92,7 +92,6 @@ Strictly follow these rules while using the browser and navigating the web:
- User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.
- Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.
- Trying to hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted result and harmful side-effects. User would rather you to complete the task with a fail.
- If you are not clear about the request or steps. `ask_user` to clarify it.
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
</capability>
@@ -120,7 +119,7 @@ Exhibit the following reasoning patterns to successfully achieve the <user_reque
- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.
- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.
- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.
- `ask_user` for help if you have any difficulty. Users want to be kept in the loop.
- Ask user for help if you have any difficulty. Keep user in the loop.
- If you see information relevant to <user_request>, plan saving the information to memory.
- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.
</reasoning_rules>
@@ -4,23 +4,8 @@
*/
import zod, { type z } from 'zod'
import type { PageAgent } from '@/PageAgent'
import {
clickElement,
getElementByIndex,
getSystemInfo,
inputTextElement,
scrollHorizontally,
scrollVertically,
selectOptionElement,
waitFor,
} from './actions'
// debug
import * as utils from './actions'
// @ts-expect-error debug only
window.utils = utils
import type { PageAgentCore } from '../PageAgentCore'
import { waitFor } from '../utils'
/**
* Internal tool definition that has access to PageAgent `this` context
@@ -29,7 +14,7 @@ export interface PageAgentTool<TParams = any> {
// name: string
description: string
inputSchema: z.ZodType<TParams>
execute: (this: PageAgent, args: TParams) => Promise<string>
execute: (this: PageAgentCore, args: TParams) => Promise<string>
}
export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TParams> {
@@ -42,18 +27,6 @@ export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TP
*/
export const tools = new Map<string, PageAgentTool>()
// tools.set(
// 'get_current_html',
// tool({
// description: 'Get the current (updated) simplified HTML of the page',
// inputSchema: zod.object({}),
// execute: function (this: PageAgent) {
// this.updateTree()
// return this.simplifiedHTML
// },
// })
// )
tools.set(
'done',
tool({
@@ -63,7 +36,7 @@ tools.set(
text: zod.string(),
success: zod.boolean().default(true),
}),
execute: async function (this: PageAgent, input) {
execute: async function (this: PageAgentCore, input) {
// @note main loop will handle this one
// this.onDone(input.text, input.success)
return Promise.resolve('Task completed')
@@ -79,12 +52,21 @@ tools.set(
inputSchema: zod.object({
seconds: zod.number().min(1).max(10).default(1),
}),
execute: async function (this: PageAgent, input) {
const lastTimeUpdate = this.lastTimeUpdate
execute: async function (this: PageAgentCore, input) {
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
await waitFor(actualWaitTime)
return `✅ Waited for ${input.seconds} seconds.` + (await getSystemInfo())
this.states.totalWaitTime += input.seconds
if (this.states.totalWaitTime >= 3) {
this.pushObservation(
`You have waited ${this.states.totalWaitTime} seconds accumulatively. Do NOT wait any longer unless you have a good reason.`
)
}
return `✅ Waited for ${input.seconds} seconds.`
},
})
)
@@ -97,9 +79,12 @@ tools.set(
inputSchema: zod.object({
question: zod.string(),
}),
execute: async function (this: PageAgent, input) {
const answer = await this.panel.askUser(input.question)
return `✅ Received user answer: ${answer}` + (await getSystemInfo())
execute: async function (this: PageAgentCore, input) {
if (!this.onAskUser) {
throw new Error('ask_user tool requires onAskUser callback to be set')
}
const answer = await this.onAskUser(input.question)
return `User answered: ${answer}`
},
})
)
@@ -111,17 +96,9 @@ tools.set(
inputSchema: zod.object({
index: zod.int().min(0),
}),
execute: async function (this: PageAgent, input) {
const element = getElementByIndex(this, input.index)
const elemText = this.elementTextMap.get(input.index)
await clickElement(element)
// @workaround: Handle links that open in new tabs
if (element instanceof HTMLAnchorElement && element.target === '_blank') {
return `⚠️ Clicked link that opens in a new tab (${elemText ? elemText : input.index}). You are not capable of reading new tabs.`
}
return `✅ Clicked element (${elemText ? elemText : input.index}).` + (await getSystemInfo())
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.clickElement(input.index)
return result.message
},
})
)
@@ -134,14 +111,9 @@ tools.set(
index: zod.int().min(0),
text: zod.string(),
}),
execute: async function (this: PageAgent, input) {
const element = getElementByIndex(this, input.index)
const elemText = this.elementTextMap.get(input.index)
await inputTextElement(element, input.text)
return (
`✅ Input text (${input.text}) into element (${elemText ? elemText : input.index}).` +
(await getSystemInfo())
)
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.inputText(input.index, input.text)
return result.message
},
})
)
@@ -155,14 +127,9 @@ tools.set(
index: zod.int().min(0),
text: zod.string(),
}),
execute: async function (this: PageAgent, input) {
const element = getElementByIndex(this, input.index)
const elemText = this.elementTextMap.get(input.index)
await selectOptionElement(element as HTMLSelectElement, input.text)
return (
`✅ Selected option (${input.text}) in element (${elemText ? elemText : input.index}).` +
(await getSystemInfo())
)
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.selectOption(input.index, input.text)
return result.message
},
})
)
@@ -181,14 +148,12 @@ tools.set(
pixels: zod.number().int().min(0).optional(),
index: zod.number().int().min(0).optional(),
}),
execute: async function (this: PageAgent, input) {
const { down, num_pages, index, pixels } = input
const scroll_amount = pixels ? pixels : num_pages * (down ? 1 : -1) * window.innerHeight
const element = index !== undefined ? getElementByIndex(this, index) : null
return (await scrollVertically(down, scroll_amount, element)) + (await getSystemInfo())
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.scroll({
...input,
numPages: input.num_pages,
})
return result.message
},
})
)
@@ -203,14 +168,24 @@ tools.set(
pixels: zod.number().int().min(0),
index: zod.number().int().min(0).optional(),
}),
execute: async function (this: PageAgent, input) {
const { right, pixels, index } = input
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.scrollHorizontally(input)
return result.message
},
})
)
const scroll_amount = pixels * (right ? 1 : -1)
const element = index !== undefined ? getElementByIndex(this, index) : null
return (await scrollHorizontally(right, scroll_amount, element)) + (await getSystemInfo())
tools.set(
'execute_javascript',
tool({
description:
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
inputSchema: zod.object({
script: zod.string(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.executeJavascript(input.script)
return result.message
},
})
)
+109
View File
@@ -0,0 +1,109 @@
/**
* Agent reflection 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 AgentReflection {
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 Partial<AgentReflection> {
action: Record<string, any>
}
/**
* MacroTool output structure
*/
export interface MacroToolResult {
input: MacroToolInput
output: string
}
/**
* A single agent step with reflection and action
*/
export interface AgentStep {
type: 'step'
reflection: Partial<AgentReflection>
action: {
name: string
input: any
output: string
}
usage: {
promptTokens: number
completionTokens: number
totalTokens: number
cachedTokens?: number
reasoningTokens?: number
}
}
/**
* Persistent observation event (stays in memory)
*/
export interface ObservationEvent {
type: 'observation'
content: string
}
/**
* User takeover event
*/
export interface UserTakeoverEvent {
type: 'user_takeover'
}
/**
* Error event (retry or error from LLM)
*/
export interface ErrorEvent {
type: 'error'
errorType: 'retry' | 'error'
message: string
attempt?: number
maxAttempts?: number
}
/**
* Union type for all history events
*/
export type HistoricalEvent = AgentStep | ObservationEvent | UserTakeoverEvent | ErrorEvent
/**
* Agent execution status
*/
export type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
/**
* Agent activity - transient state for immediate UI feedback.
*
* Unlike historical events (which are persisted), activities are ephemeral
* and represent "what the agent is doing right now". UI components should
* listen to 'activity' events to show real-time feedback.
*
* Note: There is no 'idle' activity - absence of activity events means idle.
*/
export type AgentActivity =
| { type: 'thinking' }
| { type: 'executing'; tool: string; input: unknown }
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
| { type: 'retrying'; attempt: number; maxAttempts: number }
| { type: 'error'; message: string }
export interface ExecutionResult {
success: boolean
data: string
history: HistoricalEvent[]
}
+157
View File
@@ -0,0 +1,157 @@
import chalk from 'chalk'
/**
* Normalize LLM response and fix common format issues.
*
* Handles:
* - No tool_calls but JSON in message.content (fallback)
* - Model returns action name as tool call instead of AgentOutput
* - Arguments wrapped as double JSON string
* - Nested function call format
* - Missing action field (fallback to wait)
* - etc.
*/
export function normalizeResponse(response: any): any {
let resolvedArguments = null as any
const choice = (response as { choices?: Choice[] }).choices?.[0]
if (!choice) throw new Error('No choices in response')
const message = choice.message
if (!message) throw new Error('No message in choice')
const toolCall = message.tool_calls?.[0]
// fix level and location of arguments
if (toolCall?.function?.arguments) {
resolvedArguments = safeJsonParse(toolCall.function.arguments)
// case: sometimes the model only returns the action level
if (toolCall.function.name && toolCall.function.name !== 'AgentOutput') {
console.log(chalk.yellow(`[normalizeResponse] #1: fixing tool_call`))
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
// case: sometimes the model returns json in content instead of tool_calls
if (message.content) {
const content = message.content.trim()
const jsonInContent = retrieveJsonFromString(content)
if (jsonInContent) {
resolvedArguments = safeJsonParse(jsonInContent)
// case: sometimes the content json includes upper level wrapper
if (resolvedArguments?.name === 'AgentOutput') {
console.log(chalk.yellow(`[normalizeResponse] #2: fixing tool_call`))
resolvedArguments = safeJsonParse(resolvedArguments.arguments)
}
// case: sometimes even 2-levels of wrapping
if (resolvedArguments?.type === 'function') {
console.log(chalk.yellow(`[normalizeResponse] #3: fixing tool_call`))
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments)
}
// case: and sometimes action level only
// todo: needs better detection logic
if (
!resolvedArguments?.action &&
!resolvedArguments?.evaluation_previous_goal &&
!resolvedArguments?.memory &&
!resolvedArguments?.next_goal &&
!resolvedArguments?.thinking
) {
console.log(chalk.yellow(`[normalizeResponse] #4: fixing tool_call`))
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
throw new Error('No tool_call and the message content does not contain valid JSON')
}
} else {
throw new Error('No tool_call nor message content is present')
}
}
// fix double stringified arguments
resolvedArguments = safeJsonParse(resolvedArguments)
if (resolvedArguments.action) {
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
}
// fix incomplete formats
if (!resolvedArguments.action) {
console.log(chalk.yellow(`[normalizeResponse] #5: fixing tool_call`))
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
}
// pack back to standard format
return {
...response,
choices: [
{
...choice,
message: {
...message,
tool_calls: [
{
...(toolCall || {}),
function: {
...(toolCall?.function || {}),
name: 'AgentOutput',
arguments: JSON.stringify(resolvedArguments),
},
},
],
},
},
],
}
}
/**
* Safely parse JSON, return original input if not json.
*/
function safeJsonParse(input: any): any {
if (typeof input === 'string') {
try {
return JSON.parse(input.trim())
} catch {
return input
}
}
return input
}
/**
* Extract and parse JSON from a string.
* - Treat content between the first `{` and the last `}` as JSON.
* - Try to parse that content as JSON and return the parsed value (object/array/primitive) if successful, otherwise return null.
*/
function retrieveJsonFromString(str: string): any {
try {
const json = /({[\s\S]*})/.exec(str) ?? []
if (json.length === 0) {
return null
}
return JSON.parse(json[0]!)
} catch {
return null
}
}
interface Choice {
message?: {
role?: 'assistant'
content?: string
tool_calls?: {
id?: string
type?: 'function'
function?: {
name?: string
arguments?: string
}
}[]
}
index?: 0
finish_reason?: 'tool_calls'
}
@@ -1,3 +1,5 @@
export { normalizeResponse } from './autoFixer'
/**
* Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise
@@ -20,6 +22,10 @@ export async function waitUntil(check: () => boolean, timeout = 60 * 60_1000): P
})
}
export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
//
export function truncate(text: string, maxLength: number): string {
@@ -62,12 +68,13 @@ export function randomID(existingIDs?: string[]): string {
}
//
const _global = globalThis as any
if (!window.__PAGE_AGENT_IDS__) {
window.__PAGE_AGENT_IDS__ = []
if (!_global.__PAGE_AGENT_IDS__) {
_global.__PAGE_AGENT_IDS__ = []
}
const ids = window.__PAGE_AGENT_IDS__
const ids = _global.__PAGE_AGENT_IDS__
/**
* Generate a random ID.
+9
View File
@@ -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": {}
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": {
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" }
]
}
+44
View File
@@ -0,0 +1,44 @@
// @ts-check
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))
// ES Module for NPM Package
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/PageAgentCore.ts'),
name: 'PageAgentCore',
fileName: 'page-agent-core',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'esm'),
rollupOptions: {
external: [
'chalk',
'zod',
// all the internal packages
/^@page-agent\//,
],
},
minify: false,
sourcemap: true,
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@page-agent/llms",
"version": "0.3.0-beta.1",
"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.3.5"
}
}
+206
View File
@@ -0,0 +1,206 @@
/**
* OpenAI Client implementation
*/
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
import { modelPatch, zodToOpenAITool } from './utils'
/**
* Client for OpenAI compatible APIs
*/
export class OpenAIClient implements LLMClient {
config: Required<LLMConfig>
private fetch: typeof globalThis.fetch
constructor(config: Required<LLMConfig>) {
this.config = config
this.fetch = config.customFetch
}
async invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> {
// 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, t]) => zodToOpenAITool(name, t))
// Build request body
const requestBody: Record<string, unknown> = {
model: this.config.model,
temperature: this.config.temperature,
messages,
tools: openaiTools,
parallel_tool_calls: false,
// Require tool call: specific tool if provided, otherwise any tool
tool_choice: options?.toolChoiceName
? { type: 'function', function: { name: options.toolChoiceName } }
: 'required',
}
// 2. Call API
let response: Response
try {
response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(modelPatch(requestBody)),
signal: abortSignal,
})
} catch (error: unknown) {
console.error(error)
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
}
// 3. Handle HTTP errors
if (!response.ok) {
const errorData = await response.json().catch()
const errorMessage =
(errorData as { error?: { message?: string } }).error?.message || response.statusText
if (response.status === 401 || response.status === 403) {
throw new InvokeError(
InvokeErrorType.AUTH_ERROR,
`Authentication failed: ${errorMessage}`,
errorData
)
}
if (response.status === 429) {
throw new InvokeError(
InvokeErrorType.RATE_LIMIT,
`Rate limit exceeded: ${errorMessage}`,
errorData
)
}
if (response.status >= 500) {
throw new InvokeError(
InvokeErrorType.SERVER_ERROR,
`Server error: ${errorMessage}`,
errorData
)
}
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`HTTP ${response.status}: ${errorMessage}`,
errorData
)
}
// 4. Parse and validate response
const data = await response.json()
const choice = data.choices?.[0]
if (!choice) {
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data)
}
// Check finish_reason
switch (choice.finish_reason) {
case 'tool_calls':
case 'function_call': // gemini
case 'stop': // some models use this even with tool calls
break
case 'length':
throw new InvokeError(
InvokeErrorType.CONTEXT_LENGTH,
'Response truncated: max tokens reached'
)
case 'content_filter':
throw new InvokeError(InvokeErrorType.CONTENT_FILTER, 'Content filtered by safety system')
default:
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Unexpected finish_reason: ${choice.finish_reason}`
)
}
// Apply normalizeResponse if provided (for fixing format issues automatically)
const normalizedData = options?.normalizeResponse ? options.normalizeResponse(data) : data
const normalizedChoice = (normalizedData as any).choices?.[0]
// Get tool name from response
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
if (!toolCallName) {
throw new InvokeError(
InvokeErrorType.NO_TOOL_CALL,
'No tool call found in response',
normalizedData
)
}
const tool = tools[toolCallName]
if (!tool) {
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Tool "${toolCallName}" not found in tools`,
normalizedData
)
}
// Extract and parse tool arguments
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments
if (!argString) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'No tool call arguments found',
normalizedData
)
}
let parsedArgs: unknown
try {
parsedArgs = JSON.parse(argString)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error
)
}
// Validate with schema
const validation = tool.inputSchema.safeParse(parsedArgs)
if (!validation.success) {
console.error(validation.error)
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Tool arguments validation failed',
validation.error
)
}
const toolInput = validation.data
// 5. Execute tool
let toolResult: unknown
try {
toolResult = await tool.execute(toolInput)
} catch (e) {
throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(e as Error).message}`,
e
)
}
// Return result
return {
toolCall: {
name: toolCallName,
args: toolInput,
},
toolResult,
usage: {
promptTokens: data.usage?.prompt_tokens ?? 0,
completionTokens: data.usage?.completion_tokens ?? 0,
totalTokens: data.usage?.total_tokens ?? 0,
cachedTokens: data.usage?.prompt_tokens_details?.cached_tokens,
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
},
rawResponse: data,
}
}
}
+3
View File
@@ -0,0 +1,3 @@
// Internal constants
export const LLM_MAX_RETRIES = 2
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+108
View File
@@ -0,0 +1,108 @@
import { OpenAIClient } from './OpenAIClient'
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
import { InvokeError } from './errors'
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
// Runtime validation as defensive programming (types already guarantee these)
if (!config.baseURL || !config.apiKey || !config.model) {
throw new Error(
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' +
'See: https://alibaba.github.io/page-agent/#/docs/features/models'
)
}
return {
baseURL: config.baseURL,
apiKey: config.apiKey,
model: config.model,
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
}
}
export class LLM extends EventTarget {
config: Required<LLMConfig>
client: LLMClient
constructor(config: LLMConfig) {
super()
this.config = parseLLMConfig(config)
// Default to OpenAI client
this.client = new OpenAIClient(this.config)
}
/**
* - call llm api *once*
* - invoke tool call *once*
* - return the result of the tool
*/
async invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> {
return await withRetry(
async () => {
const result = await this.client.invoke(messages, tools, abortSignal, options)
return result
},
// retry settings
{
maxRetries: this.config.maxRetries,
onRetry: (attempt: number) => {
this.dispatchEvent(
new CustomEvent('retry', { detail: { attempt, maxAttempts: this.config.maxRetries } })
)
},
onError: (error: Error) => {
this.dispatchEvent(new CustomEvent('error', { detail: { error } }))
},
}
)
}
}
async function withRetry<T>(
fn: () => Promise<T>,
settings: {
maxRetries: number
onRetry: (attempt: number) => void
onError: (error: Error) => void
}
): Promise<T> {
let attempt = 0
let lastError: Error | null = null
while (attempt <= settings.maxRetries) {
if (attempt > 0) {
settings.onRetry(attempt)
await new Promise((resolve) => setTimeout(resolve, 100))
}
try {
return await fn()
} catch (error: unknown) {
console.error(error)
settings.onError(error as Error)
// do not retry if aborted by user
if ((error as { name?: string })?.name === 'AbortError') throw error
// do not retry if error is not retryable (InvokeError)
if (error instanceof InvokeError && !error.retryable) throw error
lastError = error as Error
attempt++
await new Promise((resolve) => setTimeout(resolve, 100))
}
}
throw lastError!
}
@@ -32,6 +32,24 @@ export interface Tool<TParams = any, TResult = any> {
execute: (args: TParams) => Promise<TResult>
}
/**
* Invoke options for LLM call
*/
export interface InvokeOptions {
/**
* Force LLM to call a specific tool by name.
* If provided: tool_choice = { type: 'function', function: { name: toolChoiceName } }
* If not provided: tool_choice = 'required' (must call some tool, but model chooses which)
*/
toolChoiceName?: string
/**
* Response normalization function.
* Called before parsing the response.
* Used to fix various response format errors from the model.
*/
normalizeResponse?: (response: any) => any
}
/**
* LLM Client interface
* Note: Does not use generics because each tool in the tools array has different types
@@ -40,7 +58,8 @@ export interface LLMClient {
invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal?: AbortSignal
abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult>
}
@@ -65,13 +84,20 @@ export interface InvokeResult<TResult = unknown> {
}
/**
* OpenAI Client config
* LLM configuration
*/
export interface OpenAIClientConfig {
model: string
apiKey: string
export interface LLMConfig {
baseURL: string
apiKey: string
model: string
temperature?: number
maxTokens?: number
maxRetries?: number
/**
* Custom fetch function for LLM API requests.
* Use this to customize headers, credentials, proxy, etc.
* The response should follow OpenAI API format.
*/
customFetch?: typeof globalThis.fetch
}
+120
View File
@@ -0,0 +1,120 @@
/**
* Utility functions for LLM integration
*/
import chalk from 'chalk'
import { z } from 'zod'
import type { Tool } from './types'
function debug(message: string) {
console.debug(chalk.gray('[LLM]'), message)
}
/**
* Convert Zod schema to OpenAI tool format
* Uses Zod 4 native z.toJSONSchema()
*/
export function zodToOpenAITool(name: string, tool: Tool) {
return {
type: 'function' as const,
function: {
name,
description: tool.description,
parameters: z.toJSONSchema(tool.inputSchema, { target: 'openapi-3.0' }),
},
}
}
/**
* Patch model specific parameters
*/
export function modelPatch(body: Record<string, any>) {
const model: string = body.model || ''
if (!model) return body
const modelName = normalizeModelName(model)
if (modelName.startsWith('qwen')) {
debug('Applying Qwen patch: use higher temperature for auto fixing')
body.temperature = Math.max(body.temperature || 0, 1.0)
}
if (modelName.startsWith('claude')) {
debug('Applying Claude patch: disable thinking')
body.thinking = { type: 'disabled' }
// Convert tool_choice to Claude format
if (body.tool_choice === 'required') {
// 'required' -> { type: 'any' } (must call some tool)
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }')
body.tool_choice = { type: 'any' }
} else if (body.tool_choice?.function?.name) {
// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }
debug('Applying Claude patch: convert tool_choice format')
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
}
}
if (modelName.startsWith('grok')) {
debug('Applying Grok patch: removing tool_choice')
delete body.tool_choice
debug('Applying Grok patch: disable reasoning and thinking')
body.thinking = { type: 'disabled', effort: 'minimal' }
body.reasoning = { enabled: false, effort: 'low' }
}
if (modelName.startsWith('gpt')) {
debug('Applying GPT patch: set verbosity to low')
body.verbosity = 'low'
if (modelName.startsWith('gpt-52')) {
debug('Applying GPT-52 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-51')) {
debug('Applying GPT-51 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-5')) {
debug('Applying GPT-5 patch: set reasoning effort to low')
body.reasoning_effort = 'low'
}
}
if (modelName.startsWith('gemini')) {
debug('Applying Gemini patch: set reasoning effort to minimal')
body.reasoning_effort = 'minimal'
}
return body
}
/**
* check if a given model ID fits a specific model name
*
* @note
* Different model providers may use different model IDs for the same model.
* For example, openai's `gpt-5.2` may called:
*
* - `gpt-5.2-version`
* - `gpt-5_2-date`
* - `GPT-52-version-date`
* - `openai/gpt-5.2-chat`
*
* They should be treated as the same model.
* Normalize them to `gpt-52`
*/
function normalizeModelName(modelName: string): string {
let normalizedName = modelName.toLowerCase()
// remove prefix before '/'
if (normalizedName.includes('/')) {
normalizedName = normalizedName.split('/')[1]
}
// remove '_'
normalizedName = normalizedName.replace(/_/g, '')
// remove '.'
normalizedName = normalizedName.replace(/\./g, '')
return normalizedName
}
+9
View File
@@ -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": {}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"]
}
+36
View File
@@ -0,0 +1,36 @@
// @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"',
},
})
+54
View File
@@ -0,0 +1,54 @@
{
"name": "page-agent",
"private": false,
"version": "0.3.0-beta.1",
"type": "module",
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
"types": "./dist/esm/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgent.d.ts",
"import": "./dist/esm/page-agent.js",
"default": "./dist/esm/page-agent.js"
}
},
"files": [
"dist/"
],
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
"keywords": [
"ai",
"automation",
"ui-agent",
"GUI-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"web-automation",
"GUI-simulation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build && npm run build:demo",
"build:demo": "vite build --config vite.iife.config.js",
"dev:demo": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
"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": {
"chalk": "^5.6.2",
"zod": "^4.3.5",
"@page-agent/llms": "0.3.0-beta.1",
"@page-agent/page-controller": "0.3.0-beta.1",
"@page-agent/core": "0.3.0-beta.1",
"@page-agent/ui": "0.3.0-beta.1"
}
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
import { type PageAgentConfig, PageAgentCore } from '@page-agent/core'
import { PageController } from '@page-agent/page-controller'
import { Panel } from '@page-agent/ui'
export type { PageAgentConfig }
export class PageAgent extends PageAgentCore {
panel: Panel
constructor(config: PageAgentConfig) {
const pageController = new PageController({
...config,
enableMask: config.enableMask ?? true,
})
super({ ...config, pageController })
this.panel = new Panel(this, {
language: config.language,
})
}
}
+46
View File
@@ -0,0 +1,46 @@
/**
* IIFE demo entry - auto-initializes with built-in demo API for testing
*/
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
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
setTimeout(() => {
const currentScript = document.currentScript as HTMLScriptElement | null
let config: PageAgentConfig
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'
config = { model, baseURL, apiKey, language }
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
config = {
model: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,
baseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,
apiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,
}
}
// Create agent
window.pageAgent = new PageAgent(config)
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
})
+9
View File
@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
import type { PageAgent } from './PageAgent'
declare global {
interface Window {
pageAgent?: PageAgent
PageAgent: typeof PageAgent
}
}
+9
View File
@@ -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": {}
}
}
+26
View File
@@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": {
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
"@page-agent/ui": ["../ui/src/index.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../core" },
{ "path": "../ui" }
]
}
+44
View File
@@ -0,0 +1,44 @@
// @ts-check
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))
// ES Module for NPM Package
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/PageAgent.ts'),
name: 'PageAgent',
fileName: 'page-agent',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'esm'),
rollupOptions: {
external: [
'chalk',
'zod',
// all the internal packages
/^@page-agent\//,
],
},
minify: false,
sourcemap: true,
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+57
View File
@@ -0,0 +1,57 @@
// @ts-check
import { config as dotenvConfig } from 'dotenv'
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))
// Load .env from repo root
dotenvConfig({ path: resolve(__dirname, '../../.env') })
// 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(({ mode }) => ({
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/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
},
},
build: {
lib: {
entry: resolve(__dirname, 'src/demo.ts'),
name: 'PageAgent',
fileName: () => `page-agent.demo.js`,
formats: ['iife'],
},
outDir: resolve(__dirname, 'dist', 'iife'),
cssCodeSplit: true,
// minify: false,
rollupOptions: {
// output: {
// // force use .js as extension
// entryFileNames: 'page-agent.js',
// },
onwarn: function (message, handler) {
if (message.code === 'EVAL') return
handler(message)
},
},
},
define: {
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),
'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
},
}))
+41
View File
@@ -0,0 +1,41 @@
{
"name": "@page-agent/page-controller",
"version": "0.3.0-beta.1",
"type": "module",
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
"types": "./dist/lib/PageController.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageController.d.ts",
"import": "./dist/lib/page-controller.js",
"default": "./dist/lib/page-controller.js"
}
},
"files": [
"dist/"
],
"description": "Page controller for page-agent - DOM operations and element interactions",
"keywords": [
"page-agent",
"dom",
"browser-automation",
"web-automation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/page-controller"
},
"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.8"
}
}
@@ -0,0 +1,400 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*
* PageController - Manages DOM operations and element interactions.
* Designed to be independent of LLM and can be tested in unit tests.
* All public methods are async for potential remote calling support.
*/
import {
clickElement,
getElementByIndex,
inputTextElement,
scrollHorizontally,
scrollVertically,
selectOptionElement,
} from './actions'
import { VIEWPORT_EXPANSION } from './constants'
import * as dom from './dom'
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
import { getPageInfo } from './dom/getPageInfo'
import { patchReact } from './patches/react'
/**
* Configuration for PageController
*/
export interface PageControllerConfig extends dom.DomConfig {
viewportExpansion?: number
/** Enable visual mask overlay during operations (default: false) */
enableMask?: boolean
}
/**
* Structured browser state for LLM consumption
*/
export interface BrowserState {
url: string
title: string
/** Page info + scroll position hint (e.g. "Page info: 1920x1080px...\n[Start of page]") */
header: string
/** Simplified HTML of interactive elements */
content: string
/** Page footer hint (e.g. "... 300 pixels below ..." or "[End of page]") */
footer: string
}
interface ActionResult {
success: boolean
message: string
}
/**
* PageController manages DOM state and element interactions.
* It provides async methods for all DOM operations, keeping state isolated.
*
* @lifecycle
* - beforeUpdate: Emitted before the DOM tree is updated.
* - afterUpdate: Emitted after the DOM tree is updated.
*/
export class PageController extends EventTarget {
private config: PageControllerConfig
/** Corresponds to eval_page in browser-use */
private flatTree: FlatDomTree | null = null
/**
* All highlighted index-mapped interactive elements
* Corresponds to DOMState.selector_map in browser-use
*/
private selectorMap = new Map<number, InteractiveElementDomNode>()
/** Index -> element text description mapping */
private elementTextMap = new Map<number, string>()
/**
* Simplified HTML for LLM consumption.
* Corresponds to clickable_elements_to_string in browser-use
*/
private simplifiedHTML = '<EMPTY>'
/** last time the tree was updated */
private lastTimeUpdate = 0
/** Visual mask overlay for blocking user interaction during automation */
private mask: InstanceType<typeof import('./mask/SimulatorMask').SimulatorMask> | null = null
private maskReady: Promise<void> | null = null
constructor(config: PageControllerConfig = {}) {
super()
this.config = config
patchReact(this)
if (config.enableMask) {
this.maskReady = this.initMask()
}
}
/**
* Initialize mask asynchronously (dynamic import to avoid CSS loading in Node)
*/
private async initMask(): Promise<void> {
const { SimulatorMask } = await import('./mask/SimulatorMask')
this.mask = new SimulatorMask()
}
// ======= State Queries =======
/**
* Get current page URL
*/
async getCurrentUrl(): Promise<string> {
return window.location.href
}
/**
* Get last tree update timestamp
*/
async getLastUpdateTime(): Promise<number> {
return this.lastTimeUpdate
}
/**
* Get structured browser state for LLM consumption.
* Automatically calls updateTree() to refresh the DOM state.
*/
async getBrowserState(): Promise<BrowserState> {
const url = window.location.href
const title = document.title
const pi = getPageInfo()
const viewportExpansion = this.config.viewportExpansion ?? VIEWPORT_EXPANSION
await this.updateTree()
const content = this.simplifiedHTML
// Build header: page info + scroll position hint
const pageInfoLine = `Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page`
const elementsLabel =
viewportExpansion === -1
? 'Interactive elements from top layer of the current page (full page):'
: 'Interactive elements from top layer of the current page inside the viewport:'
const hasContentAbove = pi.pixels_above > 4
const scrollHintAbove =
hasContentAbove && viewportExpansion !== -1
? `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...`
: '[Start of page]'
const header = `${pageInfoLine}\n\n${elementsLabel}\n\n${scrollHintAbove}`
// Build footer: scroll position hint
const hasContentBelow = pi.pixels_below > 4
const footer =
hasContentBelow && viewportExpansion !== -1
? `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...`
: '[End of page]'
return { url, title, header, content, footer }
}
// ======= DOM Tree Operations =======
/**
* Update DOM tree, returns simplified HTML for LLM.
* This is the main method to refresh the page state.
* Automatically bypasses mask during DOM extraction if enabled.
*/
async updateTree(): Promise<string> {
this.dispatchEvent(new Event('beforeUpdate'))
this.lastTimeUpdate = Date.now()
// Temporarily bypass mask to allow DOM extraction
if (this.mask) {
this.mask.wrapper.style.pointerEvents = 'none'
}
dom.cleanUpHighlights()
const blacklist = [
...(this.config.interactiveBlacklist || []),
...document.querySelectorAll('[data-page-agent-not-interactive]').values(),
]
this.flatTree = dom.getFlatTree({
...this.config,
interactiveBlacklist: blacklist,
})
this.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.include_attributes)
this.selectorMap.clear()
this.selectorMap = dom.getSelectorMap(this.flatTree)
this.elementTextMap.clear()
this.elementTextMap = dom.getElementTextMap(this.simplifiedHTML)
// Restore mask blocking
if (this.mask) {
this.mask.wrapper.style.pointerEvents = 'auto'
}
this.dispatchEvent(new Event('afterUpdate'))
return this.simplifiedHTML
}
/**
* Clean up all element highlights
*/
async cleanUpHighlights(): Promise<void> {
dom.cleanUpHighlights()
}
// ======= Element Actions =======
/**
* Click element by index
*/
async clickElement(index: number): Promise<ActionResult> {
try {
const element = getElementByIndex(this.selectorMap, index)
const elemText = this.elementTextMap.get(index)
await clickElement(element)
// Handle links that open in new tabs
if (element instanceof HTMLAnchorElement && element.target === '_blank') {
return {
success: true,
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opens in a new tab. You are not capable of reading new tabs.`,
}
}
return {
success: true,
message: `✅ Clicked element (${elemText ?? index}).`,
}
} catch (error) {
return {
success: false,
message: `❌ Failed to click element: ${error}`,
}
}
}
/**
* Input text into element by index
*/
async inputText(index: number, text: string): Promise<ActionResult> {
try {
const element = getElementByIndex(this.selectorMap, index)
const elemText = this.elementTextMap.get(index)
await inputTextElement(element, text)
return {
success: true,
message: `✅ Input text (${text}) into element (${elemText ?? index}).`,
}
} catch (error) {
return {
success: false,
message: `❌ Failed to input text: ${error}`,
}
}
}
/**
* Select dropdown option by index and option text
*/
async selectOption(index: number, optionText: string): Promise<ActionResult> {
try {
const element = getElementByIndex(this.selectorMap, index)
const elemText = this.elementTextMap.get(index)
await selectOptionElement(element as HTMLSelectElement, optionText)
return {
success: true,
message: `✅ Selected option (${optionText}) in element (${elemText ?? index}).`,
}
} catch (error) {
return {
success: false,
message: `❌ Failed to select option: ${error}`,
}
}
}
/**
* Scroll vertically
*/
async scroll(options: {
down: boolean
numPages: number
pixels?: number
index?: number
}): Promise<ActionResult> {
try {
const { down, numPages, pixels, index } = options
const scrollAmount = pixels ?? numPages * (down ? 1 : -1) * window.innerHeight
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
const message = await scrollVertically(down, scrollAmount, element)
return {
success: true,
message,
}
} catch (error) {
return {
success: false,
message: `❌ Failed to scroll: ${error}`,
}
}
}
/**
* Scroll horizontally
*/
async scrollHorizontally(options: {
right: boolean
pixels: number
index?: number
}): Promise<ActionResult> {
try {
const { right, pixels, index } = options
const scrollAmount = pixels * (right ? 1 : -1)
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
const message = await scrollHorizontally(right, scrollAmount, element)
return {
success: true,
message,
}
} catch (error) {
return {
success: false,
message: `❌ Failed to scroll horizontally: ${error}`,
}
}
}
/**
* Execute arbitrary JavaScript on the page
*/
async executeJavascript(script: string): Promise<ActionResult> {
try {
// Wrap script in async function to support await
const asyncFunction = eval(`(async () => { ${script} })`)
const result = await asyncFunction()
return {
success: true,
message: `✅ Executed JavaScript. Result: ${result}`,
}
} catch (error) {
return {
success: false,
message: `❌ Error executing JavaScript: ${error}`,
}
}
}
// ======= Mask Operations =======
/**
* Show the visual mask overlay.
* Only works if enableMask was set to true in config.
*/
async showMask(): Promise<void> {
await this.maskReady
this.mask?.show()
}
/**
* Hide the visual mask overlay.
* Only works if enableMask was set to true in config.
*/
async hideMask(): Promise<void> {
await this.maskReady
this.mask?.hide()
}
/**
* Dispose and clean up resources
*/
dispose(): void {
dom.cleanUpHighlights()
this.flatTree = null
this.selectorMap.clear()
this.elementTextMap.clear()
this.simplifiedHTML = '<EMPTY>'
this.mask?.dispose()
this.mask = null
}
}
@@ -2,26 +2,14 @@
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
import type { PageAgent } from '../PageAgent'
import type { InteractiveElementDomNode } from './dom/dom_tree/type'
// ======= general utils =======
export async function waitFor(seconds: number): Promise<void> {
async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
let currentUrl = window.location.href
export async function getSystemInfo() {
// If current URL is already up to date, no need to add message
if (currentUrl === window.location.href) return ''
await waitFor(0.3) // Wait a bit longer for page to load
currentUrl = window.location.href
return `\n<sys> Current URL changed to: ${currentUrl} </sys>`
}
// ======= dom utils =======
export async function movePointerToElement(element: HTMLElement) {
@@ -35,10 +23,13 @@ export async function movePointerToElement(element: HTMLElement) {
}
/**
* Get the HTMLElement by index from the selectorMap in PageAgent.
* Get the HTMLElement by index from a selectorMap.
*/
export function getElementByIndex(pageAgent: PageAgent, index: number): HTMLElement {
const interactiveNode = pageAgent.selectorMap.get(index)
export function getElementByIndex(
selectorMap: Map<number, InteractiveElementDomNode>,
index: number
): HTMLElement {
const interactiveNode = selectorMap.get(index)
if (!interactiveNode) {
throw new Error(`No interactive element found at index ${index}`)
}
@@ -170,7 +161,6 @@ export async function selectOptionElement(selectElement: HTMLSelectElement, opti
await waitFor(0.1) // Wait to ensure change event processing completes
}
// eslint-disable-next-line @typescript-eslint/require-await
export async function scrollIntoViewIfNeeded(element: HTMLElement) {
const el = element as any
if (el.scrollIntoViewIfNeeded) {
@@ -297,12 +287,52 @@ export async function scrollVertically(
(document.documentElement as HTMLElement)
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
// Page-level scroll
const scrollBefore = window.scrollY
const scrollMax = document.documentElement.scrollHeight - window.innerHeight
window.scrollBy(0, dy)
return `✅ Scrolled page by ${dy}px.`
const scrollAfter = window.scrollY
const scrolled = scrollAfter - scrollBefore
if (Math.abs(scrolled) < 1) {
return dy > 0
? `⚠️ Already at the bottom of the page, cannot scroll down further.`
: `⚠️ Already at the top of the page, cannot scroll up further.`
}
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
const reachedTop = dy < 0 && scrollAfter <= 1
if (reachedBottom) return `✅ Scrolled page by ${scrolled}px. Reached the bottom of the page.`
if (reachedTop) return `✅ Scrolled page by ${scrolled}px. Reached the top of the page.`
return `✅ Scrolled page by ${scrolled}px.`
} else {
// Container scroll
const scrollBefore = el!.scrollTop
const scrollMax = el!.scrollHeight - el!.clientHeight
el!.scrollBy({ top: dy, behavior: 'smooth' })
await waitFor(0.1) // Animation playback
return `✅ Scrolled container (${el!.tagName}) by ${dy}px.`
await waitFor(0.1)
const scrollAfter = el!.scrollTop
const scrolled = scrollAfter - scrollBefore
if (Math.abs(scrolled) < 1) {
return dy > 0
? `⚠️ Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
: `⚠️ Already at the top of container (${el!.tagName}), cannot scroll up further.`
}
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
const reachedTop = dy < 0 && scrollAfter <= 1
if (reachedBottom)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
if (reachedTop)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px.`
}
}
@@ -420,11 +450,52 @@ export async function scrollHorizontally(
(document.documentElement as HTMLElement)
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
// Page-level scroll
const scrollBefore = window.scrollX
const scrollMax = document.documentElement.scrollWidth - window.innerWidth
window.scrollBy(dx, 0)
return `✅ Scrolled page horizontally by ${dx}px`
const scrollAfter = window.scrollX
const scrolled = scrollAfter - scrollBefore
if (Math.abs(scrolled) < 1) {
return dx > 0
? `⚠️ Already at the right edge of the page, cannot scroll right further.`
: `⚠️ Already at the left edge of the page, cannot scroll left further.`
}
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
const reachedLeft = dx < 0 && scrollAfter <= 1
if (reachedRight)
return `✅ Scrolled page by ${scrolled}px. Reached the right edge of the page.`
if (reachedLeft) return `✅ Scrolled page by ${scrolled}px. Reached the left edge of the page.`
return `✅ Scrolled page horizontally by ${scrolled}px.`
} else {
// Container scroll
const scrollBefore = el!.scrollLeft
const scrollMax = el!.scrollWidth - el!.clientWidth
el!.scrollBy({ left: dx, behavior: 'smooth' })
await waitFor(0.1) // Animation playback
return `✅ Scrolled container (${el!.tagName}) horizontally by ${dx}px`
await waitFor(0.1)
const scrollAfter = el!.scrollLeft
const scrolled = scrollAfter - scrollBefore
if (Math.abs(scrolled) < 1) {
return dx > 0
? `⚠️ Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
: `⚠️ Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
}
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
const reachedLeft = dx < 0 && scrollAfter <= 1
if (reachedRight)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
if (reachedLeft)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
return `✅ Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
}
}
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
/**
* Viewport expansion for DOM tree extraction.
* -1 means full page (no viewport restriction)
* 0 means viewport only
* positive values expand the viewport by that many pixels
*
* @note Since isTopElement depends on elementFromPoint,
* it returns null when out of viewport, this feature has no practical use, only differ between -1 and 0
*/
// export const VIEWPORT_EXPANSION = 100
export const VIEWPORT_EXPANSION = -1
@@ -16,6 +16,7 @@
* @edit scrollable element detection
* @edit add `data-browser-use-ignore` attribute
* @edit improve `sampleRect`, filter out rects with 0 area
* @edit exclude aria-hidden elements
*/
export default (
@@ -1446,7 +1447,14 @@ export default (
* @edit add `data-browser-use-ignore` attribute
*/
if (node.dataset?.browserUseIgnore === 'true') {
return true // Skip this node and its children
return null // Skip this node and its children
}
/**
* @edit exclude aria-hidden elements
*/
if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') {
return null // Skip this node and its children
}
// Special handling for root node (body)
@@ -1,11 +1,11 @@
import { VIEWPORT_EXPANSION } from '@/config/constants'
import domTree from '@/dom/dom_tree/index'
import { VIEWPORT_EXPANSION } from '../constants'
import domTree from './dom_tree/index.js'
import {
ElementDomNode,
FlatDomTree,
InteractiveElementDomNode,
TextDomNode,
} from '@/dom/dom_tree/type'
} from './dom_tree/type'
export interface DomConfig {
interactiveBlacklist?: (Element | (() => Element))[]
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
@@ -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
}
@@ -1,4 +1,4 @@
import type { PageAgent } from '@/PageAgent'
import type { PageController } from '../PageController'
const clearFunctions = [] as (() => void)[]
@@ -11,9 +11,9 @@ function fixAntdSelect() {
// for (const select of selects) {}
}
export function patchAntd(pageAgent: PageAgent) {
pageAgent.addEventListener('beforeUpdate', fixAntdSelect)
pageAgent.addEventListener('afterUpdate', () => {
export function patchAntd(pageController: PageController) {
pageController.addEventListener('beforeUpdate', fixAntdSelect)
pageController.addEventListener('afterUpdate', () => {
for (const fn of clearFunctions) fn()
clearFunctions.length = 0
})
@@ -1,7 +1,7 @@
import type { PageAgent } from '@/PageAgent'
import type { PageController } from '../PageController'
// Find common React root elements and add data-page-agent-not-interactive attribute
export function patchReact(pageAgent: PageAgent) {
export function patchReact(pageController: PageController) {
const reactRootElements = document.querySelectorAll(
'[data-reactroot], [data-reactid], [data-react-checksum], #root, #app, [id^="root-"], [id^="app-"], #adex-wrapper, #adex-root'
)
@@ -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": {}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"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"]
}
+45
View File
@@ -0,0 +1,45 @@
// @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/page-controller`))
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/PageController.ts'),
name: 'PageController',
fileName: 'page-controller',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: {
external: ['@page-agent/*', 'ai-motion'],
onwarn: function (message, handler) {
if (message.code === 'EVAL') return
handler(message)
},
},
minify: false,
sourcemap: true,
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@page-agent/ui",
"version": "0.3.0-beta.1",
"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 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{}})\""
}
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
@@ -12,7 +12,7 @@ export class I18n {
constructor(language: SupportedLanguage = 'en-US') {
this.language = language in locales ? language : 'en-US'
this.translations = locales[language]
this.translations = locales[this.language]
}
// 类型安全的翻译方法
@@ -4,17 +4,13 @@ const enUS = {
panel: {
ready: 'Ready',
thinking: 'Thinking...',
paused: 'Paused',
taskInput: 'Enter new task, describe steps in detail, press Enter to submit',
userAnswerPrompt: 'Please answer the question above, press Enter to submit',
taskTerminated: 'Task terminated',
taskCompleted: 'Task completed',
continueExecution: 'Continue execution',
userAnswer: 'User answer: {{input}}',
question: 'Question: {{question}}',
waitingPlaceholder: 'Waiting for task to start...',
pause: 'Pause',
continue: 'Continue',
stop: 'Stop',
expand: 'Expand history',
collapse: 'Collapse history',
@@ -26,6 +22,7 @@ const enUS = {
selecting: 'Selecting option "{{text}}"...',
scrolling: 'Scrolling page...',
waiting: 'Waiting {{seconds}} seconds...',
askingUser: 'Asking user...',
done: 'Task done',
clicked: '🖱️ Clicked element [{{index}}]',
inputted: '⌨️ Inputted text "{{text}}"',
@@ -54,17 +51,13 @@ const zhCN = {
panel: {
ready: '准备就绪',
thinking: '正在思考...',
paused: '暂停中,稍后',
taskInput: '输入新任务,详细描述步骤,回车提交',
userAnswerPrompt: '请回答上面问题,回车提交',
taskTerminated: '任务已终止',
taskCompleted: '任务结束',
continueExecution: '继续执行',
userAnswer: '用户回答: {{input}}',
question: '询问: {{question}}',
waitingPlaceholder: '等待任务开始...',
pause: '暂停',
continue: '继续',
stop: '终止',
expand: '展开历史',
collapse: '收起历史',
@@ -76,6 +69,7 @@ const zhCN = {
selecting: '正在选择选项 "{{text}}"...',
scrolling: '正在滚动页面...',
waiting: '等待 {{seconds}} 秒...',
askingUser: '正在询问用户...',
done: '结束任务',
clicked: '🖱️ 已点击元素 [{{index}}]',
inputted: '⌨️ 已输入文本 "{{text}}"',
+3
View File
@@ -0,0 +1,3 @@
export { Panel, type PanelConfig } from './panel/Panel'
export type { AgentActivity, PanelAgentAdapter } from './panel/types'
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
@@ -2,4 +2,4 @@ This is the CSS implementation of ai-motion.
Easy to use but Terrible performance. Causing full screen glitching in some browsers.
Use it only in a small area.
Use it only in a small area.
@@ -209,18 +209,6 @@
}
}
.pauseButton {
font-weight: 600;
&.paused {
background: rgba(34, 197, 94, 0.2); /* 绿色背景表示可以继续 */
color: rgb(34, 197, 94);
&:hover {
background: rgba(34, 197, 94, 0.3);
}
}
}
.stopButton {
background: rgba(239, 68, 68, 0.2);
color: rgb(255, 41, 41);
@@ -364,6 +352,16 @@
background: linear-gradient(135deg, rgba(255, 214, 0, 0.1), rgba(255, 214, 0, 0.05));
}
&.observation {
border-left-color: rgb(147, 51, 234);
background: linear-gradient(135deg, rgba(147, 51, 234, 0.1), rgba(147, 51, 234, 0.05));
}
&.question {
border-left-color: rgb(255, 159, 67);
background: linear-gradient(135deg, rgba(255, 159, 67, 0.15), rgba(255, 159, 67, 0.08));
}
/* 突出显示 done 成功结果 */
&.doneSuccess {
background: linear-gradient(
@@ -446,7 +444,7 @@
.historyContent {
display: flex;
align-items: center;
align-items: flex-start;
gap: 8px;
word-break: break-all;
@@ -460,6 +458,12 @@
line-height: 1;
transition: all 0.3s ease;
}
.reflectionLines {
display: flex;
flex-direction: column;
gap: 4px;
}
}
.historyMeta {
+649
View File
@@ -0,0 +1,649 @@
import { I18n, type SupportedLanguage } from '../i18n'
import { truncate } from '../utils'
import { createCard, createReflectionLines, formatTime } from './cards'
import type { AgentActivity, PanelAgentAdapter } from './types'
import styles from './Panel.module.css'
/**
* Panel configuration
*/
export interface PanelConfig {
language?: SupportedLanguage
/**
* Whether to prompt for next task after task completion
* @default true
*/
promptForNextTask?: boolean
}
/**
* Agent control panel
*
* Architecture:
* - History list: renders directly from agent.history (historical events)
* - Header bar: shows activity events (transient state) and agent status
*
* This separation ensures data consistency - history is the single source of truth
* for what has been done, while activity shows what is happening now.
*/
export class Panel {
#wrapper: HTMLElement
#indicator: HTMLElement
#statusText: HTMLElement
#historySection: HTMLElement
#expandButton: HTMLElement
#stopButton: HTMLElement
#inputSection: HTMLElement
#taskInput: HTMLInputElement
#agent: PanelAgentAdapter
#config: PanelConfig
#isExpanded = false
#i18n: I18n
#userAnswerResolver: ((input: string) => void) | null = null
#isWaitingForUserAnswer: boolean = false
#headerUpdateTimer: ReturnType<typeof setInterval> | null = null
#pendingHeaderText: string | null = null
#isAnimating = false
// Event handlers (bound for removal)
#onStatusChange = () => this.#handleStatusChange()
#onHistoryChange = () => this.#handleHistoryChange()
#onActivity = (e: Event) => this.#handleActivity((e as CustomEvent<AgentActivity>).detail)
#onAgentDispose = () => this.dispose()
get wrapper(): HTMLElement {
return this.#wrapper
}
/**
* Create a Panel bound to an agent
* @param agent - Agent instance that implements PanelAgentAdapter
* @param config - Optional panel configuration
*/
constructor(agent: PanelAgentAdapter, config: PanelConfig = {}) {
this.#agent = agent
this.#config = config
this.#i18n = new I18n(config.language ?? 'en-US')
// Set up askUser callback on agent
this.#agent.onAskUser = (question) => this.#askUser(question)
// Create UI elements
this.#wrapper = this.#createWrapper()
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!
this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)!
this.#stopButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!
this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)!
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!
// Listen to agent events
this.#agent.addEventListener('statuschange', this.#onStatusChange)
this.#agent.addEventListener('historychange', this.#onHistoryChange)
this.#agent.addEventListener('activity', this.#onActivity)
this.#agent.addEventListener('dispose', this.#onAgentDispose)
this.#setupEventListeners()
this.#startHeaderUpdateLoop()
this.#showInputArea()
this.hide() // Start hidden
}
// ========== Agent event handlers ==========
/** Handle agent status change */
#handleStatusChange(): void {
const status = this.#agent.status
// Map agent status to UI indicator type
const indicatorType =
status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status
this.#updateStatusIndicator(indicatorType)
// Show/hide based on status
if (status === 'running') {
this.show()
this.#hideInputArea() // Hide input while running
}
// Handle completion
if (status === 'completed' || status === 'error') {
if (!this.#isExpanded) {
this.#expand()
}
if (this.#shouldShowInputArea()) {
this.#showInputArea()
}
}
}
/** Handle agent history change - re-render history list from agent.history */
#handleHistoryChange(): void {
this.#renderHistory()
}
/**
* Handle agent activity - transient state for immediate UI feedback
* Activity events are NOT persisted in history, only used for header bar updates
*/
#handleActivity(activity: AgentActivity): void {
switch (activity.type) {
case 'thinking':
this.#pendingHeaderText = this.#i18n.t('ui.panel.thinking')
this.#updateStatusIndicator('thinking')
break
case 'executing':
this.#pendingHeaderText = this.#getToolExecutingText(activity.tool, activity.input)
this.#updateStatusIndicator('executing')
break
case 'executed':
this.#pendingHeaderText = truncate(activity.output, 50)
break
case 'retrying':
this.#pendingHeaderText = `Retrying (${activity.attempt}/${activity.maxAttempts})`
this.#updateStatusIndicator('retrying')
break
case 'error':
this.#pendingHeaderText = truncate(activity.message, 50)
this.#updateStatusIndicator('error')
break
}
}
/**
* Ask for user input (internal, called by agent via onAskUser)
*/
#askUser(question: string): Promise<string> {
return new Promise((resolve) => {
// Set `waiting for user answer` state
this.#isWaitingForUserAnswer = true
this.#userAnswerResolver = resolve
// Expand history panel
if (!this.#isExpanded) {
this.#expand()
}
// Add temporary question card so user can see the full question
const tempCard = document.createElement('div')
tempCard.innerHTML = createCard({
icon: '❓',
content: `Question: ${question}`,
meta: formatTime(this.#config.language ?? 'en-US'),
type: 'question',
})
const cardElement = tempCard.firstElementChild as HTMLElement
cardElement.setAttribute('data-temp-card', 'true')
this.#historySection.appendChild(cardElement)
this.#scrollToBottom()
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.#statusText.textContent = this.#i18n.t('ui.panel.ready')
this.#updateStatusIndicator('thinking')
this.#renderHistory()
this.#collapse()
// Reset user input state
this.#isWaitingForUserAnswer = false
this.#userAnswerResolver = null
// Show input area
this.#showInputArea()
}
expand(): void {
this.#expand()
}
collapse(): void {
this.#collapse()
}
/**
* Dispose panel and clean up event listeners
*/
dispose(): void {
// Remove agent event listeners
this.#agent.removeEventListener('statuschange', this.#onStatusChange)
this.#agent.removeEventListener('historychange', this.#onHistoryChange)
this.#agent.removeEventListener('activity', this.#onActivity)
this.#agent.removeEventListener('dispose', this.#onAgentDispose)
// Clean up UI
this.#isWaitingForUserAnswer = false
this.#stopHeaderUpdateLoop()
this.wrapper.remove()
}
// ========== Private methods ==========
#getToolExecutingText(toolName: string, args: unknown): string {
const a = args as Record<string, string | number>
switch (toolName) {
case 'click_element_by_index':
return this.#i18n.t('ui.tools.clicking', { index: a.index })
case 'input_text':
return this.#i18n.t('ui.tools.inputting', { index: a.index })
case 'select_dropdown_option':
return this.#i18n.t('ui.tools.selecting', { text: a.text })
case 'scroll':
return this.#i18n.t('ui.tools.scrolling')
case 'wait':
return this.#i18n.t('ui.tools.waiting', { seconds: a.seconds })
case 'ask_user':
return this.#i18n.t('ui.tools.askingUser')
case 'done':
return this.#i18n.t('ui.tools.done')
default:
return this.#i18n.t('ui.tools.executing', { toolName })
}
}
/**
* Stop Agent
*/
#stopAgent(): void {
this.#agent.dispose()
}
/**
* Submit task
*/
#submitTask() {
const input = this.#taskInput.value.trim()
if (!input) return
// Hide input area
this.#hideInputArea()
if (this.#isWaitingForUserAnswer) {
// Handle user input mode
this.#handleUserAnswer(input)
} else {
// Execute task via agent
this.#agent.execute(input)
}
}
/**
* Handle user answer
*/
#handleUserAnswer(input: string): void {
// Remove temporary question cards (only direct children for safety)
Array.from(this.#historySection.children).forEach((child) => {
if (child.getAttribute('data-temp-card') === 'true') {
child.remove()
}
})
// Reset state
this.#isWaitingForUserAnswer = false
// Call resolver to return user input
if (this.#userAnswerResolver) {
this.#userAnswerResolver(input)
this.#userAnswerResolver = null
}
}
/**
* Show input area
*/
#showInputArea(placeholder?: string): void {
// Clear input field
this.#taskInput.value = ''
this.#taskInput.placeholder = placeholder || this.#i18n.t('ui.panel.taskInput')
this.#inputSection.classList.remove(styles.hidden)
// Focus on input field
setTimeout(() => {
this.#taskInput.focus()
}, 100)
}
/**
* Hide input area
*/
#hideInputArea(): void {
this.#inputSection.classList.add(styles.hidden)
}
/**
* Check if input area should be shown
*/
#shouldShowInputArea(): boolean {
// Always show input area if waiting for user input
if (this.#isWaitingForUserAnswer) return true
const history = this.#agent.history
if (history.length === 0) {
return true // Initial state
}
const status = this.#agent.status
const isTaskEnded = status === 'completed' || status === 'error'
// Only show input area after task completion if configured to do so
if (isTaskEnded) {
return this.#config.promptForNextTask ?? true
}
return false
}
#createWrapper(): HTMLElement {
const wrapper = document.createElement('div')
wrapper.id = 'page-agent-runtime_agent-panel'
wrapper.className = styles.wrapper
wrapper.setAttribute('data-browser-use-ignore', 'true')
wrapper.innerHTML = `
<div class="${styles.background}"></div>
<div class="${styles.historySectionWrapper}">
<div class="${styles.historySection}">
<div class="${styles.historyItem}">
<div class="${styles.historyContent}">
<span class="${styles.statusIcon}">🧠</span>
<span>${this.#i18n.t('ui.panel.waitingPlaceholder')}</span>
</div>
</div>
</div>
</div>
<div class="${styles.header}">
<div class="${styles.statusSection}">
<div class="${styles.indicator} ${styles.thinking}"></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.#i18n.t('ui.panel.expand')}">
</button>
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
X
</button>
</div>
</div>
<div class="${styles.inputSectionWrapper} ${styles.hidden}">
<div class="${styles.inputSection}">
<input
type="text"
class="${styles.taskInput}"
maxlength="200"
/>
</div>
</div>
`
document.body.appendChild(wrapper)
return wrapper
}
#setupEventListeners(): void {
// Click header area to expand/collapse
const header = this.wrapper.querySelector(`.${styles.header}`)!
header.addEventListener('click', (e) => {
// Don't trigger expand/collapse if clicking on buttons
if ((e.target as HTMLElement).closest(`.${styles.controlButton}`)) {
return
}
this.#toggle()
})
// Expand button
this.#expandButton.addEventListener('click', (e) => {
e.stopPropagation()
this.#toggle()
})
// Stop button
this.#stopButton.addEventListener('click', (e) => {
e.stopPropagation()
this.#stopAgent()
})
// Submit on Enter key in input field
this.#taskInput.addEventListener('keydown', (e) => {
if (e.isComposing) return // Ignore IME composition keys
if (e.key === 'Enter') {
e.preventDefault()
this.#submitTask()
}
})
// Prevent input area click event bubbling
this.#inputSection.addEventListener('click', (e) => {
e.stopPropagation()
})
}
#toggle(): void {
if (this.#isExpanded) {
this.#collapse()
} else {
this.#expand()
}
}
#expand(): void {
this.#isExpanded = true
this.wrapper.classList.add(styles.expanded)
this.#expandButton.textContent = '▲'
}
#collapse(): void {
this.#isExpanded = false
this.wrapper.classList.remove(styles.expanded)
this.#expandButton.textContent = '▼'
}
/**
* Start periodic header update loop
*/
#startHeaderUpdateLoop(): void {
// Check every 450ms (same as total animation duration)
this.#headerUpdateTimer = setInterval(() => {
this.#checkAndUpdateHeader()
}, 450)
}
/**
* Stop periodic header update loop
*/
#stopHeaderUpdateLoop(): void {
if (this.#headerUpdateTimer) {
clearInterval(this.#headerUpdateTimer)
this.#headerUpdateTimer = null
}
}
/**
* Check if header needs update and trigger animation if not currently animating
*/
#checkAndUpdateHeader(): void {
// If no pending text or currently animating, skip
if (!this.#pendingHeaderText || this.#isAnimating) {
return
}
// If text is already displayed, clear pending and skip
if (this.#statusText.textContent === this.#pendingHeaderText) {
this.#pendingHeaderText = null
return
}
// Start animation
const textToShow = this.#pendingHeaderText
this.#pendingHeaderText = null
this.#animateTextChange(textToShow)
}
/**
* Animate text change with fade out/in effect
*/
#animateTextChange(newText: string): void {
this.#isAnimating = true
// Fade out current text
this.#statusText.classList.add(styles.fadeOut)
setTimeout(() => {
// Update text content
this.#statusText.textContent = newText
// Fade in new text
this.#statusText.classList.remove(styles.fadeOut)
this.#statusText.classList.add(styles.fadeIn)
setTimeout(() => {
this.#statusText.classList.remove(styles.fadeIn)
this.#isAnimating = false
}, 300)
}, 150) // Half the duration of fade out animation
}
#updateStatusIndicator(
type: 'thinking' | 'executing' | 'executed' | 'retrying' | 'completed' | 'error'
): void {
// Clear all status classes
this.#indicator.className = styles.indicator
// Add corresponding status class
this.#indicator.classList.add(styles[type])
}
#scrollToBottom(): void {
// Execute in next event loop to ensure DOM update completion
setTimeout(() => {
this.#historySection.scrollTop = this.#historySection.scrollHeight
}, 0)
}
/**
* Render history directly from agent.history
*
* Renders:
* 1. Task (first item, from agent.task)
* 2. Reflection cards (evaluation, memory, next_goal)
* 3. Tool execution with output
* 4. Observations
*/
#renderHistory(): void {
const items: string[] = []
// 1. Task card (always first)
const task = this.#agent.task
if (task) {
items.push(this.#createTaskCard(task))
}
// 2. Render each history event
const history = this.#agent.history
for (let i = 0; i < history.length; i++) {
const event = history[i]
items.push(...this.#createHistoryCards(event, i + 1))
}
this.#historySection.innerHTML = items.join('')
this.#scrollToBottom()
}
#createTaskCard(task: string): string {
return createCard({ icon: '🎯', content: task, type: 'input' })
}
/** Create cards for a history event */
#createHistoryCards(event: PanelAgentAdapter['history'][number], stepNumber: number): string[] {
const cards: string[] = []
const time = formatTime(this.#config.language ?? 'en-US')
const meta = this.#i18n.t('ui.panel.step', {
number: stepNumber.toString(),
time,
duration: '',
})
if (event.type === 'step') {
// Reflection card
if (event.reflection) {
const lines = createReflectionLines(event.reflection)
if (lines.length > 0) {
cards.push(createCard({ icon: '🧠', content: lines, meta }))
}
}
// Action card
const action = event.action
if (action) {
cards.push(...this.#createActionCards(action, meta))
}
} else if (event.type === 'observation') {
cards.push(
createCard({ icon: '👁️', content: event.content || '', meta, type: 'observation' })
)
} else if (event.type === 'user_takeover') {
cards.push(createCard({ icon: '👤', content: 'User takeover', meta, type: 'input' }))
}
return cards
}
/** Create cards for an action */
#createActionCards(
action: { name: string; input: unknown; output: string },
meta: string
): string[] {
const cards: string[] = []
if (action.name === 'done') {
const input = action.input as { text?: string }
const text = input.text || action.output || ''
if (text) {
cards.push(createCard({ icon: '🤖', content: text, meta, type: 'output' }))
}
} else if (action.name === 'ask_user') {
const input = action.input as { question?: string }
const answer = action.output.replace(/^User answered:\s*/i, '')
cards.push(
createCard({
icon: '❓',
content: `Question: ${input.question || ''}`,
meta,
type: 'question',
})
)
cards.push(createCard({ icon: '💬', content: `Answer: ${answer}`, meta, type: 'input' }))
} else {
const toolText = this.#getToolExecutingText(action.name, action.input)
cards.push(createCard({ icon: '🔨', content: toolText, meta }))
if (action.output?.length > 0) {
cards.push(createCard({ icon: '🔨', content: action.output, meta, type: 'output' }))
}
}
return cards
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* Card HTML generation utilities for Panel
*/
import { escapeHtml } from '../utils'
import styles from './Panel.module.css'
type CardType = 'default' | 'input' | 'output' | 'question' | 'observation'
interface CardOptions {
icon: string
content: string | string[]
meta?: string
type?: CardType
}
/** Create a single history card */
export function createCard({ icon, content, meta, type }: CardOptions): string {
const typeClass = type ? styles[type] : ''
const contentHtml = Array.isArray(content)
? `<div class="${styles.reflectionLines}">${content.join('')}</div>`
: `<span>${escapeHtml(content)}</span>`
return `
<div class="${styles.historyItem} ${typeClass}">
<div class="${styles.historyContent}">
<span class="${styles.statusIcon}">${icon}</span>
${contentHtml}
</div>
${meta ? `<div class="${styles.historyMeta}">${meta}</div>` : ''}
</div>
`
}
/** Format timestamp for cards */
export function formatTime(locale: string = 'en-US'): string {
return new Date().toLocaleTimeString(locale, {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
/** Create reflection lines from reflection object */
export function createReflectionLines(reflection: {
evaluation_previous_goal?: string
memory?: string
next_goal?: string
}): string[] {
const lines: string[] = []
if (reflection.evaluation_previous_goal) {
lines.push(`<div>🔍 ${escapeHtml(reflection.evaluation_previous_goal)}</div>`)
}
if (reflection.memory) {
lines.push(`<div>💾 ${escapeHtml(reflection.memory)}</div>`)
}
if (reflection.next_goal) {
lines.push(`<div>🎯 ${escapeHtml(reflection.next_goal)}</div>`)
}
return lines
}
+67
View File
@@ -0,0 +1,67 @@
/**
* Agent activity - transient state for immediate UI feedback.
*
* Unlike historical events (which are persisted), activities are ephemeral
* and represent "what the agent is doing right now". UI components should
* listen to 'activity' events to show real-time feedback.
*
* Note: There is no 'idle' activity - absence of activity events means idle.
*
* Events dispatched: CustomEvent<AgentActivity>
*/
export type AgentActivity =
| { type: 'thinking' }
| { type: 'executing'; tool: string; input: unknown }
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
| { type: 'retrying'; attempt: number; maxAttempts: number }
| { type: 'error'; message: string }
/**
* Minimal interface that Panel expects from an agent.
* Panel does not depend on PageAgent directly - it only requires this interface.
* This enables decoupling and allows any agent implementation to work with Panel.
*
* Events:
* - 'statuschange': Agent status changed (idle/running/completed/error)
* - 'historychange': Historical events updated (persisted)
* - 'activity': Transient activity for immediate UI feedback (thinking/executing/etc)
* - 'dispose': Agent is being disposed
*/
export interface PanelAgentAdapter extends EventTarget {
/** Current agent status */
readonly status: 'idle' | 'running' | 'completed' | 'error'
/** History of agent events */
readonly history: readonly {
type: 'step' | 'observation' | 'user_takeover' | 'error'
/** For 'step' type */
reflection?: {
evaluation_previous_goal?: string
memory?: string
next_goal?: string
}
/** For 'step' type */
action?: {
name: string
input: unknown
output: string
}
/** For 'observation' type */
content?: string
}[]
/** Current task being executed */
readonly task: string
/**
* Callback for when agent needs user input.
* Panel will set this to handle user questions via its UI.
*/
onAskUser?: (question: string) => Promise<string>
/** Execute a task */
execute(task: string): Promise<unknown>
/** Dispose the agent */
dispose(): void
}
+18
View File
@@ -0,0 +1,18 @@
export function truncate(text: string, maxLength: number): string {
if (text.length > maxLength) {
return text.substring(0, maxLength) + '...'
}
return text
}
/**
* Escape HTML special characters to prevent XSS and rendering issues
*/
export function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
+9
View File
@@ -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": {}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"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: [],
},
minify: false,
sourcemap: true,
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+110
View File
@@ -0,0 +1,110 @@
# Website Package - Instructions for Coding Assistants
## Tech Stack
- **React** with TypeScript
- **Vite** for dev server and build
- **Tailwind CSS** for styling
- **shadcn/ui** (new-york style) for UI components
- **Magic UI** for animations and effects
- **wouter** with hash routing for static hosting
- **lucide-react** for icons
## Component Guidelines
### Use shadcn/ui Components First
**ALWAYS prefer shadcn/ui components over custom implementations.**
Before creating any UI component, check if shadcn already provides it:
```bash
# IMPORTANT: Run from packages/website/, NOT from repo root
cd packages/website
# Add a new shadcn component
npx shadcn@latest add <component-name>
# Add a Magic UI component
npx shadcn@latest add "@magicui/<component-name>"
```
Available shadcn components: https://ui.shadcn.com/docs/components
Available Magic UI components: https://magicui.design/docs/components
### Current UI Components
Located in `src/components/ui/`:
**From shadcn/ui:**
- `alert`, `badge`, `button`, `separator`, `sonner`, `switch`, `tooltip`
**From Magic UI:**
- `animated-gradient-text`, `animated-shiny-text`, `aurora-text`
- `hyper-text`, `magic-card`, `neon-gradient-card`, `particles`
- `sparkles-text`, `text-animate`, `typing-animation`
**Custom:**
- `highlighter`, `kbd`, `spinner`
### Styling Rules
1. **Prefer Tailwind classes** over custom CSS
2. Use CSS modules only for complex component-specific styles
3. Support dark mode via `dark:` classes
4. Use CSS variables from `src/index.css` for theme colors
## Project Structure
```
src/
├── pages/
│ ├── Home.tsx # Homepage
│ └── docs/
│ ├── Layout.tsx # Documentation sidebar
│ └── [section]/[topic]/page.tsx
├── components/
│ ├── ui/ # shadcn/ui + Magic UI components
│ ├── Header.tsx # Site header
│ └── Footer.tsx # Site footer
├── i18n/ # Internationalization
├── router.tsx # Central routing
└── main.tsx # App entry
```
## Adding New Pages
### Documentation Page
1. Create `src/pages/docs/<section>/<slug>/page.tsx`
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
3. Add navigation item to `pages/docs/Layout.tsx`
## Routing
Uses hash-based routing for static hosting:
```tsx
import { Router } from 'wouter'
import { useHashLocation } from 'wouter/use-hash-location'
;<Router hook={useHashLocation}>{/* routes */}</Router>
```
## Configuration Files
| File | Purpose |
| ----------------- | ----------------------- |
| `components.json` | shadcn/ui configuration |
| `vite.config.js` | Vite build settings |
| `tsconfig.json` | TypeScript config |
## Commands
```bash
npm start # Dev server (from root)
npm run build:website # Build website (from root)
```
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
}
}
+60
View File
@@ -0,0 +1,60 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="https://img.alicdn.com/imgextra/i2/O1CN012eGDRI1X6nnMt9clU_!!6000000002875-49-tps-64-64.webp"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PageAgent - The GUI Agent Living in Your Webpage</title>
<meta
name="description"
content="PageAgent.js: Intelligent GUI Agent for any website. Modern web AI automation with minimal integration."
/>
<meta
name="keywords"
content="PageAgent, AI Agent, GUI Agent, Web Automation, GUI Automation, Frontend, CDN, JavaScript, React, Vite, LLM"
/>
<meta
property="og:image"
content="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
/>
<meta property="og:url" content="https://alibaba.github.io/page-agent" />
<meta property="og:type" content="website" />
<meta name="theme-color" content="#58c0fc" />
<meta name="color-scheme" content="light dark" />
<meta name="author" content="PageAgent.js Team" />
<meta property="og:title" content="PageAgent.js - AI-powered GUI Agent" />
<meta property="og:description" content="The GUI Agent living in your website." />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="zh_CN" />
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-HCGRJTN3HM"></script>
<script>
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments)
}
gtag('js', new Date())
gtag('config', 'G-HCGRJTN3HM')
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.tsx"></script>
<script>
// Dynamically update html lang attribute based on i18n detection
const updateHtmlLang = () => {
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
document.documentElement.lang = lang
}
updateHtmlLang()
window.addEventListener('storage', updateHtmlLang)
</script>
</body>
</html>
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@page-agent/website",
"private": true,
"version": "0.3.0-beta.1",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build:website": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-i18next": "^16.5.2",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"wouter": "^3.9.0"
},
"dependencies": {
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.562.0",
"motion": "^12.26.1",
"next-themes": "^0.4.6",
"rough-notation": "^0.5.1",
"simple-icons": "^16.5.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
}
}
@@ -6,7 +6,7 @@ export default function BetaNotice() {
return (
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-8">
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="shrink-0">
<span className="text-xl">🚧</span>
</div>
<div className="ml-3">
@@ -28,7 +28,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
// 使用 Tailwind 的 dark: 前缀实现自动主题切换
const containerClasses =
'bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-gray-300 dark:border-gray-700'
'bg-linear-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-gray-300 dark:border-gray-700'
const headerClasses = 'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700'
const headerTextClasses = 'text-gray-700 dark:text-gray-300'
const languageTextClasses = 'text-gray-600 dark:text-gray-400'
@@ -72,7 +72,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
<div className="flex">
{/* 行号 */}
{showLineNumbers && (
<div className={`flex-shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
<div className={`shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
<div className="text-xs font-mono leading-6">
{lines.map((line, lineIdx) => {
const lineNum = lineIdx + 1
@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next'
import { siGithub } from 'simple-icons'
export default function Footer() {
const { t } = useTranslation('common')
@@ -19,8 +20,13 @@ export default function Footer() {
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200"
aria-label={t('footer.github_label')}
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
<svg
role="img"
viewBox="0 0 24 24"
className="w-5 h-5 fill-current"
aria-hidden="true"
>
<path d={siGithub.path} />
</svg>
</a>
</div>
+174
View File
@@ -0,0 +1,174 @@
import { BookOpen, Menu, X } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { siGithub } from 'simple-icons'
import { Link } from 'wouter'
import LanguageSwitcher from './LanguageSwitcher'
import ThemeSwitcher from './ThemeSwitcher'
import { HyperText } from './ui/hyper-text'
export default function Header() {
const { t } = useTranslation('common')
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
<>
<header
className="relative z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
role="banner"
>
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between gap-2">
{/* Logo */}
<Link
href="~/"
className="flex items-center gap-2 sm:gap-3 group shrink-0"
aria-label={t('header.logo_alt')}
onClick={() => setMobileMenuOpen(false)}
>
<img
src="https://img.alicdn.com/imgextra/i2/O1CN01HB8ylu1uozANEMZw2_!!6000000006085-49-tps-128-128.webp"
alt="PageAgent Logo"
className="w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200"
/>
<div>
<span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white block leading-tight">
page-agent
</span>
<HyperText
as="p"
className="hidden sm:block text-xs text-gray-600 dark:text-gray-300 py-0 font-normal overflow-visible"
duration={600}
animateOnHover={true}
aria-hidden="true"
>
{t('header.slogan')}
</HyperText>
</div>
</Link>
{/* Mobile Icon Navigation (横向滚动) */}
<nav
className="md:hidden flex items-center gap-1 overflow-x-auto scrollbar-hide flex-1"
role="navigation"
aria-label="Mobile navigation"
>
<Link
href="~/docs/introduction/overview"
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
aria-label={t('header.nav_docs')}
>
<BookOpen className="w-5 h-5" />
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
aria-label={t('header.nav_source')}
>
<svg
role="img"
viewBox="0 0 24 24"
className="w-5 h-5 fill-current"
aria-hidden="true"
>
<path d={siGithub.path} />
</svg>
</a>
</nav>
{/* Desktop Navigation */}
<nav
className="hidden md:flex items-center space-x-6"
role="navigation"
aria-label={t('header.nav_docs')}
>
<span className="text-xs font-mono text-gray-400 dark:text-gray-500 tabular-nums before:content-['v']">
{import.meta.env.VERSION}
</span>
<Link
href="~/docs/introduction/overview"
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
>
<BookOpen className="w-4 h-4" />
{t('header.nav_docs')}
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
aria-label={t('header.nav_source')}
>
<svg
role="img"
viewBox="0 0 24 24"
className="w-4 h-4 fill-current"
aria-hidden="true"
>
<path d={siGithub.path} />
</svg>
{t('header.nav_source')}
</a>
<ThemeSwitcher />
<LanguageSwitcher />
</nav>
{/* Mobile menu button */}
<button
type="button"
className="md:hidden p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200 shrink-0"
aria-label={t('header.mobile_menu')}
aria-expanded={mobileMenuOpen}
aria-controls="mobile-menu"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
{/* Mobile Navigation */}
{mobileMenuOpen && (
<nav
id="mobile-menu"
className="md:hidden pt-4 pb-2 space-y-3 border-t border-gray-200 dark:border-gray-700 mt-4"
role="navigation"
>
<Link
href="~/docs/introduction/overview"
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
onClick={() => setMobileMenuOpen(false)}
>
<BookOpen className="w-5 h-5" />
{t('header.nav_docs')}
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
aria-label={t('header.nav_source')}
>
<svg
role="img"
viewBox="0 0 24 24"
className="w-5 h-5 fill-current"
aria-hidden="true"
>
<path d={siGithub.path} />
</svg>
{t('header.nav_source')}
</a>
<div className="flex items-center gap-3 px-3 py-2">
<ThemeSwitcher />
<LanguageSwitcher />
</div>
</nav>
)}
</div>
</header>
</>
)
}
@@ -0,0 +1,126 @@
.syntax {
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
font-family: monospace;
color: #171717;
}
:global(.dark) .syntax {
color: #e0e0e0;
}
/* JavaScript/TypeScript 关键字 */
.keyword {
color: #d73a49;
font-weight: 600;
}
:global(.dark) .keyword {
color: #ff6b6b;
}
/* TypeScript 特定关键字 (interface, type, enum, etc.) */
.tsKeyword {
color: #af00db;
font-weight: 600;
}
:global(.dark) .tsKeyword {
color: #c792ea;
}
/* TypeScript 内置类型 */
.type {
color: #267f99;
font-weight: 500;
}
:global(.dark) .type {
color: #4ec9b0;
}
/* 字符串 */
.string {
color: #1d6eca;
}
:global(.dark) .string {
color: #4fc3f7;
}
/* 数字 */
.number {
color: #00c583;
}
:global(.dark) .number {
color: #66bb6a;
}
/* 布尔值和字面量 (true, false, null, undefined) */
.literal {
color: #0000ff;
font-weight: 500;
}
:global(.dark) .literal {
color: #569cd6;
}
/* 注释 */
.comment {
color: #6a737d;
font-style: italic;
}
:global(.dark) .comment {
color: #9e9e9e;
}
/* 装饰器 (@decorator) */
.decorator {
color: #e0aa00;
font-weight: 500;
}
:global(.dark) .decorator {
color: #dcdcaa;
}
/* 箭头函数 (=>) */
.arrow {
color: #d73a49;
font-weight: bold;
}
:global(.dark) .arrow {
color: #ff6b6b;
}
/* 标识符(变量名、函数名等) */
.identifier {
color: #171717;
}
:global(.dark) .identifier {
color: #e0e0e0;
}
/* 属性访问 (.property) */
.property {
color: #0550ae;
}
:global(.dark) .property {
color: #9cdcfe;
}
/* 运算符 */
.operator {
color: #5a5a5a;
}
:global(.dark) .operator {
color: #d4d4d4;
}
@@ -0,0 +1,176 @@
/**
* js 语法高亮组件,适合在文章中演示代码片段
*/
import React from 'react'
import styles from './HighlightSyntax.module.css'
interface HighlightSyntaxProps {
code: string
}
// JavaScript/TypeScript 关键字
const keywords =
'async|await|function|const|let|var|if|else|for|while|return|try|catch|finally|class|extends|from|import|export|default|undefined|throw|break|continue|switch|case|do|with|yield|delete|typeof|void|static|get|set|super|debugger'
// TypeScript 特定关键字
const tsKeywords =
'interface|type|enum|namespace|module|declare|abstract|implements|public|private|protected|readonly|as|satisfies|infer|keyof|is'
// 布尔值和空值
const literals = 'true|false|null|undefined|NaN|Infinity'
// TypeScript 内置类型
const tsTypes =
'string|number|boolean|any|unknown|never|void|object|symbol|bigint|Array|Promise|Record|Partial|Required|Readonly|Pick|Omit|Exclude|Extract|NonNullable|ReturnType|Parameters|ConstructorParameters|InstanceType|ThisType|Uppercase|Lowercase|Capitalize|Uncapitalize'
// 辅助函数:转义 HTML 特殊字符
function escapeHtml(text: string): string {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
// 语法高亮函数,先提取 token 再转义和高亮
function highlightSyntax(code: string): string {
// 构建正则模式,包含更多 token 类型(在原始文本上匹配)
const pattern = new RegExp(
'(' +
// 1. 字符串(双引号、单引号、模板字符串)
'"([^"\\\\]|\\\\.)*"|' +
"'([^'\\\\]|\\\\.)*'|" +
'`([^`\\\\]|\\\\.)*`|' +
// 2. 注释(单行和多行)
'//[^\\n]*|' +
'/\\*[\\s\\S]*?\\*/|' +
// 3. 装饰器
'@[a-zA-Z_$][\\w$]*|' +
// 4. 数字(包括小数、十六进制、科学计数法)
'\\b0[xX][0-9a-fA-F]+\\b|' +
'\\b\\d+\\.?\\d*(?:[eE][+-]?\\d+)?\\b|' +
// 5. TypeScript/JavaScript 关键字
'\\b(?:' +
keywords +
'|' +
tsKeywords +
'|' +
literals +
')\\b|' +
// 6. TypeScript 内置类型
'\\b(?:' +
tsTypes +
')\\b|' +
// 7. 箭头函数
'=>|' +
// 8. 函数调用(函数名后跟括号)
'\\b[a-zA-Z_$][\\w$]*(?=\\()|' +
// 9. 属性访问
'\\.[a-zA-Z_$][\\w$]*|' +
// 10. 运算符和特殊符号
'[+\\-*/%&|^!~<>=?:]+|' +
'[{}\\[\\]();,.]' +
')',
'g'
)
const tokens: string[] = []
let lastIndex = 0
let match: RegExpExecArray | null
while ((match = pattern.exec(code)) !== null) {
if (match.index > lastIndex) {
const gap = code.slice(lastIndex, match.index)
// 将间隙按空白符分割,保留空白符
tokens.push(...gap.split(/(\s+)/))
}
tokens.push(match[0])
lastIndex = pattern.lastIndex
}
if (lastIndex < code.length) {
tokens.push(...code.slice(lastIndex).split(/(\s+)/))
}
const highlighted = tokens
.map((token) => {
// 空白符直接返回
if (/^\s+$/.test(token)) {
return token
}
// 1. 注释(单行和多行)
if (/^\/\/.*$/.test(token) || /^\/\*[\s\S]*?\*\/$/.test(token)) {
return `<span class="${styles.comment}">${escapeHtml(token)}</span>`
}
// 2. 字符串
if (
/^"([^"\\]|\\.)*"$/.test(token) ||
/^'([^'\\]|\\.)*'$/.test(token) ||
/^`([^`\\]|\\.)*`$/.test(token)
) {
return `<span class="${styles.string}">${escapeHtml(token)}</span>`
}
// 3. 数字
if (/^(0[xX][0-9a-fA-F]+|\d+\.?\d*(?:[eE][+-]?\d+)?)$/.test(token)) {
return `<span class="${styles.number}">${escapeHtml(token)}</span>`
}
// 4. 布尔值和特殊字面量
if (new RegExp(`^(?:${literals})$`).test(token)) {
return `<span class="${styles.literal}">${escapeHtml(token)}</span>`
}
// 5. JavaScript/TypeScript 关键字
if (new RegExp(`^(?:${keywords})$`).test(token)) {
return `<span class="${styles.keyword}">${escapeHtml(token)}</span>`
}
// 6. TypeScript 特定关键字
if (new RegExp(`^(?:${tsKeywords})$`).test(token)) {
return `<span class="${styles.tsKeyword}">${escapeHtml(token)}</span>`
}
// 7. TypeScript 内置类型
if (new RegExp(`^(?:${tsTypes})$`).test(token)) {
return `<span class="${styles.type}">${escapeHtml(token)}</span>`
}
// 8. 装饰器
if (/^@[a-zA-Z_$][\w$]*$/.test(token)) {
return `<span class="${styles.decorator}">${escapeHtml(token)}</span>`
}
// 9. 箭头函数
if (token === '=>') {
return `<span class="${styles.arrow}">${escapeHtml(token)}</span>`
}
// 10. 函数调用和标识符
if (/^[a-zA-Z_$][\w$]*$/.test(token)) {
return `<span class="${styles.identifier}">${escapeHtml(token)}</span>`
}
// 11. 属性访问
if (/^\.[a-zA-Z_$][\w$]*$/.test(token)) {
return `<span class="${styles.property}">${escapeHtml(token)}</span>`
}
// 12. 运算符
if (/^[+\-*/%&|^!~<>=?:]+$/.test(token)) {
return `<span class="${styles.operator}">${escapeHtml(token)}</span>`
}
// 13. 其他符号,需要转义
return escapeHtml(token)
})
.join('')
return highlighted
}
const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {
const htmlContent = highlightSyntax(code)
// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml
return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />
}
export default HighlightSyntaxClient

Some files were not shown because too many files have changed in this diff Show More