Compare commits

..

204 Commits

Author SHA1 Message Date
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
Simon b85a82785f chore: bump version to 0.0.4 2025-10-22 23:02:49 +08:00
Simon 2314d28e60 fix: mobile panel styles 2025-10-22 23:02:17 +08:00
Simon 58475c63da feat: enhance mobile navigation and update i18n labels 2025-10-22 22:58:52 +08:00
Simon 55f07d971a docs: update i18n status in ROADMAP 2025-10-22 22:41:48 +08:00
Simon cd84269427 feat: i18n for website 2025-10-22 22:35:25 +08:00
Simon fedeb57f48 chore: add homepage 2025-10-22 19:04:19 +08:00
Simon 386167e99b chore: wording 2025-10-22 17:48:06 +08:00
Simon 190d264bda docs: update ROADMAP 2025-10-22 17:18:39 +08:00
Simon 7af3b580d5 docs: updating ROADMAP 2025-10-22 17:16:15 +08:00
Simon 4af927f218 fix: action 2025-10-22 16:59:10 +08:00
190 changed files with 11624 additions and 8313 deletions
+21 -27
View File
@@ -6,32 +6,11 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Please follow our community communication guidelines when filing issues. Thanks for your interest in improving the project! Before submitting, please read our guidelines.
- 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.
提交 Issue 时请遵守社区沟通规范: - [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)
- 提供建设性、可行动的反馈,避免情绪化表达。
- 请包含清晰的复现步骤与环境信息。
- 如果可以,欢迎直接提交修复 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
- type: textarea - type: textarea
id: description id: description
@@ -48,7 +27,7 @@ body:
render: typescript render: typescript
placeholder: Minimal reproduction code placeholder: Minimal reproduction code
validations: validations:
required: true required: false
- type: input - type: input
id: browser id: browser
@@ -56,12 +35,27 @@ body:
label: Browser label: Browser
placeholder: 'Chrome 120, Firefox 119, etc.' placeholder: 'Chrome 120, Firefox 119, etc.'
validations: validations:
required: true required: false
- type: input - type: input
id: version id: version
attributes: attributes:
label: version label: version
placeholder: '0.0.0' 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: validations:
required: true required: true
-3
View File
@@ -12,6 +12,3 @@ contact_links:
- name: Code of Conduct / 行为准则 - name: Code of Conduct / 行为准则
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md
about: Community expectations and standards. 社区行为期望与标准。 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 - type: markdown
attributes: attributes:
value: | value: |
Please follow our community communication guidelines when proposing features. Thanks for your interest in improving the project! Before submitting, please read our guidelines.
- Be polite and respectful. 感谢您对改进项目的兴趣!提交前请阅读我们的指南。
- Explain the problem first, then propose solutions.
- Prefer constructive, actionable suggestions.
- Contributions are encouraged — consider opening a 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)
- 先阐述问题,再提出解决方案。
- 提供建设性、可行动的建议。
- 鼓励代码贡献,欢迎直接提交 PR。
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 - type: checkboxes
id: community id: community
@@ -32,27 +43,5 @@ body:
required: true required: true
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。 - label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
required: true required: true
- type: textarea
id: problem
attributes:
label: Problem
placeholder: What problem does this solve?
validations: validations:
required: true 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 ## Type
- [ ] Bug fix - [ ] Bug fix
- [ ] New feature - [ ] Feature / Improvement
- [ ] Breaking change - [ ] Refactor
- [ ] Documentation - [ ] Documentation
- [ ] Website
- [ ] Demo / Testing
- [ ] Breaking change
## Testing ## Testing
@@ -19,10 +22,4 @@ Closes #(issue)
## Requirements / 要求 ## 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) . / 我已阅读并遵守行为准则。 - [ ] 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 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [20, 22] node-version: [20, 24, 25]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6 uses: actions/setup-node@v6
@@ -24,6 +24,13 @@ jobs:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'npm' 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 - name: Install dependencies
run: npm install run: npm install
+4 -4
View File
@@ -13,19 +13,19 @@ jobs:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 20 node-version: 24
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build demo - name: Build demo
run: npm run build run: npm run build:website
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v5 uses: actions/configure-pages@v5
@@ -33,7 +33,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v4 uses: actions/upload-pages-artifact@v4
with: with:
path: './dist' path: './packages/website/dist'
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
+9 -6
View File
@@ -13,20 +13,23 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 20 node-version: 24
cache: 'npm'
registry-url: 'https://registry.npmjs.org' 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 - name: Install dependencies
run: npm ci run: npm ci
- name: Build - name: Build
run: npm run build:lib run: npm run build:libs
- name: Publish to npm - name: Publish all public packages
run: npm publish run: npm publish --workspaces --access public
+6
View File
@@ -23,7 +23,13 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.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"]
} }
+11 -1
View File
@@ -1,6 +1,16 @@
{ {
"editor.fontLigatures": true, "editor.fontLigatures": true,
"cSpell.words": ["HITL", "opensource", "retryable"], "cSpell.words": [
"deepseek",
"HITL",
"innerhtml",
"llms",
"opensource",
"qwen",
"retryable",
"shadcn",
"wouter"
],
"markdownlint.config": { "markdownlint.config": {
// "comment": "Relaxed rules", // "comment": "Relaxed rules",
"default": true, "default": true,
+90 -120
View File
@@ -1,161 +1,131 @@
# Instructions for coding assistants # Instructions for Coding Assistants
## Project Overview ## 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 - **Core Library** (`packages/page-agent/`) - AI agent for browser DOM automation, published as `page-agent` on npm
2. **Demo&docs Website** (`pages/`) - React documentation and landing page - **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
Internal packages:
- **CDN** (`packages/cdn/`) - IIFE builds for script tag usage (npm: `@page-agent/cdn`)
- **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 ## Development Commands
### Core Commands
```bash ```bash
npm start # Start React website development server npm start # Start website dev server
npm run build # Build both library AND website npm run build # Build all packages
npm run build:lib # Build pure JS library only (src/ → dist/lib/) npm run build:libs # Build all libraries
npm run build:lib:watch # Library development with auto-rebuild
npm run lint # ESLint with TypeScript strict rules 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/` Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig and vite config when adding/removing packages.
- **Library build**: `vite.lib.config.js` → UMD/ES modules → `dist/lib/`
- **Entry points**: `src/entry.ts` (library), `pages/main.tsx` (website)
### Module Boundaries (Critical) ```
packages/
├── page-agent/ # npm: "page-agent" ⭐ MAIN
├── cdn/ # npm: "@page-agent/cdn" (IIFE builds)
├── 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 `workspaces` in `package.json` must be in topological order.
- **Website** (`pages/`): CAN import from `src/` via `@/` alias for demos
- **Import aliases**: `@/``src/`, `@pages/``pages/` ### Module Boundaries
- **Page Agent**: Core lib. Imports from `@page-agent/llms`, `@page-agent/page-controller`, `@page-agent/ui`
- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent
- **UI**: Panel and i18n. No dependency on page-agent
- **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 ### DOM Pipeline
1. **DOM Extraction**: Convert live DOM to `FlatDomTree` via `src/dom/dom_tree/` 1. **DOM Extraction**: Live DOM `FlatDomTree` via `page-controller/src/dom/dom_tree/`
2. **Dehydration**: DOM tree → simplified text for LLM processing 2. **Dehydration**: DOM tree → simplified text for LLM
3. **LLM Processing**: AI model returns action plans 3. **LLM Processing**: AI returns action plans (page-agent)
4. **Indexed Operations**: Map LLM responses back to specific DOM elements 4. **Indexed Operations**: PageAgent calls PageController by element index
### Event Bus Communication ### CDN Builds (`packages/cdn/`)
Use `src/utils/bus.ts` for decoupled PageAgent ↔ UI communication: Two IIFE builds for script tag usage:
```typescript | Build | File | Description |
// Emit from PageAgent | ----- | -------------------- | -------------------------------- |
getEventBus().emit('panel:show') | Demo | `page-agent.demo.js` | Auto-init with built-in test API |
getEventBus().emit('panel:update', { status: 'thinking' }) | Full | `page-agent.js` | Exposes `PageAgent` class only |
// Listen in UI components Demo build supports query params (e.g., `?model=gpt-4&lang=en-US`).
getEventBus().on('panel:show', () => panel.show())
```
### Hash Routing Requirement ## Key Files Reference
Uses wouter with `useHashLocation` for static hosting: ### Page Agent (`packages/page-agent/`)
```tsx | File | Description |
<Router hook={useHashLocation}> // Always hash-based routes | ------------------ | --------------------------------------- |
``` | `src/PageAgent.ts` | ⭐ Main AI agent class |
| `src/umd.ts` | CDN/UMD entry with auto-init |
| `src/tools/` | Tool definitions calling PageController |
### CDN Auto-Injection Pattern ### LLMs (`packages/llms/`)
Library auto-initializes when injected via script tag: | File | Description |
| ---------------------------- | ------------------------------------- |
| `src/index.ts` | ⭐ LLM class with retry logic |
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig |
| `src/OpenAILenientClient.ts` | OpenAI-compatible client |
```html ### Page Controller (`packages/page-controller/`)
<script src="page-agent.js?model=gpt-4"></script>
```
Query params configure `PageAgentConfig` automatically in `src/entry.ts`. | File | Description |
| --------------------------- | ---------------------------------------------------------- |
## File Organization | `src/PageController.ts` | ⭐ Main controller class with optional mask support |
| `src/SimulatorMask.ts` | Visual overlay blocking user interaction during automation |
### Core Library (`src/`) | `src/actions.ts` | Element interactions (click, input, scroll) |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine |
- `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
## Adding New Features ## 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 ### New Agent Tool
1. Implement under `src/tools/` 1. Implement in `packages/page-agent/src/tools/index.ts`
2. Export via `src/tools/index.ts` 2. If tool needs DOM ops, add method to PageController first
3. Wire into `PageAgent.ts` if needed 3. Tool calls `this.pageController.methodName()` for DOM interactions
### New UI Component ### New PageController Action
1. Create in `src/ui/` with colocated CSS modules 1. Add implementation in `packages/page-controller/src/actions.ts`
2. Use event bus for PageAgent communication 2. Expose via async method in `PageController.ts`
3. Test via `pages/test-pages/` 3. Export from `packages/page-controller/src/index.ts`
## Code Standards ## Code Standards
### TypeScript
- Explicit typing for exported/public APIs - Explicit typing for exported/public APIs
- ESLint relaxes some unsafe rules for rapid iteration - ESLint relaxes some unsafe rules for rapid iteration
- Every change you make should not only implement the desired functionality but also improve the quality of the codebase
### CSS & Styling - All code and comments must be in English.
- **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
+20 -27
View File
@@ -7,11 +7,9 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
### Development Setup ### Development Setup
1. **Prerequisites** 1. **Prerequisites**
- Node.js 20+ - `node.js >= 20` with `npm >= 10`
- npm 10+ - An editor that supports `ts/eslint/prettier`
- typescript as required in package.json - Make sure `eslint`, `prettier` and `commitlint` work well
- vscode or other editors that support ts/eslint/prettier
- make sure `eslint`, `auto-format` and `commit-lint` all work
2. **Setup** 2. **Setup**
@@ -22,10 +20,16 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
### Project Structure ### Project Structure
This project has **two separate parts**: This is a **monorepo** with npm workspaces containing **two main packages**:
- **Core Library** (`src/`) - Pure JavaScript AI agent library 1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
- **Documentation Website** (`pages/`) - React web app for landing page and docs 2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
- When developing. Use alias so that we don't have to pre-build.
- When bundling. Use external and disable ts `paths` alias to leave deps out.
- When bundling `UMD` and `Website`. Bundle everything including local packages.
## 🤝 How to Contribute ## 🤝 How to Contribute
@@ -95,19 +99,6 @@ This project has **two separate parts**:
- Github Copilot: `./.github/copilot-instructions.md` - Github Copilot: `./.github/copilot-instructions.md`
- Claude Code: `./CLAUDE.md` - Claude Code: `./CLAUDE.md`
### Core Library (`src/`)
- **No React dependencies** - Pure JavaScript/TypeScript only
- Use event bus for component communication
- Follow browser-use patterns for DOM operations
### Doc&demo Site (`pages/`)
- Use React 19 + TypeScript
- Follow hash routing patterns (`useHashLocation`)
- Use Tailwind CSS for styling
- Add new docs pages to both router and sidebar
## 🔧 Development Workflows ## 🔧 Development Workflows
### Website Development ### Website Development
@@ -118,24 +109,26 @@ npm start # React development server
### Core Lib Development and Testing ### Core Lib Development and Testing
> @TODO this part is outdated. Update this.
- Config your LLM API - Config your LLM API
- Start a local script - Start and serve a local umd script
```bash ```bash
npm run build:lib:watch # Auto-rebuild on changes npm run dev:umd # Serving UMD with auto rebuild at http://localhost:5173/page-agent.umd.cjs
``` ```
- Add a new bookmark enable it on other website - Add a new bookmark enable it on other website
``` ```
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/dist/lib/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})(); javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
``` ```
### Adding Documentation ### Adding Documentation
1. Create `pages/docs/section/page-name/page.tsx` 1. Create `website/src/docs/section/page-name/page.tsx`
2. Add route to `pages/router.tsx` 2. Add route to `website/src/router.tsx`
3. Add navigation link to `pages/components/DocsLayout.tsx` 3. Add navigation link to `website/src/components/DocsLayout.tsx`
## 🎯 Contribution Areas ## 🎯 Contribution Areas
+49 -31
View File
@@ -4,13 +4,13 @@
[![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) [![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)
**让你的网页支持 AI 自动化。** 纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
运行在页面内的 UI agent. 使用自然语言操作 Web 应用。
🌐 [English](./README.md) | **中文** 🌐 [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/141bbb01-8022-4d1f-919d-9efc9a1dc1cf" width="640" crossorigin muted autoplay loop></video>
--- ---
@@ -24,22 +24,26 @@
## 🗺️ Roadmap ## 🗺️ Roadmap
👉 [**Roadmap**](./ROADMAP.md) 👉 [**Roadmap**](https://github.com/alibaba/page-agent/issues/96)
## 🚀 快速开始 ## 🚀 快速开始
### CDN 集成 ### 快速体验 (Demo CDN)
最快的体验方式:
```html ```html
<!-- 临时 CDN URL. 未来会变更 --> <script src="https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js" crossorigin="true"></script>
<script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
``` ```
### NPM 安装 > ⚠️ **仅用于技术评估。** Demo 模型有速率限制和使用限制,生产环境请使用 NPM 方式。
| 位置 | URL |
| ------ | --------------------------------------------------------------------------------------- |
| 全球 | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js |
| 中国 | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js |
### NPM 安装(推荐)
```bash ```bash
npm install page-agent npm install page-agent
@@ -48,35 +52,49 @@ npm install page-agent
```javascript ```javascript
import { PageAgent } from 'page-agent' 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({ const agent = new PageAgent({
modelName: DEMO_MODEL, model: 'deepseek-chat',
baseURL: DEMO_BASE_URL, baseURL: 'https://api.deepseek.com',
apiKey: DEMO_API_KEY, apiKey: 'YOUR_API_KEY',
language: 'zh-CN', language: 'zh-CN',
}) })
await agent.execute('点击登录按钮') await agent.execute('点击登录按钮')
``` ```
### CDN 引入
适用于无法使用 NPM 的环境,用法与 NPM 一致:
```html
<script src="https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js" crossorigin="true"></script>
<script>
const agent = new PageAgent({
model: 'deepseek-chat',
baseURL: 'https://api.deepseek.com',
apiKey: 'YOUR_API_KEY',
})
await agent.execute('点击登录按钮')
</script>
```
| 位置 | URL |
| ------ | ----------------------------------------------------------------------------------- |
| 全球 | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js |
| 中国 | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js |
## 🏗️ 架构设计 ## 🏗️ 架构设计
PageAgent 采用清晰的模块化架构: PageAgent adopts a simplified monorepo structure:
``` ```
src/ packages/
├── PageAgent.ts # Agent 主流程 ├── page-agent/ # AI agent (npm: page-agent)
├── dom/ # DOM 理解 ├── llms/ # LLM 客户端 (npm: @page-agent/llms)
├── tools/ # 代理交互工具 ├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
├── ui/ # UI 组件和面板 ├── ui/ # 面板 & i18n (npm: @page-agent/ui)
├── llms/ # LLM 集成层 ├── cdn/ # CDN IIFE builds (npm: @page-agent/cdn)
└── utils/ # 事件总线和工具 └── website/ # 文档站点
``` ```
## 🤝 贡献 ## 🤝 贡献
+51 -33
View File
@@ -1,22 +1,22 @@
# PageAgent 🤖🪄 # PageAgent 🤖🪄
![banner](https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg) ![banner](https://img.alicdn.com/imgextra/i3/O1CN01MyVCS21EoKkIHUT1s_!!6000000000398-49-tps-1280-353.webp)
[![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent) [![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)
**Transform your webpage into an AI-powered application with a single script tag.** The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
An in-page UI agent in javascript. Control web interfaces with natural language.
🌐 **English** | [中文](./README-zh.md) 🌐 **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/de8d1964-8bde-494f-a52f-2975469557a5" width="640" crossorigin muted autoplay loop></video>
--- ---
## ✨ Features ## ✨ Features
- **🎯 Easy Integration** - **🎯 Easy Integration** - Transform your webpage into an agent with a single script tag.
- **🔐 Client-Side Processing** - **🔐 Client-Side Processing**
- **🧠 DOM Extraction** - **🧠 DOM Extraction**
- **💬 Natural Language Interface** - **💬 Natural Language Interface**
@@ -24,22 +24,26 @@ An in-page UI agent in javascript. Control web interfaces with natural language.
## 🗺️ Roadmap ## 🗺️ Roadmap
👉 [**Roadmap**](./ROADMAP.md) 👉 [**Roadmap**](https://github.com/alibaba/page-agent/issues/96)
## 🚀 Quick Start ## 🚀 Quick Start
### CDN Integration ### Quick Try (Demo CDN)
Fastest way to try PageAgent:
```html ```html
<!-- temporary CDN URL. May change in the future --> <script src="https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js" crossorigin="true"></script>
<script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
``` ```
### NPM Installation > ⚠️ **For technical evaluation only.** Demo model has rate limits and usage restrictions. Use NPM for production.
| Location | 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 Installation (Recommended)
```bash ```bash
npm install page-agent npm install page-agent
@@ -48,35 +52,49 @@ npm install page-agent
```javascript ```javascript
import { PageAgent } from 'page-agent' 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({ const agent = new PageAgent({
modelName: DEMO_MODEL, model: 'deepseek-chat',
baseURL: DEMO_BASE_URL, baseURL: 'https://api.deepseek.com',
apiKey: DEMO_API_KEY, apiKey: 'YOUR_API_KEY',
language: 'en-US', language: 'en-US',
}) })
await agent.execute('Click the login button') await agent.execute('Click the login button')
``` ```
### CDN Build
For environments where NPM is not available. Usage is identical to NPM:
```html
<script src="https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js" crossorigin="true"></script>
<script>
const agent = new PageAgent({
model: 'deepseek-chat',
baseURL: 'https://api.deepseek.com',
apiKey: 'YOUR_API_KEY',
})
await agent.execute('Click the login button')
</script>
```
| Location | URL |
| -------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js |
| China | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js |
## 🏗️ Structure ## 🏗️ Structure
PageAgent follows a clean, modular architecture: PageAgent adopts a simplified monorepo structure:
``` ```
src/ packages/
├── PageAgent.ts # Agent main loop ├── page-agent/ # AI agent (npm: page-agent)
├── dom/ # DOM processing ├── llms/ # LLM client (npm: @page-agent/llms)
├── tools/ # Agent tools ├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
├── ui/ # UI components & panels ├── ui/ # Panel & i18n (npm: @page-agent/ui)
├── llms/ # LLM integration layer ├── cdn/ # CDN IIFE builds (npm: @page-agent/cdn)
└── utils/ # Event bus & utilities └── website/ # Demo & Documentation site
``` ```
## 🤝 Contributing ## 🤝 Contributing
-42
View File
@@ -1,42 +0,0 @@
# 🗺️ PageAgent Roadmap
The development progress and future plans for PageAgent.
## 🚀 Current Works
- [x] **MVP** - Core functionality implemented
- [x] **SPA interaction** - Single Page Application support
- [x] **Reasoning and (short) memory**
- [x] **Multi model provider integration and testing**
- [x] **UI with HITL** - Human-in-the-loop user interface
- [x] **Landing and doc pages**
- [x] **Remove ai-sdk** - Only one function is being used
- [x] **Robust LLM output**
- [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**
- [ ] **❗Hijack `page_open/page_change/page_unload` behavior**
- [ ] **Custom knowledge base and instructions**
- [ ] **Black/white-list safeguard**
- [ ] **Data-masking**
- [ ] **Optimize for popular UI frameworks**
- [ ] **Testing suits**
- [ ] **Support custom llm fetch**
- [ ] **Refactor: Separate Agent and Page-Controller** - Agent should run w/o dom
♻️ 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
+8 -3
View File
@@ -8,7 +8,7 @@ import globals from 'globals'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
export default defineConfig([ export default defineConfig([
globalIgnores(['dist', 'test-pages']), globalIgnores(['**/dist', '**/node_modules', 'packages/website/src/components/ui']),
{ {
plugins: { plugins: {
'react-hooks': reactHooks, 'react-hooks': reactHooks,
@@ -37,8 +37,10 @@ export default defineConfig([
], ],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: ['./tsconfig.json'], // project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname, // project: ['./packages/*/tsconfig.json'],
// tsconfigRootDir: import.meta.dirname,
projectService: true,
}, },
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
@@ -67,6 +69,9 @@ export default defineConfig([
'react-dom/no-missing-button-type': 'off', 'react-dom/no-missing-button-type': 'off',
'react-x/no-nested-component-definitions': 'off', 'react-x/no-nested-component-definitions': 'off',
'@typescript-eslint/prefer-optional-chain': 'off', '@typescript-eslint/prefer-optional-chain': 'off',
// 'require-await': 'off',
'@typescript-eslint/require-await': 'off',
}, },
}, },
]) ])
-32
View File
@@ -1,32 +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 - 网页内的交互助手</title>
<meta
name="description"
content="PageAgent.js:一行 CDN 引入,为任何网站添加智能 UI Agent。现代网页 AI 自动化,极简集成,开发者专用。"
/>
<meta
name="keywords"
content="PageAgent, AI Agent, Web Automation, UI 自动化, 前端, CDN, JavaScript, React, Vite"
/>
<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" />
<meta property="og:description" content="一行 CDN 引入,为任何网站添加智能 UI Agent。" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="zh_CN" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./pages/main.tsx"></script>
</body>
</html>
+2248 -999
View File
File diff suppressed because it is too large Load Diff
+38 -64
View File
@@ -1,89 +1,63 @@
{ {
"name": "page-agent", "name": "root",
"private": false, "private": true,
"version": "0.0.3", "version": "0.2.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-agent.js", "workspaces": [
"module": "./dist/lib/page-agent.js", "packages/page-controller",
"types": "./dist/lib/PageAgent.d.ts", "packages/ui",
"exports": { "packages/llms",
".": { "packages/page-agent",
"types": "./dist/lib/PageAgent.d.ts", "packages/cdn",
"import": "./dist/lib/page-agent.js", "packages/website"
"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"
], ],
"description": "AI-powered UI agent for web applications",
"author": "Simon<gaomeng1900>", "author": "Simon<gaomeng1900>",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/alibaba/page-agent.git" "url": "https://github.com/alibaba/page-agent.git"
}, },
"homepage": "https://alibaba.github.io/page-agent/",
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0",
"npm": ">=10.0.0"
}, },
"scripts": { "scripts": {
"dev": "vite", "cleanup": "rm -rf packages/*/dist",
"start": "vite", "start": "npm run dev --workspace=@page-agent/website",
"build": "tsc -b && vite build && npm run build:lib && npm run build:umd", "build:website": "npm run build:website --workspace=@page-agent/website",
"build:lib": "MODE=lib vite build", "build:libs": "npm run build --workspaces --if-present",
"build:lib:watch": "MODE=lib vite build --watch", "build": "npm run build:libs && npm run build:website",
"build:umd": "MODE=umd vite build", "dev:umd": "npm run dev:umd --workspace=page-agent",
"version": "node scripts/sync-version.js",
"lint": "eslint .", "lint": "eslint .",
"prepare": "husky" "prepare": "husky"
}, },
"dependencies": {
"ai-motion": "^0.4.7",
"chalk": "^5.6.2",
"zod": "^4.1.12"
},
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.1.0", "@commitlint/cli": "^20.3.1",
"@commitlint/config-conventional": "^20.0.0", "@commitlint/config-conventional": "^20.3.1",
"@eslint/js": "^9.37.0", "@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.53.1", "@microsoft/api-extractor": "^7.55.2",
"@tailwindcss/vite": "^4.1.14", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/node": "^25.0.7",
"@types/react": "^19.2.2", "concurrently": "^9.2.1",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"eslint": "^9.37.0", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.0.6", "eslint-plugin-react-dom": "^2.5.7",
"eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.23", "eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react-x": "^2.0.6", "eslint-plugin-react-x": "^2.5.7",
"globals": "^16.4.0", "globals": "^17.0.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.2.4", "lint-staged": "^16.2.4",
"prettier": "^3.6.2", "prettier": "^3.7.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.14",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.46.0", "typescript-eslint": "^8.53.0",
"unplugin-dts": "^1.0.0-beta.6", "unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.1.9", "vite": "^7.3.1",
"vite-plugin-css-injected-by-js": "^3.5.2", "vite-plugin-css-injected-by-js": "^3.5.2"
"wouter": "^3.7.1"
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [ "*.{js,ts,cjs,cts,mjs,mts}": [
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@page-agent/cdn",
"private": false,
"version": "0.2.0",
"type": "module",
"files": [
"dist/"
],
"description": "CDN builds for page-agent - IIFE bundles for script tag usage",
"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 && vite build --mode demo"
},
"dependencies": {
"page-agent": "0.2.0"
}
}
+64
View File
@@ -0,0 +1,64 @@
// @ts-nocheck
/**
* Demo CDN build for page-agent
* Auto-initializes with built-in demo API for testing
*
* Usage:
* <script src="https://unpkg.com/@page-agent/cdn/dist/page-agent.demo.js"></script>
*/
import { PageAgent } from 'page-agent'
// Clean up existing instances to prevent multiple injections from bookmarklet
if (window.pageAgent) {
window.pageAgent.dispose()
}
// Mount to global window object
window.PageAgent = PageAgent
// Export for IIFE
export { PageAgent }
console.log('🚀 page-agent.js loaded!')
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
// Demo warning
console.log(
'%c⚠️ DEMO MODE %c\n' +
'This build uses a shared testing LLM with rate limits.\n' +
'For production, use page-agent.js with your own API key:\n' +
'https://alibaba.github.io/page-agent/#/docs/features/models',
'background: #f59e0b; color: #000; font-size: 14px; font-weight: bold; padding: 4px 8px; border-radius: 4px;',
'color: #f59e0b; font-size: 12px;'
)
// in case document.x is not ready yet
setTimeout(() => {
const currentScript = document.currentScript
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') || 'zh-CN'
const config = { model, baseURL, apiKey, language }
window.pageAgent = new PageAgent(config)
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
const config = {
model: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
}
window.pageAgent = new PageAgent(config)
}
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
window.pageAgent.panel.show()
})
+15
View File
@@ -0,0 +1,15 @@
// @ts-nocheck
/**
* Full CDN build for page-agent
* Exposes PageAgent class for manual instantiation and configuration
*
* Usage:
* <script src="https://unpkg.com/@page-agent/cdn/dist/page-agent.js"></script>
* <script>
* const agent = new PageAgent({ model: 'gpt-4o', apiKey: 'your-key' })
* agent.panel.show()
* </script>
*/
import { PageAgent } from 'page-agent'
export { PageAgent }
+42
View File
@@ -0,0 +1,42 @@
// @ts-nocheck
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))
/**
* CDN IIFE builds for page-agent
*
* Build targets (via --mode):
* - demo: page-agent.demo.js with auto-init and built-in API
* - full (default): page-agent.js exposes PageAgent class only
*/
export default defineConfig(({ mode }) => {
const isDemo = mode === 'demo'
const entry = isDemo ? resolve(__dirname, 'src/demo.js') : resolve(__dirname, 'src/full.js')
const fileName = isDemo ? 'page-agent.demo' : 'page-agent'
return {
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry,
name: 'PageAgent',
fileName: () => `${fileName}.js`,
formats: ['iife'],
},
outDir: resolve(__dirname, 'dist'),
emptyOutDir: !isDemo, // only empty on first build (full)
minify: false,
},
define: {
'process.env.NODE_ENV': '"production"',
},
}
})
+41
View File
@@ -0,0 +1,41 @@
# @page-agent/llms
LLM client with a **reflection-before-action** mental model for page-agent.
## Why This Package Exists
The LLM module and the agent logic are inherently coupled. This package exists not to decouple them, but to **define the interface contract** between the LLM and the agent.
The core abstraction is the `MacroToolInput` — a structured output format that **forces the model to reflect before acting**.
## The Reflection-Before-Action Model
Every tool call must first output its reasoning state before the actual action:
```typescript
interface MacroToolInput {
// Reflection (mandatory before any action)
evaluation_previous_goal?: string // How well did the previous action work?
memory?: string // Key information to remember
next_goal?: string // What to accomplish next
// Action (the actual operation)
action: Record<string, any>
}
```
This design ensures that:
1. **The model evaluates its previous action** before deciding the next step
2. **Working memory is explicitly maintained** across conversation turns
3. **Goals are clearly stated**, making the agent's reasoning transparent and debuggable
## Key Components
| Export | Description |
|--------|-------------|
| `LLM` | Main LLM client class with retry logic |
| `MacroToolInput` | The reflection-before-action input schema |
| `AgentBrain` | Agent's thinking state (eval, memory, goal) |
| `LLMConfig` | Configuration for LLM connection |
| `parseLLMConfig` | Parse and apply defaults to config |
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@page-agent/llms",
"version": "0.2.0",
"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: (current: number) => {
this.dispatchEvent(
new CustomEvent('retry', { detail: { current, max: this.config.maxRetries } })
)
},
onError: (error: Error) => {
this.dispatchEvent(new CustomEvent('error', { detail: { error } }))
},
}
)
}
}
async function withRetry<T>(
fn: () => Promise<T>,
settings: {
maxRetries: number
onRetry: (retries: number) => void
onError: (error: Error) => void
}
): Promise<T> {
let retries = 0
let lastError: Error | null = null
while (retries <= settings.maxRetries) {
if (retries > 0) {
settings.onRetry(retries)
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
retries++
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> 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 * LLM Client interface
* Note: Does not use generics because each tool in the tools array has different types * Note: Does not use generics because each tool in the tools array has different types
@@ -40,7 +58,8 @@ export interface LLMClient {
invoke( invoke(
messages: Message[], messages: Message[],
tools: Record<string, Tool>, tools: Record<string, Tool>,
abortSignal?: AbortSignal abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> ): Promise<InvokeResult>
} }
@@ -65,13 +84,20 @@ export interface InvokeResult<TResult = unknown> {
} }
/** /**
* OpenAI Client config * LLM configuration
*/ */
export interface OpenAIClientConfig { export interface LLMConfig {
model: string
apiKey: string
baseURL: string baseURL: string
apiKey: string
model: string
temperature?: number temperature?: number
maxTokens?: number
maxRetries?: 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
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"]
}
+37
View File
@@ -0,0 +1,37 @@
// @ts-check
import chalk from 'chalk'
import { dirname, resolve } from 'path'
import dts from 'unplugin-dts/vite'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
const __dirname = dirname(fileURLToPath(import.meta.url))
console.log(chalk.cyan(`📦 Building @page-agent/llms`))
export default defineConfig({
clearScreen: false,
plugins: [dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true })],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'PageAgentLLMs',
fileName: 'page-agent-llms',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: {
external: ['chalk', 'zod'],
},
minify: false,
sourcemap: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+53
View File
@@ -0,0 +1,53 @@
{
"name": "page-agent",
"private": false,
"version": "0.2.0",
"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 && vite build --config vite.umd.config.js",
"serve": "npx serve dist/umd -p 5173",
"dev:umd": "concurrently \"vite build --config vite.umd.config.js --watch\" \"npm run serve\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"zod": "^4.3.5",
"@page-agent/llms": "0.2.0",
"@page-agent/page-controller": "0.2.0",
"@page-agent/ui": "0.2.0"
}
}
+630
View File
@@ -0,0 +1,630 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
import { LLM, type Tool } from '@page-agent/llms'
import { PageController } from '@page-agent/page-controller'
import { Panel } from '@page-agent/ui'
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 { normalizeResponse, trimLines, uid, waitUntil } from './utils'
import { assert } from './utils/assert'
/**
* 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
}
export type { PageAgentConfig }
export { tool, type PageAgentTool } from './tools'
/**
* 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'
}
/**
* Union type for all history events
*/
export type HistoryEvent = AgentStep | ObservationEvent | UserTakeoverEvent
/** @deprecated Use AgentStep instead */
export type AgentHistory = AgentStep
export interface ExecutionResult {
success: boolean
data: string
history: HistoryEvent[]
}
export class PageAgent extends EventTarget {
config: PageAgentConfig
id = uid()
panel: Panel
tools: typeof tools
paused = false
disposed = false
task = ''
taskId = ''
#llm: LLM
#abortController = new AbortController()
#llmRetryListener: ((e: Event) => void) | null = null
#llmErrorListener: ((e: Event) => void) | null = null
#beforeUnloadListener: ((e: Event) => void) | null = null
/** PageController for DOM operations */
pageController: PageController
/** 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 event stream */
history: HistoryEvent[] = []
constructor(config: PageAgentConfig) {
super()
this.config = config
this.#llm = new LLM(this.config)
this.panel = new Panel({
language: this.config.language,
onExecuteTask: (task) => this.execute(task),
onStop: () => this.dispose(),
onPauseToggle: () => {
this.paused = !this.paused
return this.paused
},
getPaused: () => this.paused,
})
this.tools = new Map(tools)
// Initialize PageController with config (mask enabled by default)
this.pageController = new PageController({
...this.config,
enableMask: this.config.enableMask ?? true,
})
// Listen to LLM events
this.#llmRetryListener = (e) => {
const { current, max } = (e as CustomEvent).detail
this.panel.update({ type: 'retry', current, max })
}
this.#llmErrorListener = (e) => {
const { error } = (e as CustomEvent).detail
this.panel.update({ type: 'error', message: `step failed: ${error.message}` })
}
this.#llm.addEventListener('retry', this.#llmRetryListener)
this.#llm.addEventListener('error', this.#llmErrorListener)
if (this.config.customTools) {
for (const [name, tool] of Object.entries(this.config.customTools)) {
if (tool === null) {
this.tools.delete(name)
continue
}
this.tools.set(name, tool)
}
}
if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript')
}
this.#beforeUnloadListener = (e) => {
if (!this.disposed) this.dispose('PAGE_UNLOADING')
}
window.addEventListener('beforeunload', this.#beforeUnloadListener)
}
/**
* 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 })
}
async execute(task: string): Promise<ExecutionResult> {
if (!task) throw new Error('Task is required')
this.task = task
this.taskId = uid()
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 and panel
this.pageController.showMask()
this.panel.show()
this.panel.reset()
this.panel.update({ type: 'input', task: this.task })
if (this.#abortController) {
this.#abortController.abort()
this.#abortController = new AbortController()
}
this.history = []
// 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')
// pause
await waitUntil(() => !this.paused)
// Update status to thinking
console.log(chalk.blue('Thinking...'))
this.panel.update({ 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)
console.log(chalk.green('Step finished:'), actionName)
console.groupEnd()
await onAfterStep.call(this, step, 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)
this.#onDone(String(error), false)
const result: ExecutionResult = {
success: false,
data: String(error),
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')
// pause
await waitUntil(() => !this.paused)
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)
this.panel.update({ type: 'thinking', text: 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)
this.panel.update({ type: 'toolExecuting', toolName, args: 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)
// Reset wait time for non-wait tools
if (toolName !== 'wait') {
this.states.totalWaitTime = 0
}
// Briefly display execution result
this.panel.update({
type: 'toolCompleted',
toolName,
args: toolInput,
result,
duration,
})
// Wait a moment to let user see the result
await new Promise((resolve) => setTimeout(resolve, 100))
// 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`
}
}
prompt += '</agent_history>\n\n'
// <browser_state>
prompt += await this.#getBrowserState()
return trimLines(prompt)
}
#onDone(text: string, success = true) {
this.pageController.cleanUpHighlights()
// Update panel status
if (success) {
this.panel.update({ type: 'output', text })
} else {
this.panel.update({ type: 'error', message: text })
}
// Task completed
this.panel.update({ type: 'completed' })
this.pageController.hideMask()
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.panel.dispose()
this.history = []
this.#abortController.abort(reason ?? 'PageAgent disposed')
// Clean up LLM event listeners
if (this.#llmRetryListener) {
this.#llm.removeEventListener('retry', this.#llmRetryListener)
this.#llmRetryListener = null
}
if (this.#llmErrorListener) {
this.#llm.removeEventListener('error', this.#llmErrorListener)
this.#llmErrorListener = null
}
// Clean up window event listeners
if (this.#beforeUnloadListener) {
window.removeEventListener('beforeunload', this.#beforeUnloadListener)
this.#beforeUnloadListener = null
}
this.config.onDispose?.call(this, reason)
}
}
@@ -0,0 +1,2 @@
// Agent-specific constants (LLM constants moved to @page-agent/llms)
export const MAX_STEPS = 20
@@ -1,25 +1,11 @@
import type { AgentHistory, ExecutionResult, PageAgent } from '@/PageAgent' import type { LLMConfig } from '@page-agent/llms'
import type { DomConfig } from '@/dom' import type { PageControllerConfig } from '@page-agent/page-controller'
import type { SupportedLanguage } from '@/i18n' import type { SupportedLanguage } from '@page-agent/ui'
import type { PageAgentTool } from '@/tools'
import { import type { ExecutionResult, HistoryEvent, PageAgent } from '../PageAgent'
DEFAULT_API_KEY, import type { PageAgentTool } from '../tools'
DEFAULT_BASE_URL,
DEFAULT_MAX_TOKENS,
DEFAULT_MODEL_NAME,
DEFAULT_TEMPERATURE,
LLM_MAX_RETRIES,
} from './constants'
export interface LLMConfig { export type { LLMConfig }
baseURL?: string
apiKey?: string
model?: string
temperature?: number
maxTokens?: number
maxRetries?: number
}
export interface AgentConfig { export interface AgentConfig {
// theme?: 'light' | 'dark' // theme?: 'light' | 'dark'
@@ -56,26 +42,71 @@ export interface AgentConfig {
*/ */
customTools?: Record<string, PageAgentTool | null> 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 // lifecycle hooks
// @todo: use event instead of hooks // @todo: use event instead of hooks
// @todo: remove `this` binding, pass agent as explicit parameter instead
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void onAfterStep?: (this: PageAgent, stepCnt: number, history: HistoryEvent[]) => Promise<void> | void
onBeforeTask?: (this: PageAgent) => Promise<void> | void onBeforeTask?: (this: PageAgent) => Promise<void> | void
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
/** /**
* @note this hook can block the disposal process * @note this hook can block the disposal process
* @note when dispose caused by page unload, reason will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut. * @note when dispose caused by page unload, reason will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut.
* @todo remove `this` binding, pass agent as explicit parameter instead
*/ */
onDispose?: (this: PageAgent, reason?: string) => void onDispose?: (this: PageAgent, reason?: string) => void
// page behavior hooks // 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 * TODO: @unimplemented
* hook when action causes a new page to be opened * 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. * @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 onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
@@ -87,15 +118,4 @@ export interface AgentConfig {
experimentalPreventNewPage?: boolean experimentalPreventNewPage?: boolean
} }
export type PageAgentConfig = LLMConfig & AgentConfig & DomConfig export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
return {
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
apiKey: config.apiKey ?? DEFAULT_API_KEY,
model: config.model ?? DEFAULT_MODEL_NAME,
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
}
}
+1 -6
View File
@@ -1,5 +1,5 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
import type { PageAgent } from './src/PageAgent' import type { PageAgent } from './PageAgent'
declare module '*.module.css' { declare module '*.module.css' {
const classes: Record<string, string> const classes: Record<string, string>
@@ -11,15 +11,10 @@ declare module '*.md?raw' {
export default content export default content
} }
/**
* for local dev and umd demo
*/
declare global { declare global {
interface Window { interface Window {
pageAgent?: PageAgent pageAgent?: PageAgent
PageAgent: typeof PageAgent PageAgent: typeof PageAgent
__PAGE_AGENT_IDS__: string[] __PAGE_AGENT_IDS__: string[]
} }
} }
@@ -4,23 +4,8 @@
*/ */
import zod, { type z } from 'zod' import zod, { type z } from 'zod'
import type { PageAgent } from '@/PageAgent' import type { PageAgent } from '../PageAgent'
import { waitFor } from '../utils'
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
/** /**
* Internal tool definition that has access to PageAgent `this` context * Internal tool definition that has access to PageAgent `this` context
@@ -42,18 +27,6 @@ export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TP
*/ */
export const tools = new Map<string, PageAgentTool>() 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( tools.set(
'done', 'done',
tool({ tool({
@@ -80,11 +53,20 @@ tools.set(
seconds: zod.number().min(1).max(10).default(1), seconds: zod.number().min(1).max(10).default(1),
}), }),
execute: async function (this: PageAgent, input) { execute: async function (this: PageAgent, input) {
const lastTimeUpdate = this.lastTimeUpdate const lastTimeUpdate = await this.pageController.getLastUpdateTime()
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000) const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
console.log(`actualWaitTime: ${actualWaitTime} seconds`) console.log(`actualWaitTime: ${actualWaitTime} seconds`)
await waitFor(actualWaitTime) 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.`
}, },
}) })
) )
@@ -99,7 +81,7 @@ tools.set(
}), }),
execute: async function (this: PageAgent, input) { execute: async function (this: PageAgent, input) {
const answer = await this.panel.askUser(input.question) const answer = await this.panel.askUser(input.question)
return `✅ Received user answer: ${answer}` + (await getSystemInfo()) return `✅ Received user answer: ${answer}`
}, },
}) })
) )
@@ -112,16 +94,8 @@ tools.set(
index: zod.int().min(0), index: zod.int().min(0),
}), }),
execute: async function (this: PageAgent, input) { execute: async function (this: PageAgent, input) {
const element = getElementByIndex(this, input.index) const result = await this.pageController.clickElement(input.index)
const elemText = this.elementTextMap.get(input.index) return result.message
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())
}, },
}) })
) )
@@ -135,13 +109,8 @@ tools.set(
text: zod.string(), text: zod.string(),
}), }),
execute: async function (this: PageAgent, input) { execute: async function (this: PageAgent, input) {
const element = getElementByIndex(this, input.index) const result = await this.pageController.inputText(input.index, input.text)
const elemText = this.elementTextMap.get(input.index) return result.message
await inputTextElement(element, input.text)
return (
`✅ Input text (${input.text}) into element (${elemText ? elemText : input.index}).` +
(await getSystemInfo())
)
}, },
}) })
) )
@@ -156,13 +125,8 @@ tools.set(
text: zod.string(), text: zod.string(),
}), }),
execute: async function (this: PageAgent, input) { execute: async function (this: PageAgent, input) {
const element = getElementByIndex(this, input.index) const result = await this.pageController.selectOption(input.index, input.text)
const elemText = this.elementTextMap.get(input.index) return result.message
await selectOptionElement(element as HTMLSelectElement, input.text)
return (
`✅ Selected option (${input.text}) in element (${elemText ? elemText : input.index}).` +
(await getSystemInfo())
)
}, },
}) })
) )
@@ -182,13 +146,11 @@ tools.set(
index: zod.number().int().min(0).optional(), index: zod.number().int().min(0).optional(),
}), }),
execute: async function (this: PageAgent, input) { execute: async function (this: PageAgent, input) {
const { down, num_pages, index, pixels } = input const result = await this.pageController.scroll({
...input,
const scroll_amount = pixels ? pixels : num_pages * (down ? 1 : -1) * window.innerHeight numPages: input.num_pages,
})
const element = index !== undefined ? getElementByIndex(this, index) : null return result.message
return (await scrollVertically(down, scroll_amount, element)) + (await getSystemInfo())
}, },
}) })
) )
@@ -204,13 +166,23 @@ tools.set(
index: zod.number().int().min(0).optional(), index: zod.number().int().min(0).optional(),
}), }),
execute: async function (this: PageAgent, input) { execute: async function (this: PageAgent, input) {
const { right, pixels, index } = input const result = await this.pageController.scrollHorizontally(input)
return result.message
},
})
)
const scroll_amount = pixels * (right ? 1 : -1) tools.set(
'execute_javascript',
const element = index !== undefined ? getElementByIndex(this, index) : null tool({
description:
return (await scrollHorizontally(right, scroll_amount, element)) + (await getSystemInfo()) 'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
inputSchema: zod.object({
script: zod.string(),
}),
execute: async function (this: PageAgent, input) {
const result = await this.pageController.executeJavascript(input.script)
return result.message
}, },
}) })
) )
+49
View File
@@ -0,0 +1,49 @@
/**
* Auto-run entry for page-agent.js. Insert this script into your page to get page-agent functionality.
*/
import { PageAgent, type PageAgentConfig } from './PageAgent'
// Clean up existing instances to prevent multiple injections from bookmarklet
if (window.pageAgent) {
window.pageAgent.dispose()
}
// Mount to global window object
window.PageAgent = PageAgent
// Export for ES module usage
// export { PageAgent }
console.log('🚀 page-agent.js loaded!')
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
// in case document.x is not ready yet
// @todo give a switch to disable auto-init
setTimeout(() => {
const currentScript = document.currentScript as HTMLScriptElement | null
if (currentScript) {
console.log('🚀 page-agent.js detected current script:', currentScript.src)
const url = new URL(currentScript.src)
const model = url.searchParams.get('model') || DEMO_MODEL
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
const config: PageAgentConfig = { model, baseURL, apiKey, language }
window.pageAgent = new PageAgent(config)
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
const config: PageAgentConfig = {
model: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
}
window.pageAgent = new PageAgent(config)
}
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
window.pageAgent.panel.show() // Show panel
})
+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,30 +1,9 @@
/**
* Type-safe event bus for decoupling PageAgent and Panel
*/
import type { Step } from '@/ui/UIState'
/** /**
* Event mapping definitions * Event mapping definitions
* @note Event bus callbacks must be repeatable without errors * @note Event bus callbacks must be repeatable without errors
*/ */
export interface PageAgentEventMap { export interface PageAgentEventMap {
// Panel control events
// call panel.show()
'panel:show': { params: undefined }
// call panel.hide()
'panel:hide': { params: undefined }
// call panel.reset()
'panel:reset': { params: undefined }
// call panel.update()
'panel:update': { params: Omit<Step, 'id' | 'stepNumber' | 'timestamp'> }
// call panel.expand()
'panel:expand': { params: undefined }
// call panel.collapse()
'panel:collapse': { params: undefined }
// PageAgent status events // PageAgent status events
// 'agent:beforeUpdate': { params: undefined }
// 'agent:afterUpdate': { params: undefined }
// 'agent:execute': { params: { task: string } } // 'agent:execute': { params: { task: string } }
// 'agent:done': { params: { text: string; success: boolean } } // 'agent:done': { params: { text: string; success: boolean } }
// 'agent:paused': { params: undefined } // 'agent:paused': { params: undefined }
@@ -33,8 +12,7 @@ export interface PageAgentEventMap {
// 'agent:error': { params: { error: string | Error } } // 'agent:error': { params: { error: string | Error } }
// Task status change events // Task status change events
// 'task:start': { params: { task: string } } 'task:start': { params: { task: string } }
// 'task:step': { params: Omit<AgentStep, 'id' | 'stepNumber' | 'timestamp'> }
// 'task:complete': { params: { text: string; success: boolean } } // 'task:complete': { params: { text: string; success: boolean } }
// 'task:error': { params: { error: string | Error } } // 'task:error': { params: { error: string | Error } }
@@ -69,10 +47,7 @@ class EventBus extends EventTarget {
/** /**
* Listen to built-in events * Listen to built-in events
*/ */
on<T extends keyof PageAgentEventMap>( on<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
event: T,
handler: EventHandler<T & keyof PageAgentEventMap>
): void {
const wrappedHandler = (e: Event) => { const wrappedHandler = (e: Event) => {
const customEvent = e as CustomEvent const customEvent = e as CustomEvent
const params = customEvent.detail?.[0] const params = customEvent.detail?.[0]
@@ -84,10 +59,7 @@ class EventBus extends EventTarget {
/** /**
* Listen to built-in events (one-time) * Listen to built-in events (one-time)
*/ */
once<T extends keyof PageAgentEventMap>( once<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
event: T,
handler: EventHandler<T & keyof PageAgentEventMap>
): void {
const wrappedHandler = (e: Event) => { const wrappedHandler = (e: Event) => {
const customEvent = e as CustomEvent const customEvent = e as CustomEvent
const params = customEvent.detail?.[0] const params = customEvent.detail?.[0]
@@ -1,3 +1,5 @@
export { normalizeResponse } from './autoFixer'
/** /**
* Wait until condition becomes true * Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise * @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 { export function truncate(text: string, maxLength: number): string {
+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": {}
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"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/ui": ["../ui/src/index.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "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"',
},
})
+45
View File
@@ -0,0 +1,45 @@
// @ts-check
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// UMD Bundle for CDN
// - alias all local packages so that they can be build in
// - no external
// - no d.ts. dts does not work with monorepo aliasing
export default defineConfig({
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
publicDir: false,
esbuild: {
keepNames: true,
},
resolve: {
alias: {
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
},
},
build: {
lib: {
entry: resolve(__dirname, 'src/umd.ts'),
name: 'PageAgent',
fileName: 'page-agent',
formats: ['umd'],
},
outDir: resolve(__dirname, 'dist', 'umd'),
rollupOptions: {
output: {
// force use .js as extension
entryFileNames: 'page-agent.js',
},
},
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+41
View File
@@ -0,0 +1,41 @@
{
"name": "@page-agent/page-controller",
"version": "0.2.0",
"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,393 @@
/**
* 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 { SimulatorMask } from './mask/SimulatorMask'
import { patchReact } from './patches/react'
export { SimulatorMask }
/**
* 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: SimulatorMask | null = null
constructor(config: PageControllerConfig = {}) {
super()
this.config = config
patchReact(this)
if (config.enableMask) {
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.
*/
showMask(): void {
this.mask?.show()
}
/**
* Hide the visual mask overlay.
* Only works if enableMask was set to true in config.
*/
hideMask(): void {
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 * Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved. * All rights reserved.
*/ */
import type { PageAgent } from '../PageAgent' import type { InteractiveElementDomNode } from './dom/dom_tree/type'
// ======= general utils ======= // ======= 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)) 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 ======= // ======= dom utils =======
export async function movePointerToElement(element: HTMLElement) { 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 { export function getElementByIndex(
const interactiveNode = pageAgent.selectorMap.get(index) selectorMap: Map<number, InteractiveElementDomNode>,
index: number
): HTMLElement {
const interactiveNode = selectorMap.get(index)
if (!interactiveNode) { if (!interactiveNode) {
throw new Error(`No interactive element found at index ${index}`) 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 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) { export async function scrollIntoViewIfNeeded(element: HTMLElement) {
const el = element as any const el = element as any
if (el.scrollIntoViewIfNeeded) { if (el.scrollIntoViewIfNeeded) {
+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 scrollable element detection
* @edit add `data-browser-use-ignore` attribute * @edit add `data-browser-use-ignore` attribute
* @edit improve `sampleRect`, filter out rects with 0 area * @edit improve `sampleRect`, filter out rects with 0 area
* @edit exclude aria-hidden elements
*/ */
export default ( export default (
@@ -1446,7 +1447,14 @@ export default (
* @edit add `data-browser-use-ignore` attribute * @edit add `data-browser-use-ignore` attribute
*/ */
if (node.dataset?.browserUseIgnore === 'true') { 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) // Special handling for root node (body)
@@ -1,11 +1,11 @@
import { VIEWPORT_EXPANSION } from '@/config/constants' import { VIEWPORT_EXPANSION } from '../constants'
import domTree from '@/dom/dom_tree/index' import domTree from './dom_tree/index.js'
import { import {
ElementDomNode, ElementDomNode,
FlatDomTree, FlatDomTree,
InteractiveElementDomNode, InteractiveElementDomNode,
TextDomNode, TextDomNode,
} from '@/dom/dom_tree/type' } from './dom_tree/type'
export interface DomConfig { export interface DomConfig {
interactiveBlacklist?: (Element | (() => Element))[] 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 { Motion } from 'ai-motion'
import { isPageDark } from '@/utils/checkDarkMode' import { isPageDark } from './checkDarkMode'
import styles from './SimulatorMask.module.css' import styles from './SimulatorMask.module.css'
import cursorStyles from './cursor.module.css' import cursorStyles from './cursor.module.css'
@@ -3,14 +3,14 @@
* @returns {boolean} - True if a common dark mode class is found. * @returns {boolean} - True if a common dark mode class is found.
*/ */
function hasDarkModeClass() { 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 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> // Check class names on <html> and <body>
for (const className of DFEAULT_DARK_MODE_CLASSES) { for (const className of DEFAULT_DARK_MODE_CLASSES) {
if (htmlElement.classList.contains(className) || bodyElement.classList.contains(className)) { if (htmlElement.classList.contains(className) || bodyElement?.classList.contains(className)) {
return true return true
} }
} }
@@ -70,7 +70,7 @@ function isColorDark(colorString: string, threshold = 128) {
function isBackgroundDark() { function isBackgroundDark() {
// We check both <html> and <body> because some pages set the color on <html> // We check both <html> and <body> because some pages set the color on <html>
const htmlStyle = window.getComputedStyle(document.documentElement) const htmlStyle = window.getComputedStyle(document.documentElement)
const bodyStyle = window.getComputedStyle(document.body) const bodyStyle = window.getComputedStyle(document.body || document.documentElement)
// Get background colors // Get background colors
const htmlBgColor = htmlStyle.backgroundColor const htmlBgColor = htmlStyle.backgroundColor
@@ -93,18 +93,23 @@ function isBackgroundDark() {
* @returns {boolean} - True if the page is likely dark. * @returns {boolean} - True if the page is likely dark.
*/ */
export function isPageDark() { export function isPageDark() {
// Strategy 1: Check for common dark mode classes try {
if (hasDarkModeClass()) { // Strategy 1: Check for common dark mode classes
return true 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)[] const clearFunctions = [] as (() => void)[]
@@ -11,9 +11,9 @@ function fixAntdSelect() {
// for (const select of selects) {} // for (const select of selects) {}
} }
export function patchAntd(pageAgent: PageAgent) { export function patchAntd(pageController: PageController) {
pageAgent.addEventListener('beforeUpdate', fixAntdSelect) pageController.addEventListener('beforeUpdate', fixAntdSelect)
pageAgent.addEventListener('afterUpdate', () => { pageController.addEventListener('afterUpdate', () => {
for (const fn of clearFunctions) fn() for (const fn of clearFunctions) fn()
clearFunctions.length = 0 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 // 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( const reactRootElements = document.querySelectorAll(
'[data-reactroot], [data-reactid], [data-react-checksum], #root, #app, [id^="root-"], [id^="app-"], #adex-wrapper, #adex-root' '[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"]
}
+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/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'],
},
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.2.0",
"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{}})\""
}
}
@@ -33,8 +33,7 @@
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 480px) { @media (max-width: 480px) {
width: calc(100vw - 40px); width: calc(100vw - 40px);
left: 20px; --width: calc(100vw - 40px);
transform: none;
} }
.background { .background {
+205 -134
View File
@@ -1,12 +1,35 @@
import type { PageAgent } from '@/PageAgent'
import type { I18n } from '@/i18n'
import { truncate } from '@/utils'
import type { EventBus } from '@/utils/bus'
import { type Step, UIState } from './UIState' import { type Step, UIState } from './UIState'
import { I18n, type SupportedLanguage } from './i18n'
import { escapeHtml, truncate } from './utils'
import styles from './Panel.module.css' import styles from './Panel.module.css'
/**
* Panel configuration
*/
export interface PanelConfig {
language?: SupportedLanguage
onExecuteTask: (task: string) => void
onStop: () => void
onPauseToggle: () => boolean // returns new paused state
getPaused: () => boolean
}
/**
* Semantic update types - Panel handles i18n internally
*/
export type PanelUpdate =
| { type: 'thinking'; text?: string } // text is optional, defaults to i18n thinking text
| { type: 'input'; task: string }
| { type: 'question'; question: string }
| { type: 'userAnswer'; input: string }
| { type: 'retry'; current: number; max: number }
| { type: 'error'; message: string }
| { type: 'output'; text: string }
| { type: 'completed' }
| { type: 'toolExecuting'; toolName: string; args: any }
| { type: 'toolCompleted'; toolName: string; args: any; result?: string; duration?: number }
/** /**
* Agent control panel * Agent control panel
*/ */
@@ -20,11 +43,11 @@ export class Panel {
#stopButton: HTMLElement #stopButton: HTMLElement
#inputSection: HTMLElement #inputSection: HTMLElement
#taskInput: HTMLInputElement #taskInput: HTMLInputElement
#bus: EventBus
#state = new UIState() #state = new UIState()
#isExpanded = false #isExpanded = false
#pageAgent: PageAgent #config: PanelConfig
#i18n: I18n
#userAnswerResolver: ((input: string) => void) | null = null #userAnswerResolver: ((input: string) => void) | null = null
#isWaitingForUserAnswer: boolean = false #isWaitingForUserAnswer: boolean = false
#headerUpdateTimer: ReturnType<typeof setInterval> | null = null #headerUpdateTimer: ReturnType<typeof setInterval> | null = null
@@ -35,9 +58,9 @@ export class Panel {
return this.#wrapper return this.#wrapper
} }
constructor(pageAgent: PageAgent) { constructor(config: PanelConfig) {
this.#pageAgent = pageAgent this.#config = config
this.#bus = pageAgent.bus this.#i18n = new I18n(config.language ?? 'en-US')
this.#wrapper = this.#createWrapper() this.#wrapper = this.#createWrapper()
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)! this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)! this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
@@ -50,16 +73,8 @@ export class Panel {
this.#setupEventListeners() this.#setupEventListeners()
this.#startHeaderUpdateLoop() this.#startHeaderUpdateLoop()
// this.#expand() // debug
this.#showInputArea() this.#showInputArea()
this.#bus.on('panel:show', () => this.#show())
this.#bus.on('panel:hide', () => this.#hide())
this.#bus.on('panel:reset', () => this.#reset())
this.#bus.on('panel:update', (stepData) => this.#update(stepData))
this.#bus.on('panel:expand', () => this.#expand())
this.#bus.on('panel:collapse', () => this.#collapse())
} }
/** /**
@@ -72,18 +87,67 @@ export class Panel {
this.#userAnswerResolver = resolve this.#userAnswerResolver = resolve
// Update state to `running` // Update state to `running`
this.#update({ this.#updateInternal({
type: 'output', type: 'output',
displayText: this.#pageAgent.i18n.t('ui.panel.question', { question }), displayText: this.#i18n.t('ui.panel.question', { question }),
}) // Expand history panel }) // Expand history panel
if (!this.#isExpanded) { if (!this.#isExpanded) {
this.#expand() this.#expand()
} }
this.#showInputArea(this.#pageAgent.i18n.t('ui.panel.userAnswerPrompt')) this.#showInputArea(this.#i18n.t('ui.panel.userAnswerPrompt'))
}) })
} }
// ========== Public control methods ==========
show(): void {
this.wrapper.style.display = 'block'
void this.wrapper.offsetHeight
this.wrapper.style.opacity = '1'
this.wrapper.style.transform = 'translateX(-50%) translateY(0)'
}
hide(): void {
this.wrapper.style.opacity = '0'
this.wrapper.style.transform = 'translateX(-50%) translateY(20px)'
this.wrapper.style.display = 'none'
}
reset(): void {
this.#state.reset()
this.#statusText.textContent = this.#i18n.t('ui.panel.ready')
this.#updateStatusIndicator('thinking')
this.#updateHistory()
this.#collapse()
// Reset pause state via callback
if (this.#config.getPaused()) {
this.#config.onPauseToggle()
}
this.#updatePauseButton()
// Reset user input state
this.#isWaitingForUserAnswer = false
this.#userAnswerResolver = null
// Show input area
this.#showInputArea()
}
expand(): void {
this.#expand()
}
collapse(): void {
this.#collapse()
}
/**
* Update panel with semantic data - i18n handled internally
*/
update(data: PanelUpdate): void {
const stepData = this.#toStepData(data)
this.#updateInternal(stepData)
}
/** /**
* Dispose panel * Dispose panel
*/ */
@@ -93,10 +157,102 @@ export class Panel {
this.wrapper.remove() this.wrapper.remove()
} }
// ========== Private methods ==========
/** /**
* Update status * Convert semantic update to step data with i18n
*/ */
#update(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): void { #toStepData(data: PanelUpdate): Omit<Step, 'id' | 'stepNumber' | 'timestamp'> {
switch (data.type) {
case 'thinking':
return { type: 'thinking', displayText: data.text ?? this.#i18n.t('ui.panel.thinking') }
case 'input':
return { type: 'input', displayText: data.task }
case 'question':
return {
type: 'output',
displayText: this.#i18n.t('ui.panel.question', { question: data.question }),
}
case 'userAnswer':
return {
type: 'input',
displayText: this.#i18n.t('ui.panel.userAnswer', { input: data.input }),
}
case 'retry':
return { type: 'retry', displayText: `retry-ing (${data.current} / ${data.max})` }
case 'error':
return { type: 'error', displayText: data.message }
case 'output':
return { type: 'output', displayText: data.text }
case 'completed':
return { type: 'completed', displayText: this.#i18n.t('ui.panel.taskCompleted') }
case 'toolExecuting':
return {
type: 'tool_executing',
toolName: data.toolName,
toolArgs: data.args,
displayText: this.#getToolExecutingText(data.toolName, data.args),
}
case 'toolCompleted': {
const displayText = this.#getToolCompletedText(data.toolName, data.args)
if (!displayText) return { type: 'tool_executing', displayText: '' } // will be filtered
return {
type: 'tool_executing',
toolName: data.toolName,
toolArgs: data.args,
toolResult: data.result,
displayText,
duration: data.duration,
}
}
}
}
#getToolExecutingText(toolName: string, args: any): string {
switch (toolName) {
case 'click_element_by_index':
return this.#i18n.t('ui.tools.clicking', { index: args.index })
case 'input_text':
return this.#i18n.t('ui.tools.inputting', { index: args.index })
case 'select_dropdown_option':
return this.#i18n.t('ui.tools.selecting', { text: args.text })
case 'scroll':
return this.#i18n.t('ui.tools.scrolling')
case 'wait':
return this.#i18n.t('ui.tools.waiting', { seconds: args.seconds })
case 'done':
return this.#i18n.t('ui.tools.done')
default:
return this.#i18n.t('ui.tools.executing', { toolName })
}
}
#getToolCompletedText(toolName: string, args: any): string | null {
switch (toolName) {
case 'click_element_by_index':
return this.#i18n.t('ui.tools.clicked', { index: args.index })
case 'input_text':
return this.#i18n.t('ui.tools.inputted', { text: args.text })
case 'select_dropdown_option':
return this.#i18n.t('ui.tools.selected', { text: args.text })
case 'scroll':
return this.#i18n.t('ui.tools.scrolled')
case 'wait':
return this.#i18n.t('ui.tools.waited')
case 'done':
return null
default:
return null
}
}
/**
* Update status (internal)
*/
#updateInternal(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): void {
// Skip empty displayText (filtered toolCompleted for 'done')
if (!stepData.displayText) return
const step = this.#state.addStep(stepData) const step = this.#state.addStep(stepData)
// Queue header text update (will be processed by periodic check) // Queue header text update (will be processed by periodic check)
@@ -121,59 +277,20 @@ export class Panel {
} }
} }
/**
* Show panel
*/
#show(): void {
this.wrapper.style.display = 'block'
// Force reflow to trigger animation
void this.wrapper.offsetHeight
this.wrapper.style.opacity = '1'
this.wrapper.style.transform = 'translateX(-50%) translateY(0)'
}
/**
* Hide panel
*/
#hide(): void {
this.wrapper.style.opacity = '0'
this.wrapper.style.transform = 'translateX(-50%) translateY(20px)'
this.wrapper.style.display = 'none'
}
/**
* Reset state
*/
#reset(): void {
this.#state.reset()
this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.ready')
this.#updateStatusIndicator('thinking')
this.#updateHistory()
this.#collapse()
// Reset pause state
this.#pageAgent.paused = false
this.#updatePauseButton()
// Reset user input state
this.#isWaitingForUserAnswer = false
this.#userAnswerResolver = null
// Show input area
this.#showInputArea()
}
/** /**
* Toggle pause state * Toggle pause state
*/ */
#togglePause(): void { #togglePause(): void {
this.#pageAgent.paused = !this.#pageAgent.paused const paused = this.#config.onPauseToggle()
this.#updatePauseButton() this.#updatePauseButton()
// Update status display // Update status display
if (this.#pageAgent.paused) { if (paused) {
this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.paused') this.#statusText.textContent = this.#i18n.t('ui.panel.paused')
this.#updateStatusIndicator('thinking') // Use existing thinking state this.#updateStatusIndicator('thinking')
} else { } else {
this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.continueExecution') this.#statusText.textContent = this.#i18n.t('ui.panel.continueExecution')
this.#updateStatusIndicator('tool_executing') // Restore to execution state this.#updateStatusIndicator('tool_executing')
} }
} }
@@ -181,13 +298,14 @@ export class Panel {
* Update pause button state * Update pause button state
*/ */
#updatePauseButton(): void { #updatePauseButton(): void {
if (this.#pageAgent.paused) { const paused = this.#config.getPaused()
if (paused) {
this.#pauseButton.textContent = '▶' this.#pauseButton.textContent = '▶'
this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.continue') this.#pauseButton.title = this.#i18n.t('ui.panel.continue')
this.#pauseButton.classList.add(styles.paused) this.#pauseButton.classList.add(styles.paused)
} else { } else {
this.#pauseButton.textContent = '⏸︎' this.#pauseButton.textContent = '⏸︎'
this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.pause') this.#pauseButton.title = this.#i18n.t('ui.panel.pause')
this.#pauseButton.classList.remove(styles.paused) this.#pauseButton.classList.remove(styles.paused)
} }
} }
@@ -197,12 +315,12 @@ export class Panel {
*/ */
#stopAgent(): void { #stopAgent(): void {
// Update status display // Update status display
this.#update({ this.#updateInternal({
type: 'error', type: 'error',
displayText: this.#pageAgent.i18n.t('ui.panel.taskTerminated'), displayText: this.#i18n.t('ui.panel.taskTerminated'),
}) })
this.#pageAgent.dispose() this.#config.onStop()
} }
/** /**
@@ -219,7 +337,7 @@ export class Panel {
// Handle user input mode // Handle user input mode
this.#handleUserAnswer(input) this.#handleUserAnswer(input)
} else { } else {
this.#pageAgent.execute(input) this.#config.onExecuteTask(input)
} }
} }
@@ -228,9 +346,9 @@ export class Panel {
*/ */
#handleUserAnswer(input: string): void { #handleUserAnswer(input: string): void {
// Add user input to history // Add user input to history
this.#update({ this.#updateInternal({
type: 'input', type: 'input',
displayText: this.#pageAgent.i18n.t('ui.panel.userAnswer', { input }), displayText: this.#i18n.t('ui.panel.userAnswer', { input }),
}) })
// Reset state // Reset state
@@ -249,7 +367,7 @@ export class Panel {
#showInputArea(placeholder?: string): void { #showInputArea(placeholder?: string): void {
// Clear input field // Clear input field
this.#taskInput.value = '' this.#taskInput.value = ''
this.#taskInput.placeholder = placeholder || this.#pageAgent.i18n.t('ui.panel.taskInput') this.#taskInput.placeholder = placeholder || this.#i18n.t('ui.panel.taskInput')
this.#inputSection.classList.remove(styles.hidden) this.#inputSection.classList.remove(styles.hidden)
// Focus on input field // Focus on input field
setTimeout(() => { setTimeout(() => {
@@ -295,23 +413,23 @@ export class Panel {
stepNumber: 0, stepNumber: 0,
timestamp: new Date(), timestamp: new Date(),
type: 'thinking', type: 'thinking',
displayText: this.#pageAgent.i18n.t('ui.panel.waitingPlaceholder'), displayText: this.#i18n.t('ui.panel.waitingPlaceholder'),
})} })}
</div> </div>
</div> </div>
<div class="${styles.header}"> <div class="${styles.header}">
<div class="${styles.statusSection}"> <div class="${styles.statusSection}">
<div class="${styles.indicator} ${styles.thinking}"></div> <div class="${styles.indicator} ${styles.thinking}"></div>
<div class="${styles.statusText}">${this.#pageAgent.i18n.t('ui.panel.ready')}</div> <div class="${styles.statusText}">${this.#i18n.t('ui.panel.ready')}</div>
</div> </div>
<div class="${styles.controls}"> <div class="${styles.controls}">
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#pageAgent.i18n.t('ui.panel.expand')}"> <button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
</button> </button>
<button class="${styles.controlButton} ${styles.pauseButton}" title="${this.#pageAgent.i18n.t('ui.panel.pause')}"> <button class="${styles.controlButton} ${styles.pauseButton}" title="${this.#i18n.t('ui.panel.pause')}">
</button> </button>
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#pageAgent.i18n.t('ui.panel.stop')}"> <button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
X X
</button> </button>
</div> </div>
@@ -472,10 +590,7 @@ export class Panel {
#updateHistory(): void { #updateHistory(): void {
const steps = this.#state.getAllSteps() const steps = this.#state.getAllSteps()
this.#historySection.innerHTML = steps.map((step) => this.#createHistoryItem(step)).join('') this.#historySection.innerHTML = steps.map((step) => this.#createHistoryItem(step)).join('')
// Scroll to bottom to show latest records
this.#scrollToBottom() this.#scrollToBottom()
} }
@@ -502,8 +617,8 @@ export class Panel {
// Check if this is a result from done tool // Check if this is a result from done tool
if (step.toolName === 'done') { if (step.toolName === 'done') {
// Judge success or failure based on result // Judge success or failure based on result
const failureKeyword = this.#pageAgent.i18n.t('ui.tools.resultFailure') const failureKeyword = this.#i18n.t('ui.tools.resultFailure')
const errorKeyword = this.#pageAgent.i18n.t('ui.tools.resultError') const errorKeyword = this.#i18n.t('ui.tools.resultError')
const isSuccess = const isSuccess =
!step.toolResult || !step.toolResult ||
(!step.toolResult.includes(failureKeyword) && !step.toolResult.includes(errorKeyword)) (!step.toolResult.includes(failureKeyword) && !step.toolResult.includes(errorKeyword))
@@ -532,7 +647,7 @@ export class Panel {
} }
const durationText = step.duration ? ` · ${step.duration}ms` : '' const durationText = step.duration ? ` · ${step.duration}ms` : ''
const stepLabel = this.#pageAgent.i18n.t('ui.panel.step', { const stepLabel = this.#i18n.t('ui.panel.step', {
number: step.stepNumber.toString(), number: step.stepNumber.toString(),
time, time,
duration: durationText || '', // Explicitly pass empty string to replace template duration: durationText || '', // Explicitly pass empty string to replace template
@@ -542,7 +657,7 @@ export class Panel {
<div class="${styles.historyItem} ${typeClass}"> <div class="${styles.historyItem} ${typeClass}">
<div class="${styles.historyContent}"> <div class="${styles.historyContent}">
<span class="${styles.statusIcon}">${statusIcon}</span> <span class="${styles.statusIcon}">${statusIcon}</span>
<span>${step.displayText}</span> <span>${escapeHtml(step.displayText)}</span>
</div> </div>
<div class="${styles.historyMeta}"> <div class="${styles.historyMeta}">
${stepLabel} ${stepLabel}
@@ -551,47 +666,3 @@ export class Panel {
` `
} }
} }
/**
* Get display text for tool execution
*/
export function getToolExecutingText(toolName: string, args: any, i18n: I18n): string {
switch (toolName) {
case 'click_element_by_index':
return i18n.t('ui.tools.clicking', { index: args.index })
case 'input_text':
return i18n.t('ui.tools.inputting', { index: args.index })
case 'select_dropdown_option':
return i18n.t('ui.tools.selecting', { text: args.text })
case 'scroll':
return i18n.t('ui.tools.scrolling')
case 'wait':
return i18n.t('ui.tools.waiting', { seconds: args.seconds })
case 'done':
return i18n.t('ui.tools.done')
default:
return i18n.t('ui.tools.executing', { toolName })
}
}
/**
* Get display text for tool completion
*/
export function getToolCompletedText(toolName: string, args: any, i18n: I18n): string | null {
switch (toolName) {
case 'click_element_by_index':
return i18n.t('ui.tools.clicked', { index: args.index })
case 'input_text':
return i18n.t('ui.tools.inputted', { text: args.text })
case 'select_dropdown_option':
return i18n.t('ui.tools.selected', { text: args.text })
case 'scroll':
return i18n.t('ui.tools.scrolled')
case 'wait':
return i18n.t('ui.tools.waited')
case 'done':
return null
default:
return null
}
}
+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') { constructor(language: SupportedLanguage = 'en-US') {
this.language = language in locales ? language : 'en-US' this.language = language in locales ? language : 'en-US'
this.translations = locales[language] this.translations = locales[this.language]
} }
// 类型安全的翻译方法 // 类型安全的翻译方法
+3
View File
@@ -0,0 +1,3 @@
export { Panel, type PanelConfig, type PanelUpdate } from './Panel'
export { UIState, type Step, type AgentStatus } from './UIState'
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
+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;')
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
},
"include": ["**/*.ts", "**/*.js"],
"exclude": ["dist", "node_modules"]
}
+41
View File
@@ -0,0 +1,41 @@
// @ts-check
import chalk from 'chalk'
import { dirname, resolve } from 'path'
import dts from 'unplugin-dts/vite'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
console.log(chalk.cyan(`📦 Building @page-agent/ui`))
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'PageAgentUI',
fileName: 'page-agent-ui',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: {
external: [],
},
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)
```
+1
View File
@@ -0,0 +1 @@
# Landing Page & Docs
+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.2.0",
"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"
}
}
@@ -1,17 +1,19 @@
import { useTranslation } from 'react-i18next'
export default function BetaNotice() { export default function BetaNotice() {
const { t } = useTranslation('common')
return ( 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="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 items-start">
<div className="flex-shrink-0"> <div className="shrink-0">
<span className="text-xl">🚧</span> <span className="text-xl">🚧</span>
</div> </div>
<div className="ml-3"> <div className="ml-3">
<h3 className="text-sm font-medium text-orange-800 dark:text-orange-200 mb-1"> <h3 className="text-sm font-medium text-orange-800 dark:text-orange-200 mb-1">
Beta {t('beta_notice.title')}
</h3> </h3>
<p className="text-sm text-orange-700 dark:text-orange-300"> <p className="text-sm text-orange-700 dark:text-orange-300">{t('beta_notice.content')}</p>
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -28,7 +28,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
// 使用 Tailwind 的 dark: 前缀实现自动主题切换 // 使用 Tailwind 的 dark: 前缀实现自动主题切换
const containerClasses = 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 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 headerTextClasses = 'text-gray-700 dark:text-gray-300'
const languageTextClasses = 'text-gray-600 dark:text-gray-400' const languageTextClasses = 'text-gray-600 dark:text-gray-400'
@@ -72,7 +72,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
<div className="flex"> <div className="flex">
{/* 行号 */} {/* 行号 */}
{showLineNumbers && ( {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"> <div className="text-xs font-mono leading-6">
{lines.map((line, lineIdx) => { {lines.map((line, lineIdx) => {
const lineNum = lineIdx + 1 const lineNum = lineIdx + 1
@@ -0,0 +1,37 @@
import { useTranslation } from 'react-i18next'
import { siGithub } from 'simple-icons'
export default function Footer() {
const { t } = useTranslation('common')
return (
<footer
className="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700"
role="contentinfo"
>
<div className="max-w-7xl mx-auto px-6 py-6">
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<p className="text-gray-600 dark:text-gray-300 text-sm">{t('footer.copyright')}</p>
<div className="flex items-center space-x-6">
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
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
role="img"
viewBox="0 0 24 24"
className="w-5 h-5 fill-current"
aria-hidden="true"
>
<path d={siGithub.path} />
</svg>
</a>
</div>
</div>
</div>
</footer>
)
}
+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
@@ -0,0 +1,115 @@
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function LanguageSwitcher() {
const { i18n, t } = useTranslation('common')
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const currentLang = i18n.language
const languages = [
{ code: 'zh-CN', label: '中文' },
{ code: 'en-US', label: 'English' },
]
const currentLanguage = languages.find((lang) => lang.code === currentLang) || languages[0]
const handleLanguageChange = (langCode: string) => {
i18n.changeLanguage(langCode)
setIsOpen(false)
}
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [isOpen])
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300"
aria-label={t('language.switch_label')}
aria-expanded={isOpen}
aria-haspopup="true"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
/>
</svg>
<span>{currentLanguage.label}</span>
<svg
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div
className="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50"
role="menu"
aria-orientation="vertical"
>
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => handleLanguageChange(lang.code)}
className={`w-full flex items-center gap-2 px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${
currentLang === lang.code
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300'
}`}
role="menuitem"
>
<span>{lang.label}</span>
{currentLang === lang.code && (
<svg
className="w-4 h-4 ml-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
))}
</div>
)}
</div>
)
}
@@ -0,0 +1,133 @@
import { useEffect, useState } from 'react'
type Theme = 'light' | 'dark'
export default function ThemeSwitcher() {
const [theme, setTheme] = useState<Theme>(() => {
// 初始化时读取保存的主题
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme') as Theme | null
if (savedTheme === 'light' || savedTheme === 'dark') {
return savedTheme
}
// 默认跟随系统
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
return 'light'
})
useEffect(() => {
// 应用主题
if (theme === 'dark') {
document.documentElement.classList.add('dark')
document.documentElement.style.colorScheme = 'dark'
} else {
document.documentElement.classList.remove('dark')
document.documentElement.style.colorScheme = 'light'
}
// 保存到 localStorage
localStorage.setItem('theme', theme)
}, [theme])
// 监听系统主题变化
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
// 只有在用户未手动设置时才自动跟随系统
const savedTheme = localStorage.getItem('theme')
if (!savedTheme) {
setTheme(e.matches ? 'dark' : 'light')
}
}
mediaQuery.addEventListener('change', handleSystemThemeChange)
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange)
}, [])
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
}
return (
<button
onClick={toggleTheme}
className="relative inline-flex items-center h-8 w-16 rounded-full transition-colors duration-300 ease-in-out focus:outline-none"
style={{
backgroundColor: theme === 'dark' ? '#1e293b' : '#e0f2fe',
}}
aria-label={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
role="switch"
aria-checked={theme === 'dark'}
>
{/* 滑块 */}
<span
className="inline-block h-6 w-6 transform rounded-full transition-all duration-300 ease-in-out shadow-md"
style={{
backgroundColor: theme === 'dark' ? '#475569' : '#fbbf24',
transform: theme === 'dark' ? 'translateX(2.25rem)' : 'translateX(0.25rem)',
}}
>
{/* 图标 */}
<span className="flex items-center justify-center h-full w-full">
{theme === 'light' ? (
// 太阳图标
<svg
className="w-4 h-4 text-white"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>
) : (
// 月亮图标
<svg
className="w-4 h-4 text-slate-200"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
)}
</span>
</span>
{/* 背景装饰 */}
<span
className="absolute inset-0 flex items-center justify-between px-2 pointer-events-none"
aria-hidden="true"
>
{/* 左侧太阳(浅色模式时显示) */}
<span
className={`transition-opacity duration-300 ${
theme === 'light' ? 'opacity-0' : 'opacity-40'
}`}
>
<svg className="w-4 h-4 text-sky-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>
</span>
{/* 右侧月亮(深色模式时显示) */}
<span
className={`transition-opacity duration-300 ${
theme === 'dark' ? 'opacity-0' : 'opacity-40'
}`}
>
<svg className="w-4 h-4 text-slate-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
</span>
</span>
</button>
)
}

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