Compare commits

..

99 Commits

Author SHA1 Message Date
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
112 changed files with 6239 additions and 5998 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
run: npm ci
- name: Build demo
run: npm run build
run: npm run build:website
- name: Setup Pages
uses: actions/configure-pages@v5
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
run: npm ci
- name: Build
run: npm run build
run: npm run build:libs
- name: Publish all public packages
run: npm publish --workspaces --access public
+11 -1
View File
@@ -1,6 +1,16 @@
{
"editor.fontLigatures": true,
"cSpell.words": ["HITL", "innerhtml", "opensource", "retryable", "wouter"],
"cSpell.words": [
"deepseek",
"HITL",
"innerhtml",
"llms",
"opensource",
"qwen",
"retryable",
"shadcn",
"wouter"
],
"markdownlint.config": {
// "comment": "Relaxed rules",
"default": true,
+51 -141
View File
@@ -1,95 +1,59 @@
# Instructions for coding assistants
# Instructions for Coding Assistants
## Project Overview
This is a **monorepo** with npm workspaces containing **two main packages**:
This is a **monorepo** with npm workspaces:
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
- **Core Library** (`packages/page-agent/`) - AI agent for browser DOM automation, published as `page-agent` on npm
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
And other internal packages. Such as:
Internal packages:
- **Page Controller** (`packages/page-controller/`) - DOM operations and element interactions module. Independent of LLM, can be tested in unit tests.
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
- **Page Controller** (`packages/page-controller/`) - DOM operations, independent of LLM
- **UI** (`packages/ui/`) - Panel, SimulatorMask, i18n. Decoupled from PageAgent
## Development Commands
### Core Commands
```bash
npm start # Start website dev server
npm run build # Build all packages
npm run build:lib # Build page-agent library only
npm run build:libs # Build all libraries
npm run lint # ESLint with TypeScript strict rules
```
### Package-specific Commands
```bash
# Core library
npm run build --workspace=page-agent
npm run build:watch --workspace=page-agent
# Website
npm run dev --workspace=@page-agent/website
npm run build --workspace=@page-agent/website
```
## Architecture & Critical Patterns
## Architecture
### Monorepo Structure
We adopt a very simple monorepo solution: ts reference + vite alias.
Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig and vite config when adding/removing packages.
You must update tsconfig and vite config if you add/remove/rename a package.
```bash
```
packages/
├── page-agent/ # npm: "page-agent" ⭐ MAIN
│ ├── src/ # AI agent source
├── PageAgent.ts # Main AI agent class
│ │ ├── tools/ # LLM tool definitions
│ │ ├── llms/ # LLM integration
│ │ └── ui/ # UI components
│ ├── vite.config.js # Library build (ES + UMD)
│ └── package.json
├── website/ # npm: "@page-agent/website" (private) ⭐ MAIN
│ ├── src/ # Website source
│ └── index.html # Entry of vite webpage
# ...internal packages below...
└── page-controller/ # npm: "@page-agent/page-controller"
└── src/ # DOM operations source
├── PageController.ts # Main controller class
├── actions.ts # Element interaction actions
└── dom/ # DOM tree extraction
├── website/ # @page-agent/website (private)
├── llms/ # @page-agent/llms
├── page-controller/ # @page-agent/page-controller
└── ui/ # @page-agent/ui
```
`workspaces` must be written in topological order to guarantee build order.
`workspaces` in `package.json` must be in topological order.
```json
"workspaces": [
// internal deps ...
"packages/page-controller",
"packages/page-agent",
"packages/website"
],
```
### Module Boundaries
### Module Boundaries (Critical)
- **Website** (`packages/website/`): CAN import from `page-agent` for demos. Alias `@/``website/src/`
- **Page Agent** (`packages/page-agent/`): The core lib. Imports from all internal packages. Never import from website.
- **Page Controller** (`packages/page-controller/`): Internal lib. Pure DOM operations, NO LLM dependency. Never import from page-agent.
- **Page Agent**: 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, Mask, i18n. No dependency on page-agent
- **Page Controller**: Pure DOM operations. No LLM or UI dependency
### PageController ↔ PageAgent Communication
All communication between PageAgent and PageController is async and isolated:
All communication is async and isolated:
```typescript
// PageAgent delegates DOM operations to PageController
await this.pageController.updateTree() // Refresh DOM state
await this.pageController.clickElement(index) // Click by index
await this.pageController.updateTree()
await this.pageController.clickElement(index)
await this.pageController.inputText(index, text)
await this.pageController.scroll({ down: true, numPages: 1 })
@@ -98,32 +62,22 @@ const simplifiedHTML = await this.pageController.getSimplifiedHTML()
const pageInfo = await this.pageController.getPageInfo()
```
DOM element references and internal state (selectorMap, elementTextMap) are encapsulated in PageController.
### DOM Pipeline
1. **DOM Extraction**: Convert live DOM to `FlatDomTree` via `page-controller/src/dom/dom_tree/`
2. **Dehydration**: DOM tree → simplified text for LLM processing
3. **LLM Processing**: AI model returns action plans (in page-agent)
4. **Indexed Operations**: PageAgent calls PageController methods by element index
1. **DOM Extraction**: Live DOM `FlatDomTree` via `page-controller/src/dom/dom_tree/`
2. **Dehydration**: DOM tree → simplified text for LLM
3. **LLM Processing**: AI returns action plans (page-agent)
4. **Indexed Operations**: PageAgent calls PageController by element index
### Hash Routing Requirement
### CDN Auto-Injection
Uses wouter with `useHashLocation` for static hosting:
```tsx
<Router hook={useHashLocation}> // Always hash-based routes
```
### CDN Auto-Injection Pattern
Library auto-initializes when injected via script tag:
Library auto-initializes via script tag:
```html
<script src="page-agent.js?model=gpt-4"></script>
```
Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
Query params configure `PageAgentConfig` in `src/umd.ts`.
## Key Files Reference
@@ -131,87 +85,43 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
| File | Description |
|------|-------------|
| `src/PageAgent.ts` | ⭐ Main AI agent class orchestrating tools and LLM |
| `src/entry.ts` | CDN/UMD entry point with auto-initialization |
| `src/tools/` | Tool definitions that call PageController methods |
| `src/ui/` | UI components (Panel, SimulatorMask) with CSS modules |
| `src/llms/` | LLM integration and communication layer |
| `vite.config.js` | Library build configuration (ES + UMD) |
| `src/PageAgent.ts` | ⭐ Main AI agent class |
| `src/umd.ts` | CDN/UMD entry with auto-init |
| `src/tools/` | Tool definitions calling PageController |
### LLMs (`packages/llms/`)
| File | Description |
|------|-------------|
| `src/index.ts` | ⭐ LLM class with retry logic |
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig |
| `src/OpenAILenientClient.ts` | OpenAI-compatible client |
### Page Controller (`packages/page-controller/`)
| File | Description |
|------|-------------|
| `src/PageController.ts` | ⭐ Main controller class managing DOM state and actions |
| `src/actions.ts` | Element interaction implementations (click, input, scroll) |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine (ported from browser-use) |
| `src/dom/getPageInfo.ts` | Page scroll/size information |
| `src/patches/` | Framework-specific optimizations (React, Antd) |
| `src/types.ts` | TypeScript interfaces for controller |
### Website (`packages/website/`)
| File | Description |
|------|-------------|
| `src/router.tsx` | ⭐ Central routing (manual registration required) |
| `src/components/DocsLayout.tsx` | Navigation structure (hardcoded nav items) |
| `src/main.tsx` | Site entry with hash routing setup |
| `src/docs/[section]/[topic]/page.tsx` | Documentation pages |
| `src/test-pages/` | Library integration test pages |
| `vite.config.js` | Website build configuration |
| `src/PageController.ts` | ⭐ Main controller class |
| `src/actions.ts` | Element interactions (click, input, scroll) |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine |
## Adding New Features
### New Documentation Page
1. Create `packages/website/src/docs/<section>/<slug>/page.tsx`
2. Add route to `packages/website/src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
3. Add navigation item to `DocsLayout.tsx`
### New Agent Tool
1. Implement tool in `packages/page-agent/src/tools/index.ts`
2. If tool needs DOM operations, add method to PageController first
1. Implement in `packages/page-agent/src/tools/index.ts`
2. If tool needs DOM ops, add method to PageController first
3. Tool calls `this.pageController.methodName()` for DOM interactions
### New PageController Action
1. Add action implementation in `packages/page-controller/src/actions.ts`
1. Add implementation in `packages/page-controller/src/actions.ts`
2. Expose via async method in `PageController.ts`
3. Export from `packages/page-controller/src/index.ts`
## Code Standards
### TypeScript
- Explicit typing for exported/public APIs
- ESLint relaxes some unsafe rules for rapid iteration
### CSS & Styling
- **Prefer Tailwind CSS over custom CSS**
- Custom CSS variables for theme gradients in `src/index.css`
- Dark mode support via `dark:` classes
- CSS modules for component-specific styles
### Import Organization
- External libraries first
- Internal modules (`@/`, `@pages/`)
- Relative imports last
- Blank lines between groups
## Debugging Common Issues
### Blank Documentation Pages
1. Verify route exists in `packages/website/src/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 `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly
2. Test CDN injection with query params
4. Use `packages/website/src/test-pages/` for isolated testing
- Every change you make should not only implement the desired functionality but also improve the quality of the codebase
- All code and comments must be in English.
+3 -3
View File
@@ -112,16 +112,16 @@ npm start # React development server
> @TODO this part is outdated. Update this.
- Config your LLM API
- Start a local script
- Start and serve a local umd script
```bash
npm run build:watch --workspace=page-agent # UMD with auto rebuild
npm run dev:umd # Serving UMD with auto rebuild at http://localhost:5173/page-agent.umd.cjs
```
- Add a new bookmark enable it on other website
```
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/dist/lib/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
```
### Adding Documentation
+24 -20
View File
@@ -8,7 +8,7 @@
🌐 [English](./README.md) | **中文**
👉 <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>
👉 <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,21 +24,10 @@
## 🗺️ Roadmap
👉 [**Roadmap**](./ROADMAP.md)
👉 [**Roadmap**](https://github.com/alibaba/page-agent/issues/96)
## 🚀 快速开始
### CDN 集成
```html
<!-- 临时 CDN URL. 未来会变更 -->
<script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
```
### NPM 安装
```bash
@@ -56,7 +45,7 @@ const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({
modelName: DEMO_MODEL,
model: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'zh-CN',
@@ -65,6 +54,23 @@ const agent = new PageAgent({
await agent.execute('点击登录按钮')
```
### CDN 集成
Fastest way to try PageAgent is to include it via CDN. Demo model will be used by default.
| Location | URL |
| -------- | ----------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js |
| China | https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js |
```html
<script
src="https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
```
## 🏗️ 架构设计
PageAgent adopts a simplified monorepo structure:
@@ -72,12 +78,10 @@ PageAgent adopts a simplified monorepo structure:
```
packages/
├── page-agent/ # AI agent (npm: page-agent)
│ ├── PageAgent # Agent main loop
│ ├── tools/ # LLM tool definitions
├── ui/ # UI components & panels
│ └── llms/ # LLM integration layer
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
└── website/ # Documentation site
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller)
├── ui/ # 面板 & 蒙层 & 模拟鼠标 (npm: @page-agent/ui)
└── website/ # 文档站点
```
## 🤝 贡献
+23 -19
View File
@@ -1,6 +1,6 @@
# PageAgent 🤖🪄
![banner](https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg)
![banner](https://img.alicdn.com/imgextra/i3/O1CN01MyVCS21EoKkIHUT1s_!!6000000000398-49-tps-1280-353.webp)
[![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
@@ -8,7 +8,7 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
🌐 **English** | [中文](./README-zh.md)
👉 <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>
👉 <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>
@@ -24,21 +24,10 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
## 🗺️ Roadmap
👉 [**Roadmap**](./ROADMAP.md)
👉 [**Roadmap**](https://github.com/alibaba/page-agent/issues/96)
## 🚀 Quick Start
### CDN Integration
```html
<!-- temporary CDN URL. May change in the future -->
<script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
```
### NPM Installation
```bash
@@ -56,7 +45,7 @@ const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({
modelName: DEMO_MODEL,
model: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'en-US',
@@ -65,6 +54,23 @@ const agent = new PageAgent({
await agent.execute('Click the login button')
```
### CDN Integration
Fastest way to try PageAgent is to include it via CDN. Demo model will be used by default.
| Location | URL |
| -------- | ----------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js |
| China | https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js |
```html
<script
src="https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
```
## 🏗️ Structure
PageAgent adopts a simplified monorepo structure:
@@ -72,11 +78,9 @@ PageAgent adopts a simplified monorepo structure:
```
packages/
├── page-agent/ # AI agent (npm: page-agent)
│ ├── PageAgent # Agent main loop
│ ├── tools/ # LLM tool definitions
│ ├── ui/ # UI components & panels
│ └── llms/ # LLM integration layer
├── llms/ # LLM client (npm: @page-agent/llms)
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
├── ui/ # Panel & Mask & Mouse Animation (npm: @page-agent/ui)
└── website/ # Demo & Documentation site
```
-61
View File
@@ -1,61 +0,0 @@
# 🗺️ PageAgent Roadmap
The development progress and future plans for PageAgent.
## 🚀 Current Works
- [x] **MVP**
- Core functionality implemented.
- [x] **SPA interaction**
- [x] **Reasoning and (short) memory**
- [x] **Multi model provider integration and testing**
- [x] **UI with HITL**
- Human-in-the-loop user interface. Agent can ask user questions.
- [x] **Landing and doc pages**
- [x] **Remove `ai-sdk`**
- Only one function of AI-ADK is being used.
- Our agent memory and thinking mechanism does not suite ai-sdk.
- [x] **Robust LLM output**
- Auto-fix incomplete output format of DeepSeek and QWen.
- [x] **Working homepage with live LLM API**
- [x] **~~free~~ CDN**
- [x] **Free evaluation plan**
- [x] **Custom actions and HITL**
- [ ] **Hooks and Events**
- [x] **lifecycle hooks**
- [ ] **lifecycle events**
- [ ] **User takeover**
- [#64](https://github.com/alibaba/page-agent/issues/64)
- [ ] **❗Hijack `page_open/page_change/page_unload` behavior**
- [ ] **Custom knowledge base and instructions**
- [#45](https://github.com/alibaba/page-agent/issues/45)
- [ ] **Black/white-list safeguard**
- [ ] **Data-masking**
- [#44](https://github.com/alibaba/page-agent/issues/44)
- [ ] **Improve Memory**
- [#66](https://github.com/alibaba/page-agent/issues/66)
- [#65](https://github.com/alibaba/page-agent/issues/65)
- [ ] **Optimize for popular UI frameworks**
- [x] **i18n of the website**
- [x] Chinese version
- [x] English version
- [x] **Refactor: Separate `Agent` and `PageController`**
- [ ] **Chrome-ext wrapper for multi-page tasks**
♻️ Following browser-use's update and contribute back.
## 📋 Pending Features
- [ ] **Tools for more complex tasks**
- todo list
- file sys
- [ ] **Support custom llm fetch**
- [ ] **Testing suits**
- [ ] **Same-origin multi-page-app rally**
- [ ] **Local MCP proxy**
## 🤔 To Be Decided
- [ ] **Cross-origin multi-page?**
- Tricky
- Need some kind of "memory rally"
+1 -1
View File
@@ -8,7 +8,7 @@ import globals from 'globals'
import tseslint from 'typescript-eslint'
export default defineConfig([
globalIgnores(['**/dist', '**/test-pages', '**/node_modules']),
globalIgnores(['**/dist', '**/node_modules', 'packages/website/src/components/ui']),
{
plugins: {
'react-hooks': reactHooks,
+1680 -516
View File
File diff suppressed because it is too large Load Diff
+22 -16
View File
@@ -1,11 +1,12 @@
{
"name": "root",
"private": true,
"version": "0.0.8",
"version": "0.0.21",
"type": "module",
"workspaces": [
"packages/page-controller",
"packages/ui",
"packages/llms",
"packages/page-agent",
"packages/website"
],
@@ -22,34 +23,39 @@
"npm": ">=10.0.0"
},
"scripts": {
"cleanup": "rm -rf packages/*/dist",
"start": "npm run dev --workspace=@page-agent/website",
"build": "npm run build --workspaces --if-present",
"build:lib": "npm run build:lib --workspace=page-agent",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:libs": "npm run build --workspaces --if-present",
"build": "npm run build:libs && npm run build:website",
"dev:umd": "npm run dev:umd --workspace=page-agent",
"version": "node scripts/sync-version.js",
"lint": "eslint .",
"prepare": "husky"
},
"devDependencies": {
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@eslint/js": "^9.39.1",
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
"@commitlint/cli": "^20.3.1",
"@commitlint/config-conventional": "^20.3.1",
"@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.55.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.0.7",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.3.9",
"eslint-plugin-react-dom": "^2.5.7",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-x": "^2.3.9",
"globals": "^16.4.0",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react-x": "^2.5.7",
"globals": "^17.0.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.4",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "^7.2.6",
"@microsoft/api-extractor": "^7.55.1",
"typescript-eslint": "^8.53.0",
"unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.1",
"vite-plugin-css-injected-by-js": "^3.5.2"
},
"lint-staged": {
+41
View File
@@ -0,0 +1,41 @@
# @page-agent/llms
LLM client with a **reflection-before-action** mental model for page-agent.
## Why This Package Exists
The LLM module and the agent logic are inherently coupled. This package exists not to decouple them, but to **define the interface contract** between the LLM and the agent.
The core abstraction is the `MacroToolInput` — a structured output format that **forces the model to reflect before acting**.
## The Reflection-Before-Action Model
Every tool call must first output its reasoning state before the actual action:
```typescript
interface MacroToolInput {
// Reflection (mandatory before any action)
evaluation_previous_goal?: string // How well did the previous action work?
memory?: string // Key information to remember
next_goal?: string // What to accomplish next
// Action (the actual operation)
action: Record<string, any>
}
```
This design ensures that:
1. **The model evaluates its previous action** before deciding the next step
2. **Working memory is explicitly maintained** across conversation turns
3. **Goals are clearly stated**, making the agent's reasoning transparent and debuggable
## Key Components
| Export | Description |
|--------|-------------|
| `LLM` | Main LLM client class with retry logic |
| `MacroToolInput` | The reflection-before-action input schema |
| `AgentBrain` | Agent's thinking state (eval, memory, goal) |
| `LLMConfig` | Configuration for LLM connection |
| `parseLLMConfig` | Parse and apply defaults to config |
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@page-agent/llms",
"version": "0.0.21",
"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,
}
}
}
+20
View File
@@ -0,0 +1,20 @@
// Dev environment: use .env config if available, otherwise fallback to testing api
export const DEFAULT_MODEL_NAME: string =
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
? import.meta.env.LLM_MODEL_NAME
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_API_KEY: string =
import.meta.env.DEV && import.meta.env.LLM_API_KEY
? import.meta.env.LLM_API_KEY
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_BASE_URL: string =
import.meta.env.DEV && import.meta.env.LLM_BASE_URL
? import.meta.env.LLM_BASE_URL
: 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
// internal
export const LLM_MAX_RETRIES = 2
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+106
View File
@@ -0,0 +1,106 @@
import { OpenAIClient } from './OpenAIClient'
import {
DEFAULT_API_KEY,
DEFAULT_BASE_URL,
DEFAULT_MODEL_NAME,
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> {
return {
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
apiKey: config.apiKey ?? DEFAULT_API_KEY,
model: config.model ?? DEFAULT_MODEL_NAME,
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>
}
/**
* Invoke options for LLM call
*/
export interface InvokeOptions {
/**
* Force LLM to call a specific tool by name.
* If provided: tool_choice = { type: 'function', function: { name: toolChoiceName } }
* If not provided: tool_choice = 'required' (must call some tool, but model chooses which)
*/
toolChoiceName?: string
/**
* Response normalization function.
* Called before parsing the response.
* Used to fix various response format errors from the model.
*/
normalizeResponse?: (response: any) => any
}
/**
* LLM Client interface
* Note: Does not use generics because each tool in the tools array has different types
@@ -40,7 +58,8 @@ export interface LLMClient {
invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal?: AbortSignal
abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult>
}
@@ -65,13 +84,20 @@ export interface InvokeResult<TResult = unknown> {
}
/**
* OpenAI Client config
* LLM configuration
*/
export interface OpenAIClientConfig {
model: string
apiKey: string
baseURL: string
export interface LLMConfig {
baseURL?: string
apiKey?: string
model?: string
temperature?: number
maxTokens?: number
maxRetries?: number
/**
* Custom fetch function for LLM API requests.
* Use this to customize headers, credentials, proxy, etc.
* The response should follow OpenAI API format.
*/
customFetch?: typeof globalThis.fetch
}
+120
View File
@@ -0,0 +1,120 @@
/**
* Utility functions for LLM integration
*/
import chalk from 'chalk'
import { z } from 'zod'
import type { Tool } from './types'
function debug(message: string) {
console.debug(chalk.gray('[LLM]'), message)
}
/**
* Convert Zod schema to OpenAI tool format
* Uses Zod 4 native z.toJSONSchema()
*/
export function zodToOpenAITool(name: string, tool: Tool) {
return {
type: 'function' as const,
function: {
name,
description: tool.description,
parameters: z.toJSONSchema(tool.inputSchema, { target: 'openapi-3.0' }),
},
}
}
/**
* Patch model specific parameters
*/
export function modelPatch(body: Record<string, any>) {
const model: string = body.model || ''
if (!model) return body
const modelName = normalizeModelName(model)
if (modelName.startsWith('qwen')) {
debug('Applying Qwen patch: use higher temperature for auto fixing')
body.temperature = Math.max(body.temperature || 0, 1.0)
}
if (modelName.startsWith('claude')) {
debug('Applying Claude patch: disable thinking')
body.thinking = { type: 'disabled' }
// Convert tool_choice to Claude format
if (body.tool_choice === 'required') {
// 'required' -> { type: 'any' } (must call some tool)
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }')
body.tool_choice = { type: 'any' }
} else if (body.tool_choice?.function?.name) {
// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }
debug('Applying Claude patch: convert tool_choice format')
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
}
}
if (modelName.startsWith('grok')) {
debug('Applying Grok patch: removing tool_choice')
delete body.tool_choice
debug('Applying Grok patch: disable reasoning and thinking')
body.thinking = { type: 'disabled', effort: 'minimal' }
body.reasoning = { enabled: false, effort: 'low' }
}
if (modelName.startsWith('gpt')) {
debug('Applying GPT patch: set verbosity to low')
body.verbosity = 'low'
if (modelName.startsWith('gpt-52')) {
debug('Applying GPT-52 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-51')) {
debug('Applying GPT-51 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-5')) {
debug('Applying GPT-5 patch: set reasoning effort to low')
body.reasoning_effort = 'low'
}
}
if (modelName.startsWith('gemini')) {
debug('Applying Gemini patch: set reasoning effort to minimal')
body.reasoning_effort = 'minimal'
}
return body
}
/**
* check if a given model ID fits a specific model name
*
* @note
* Different model providers may use different model IDs for the same model.
* For example, openai's `gpt-5.2` may called:
*
* - `gpt-5.2-version`
* - `gpt-5_2-date`
* - `GPT-52-version-date`
* - `openai/gpt-5.2-chat`
*
* They should be treated as the same model.
* Normalize them to `gpt-52`
*/
function normalizeModelName(modelName: string): string {
let normalizedName = modelName.toLowerCase()
// remove prefix before '/'
if (normalizedName.includes('/')) {
normalizedName = normalizedName.split('/')[1]
}
// remove '_'
normalizedName = normalizedName.replace(/_/g, '')
// remove '.'
normalizedName = normalizedName.replace(/\./g, '')
return normalizedName
}
+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"',
},
})
+14 -14
View File
@@ -1,16 +1,16 @@
{
"name": "page-agent",
"private": false,
"version": "0.0.8",
"version": "0.0.21",
"type": "module",
"main": "./dist/lib/page-agent.js",
"module": "./dist/lib/page-agent.js",
"types": "./dist/lib/PageAgent.d.ts",
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
"types": "./dist/esm/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageAgent.d.ts",
"import": "./dist/lib/page-agent.js",
"default": "./dist/lib/page-agent.js"
"types": "./dist/esm/PageAgent.d.ts",
"import": "./dist/esm/page-agent.js",
"default": "./dist/esm/page-agent.js"
}
},
"files": [
@@ -37,17 +37,17 @@
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "npm run build:lib && npm run build:umd",
"build:lib": "MODE=lib vite build",
"build:umd": "MODE=umd vite build",
"build:watch": "MODE=umd vite build --watch",
"build": "vite build && vite build --config vite.umd.config.js",
"serve": "npx serve dist/umd -p 5173",
"dev:umd": "concurrently \"vite build --config vite.umd.config.js --watch\" \"npm run serve\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"zod": "^4.1.12",
"@page-agent/page-controller": "0.0.8",
"@page-agent/ui": "0.0.8"
"zod": "^4.3.5",
"@page-agent/llms": "0.0.21",
"@page-agent/page-controller": "0.0.21",
"@page-agent/ui": "0.0.21"
}
}
+88 -36
View File
@@ -2,6 +2,7 @@
* 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, SimulatorMask } from '@page-agent/ui'
import chalk from 'chalk'
@@ -9,17 +10,20 @@ import zod from 'zod'
import type { PageAgentConfig } from './config'
import { MAX_STEPS } from './config/constants'
import { LLM, type Tool } from './llms'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools'
import { trimLines, uid, waitUntil } from './utils'
import { normalizeResponse, trimLines, uid, waitUntil } from './utils'
import { assert } from './utils/assert'
export type { PageAgentConfig }
export { tool, type PageAgentTool } from './tools'
export interface AgentBrain {
// thinking?: string
/**
* Agent brain state - the reflection-before-action model
*
* Every tool call must first reflect on:
* - evaluation_previous_goal: How well did the previous action achieve its goal?
* - memory: Key information to remember for future steps
* - next_goal: What should be accomplished in the next action?
*/
export interface AgentReflection {
evaluation_previous_goal: string
memory: string
next_goal: string
@@ -27,11 +31,11 @@ export interface AgentBrain {
/**
* 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 {
evaluation_previous_goal?: string
memory?: string
next_goal?: string
export interface MacroToolInput extends Partial<AgentReflection> {
action: Record<string, any>
}
@@ -43,8 +47,11 @@ export interface MacroToolResult {
output: string
}
export type { PageAgentConfig }
export { tool, type PageAgentTool } from './tools'
export interface AgentHistory {
brain: AgentBrain
brain: Partial<AgentReflection>
action: {
name: string
input: any
@@ -142,9 +149,6 @@ export class PageAgent extends EventTarget {
window.addEventListener('beforeunload', this.#beforeUnloadListener)
}
/**
* @todo maybe return something?
*/
async execute(task: string): Promise<ExecutionResult> {
if (!task) throw new Error('Task is required')
this.task = task
@@ -201,7 +205,11 @@ export class PageAgent extends EventTarget {
},
],
{ AgentOutput: this.#packMacroTool() },
this.#abortController.signal
this.#abortController.signal,
{
toolChoiceName: 'AgentOutput',
normalizeResponse,
}
)
const macroResult = result.toolResult as MacroToolResult
@@ -281,9 +289,7 @@ export class PageAgent extends EventTarget {
const tools = this.tools
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
return zod.object({
[toolName]: tool.inputSchema,
})
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
})
const actionSchema = zod.union(
@@ -299,6 +305,7 @@ export class PageAgent extends EventTarget {
})
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
@@ -381,25 +388,49 @@ export class PageAgent extends EventTarget {
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
}
async #assembleUserPrompt(): Promise<string> {
let prompt = ''
// <agent_history>
// - <step_>
prompt += '<agent_history>\n'
this.history.forEach((history, index) => {
prompt += `<step_${index + 1}>
Evaluation of Previous Step: ${history.brain.evaluation_previous_goal}
Memory: ${history.brain.memory}
Next Goal: ${history.brain.next_goal}
Action Results: ${history.action.output}
</step_${index + 1}>
`
})
prompt += '</agent_history>\n\n'
// <instructions> (optional)
prompt += await this.#getInstructions()
// <agent_state>
// - <user_request>
@@ -417,6 +448,23 @@ export class PageAgent extends EventTarget {
</agent_state>
`
// <agent_history>
// - <step_>
prompt += '\n<agent_history>\n'
this.history.forEach((history, index) => {
prompt += `<step_${index + 1}>
Evaluation of Previous Step: ${history.brain.evaluation_previous_goal}
Memory: ${history.brain.memory}
Next Goal: ${history.brain.next_goal}
Action Results: ${history.action.output}
</step_${index + 1}>
`
})
prompt += '</agent_history>\n\n'
// <browser_state>
prompt += await this.#getBrowserState()
@@ -452,7 +500,11 @@ export class PageAgent extends EventTarget {
await this.pageController.updateTree()
this.mask.wrapper.style.pointerEvents = 'auto'
const simplifiedHTML = await this.pageController.getSimplifiedHTML()
let simplifiedHTML = await this.pageController.getSimplifiedHTML()
if (this.config.transformPageContent) {
simplifiedHTML = await this.config.transformPageContent(simplifiedHTML)
}
let prompt = trimLines(`<browser_state>
Current Page: [${pageTitle}](${pageUrl})
+1 -21
View File
@@ -1,22 +1,2 @@
// Dev environment: use .env config if available, otherwise fallback to testing api
export const DEFAULT_MODEL_NAME: string =
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
? import.meta.env.LLM_MODEL_NAME
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_API_KEY: string =
import.meta.env.DEV && import.meta.env.LLM_API_KEY
? import.meta.env.LLM_API_KEY
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_BASE_URL: string =
import.meta.env.DEV && import.meta.env.LLM_BASE_URL
? import.meta.env.LLM_BASE_URL
: 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
// internal
export const LLM_MAX_RETRIES = 2
// Agent-specific constants (LLM constants moved to @page-agent/llms)
export const MAX_STEPS = 20
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
export const DEFAULT_MAX_TOKENS = 4096
+39 -27
View File
@@ -1,25 +1,11 @@
import type { LLMConfig } from '@page-agent/llms'
import type { PageControllerConfig } from '@page-agent/page-controller'
import type { SupportedLanguage } from '@page-agent/ui'
import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent'
import type { PageAgentTool } from '../tools'
import {
DEFAULT_API_KEY,
DEFAULT_BASE_URL,
DEFAULT_MAX_TOKENS,
DEFAULT_MODEL_NAME,
DEFAULT_TEMPERATURE,
LLM_MAX_RETRIES,
} from './constants'
export interface LLMConfig {
baseURL?: string
apiKey?: string
model?: string
temperature?: number
maxTokens?: number
maxRetries?: number
}
export type { LLMConfig }
export interface AgentConfig {
// theme?: 'light' | 'dark'
@@ -56,8 +42,27 @@ export interface AgentConfig {
*/
customTools?: Record<string, PageAgentTool | null>
/**
* Instructions to guide the agent's behavior
*/
instructions?: {
/**
* Global system-level instructions, applied to all tasks
*/
system?: string
/**
* Dynamic page-level instructions callback
* Called before each step to get instructions for the current page
* @param url - Current page URL (window.location.href)
* @returns Instructions string, or undefined/null to skip
*/
getPageInstructions?: (url: string) => string | undefined | null
}
// lifecycle hooks
// @todo: use event instead of hooks
// @todo: remove `this` binding, pass agent as explicit parameter instead
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void
@@ -67,6 +72,7 @@ export interface AgentConfig {
/**
* @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.
* @todo remove `this` binding, pass agent as explicit parameter instead
*/
onDispose?: (this: PageAgent, reason?: string) => void
@@ -80,10 +86,27 @@ export interface AgentConfig {
*/
experimentalScriptExecutionTool?: boolean
/**
* Transform page content before sending to LLM.
* Called after DOM extraction and simplification, before LLM invocation.
* Use cases: inspect extraction results, modify page info, mask sensitive data.
*
* @param content - Simplified page content that will be sent to LLM
* @returns Transformed content
*
* @example
* // Mask phone numbers
* transformPageContent: async (content) => {
* return content.replace(/1[3-9]\d{9}/g, '***********')
* }
*/
transformPageContent?: (content: string) => Promise<string> | string
/**
* TODO: @unimplemented
* hook when action causes a new page to be opened
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
* @todo remove `this` binding, pass agent as explicit parameter instead
*/
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
@@ -96,14 +119,3 @@ export interface AgentConfig {
}
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
return {
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
apiKey: config.apiKey ?? DEFAULT_API_KEY,
model: config.model ?? DEFAULT_MODEL_NAME,
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
}
}
@@ -1,188 +0,0 @@
/**
* OpenAI Client implementation
*/
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types'
import { modelPatch, zodToOpenAITool } from './utils'
export class OpenAIClient implements LLMClient {
config: OpenAIClientConfig
constructor(config: OpenAIClientConfig) {
this.config = config
}
async invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal?: AbortSignal
): Promise<InvokeResult> {
// 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, tool]) => zodToOpenAITool(name, tool))
// 2. Call API
let response: Response
try {
response = await fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(
modelPatch({
model: this.config.model,
temperature: this.config.temperature,
max_tokens: this.config.maxTokens,
messages,
tools: openaiTools,
// tool_choice: 'required',
tool_choice: { type: 'function', function: { name: 'AgentOutput' } },
// model specific params
// reasoning_effort: 'minimal',
// verbosity: 'low',
parallel_tool_calls: false,
})
),
signal: abortSignal,
})
} catch (error: unknown) {
// Network 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
)
}
const data = await response.json()
// 4. Check finish_reason
const choice = data.choices?.[0]
if (!choice) {
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data)
}
switch (choice.finish_reason) {
case 'tool_calls':
// ✅ Normal
break
case 'length':
// ⚠️ Token limit reached
throw new InvokeError(
InvokeErrorType.CONTEXT_LENGTH,
'Response truncated: max tokens reached',
data
)
case 'content_filter':
// ❌ Content filtered
throw new InvokeError(
InvokeErrorType.CONTENT_FILTER,
'Content filtered by safety system',
data
)
case 'stop':
// ❌ Did not call tool (we require tool call)
throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'Model did not call any tool', data)
default:
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Unexpected finish_reason: ${choice.finish_reason}`,
data
)
}
// 5. Parse tool call
const toolCall = choice.message?.tool_calls?.[0]
if (!toolCall) {
throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'No tool call found in response', data)
}
const toolName = toolCall.function.name
const tool = tools[toolName]
if (!tool) {
throw new InvokeError(InvokeErrorType.UNKNOWN, `Tool ${toolName} not found`, data)
}
// 6. Parse and validate arguments
let toolArgs: unknown
try {
toolArgs = JSON.parse(toolCall.function.arguments)
} catch (e) {
throw new InvokeError(InvokeErrorType.INVALID_TOOL_ARGS, 'Invalid JSON in tool arguments', e)
}
// Validate against zod schema
const validation = tool.inputSchema.safeParse(toolArgs)
if (!validation.success) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Tool arguments validation failed: ${validation.error.message}`,
validation.error
)
}
// 7. Execute tool
let toolResult: unknown
try {
toolResult = await tool.execute(validation.data)
} catch (e) {
throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(e as Error).message}`,
e
)
}
// 8. Return result (including cache tokens)
return {
toolCall: {
// id: toolCall.id,
name: toolName,
args: validation.data as Record<string, unknown>,
},
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,
}
}
}
@@ -1,128 +0,0 @@
/**
* OpenAI Client implementation
*/
import type { MacroToolInput } from '../PageAgent'
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types'
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
export class OpenAIClient implements LLMClient {
config: OpenAIClientConfig
constructor(config: OpenAIClientConfig) {
this.config = config
}
async invoke(
messages: Message[],
tools: { AgentOutput: Tool<MacroToolInput> },
abortSignal?: AbortSignal
): Promise<InvokeResult> {
// 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, tool]) => zodToOpenAITool(name, tool))
// 2. Call API
let response: Response
try {
response = await fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(
modelPatch({
model: this.config.model,
temperature: this.config.temperature,
max_tokens: this.config.maxTokens,
messages,
tools: openaiTools,
// tool_choice: 'required',
tool_choice: { type: 'function', function: { name: 'AgentOutput' } },
// model specific params
// reasoning_effort: 'minimal',
// verbosity: 'low',
parallel_tool_calls: false,
})
),
signal: abortSignal,
})
} catch (error: unknown) {
// Network 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
)
}
// parse response
const data = await response.json()
const tool = tools.AgentOutput
const macroToolInput = lenientParseMacroToolCall(data, tool.inputSchema as any)
// Execute tool
let toolResult: unknown
try {
toolResult = await tool.execute(macroToolInput)
} catch (e) {
throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(e as Error).message}`,
e
)
}
// Return result (including cache tokens)
return {
toolCall: {
// id: toolCall.id,
name: 'AgentOutput',
args: macroToolInput,
},
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,
}
}
}
-128
View File
@@ -1,128 +0,0 @@
/**
* @topic LLM
* @reasoning
* llm
* agent tool call llm llm
* tools llm tool
* history tool call
* @reasoning
* -
* - llm structured output tool call
* - llm api tool call
* @conclusion
* - @llm responsibility boundary:
* - call llm api with given messages and tools
* - invoke tool call and get the result of the tool
* - return the result to main loop
* - @main_loop responsibility boundary:
* - maintain all behaviors of an **agent**
* @conclusion
* - llm agent llm
*/
/**
* @topic
* @facts
* - tool call schema
* - json
* - openAI/grok/gemini schema
* - tool_choice: required
* - qwen (9)
* @conclusion
* - 使 tool call
* - tool 使 tool call
*/
import type { LLMConfig } from '../config'
import { parseLLMConfig } from '../config'
import { OpenAIClient } from './OpenAILenientClient'
import { InvokeError } from './errors'
import type { InvokeResult, LLMClient, Message, Tool } from './types'
export type { Message, Tool, InvokeResult, LLMClient }
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({
model: this.config.model,
apiKey: this.config.apiKey,
baseURL: this.config.baseURL,
temperature: this.config.temperature,
maxTokens: this.config.maxTokens,
})
}
/**
* - call llm api *once*
* - invoke tool call *once*
* - return the result of the tool
*/
async invoke(
messages: Message[],
tools: Record<string, Tool>,
abortSignal: AbortSignal
): Promise<InvokeResult> {
return await withRetry(
async () => {
const result = await this.client.invoke(messages, tools, abortSignal)
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!
}
-214
View File
@@ -1,214 +0,0 @@
/**
* Utility functions for LLM integration
*/
import chalk from 'chalk'
import { z } from 'zod'
import type { MacroToolInput } from '../PageAgent'
import { InvokeError, InvokeErrorType } from './errors'
import type { Tool } from './types'
/**
* 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' }),
},
}
}
/**
* Although some models cannot guarantee correct response. Common issues are fixable:
* - Instead of returning a proper tool call. Return the tool call parameters in the message content.
* - Returned tool calls or messages don't follow the nested MacroToolInput format.
*/
export function lenientParseMacroToolCall(
responseData: any,
inputSchema: z.ZodObject<MacroToolInput & Record<string, any>>
): MacroToolInput {
// check
const choice = responseData.choices?.[0]
if (!choice) {
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', responseData)
}
// check
switch (choice.finish_reason) {
case 'tool_calls':
case 'function_call': // gemini
case 'stop': // will try a robust parse
// ✅ Normal
break
case 'length':
// ⚠️ Token limit reached
throw new InvokeError(
InvokeErrorType.CONTEXT_LENGTH,
'Response truncated: max tokens reached'
)
case 'content_filter':
// ❌ Content filtered
throw new InvokeError(InvokeErrorType.CONTENT_FILTER, 'Content filtered by safety system')
default:
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Unexpected finish_reason: ${choice.finish_reason}`
)
}
// Extract action schema from MacroToolInput schema
const actionSchema = inputSchema.shape.action
if (!actionSchema) {
throw new Error('inputSchema must have an "action" field')
}
// patch stopReason mis-format
let arg: string | null = null
// try to use tool call
const toolCall = choice.message?.tool_calls?.[0]?.function
arg = toolCall?.arguments ?? null
if (arg && toolCall.name !== 'AgentOutput') {
// TODO: check if toolCall.name is a valid action name
// case: instead of AgentOutput, the model returned a action name as tool call
console.log(chalk.yellow('lenientParseMacroToolCall: #1 fixing incorrect tool call'))
let tmpArg
try {
tmpArg = JSON.parse(arg)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error
)
}
arg = JSON.stringify({ action: { [toolCall.name]: tmpArg } })
}
if (!arg) {
// try to use message content as JSON
arg = choice.message?.content.trim() || null
}
if (!arg) {
throw new InvokeError(
InvokeErrorType.NO_TOOL_CALL,
'No tool call or content found in response',
responseData
)
}
// make sure is valid JSON
let parsedArgs: any
try {
parsedArgs = JSON.parse(arg)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error
)
}
// patch incomplete formats
if (parsedArgs.action || parsedArgs.evaluation_previous_goal || parsedArgs.next_goal) {
// case: nested MacroToolInput format (correct format)
// some models may give a empty action (they may think reasoning and action should be separate)
if (!parsedArgs.action) {
console.log(chalk.yellow('lenientParseMacroToolCall: #2 fixing incorrect tool call'))
parsedArgs.action = {
wait: { seconds: 1 },
}
}
} else if (parsedArgs.type && parsedArgs.function) {
// case: upper level function call format provided. only keep its arguments
// TODO: check if function name is a valid action name
if (parsedArgs.function.name !== 'AgentOutput')
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Expected function name "AgentOutput", got "${parsedArgs.function.name}"`,
null
)
console.log(chalk.yellow('lenientParseMacroToolCall: #3 fixing incorrect tool call'))
parsedArgs = parsedArgs.function.arguments
} else if (parsedArgs.name && parsedArgs.arguments) {
// case: upper level function call format provided. only keep its arguments
// TODO: check if function name is a valid action name
if (parsedArgs.name !== 'AgentOutput')
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Expected function name "AgentOutput", got "${parsedArgs.name}"`,
null
)
console.log(chalk.yellow('lenientParseMacroToolCall: #4 fixing incorrect tool call'))
parsedArgs = parsedArgs.arguments
} else {
// case: only action parameters provided, wrap into MacroToolInput
// TODO: check if action name is valid
console.log(chalk.yellow('lenientParseMacroToolCall: #5 fixing incorrect tool call'))
parsedArgs = { action: parsedArgs } as MacroToolInput
}
// make sure it's not wrapped as string
if (typeof parsedArgs === 'string') {
console.log(chalk.yellow('lenientParseMacroToolCall: #6 fixing incorrect tool call'))
try {
parsedArgs = JSON.parse(parsedArgs)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse nested tool arguments as JSON',
error
)
}
}
const validation = inputSchema.safeParse(parsedArgs)
if (validation.success) {
return validation.data as unknown as MacroToolInput
} else {
const action = parsedArgs.action ?? {}
const actionName = Object.keys(action)[0] || 'unknown'
const actionArgs = JSON.stringify(action[actionName] || 'unknown')
// TODO: check if action name is valid. give a readable error message
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Tool arguments validation failed: action "${actionName}" with args ${actionArgs}`,
validation.error
)
}
}
export function modelPatch(body: Record<string, any>) {
const model: string = body.model || ''
if (model.toLowerCase().startsWith('claude')) {
body.tool_choice = { type: 'tool', name: 'AgentOutput' }
body.thinking = { type: 'disabled' }
// body.reasoning = { enabled: 'disabled' }
}
if (model.toLowerCase().includes('grok')) {
console.log('Applying Grok patch: removing tool_choice')
delete body.tool_choice
console.log('Applying Grok patch: disable reasoning and thinking')
body.thinking = { type: 'disabled', effort: 'minimal' }
body.reasoning = { enabled: false, effort: 'low' }
}
return body
}
+20 -16
View File
@@ -20,21 +20,25 @@ const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const currentScript = document.currentScript as HTMLScriptElement | null
if (currentScript) {
console.log('🚀 page-agent.js detected current script:', currentScript.src)
const url = new URL(currentScript.src)
const model = url.searchParams.get('model') || DEMO_MODEL
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
const config: PageAgentConfig = { model, baseURL, apiKey, language }
window.pageAgent = new PageAgent(config)
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
window.pageAgent = new PageAgent()
}
// in case document.x is not ready yet
// @todo give a switch to disable auto-init
setTimeout(() => {
const currentScript = document.currentScript as HTMLScriptElement | null
if (currentScript) {
console.log('🚀 page-agent.js detected current script:', currentScript.src)
const url = new URL(currentScript.src)
const model = url.searchParams.get('model') || DEMO_MODEL
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
const config: PageAgentConfig = { model, baseURL, apiKey, language }
window.pageAgent = new PageAgent(config)
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
window.pageAgent = new PageAgent()
}
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
window.pageAgent.panel.show() // Show panel
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'
}
+2
View File
@@ -1,3 +1,5 @@
export { normalizeResponse } from './autoFixer'
/**
* Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise
+10 -2
View File
@@ -7,10 +7,18 @@
"baseUrl": ".",
"outDir": "dist",
"paths": {
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"@page-agent/ui": ["../ui/src/index.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [{ "path": "../page-controller" }]
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../ui" }
]
}
+10 -61
View File
@@ -1,7 +1,4 @@
// @ts-check
import chalk from 'chalk'
import 'dotenv/config'
import process from 'node:process'
import { dirname, resolve } from 'path'
import dts from 'unplugin-dts/vite'
import { fileURLToPath } from 'url'
@@ -10,15 +7,11 @@ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// ============================================================================
// Library Config (ES Module for NPM Package)
// ============================================================================
/** @type {import('vite').UserConfig} */
const libConfig = {
// ES Module for NPM Package
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
// dts({ tsconfigPath: './tsconfig.json', bundleTypes: true, compilerOptions: { paths: {} } }),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
@@ -32,9 +25,14 @@ const libConfig = {
fileName: 'page-agent',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'lib'),
outDir: resolve(__dirname, 'dist', 'esm'),
rollupOptions: {
external: ['ai', 'ai-motion', 'chalk', 'zod', '@page-agent/*'],
external: [
'chalk',
'zod',
// all the internal packages
/^@page-agent\//,
],
},
minify: false,
sourcemap: true,
@@ -43,53 +41,4 @@ const libConfig = {
define: {
'process.env.NODE_ENV': '"production"',
},
}
// ============================================================================
// UMD Config (Browser 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
// ============================================================================
/** @type {import('vite').UserConfig} */
const umdConfig = {
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
publicDir: false,
esbuild: {
keepNames: true,
},
resolve: {
alias: {
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
'@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'),
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
}
// ============================================================================
const MODE = process.env.MODE
console.log(chalk.cyan(`📦 Build mode: ${chalk.bold(MODE || 'lib')}`))
let config
if (MODE === 'umd') {
config = umdConfig
} else {
config = libConfig
}
export default defineConfig(config)
})
+45
View File
@@ -0,0 +1,45 @@
// @ts-check
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// UMD Bundle for CDN
// - alias all local packages so that they can be build in
// - no external
// - no d.ts. dts does not work with monorepo aliasing
export default defineConfig({
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
publicDir: false,
esbuild: {
keepNames: true,
},
resolve: {
alias: {
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
},
},
build: {
lib: {
entry: resolve(__dirname, 'src/umd.ts'),
name: 'PageAgent',
fileName: 'page-agent',
formats: ['umd'],
},
outDir: resolve(__dirname, 'dist', 'umd'),
rollupOptions: {
output: {
// force use .js as extension
entryFileNames: 'page-agent.js',
},
},
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/page-controller",
"version": "0.0.8",
"version": "0.0.21",
"type": "module",
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
@@ -32,7 +32,6 @@
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"build:watch": "vite build --watch",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
}
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/ui",
"version": "0.0.8",
"version": "0.0.21",
"type": "module",
"main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js",
@@ -32,11 +32,10 @@
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"build:watch": "vite build --watch",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"ai-motion": "^0.4.7"
"ai-motion": "^0.4.8"
}
}
+23 -18
View File
@@ -3,14 +3,14 @@
* @returns {boolean} - True if a common dark mode class is found.
*/
function hasDarkModeClass() {
const DFEAULT_DARK_MODE_CLASSES = ['dark', 'dark-mode', 'theme-dark', 'night', 'night-mode']
const DEFAULT_DARK_MODE_CLASSES = ['dark', 'dark-mode', 'theme-dark', 'night', 'night-mode']
const htmlElement = document.documentElement
const bodyElement = document.body
const bodyElement = document.body || document.documentElement // can be null in some cases
// Check class names on <html> and <body>
for (const className of DFEAULT_DARK_MODE_CLASSES) {
if (htmlElement.classList.contains(className) || bodyElement.classList.contains(className)) {
for (const className of DEFAULT_DARK_MODE_CLASSES) {
if (htmlElement.classList.contains(className) || bodyElement?.classList.contains(className)) {
return true
}
}
@@ -70,7 +70,7 @@ function isColorDark(colorString: string, threshold = 128) {
function isBackgroundDark() {
// We check both <html> and <body> because some pages set the color on <html>
const htmlStyle = window.getComputedStyle(document.documentElement)
const bodyStyle = window.getComputedStyle(document.body)
const bodyStyle = window.getComputedStyle(document.body || document.documentElement)
// Get background colors
const htmlBgColor = htmlStyle.backgroundColor
@@ -93,18 +93,23 @@ function isBackgroundDark() {
* @returns {boolean} - True if the page is likely dark.
*/
export function isPageDark() {
// Strategy 1: Check for common dark mode classes
if (hasDarkModeClass()) {
return true
try {
// Strategy 1: Check for common dark mode classes
if (hasDarkModeClass()) {
return true
}
// Strategy 2: Analyze the computed background color
if (isBackgroundDark()) {
return true
}
// @TODO add more checks here, e.g., analyzing text color,
// or checking the background of major layout elements like <main> or #app.
return false
} catch (error) {
console.warn('Error determining if page is dark:', error)
return false
}
// Strategy 2: Analyze the computed background color
if (isBackgroundDark()) {
return true
}
// @TODO add more checks here, e.g., analyzing text color,
// or checking the background of major layout elements like <main> or #app.
return false
}
+1 -1
View File
@@ -12,7 +12,7 @@ export class I18n {
constructor(language: SupportedLanguage = 'en-US') {
this.language = language in locales ? language : 'en-US'
this.translations = locales[language]
this.translations = locales[this.language]
}
// 类型安全的翻译方法
+110
View File
@@ -0,0 +1,110 @@
# Website Package - Instructions for Coding Assistants
## Tech Stack
- **React** with TypeScript
- **Vite** for dev server and build
- **Tailwind CSS** for styling
- **shadcn/ui** (new-york style) for UI components
- **Magic UI** for animations and effects
- **wouter** with hash routing for static hosting
- **lucide-react** for icons
## Component Guidelines
### Use shadcn/ui Components First
**ALWAYS prefer shadcn/ui components over custom implementations.**
Before creating any UI component, check if shadcn already provides it:
```bash
# IMPORTANT: Run from packages/website/, NOT from repo root
cd packages/website
# Add a new shadcn component
npx shadcn@latest add <component-name>
# Add a Magic UI component
npx shadcn@latest add "@magicui/<component-name>"
```
Available shadcn components: https://ui.shadcn.com/docs/components
Available Magic UI components: https://magicui.design/docs/components
### Current UI Components
Located in `src/components/ui/`:
**From shadcn/ui:**
- `alert`, `badge`, `button`, `separator`, `sonner`, `switch`, `tooltip`
**From Magic UI:**
- `animated-gradient-text`, `animated-shiny-text`, `aurora-text`
- `hyper-text`, `magic-card`, `neon-gradient-card`, `particles`
- `sparkles-text`, `text-animate`, `typing-animation`
**Custom:**
- `highlighter`, `kbd`, `spinner`
### Styling Rules
1. **Prefer Tailwind classes** over custom CSS
2. Use CSS modules only for complex component-specific styles
3. Support dark mode via `dark:` classes
4. Use CSS variables from `src/index.css` for theme colors
## Project Structure
```
src/
├── pages/
│ ├── Home.tsx # Homepage
│ └── docs/
│ ├── Layout.tsx # Documentation sidebar
│ └── [section]/[topic]/page.tsx
├── components/
│ ├── ui/ # shadcn/ui + Magic UI components
│ ├── Header.tsx # Site header
│ └── Footer.tsx # Site footer
├── i18n/ # Internationalization
├── router.tsx # Central routing
└── main.tsx # App entry
```
## Adding New Pages
### Documentation Page
1. Create `src/pages/docs/<section>/<slug>/page.tsx`
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
3. Add navigation item to `pages/docs/Layout.tsx`
## Routing
Uses hash-based routing for static hosting:
```tsx
import { Router } from 'wouter'
import { useHashLocation } from 'wouter/use-hash-location'
;<Router hook={useHashLocation}>{/* routes */}</Router>
```
## Configuration Files
| File | Purpose |
| ----------------- | ----------------------- |
| `components.json` | shadcn/ui configuration |
| `vite.config.js` | Vite build settings |
| `tsconfig.json` | TypeScript config |
## Commands
```bash
npm start # Dev server (from root)
npm run build:website # Build website (from root)
```
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
}
}
+26 -9
View File
@@ -1,25 +1,42 @@
{
"name": "@page-agent/website",
"private": true,
"version": "0.0.8",
"version": "0.0.21",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:website": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.14",
"@types/react": "^19.2.2",
"@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.1",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^16.1.4",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-i18next": "^16.5.2",
"tailwindcss": "^4.1.14",
"wouter": "^3.7.1"
"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"
}
}
+8 -2
View File
@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next'
import { siGithub } from 'simple-icons'
export default function Footer() {
const { t } = useTranslation('common')
@@ -19,8 +20,13 @@ export default function Footer() {
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200"
aria-label={t('footer.github_label')}
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
<svg
role="img"
viewBox="0 0 24 24"
className="w-5 h-5 fill-current"
aria-hidden="true"
>
<path d={siGithub.path} />
</svg>
</a>
</div>
+38 -10
View File
@@ -1,10 +1,12 @@
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 { BookIcon, CloseIcon, GithubIcon, MenuIcon } from './icons'
import { HyperText } from './ui/hyper-text'
export default function Header() {
const { t } = useTranslation('common')
@@ -34,9 +36,14 @@ export default function Header() {
<span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white block leading-tight">
page-agent
</span>
<p className="hidden sm:block text-xs text-gray-600 dark:text-gray-300">
<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}
>
{t('header.slogan')}
</p>
</HyperText>
</div>
</Link>
@@ -51,7 +58,7 @@ export default function Header() {
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')}
>
<BookIcon className="w-5 h-5" />
<BookOpen className="w-5 h-5" />
</Link>
<a
href="https://github.com/alibaba/page-agent"
@@ -60,7 +67,14 @@ export default function Header() {
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')}
>
<GithubIcon className="w-5 h-5" />
<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>
@@ -74,7 +88,7 @@ export default function Header() {
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"
>
<BookIcon />
<BookOpen className="w-4 h-4" />
{t('header.nav_docs')}
</Link>
<a
@@ -84,7 +98,14 @@ export default function Header() {
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')}
>
<GithubIcon />
<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 />
@@ -100,7 +121,7 @@ export default function Header() {
aria-controls="mobile-menu"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
@@ -116,7 +137,7 @@ export default function Header() {
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)}
>
<BookIcon className="w-5 h-5" />
<BookOpen className="w-5 h-5" />
{t('header.nav_docs')}
</Link>
<a
@@ -126,7 +147,14 @@ export default function Header() {
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')}
>
<GithubIcon className="w-5 h-5" />
<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">
-75
View File
@@ -1,75 +0,0 @@
// SVG图标组件集合,用于Header等地方复用
interface IconProps {
className?: string
'aria-hidden'?: boolean
}
export function BookIcon({ className = 'w-4 h-4', ...props }: IconProps) {
return (
<svg
className={className}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
)
}
export function GithubIcon({ className = 'w-4 h-4', ...props }: IconProps) {
return (
<svg
className={className}
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
{...props}
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
)
}
export function MenuIcon({ className = 'w-6 h-6', ...props }: IconProps) {
return (
<svg
className={className}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
)
}
export function CloseIcon({ className = 'w-6 h-6', ...props }: IconProps) {
return (
<svg
className={className}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
{...props}
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)
}
@@ -0,0 +1,60 @@
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/utils'
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
{
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive:
'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
},
},
defaultVariants: {
variant: 'default',
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-title"
className={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}
{...props}
/>
)
}
function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-description"
className={cn(
'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }
@@ -0,0 +1,37 @@
import { ComponentPropsWithoutRef } from 'react'
import { cn } from '@/lib/utils'
export interface AnimatedGradientTextProps extends ComponentPropsWithoutRef<'div'> {
speed?: number
colorFrom?: string
colorTo?: string
}
export function AnimatedGradientText({
children,
className,
speed = 1,
colorFrom = '#ffaa40',
colorTo = '#9c40ff',
...props
}: AnimatedGradientTextProps) {
return (
<span
style={
{
'--bg-size': `${speed * 300}%`,
'--color-from': colorFrom,
'--color-to': colorTo,
} as React.CSSProperties
}
className={cn(
`animate-gradient inline bg-gradient-to-r from-[var(--color-from)] via-[var(--color-to)] to-[var(--color-from)] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
className
)}
{...props}
>
{children}
</span>
)
}
@@ -0,0 +1,38 @@
import { CSSProperties, ComponentPropsWithoutRef, FC } from 'react'
import { cn } from '@/lib/utils'
export interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<'span'> {
shimmerWidth?: number
}
export const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
children,
className,
shimmerWidth = 100,
...props
}) => {
return (
<span
style={
{
'--shiny-width': `${shimmerWidth}px`,
} as CSSProperties
}
className={cn(
'mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70',
// Shine effect
'animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
// Shine gradient
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
className
)}
{...props}
>
{children}
</span>
)
}
@@ -0,0 +1,39 @@
import React, { memo } from 'react'
interface AuroraTextProps {
children: React.ReactNode
className?: string
colors?: string[]
speed?: number
}
export const AuroraText = memo(
({
children,
className = '',
colors = ['#FF0080', '#7928CA', '#0070F3', '#38bdf8'],
speed = 1,
}: AuroraTextProps) => {
const gradientStyle = {
backgroundImage: `linear-gradient(135deg, ${colors.join(', ')}, ${colors[0]})`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
animationDuration: `${10 / speed}s`,
}
return (
<span className={`relative inline-block ${className}`}>
<span className="sr-only">{children}</span>
<span
className="animate-aurora relative bg-[length:200%_auto] bg-clip-text text-transparent"
style={gradientStyle}
aria-hidden="true"
>
{children}
</span>
</span>
)
}
)
AuroraText.displayName = 'AuroraText'
@@ -0,0 +1,37 @@
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary:
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span'
return <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }
@@ -0,0 +1,91 @@
import { ArrowRightIcon } from '@radix-ui/react-icons'
import { ComponentPropsWithoutRef, ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
interface BentoGridProps extends ComponentPropsWithoutRef<'div'> {
children: ReactNode
className?: string
}
interface BentoCardProps extends ComponentPropsWithoutRef<'div'> {
name: string
className: string
background: ReactNode
Icon: React.ElementType
description: string
href: string
cta: string
}
const BentoGrid = ({ children, className, ...props }: BentoGridProps) => {
return (
<div className={cn('grid w-full auto-rows-[22rem] grid-cols-3 gap-4', className)} {...props}>
{children}
</div>
)
}
const BentoCard = ({
name,
className,
background,
Icon,
description,
href,
cta,
...props
}: BentoCardProps) => (
<div
key={name}
className={cn(
'group relative col-span-3 flex flex-col justify-between overflow-hidden rounded-xl',
// light styles
'bg-background [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]',
// dark styles
'dark:bg-background transform-gpu dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset] dark:[border:1px_solid_rgba(255,255,255,.1)]',
className
)}
{...props}
>
<div>{background}</div>
<div className="p-4">
<div className="pointer-events-none z-10 flex transform-gpu flex-col gap-1 transition-all duration-300 lg:group-hover:-translate-y-10">
<Icon className="h-12 w-12 origin-left transform-gpu text-neutral-700 transition-all duration-300 ease-in-out group-hover:scale-75" />
<h3 className="text-xl font-semibold text-neutral-700 dark:text-neutral-300">{name}</h3>
<p className="max-w-lg text-neutral-400">{description}</p>
</div>
<div
className={cn(
'pointer-events-none flex w-full translate-y-0 transform-gpu flex-row items-center transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 lg:hidden'
)}
>
<Button variant="link" asChild size="sm" className="pointer-events-auto p-0">
<a href={href}>
{cta}
<ArrowRightIcon className="ms-2 h-4 w-4 rtl:rotate-180" />
</a>
</Button>
</div>
</div>
<div
className={cn(
'pointer-events-none absolute bottom-0 hidden w-full translate-y-10 transform-gpu flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 lg:flex'
)}
>
<Button variant="link" asChild size="sm" className="pointer-events-auto p-0">
<a href={href}>
{cta}
<ArrowRightIcon className="ms-2 h-4 w-4 rtl:rotate-180" />
</a>
</Button>
</div>
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
</div>
)
export { BentoCard, BentoGrid }
@@ -0,0 +1,79 @@
import {
AnimatePresence,
MotionProps,
UseInViewOptions,
Variants,
motion,
useInView,
} from 'motion/react'
import { useRef } from 'react'
type MarginType = UseInViewOptions['margin']
interface BlurFadeProps extends MotionProps {
children: React.ReactNode
className?: string
variant?: {
hidden: { y: number }
visible: { y: number }
}
duration?: number
delay?: number
offset?: number
direction?: 'up' | 'down' | 'left' | 'right'
inView?: boolean
inViewMargin?: MarginType
blur?: string
}
export function BlurFade({
children,
className,
variant,
duration = 0.4,
delay = 0,
offset = 6,
direction = 'down',
inView = false,
inViewMargin = '-50px',
blur = '6px',
...props
}: BlurFadeProps) {
const ref = useRef(null)
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
const isInView = !inView || inViewResult
const defaultVariants: Variants = {
hidden: {
[direction === 'left' || direction === 'right' ? 'x' : 'y']:
direction === 'right' || direction === 'down' ? -offset : offset,
opacity: 0,
filter: `blur(${blur})`,
},
visible: {
[direction === 'left' || direction === 'right' ? 'x' : 'y']: 0,
opacity: 1,
filter: `blur(0px)`,
},
}
const combinedVariants = variant || defaultVariants
return (
<AnimatePresence>
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
exit="hidden"
variants={combinedVariants}
transition={{
delay: 0.04 + delay,
duration,
ease: 'easeOut',
}}
className={className}
{...props}
>
{children}
</motion.div>
</AnimatePresence>
)
}
@@ -0,0 +1,60 @@
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
function Button({
className,
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
@@ -0,0 +1,92 @@
import { useInView } from 'motion/react'
import { useEffect, useRef } from 'react'
import type React from 'react'
import { annotate } from 'rough-notation'
import { type RoughAnnotation } from 'rough-notation/lib/model'
type AnnotationAction =
| 'highlight'
| 'underline'
| 'box'
| 'circle'
| 'strike-through'
| 'crossed-off'
| 'bracket'
interface HighlighterProps {
children: React.ReactNode
action?: AnnotationAction
color?: string
strokeWidth?: number
animationDuration?: number
iterations?: number
padding?: number
multiline?: boolean
isView?: boolean
}
export function Highlighter({
children,
action = 'highlight',
color = '#ffd1dc',
strokeWidth = 1.5,
animationDuration = 600,
iterations = 2,
padding = 2,
multiline = true,
isView = false,
}: HighlighterProps) {
const elementRef = useRef<HTMLSpanElement>(null)
const annotationRef = useRef<RoughAnnotation | null>(null)
const isInView = useInView(elementRef, {
once: true,
margin: '-10%',
})
// If isView is false, always show. If isView is true, wait for inView
const shouldShow = !isView || isInView
useEffect(() => {
if (!shouldShow) return
const element = elementRef.current
if (!element) return
const annotationConfig = {
type: action,
color,
strokeWidth,
animationDuration,
iterations,
padding,
multiline,
}
const annotation = annotate(element, annotationConfig)
annotationRef.current = annotation
annotationRef.current.show()
const resizeObserver = new ResizeObserver(() => {
annotation.hide()
annotation.show()
})
resizeObserver.observe(element)
resizeObserver.observe(document.body)
return () => {
if (element) {
annotate(element, { type: action }).remove()
resizeObserver.disconnect()
}
}
}, [shouldShow, action, color, strokeWidth, animationDuration, iterations, padding, multiline])
return (
<span ref={elementRef} className="relative inline-block bg-transparent">
{children}
</span>
)
}
@@ -0,0 +1,140 @@
import { AnimatePresence, MotionProps, motion } from 'motion/react'
import { useEffect, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
type CharacterSet = string[] | readonly string[]
interface HyperTextProps extends MotionProps {
/** The text content to be animated */
children: string
/** Optional className for styling */
className?: string
/** Duration of the animation in milliseconds */
duration?: number
/** Delay before animation starts in milliseconds */
delay?: number
/** Component to render as - defaults to div */
as?: React.ElementType
/** Whether to start animation when element comes into view */
startOnView?: boolean
/** Whether to trigger animation on hover */
animateOnHover?: boolean
/** Custom character set for scramble effect. Defaults to uppercase alphabet */
characterSet?: CharacterSet
}
const DEFAULT_CHARACTER_SET = Object.freeze(
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
) as readonly string[]
const getRandomInt = (max: number): number => Math.floor(Math.random() * max)
export function HyperText({
children,
className,
duration = 800,
delay = 0,
as: Component = 'div',
startOnView = false,
animateOnHover = true,
characterSet = DEFAULT_CHARACTER_SET,
...props
}: HyperTextProps) {
const MotionComponent = motion.create(Component, {
forwardMotionProps: true,
})
const [displayText, setDisplayText] = useState<string[]>(() => children.split(''))
const [isAnimating, setIsAnimating] = useState(false)
const iterationCount = useRef(0)
const elementRef = useRef<HTMLElement>(null)
const handleAnimationTrigger = () => {
if (animateOnHover && !isAnimating) {
iterationCount.current = 0
setIsAnimating(true)
}
}
// Handle animation start based on view or delay
useEffect(() => {
if (!startOnView) {
const startTimeout = setTimeout(() => {
setIsAnimating(true)
}, delay)
return () => clearTimeout(startTimeout)
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setTimeout(() => {
setIsAnimating(true)
}, delay)
observer.disconnect()
}
},
{ threshold: 0.1, rootMargin: '-30% 0px -30% 0px' }
)
if (elementRef.current) {
observer.observe(elementRef.current)
}
return () => observer.disconnect()
}, [delay, startOnView])
// Handle scramble animation
useEffect(() => {
if (!isAnimating) return
const maxIterations = children.length
const startTime = performance.now()
let animationFrameId: number
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
iterationCount.current = progress * maxIterations
setDisplayText((currentText) =>
currentText.map((letter, index) =>
letter === ' '
? letter
: index <= iterationCount.current
? children[index]
: characterSet[getRandomInt(characterSet.length)]
)
)
if (progress < 1) {
animationFrameId = requestAnimationFrame(animate)
} else {
setIsAnimating(false)
}
}
animationFrameId = requestAnimationFrame(animate)
return () => cancelAnimationFrame(animationFrameId)
}, [children, duration, isAnimating, characterSet])
return (
<MotionComponent
ref={elementRef}
className={cn('overflow-hidden py-2 text-4xl font-bold', className)}
onMouseEnter={handleAnimationTrigger}
{...props}
>
<AnimatePresence>
{displayText.map((letter, index) => (
<motion.span key={index} className={cn('font-mono', letter === ' ' ? 'w-3' : '')}>
{letter.toUpperCase()}
</motion.span>
))}
</AnimatePresence>
</MotionComponent>
)
}
@@ -0,0 +1,28 @@
import { cn } from '@/lib/utils'
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
return (
<kbd
data-slot="kbd"
className={cn(
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
"[&_svg:not([class*='size-'])]:size-3",
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
className
)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<kbd
data-slot="kbd-group"
className={cn('inline-flex items-center gap-1', className)}
{...props}
/>
)
}
export { Kbd, KbdGroup }
@@ -0,0 +1,101 @@
import { motion, useMotionTemplate, useMotionValue } from 'motion/react'
import React, { useCallback, useEffect } from 'react'
import { cn } from '@/lib/utils'
interface MagicCardProps {
children?: React.ReactNode
className?: string
gradientSize?: number
gradientColor?: string
gradientOpacity?: number
gradientFrom?: string
gradientTo?: string
}
export function MagicCard({
children,
className,
gradientSize = 200,
gradientColor = '#262626',
gradientOpacity = 0.8,
gradientFrom = '#9E7AFF',
gradientTo = '#FE8BBB',
}: MagicCardProps) {
const mouseX = useMotionValue(-gradientSize)
const mouseY = useMotionValue(-gradientSize)
const reset = useCallback(() => {
mouseX.set(-gradientSize)
mouseY.set(-gradientSize)
}, [gradientSize, mouseX, mouseY])
const handlePointerMove = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect()
mouseX.set(e.clientX - rect.left)
mouseY.set(e.clientY - rect.top)
},
[mouseX, mouseY]
)
useEffect(() => {
reset()
}, [reset])
useEffect(() => {
const handleGlobalPointerOut = (e: PointerEvent) => {
if (!e.relatedTarget) {
reset()
}
}
const handleVisibility = () => {
if (document.visibilityState !== 'visible') {
reset()
}
}
window.addEventListener('pointerout', handleGlobalPointerOut)
window.addEventListener('blur', reset)
document.addEventListener('visibilitychange', handleVisibility)
return () => {
window.removeEventListener('pointerout', handleGlobalPointerOut)
window.removeEventListener('blur', reset)
document.removeEventListener('visibilitychange', handleVisibility)
}
}, [reset])
return (
<div
className={cn('group relative rounded-[inherit]', className)}
onPointerMove={handlePointerMove}
onPointerLeave={reset}
onPointerEnter={reset}
>
<motion.div
className="bg-border pointer-events-none absolute inset-0 rounded-[inherit] duration-300 group-hover:opacity-100"
style={{
background: useMotionTemplate`
radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px,
${gradientFrom},
${gradientTo},
var(--border) 100%
)
`,
}}
/>
<div className="bg-background absolute inset-px rounded-[inherit]" />
<motion.div
className="pointer-events-none absolute inset-px rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100"
style={{
background: useMotionTemplate`
radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, ${gradientColor}, transparent 100%)
`,
opacity: gradientOpacity,
}}
/>
<div className="relative">{children}</div>
</div>
)
}
@@ -0,0 +1,136 @@
import { CSSProperties, ReactElement, ReactNode, useEffect, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
interface NeonColorsProps {
firstColor: string
secondColor: string
}
interface NeonGradientCardProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* @default <div />
* @type ReactElement
* @description
* The component to be rendered as the card
* */
as?: ReactElement
/**
* @default ""
* @type string
* @description
* The className of the card
*/
className?: string
/**
* @default ""
* @type ReactNode
* @description
* The children of the card
* */
children?: ReactNode
/**
* @default 5
* @type number
* @description
* The size of the border in pixels
* */
borderSize?: number
/**
* @default 20
* @type number
* @description
* The size of the radius in pixels
* */
borderRadius?: number
/**
* @default "{ firstColor: '#ff00aa', secondColor: '#00FFF1' }"
* @type string
* @description
* The colors of the neon gradient
* */
neonColors?: NeonColorsProps
}
export const NeonGradientCard: React.FC<NeonGradientCardProps> = ({
className,
children,
borderSize = 2,
borderRadius = 20,
neonColors = {
firstColor: '#ff00aa',
secondColor: '#00FFF1',
},
...props
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
const { offsetWidth, offsetHeight } = containerRef.current
setDimensions({ width: offsetWidth, height: offsetHeight })
}
}
updateDimensions()
window.addEventListener('resize', updateDimensions)
return () => {
window.removeEventListener('resize', updateDimensions)
}
}, [])
useEffect(() => {
if (containerRef.current) {
const { offsetWidth, offsetHeight } = containerRef.current
setDimensions({ width: offsetWidth, height: offsetHeight })
}
}, [children])
return (
<div
ref={containerRef}
style={
{
'--border-size': `${borderSize}px`,
'--border-radius': `${borderRadius}px`,
'--neon-first-color': neonColors.firstColor,
'--neon-second-color': neonColors.secondColor,
'--card-width': `${dimensions.width}px`,
'--card-height': `${dimensions.height}px`,
'--card-content-radius': `${borderRadius - borderSize}px`,
'--pseudo-element-background-image': `linear-gradient(0deg, ${neonColors.firstColor}, ${neonColors.secondColor})`,
'--pseudo-element-width': `${dimensions.width + borderSize * 2}px`,
'--pseudo-element-height': `${dimensions.height + borderSize * 2}px`,
'--after-blur': `${dimensions.width / 6}px`,
} as CSSProperties
}
className={cn('relative z-10 size-full rounded-[var(--border-radius)]', className)}
{...props}
>
<div
className={cn(
'relative size-full min-h-[inherit] rounded-[var(--card-content-radius)] bg-gray-100',
'before:absolute before:-top-[var(--border-size)] before:-left-[var(--border-size)] before:-z-10 before:block',
"before:h-[var(--pseudo-element-height)] before:w-[var(--pseudo-element-width)] before:rounded-[var(--border-radius)] before:content-['']",
'before:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] before:bg-[length:100%_200%]',
'before:animate-background-position-spin',
'after:absolute after:-top-[var(--border-size)] after:-left-[var(--border-size)] after:-z-10 after:block',
"after:h-[var(--pseudo-element-height)] after:w-[var(--pseudo-element-width)] after:rounded-[var(--border-radius)] after:blur-[var(--after-blur)] after:content-['']",
'after:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] after:bg-[length:100%_200%] after:opacity-80',
'after:animate-background-position-spin',
'dark:bg-neutral-900',
'break-words'
)}
>
{children}
</div>
</div>
)
}
@@ -0,0 +1,297 @@
import React, { ComponentPropsWithoutRef, useEffect, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
interface MousePosition {
x: number
y: number
}
function MousePosition(): MousePosition {
const [mousePosition, setMousePosition] = useState<MousePosition>({
x: 0,
y: 0,
})
useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
setMousePosition({ x: event.clientX, y: event.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
}, [])
return mousePosition
}
interface ParticlesProps extends ComponentPropsWithoutRef<'div'> {
className?: string
quantity?: number
staticity?: number
ease?: number
size?: number
refresh?: boolean
color?: string
vx?: number
vy?: number
}
function hexToRgb(hex: string): number[] {
hex = hex.replace('#', '')
if (hex.length === 3) {
hex = hex
.split('')
.map((char) => char + char)
.join('')
}
const hexInt = parseInt(hex, 16)
const red = (hexInt >> 16) & 255
const green = (hexInt >> 8) & 255
const blue = hexInt & 255
return [red, green, blue]
}
type Circle = {
x: number
y: number
translateX: number
translateY: number
size: number
alpha: number
targetAlpha: number
dx: number
dy: number
magnetism: number
}
export const Particles: React.FC<ParticlesProps> = ({
className = '',
quantity = 100,
staticity = 50,
ease = 50,
size = 0.4,
refresh = false,
color = '#ffffff',
vx = 0,
vy = 0,
...props
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const canvasContainerRef = useRef<HTMLDivElement>(null)
const context = useRef<CanvasRenderingContext2D | null>(null)
const circles = useRef<Circle[]>([])
const mousePosition = MousePosition()
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 })
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1
const rafID = useRef<number | null>(null)
const resizeTimeout = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
if (canvasRef.current) {
context.current = canvasRef.current.getContext('2d')
}
initCanvas()
animate()
const handleResize = () => {
if (resizeTimeout.current) {
clearTimeout(resizeTimeout.current)
}
resizeTimeout.current = setTimeout(() => {
initCanvas()
}, 200)
}
window.addEventListener('resize', handleResize)
return () => {
if (rafID.current != null) {
window.cancelAnimationFrame(rafID.current)
}
if (resizeTimeout.current) {
clearTimeout(resizeTimeout.current)
}
window.removeEventListener('resize', handleResize)
}
}, [color])
useEffect(() => {
onMouseMove()
}, [mousePosition.x, mousePosition.y])
useEffect(() => {
initCanvas()
}, [refresh])
const initCanvas = () => {
resizeCanvas()
drawParticles()
}
const onMouseMove = () => {
if (canvasRef.current) {
const rect = canvasRef.current.getBoundingClientRect()
const { w, h } = canvasSize.current
const x = mousePosition.x - rect.left - w / 2
const y = mousePosition.y - rect.top - h / 2
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
if (inside) {
mouse.current.x = x
mouse.current.y = y
}
}
}
const resizeCanvas = () => {
if (canvasContainerRef.current && canvasRef.current && context.current) {
canvasSize.current.w = canvasContainerRef.current.offsetWidth
canvasSize.current.h = canvasContainerRef.current.offsetHeight
canvasRef.current.width = canvasSize.current.w * dpr
canvasRef.current.height = canvasSize.current.h * dpr
canvasRef.current.style.width = `${canvasSize.current.w}px`
canvasRef.current.style.height = `${canvasSize.current.h}px`
context.current.scale(dpr, dpr)
// Clear existing particles and create new ones with exact quantity
circles.current = []
for (let i = 0; i < quantity; i++) {
const circle = circleParams()
drawCircle(circle)
}
}
}
const circleParams = (): Circle => {
const x = Math.floor(Math.random() * canvasSize.current.w)
const y = Math.floor(Math.random() * canvasSize.current.h)
const translateX = 0
const translateY = 0
const pSize = Math.floor(Math.random() * 2) + size
const alpha = 0
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
const dx = (Math.random() - 0.5) * 0.1
const dy = (Math.random() - 0.5) * 0.1
const magnetism = 0.1 + Math.random() * 4
return {
x,
y,
translateX,
translateY,
size: pSize,
alpha,
targetAlpha,
dx,
dy,
magnetism,
}
}
const rgb = hexToRgb(color)
const drawCircle = (circle: Circle, update = false) => {
if (context.current) {
const { x, y, translateX, translateY, size, alpha } = circle
context.current.translate(translateX, translateY)
context.current.beginPath()
context.current.arc(x, y, size, 0, 2 * Math.PI)
context.current.fillStyle = `rgba(${rgb.join(', ')}, ${alpha})`
context.current.fill()
context.current.setTransform(dpr, 0, 0, dpr, 0, 0)
if (!update) {
circles.current.push(circle)
}
}
}
const clearContext = () => {
if (context.current) {
context.current.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h)
}
}
const drawParticles = () => {
clearContext()
const particleCount = quantity
for (let i = 0; i < particleCount; i++) {
const circle = circleParams()
drawCircle(circle)
}
}
const remapValue = (
value: number,
start1: number,
end1: number,
start2: number,
end2: number
): number => {
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
return remapped > 0 ? remapped : 0
}
const animate = () => {
clearContext()
circles.current.forEach((circle: Circle, i: number) => {
// Handle the alpha value
const edge = [
circle.x + circle.translateX - circle.size, // distance from left edge
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
circle.y + circle.translateY - circle.size, // distance from top edge
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
]
const closestEdge = edge.reduce((a, b) => Math.min(a, b))
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
if (remapClosestEdge > 1) {
circle.alpha += 0.02
if (circle.alpha > circle.targetAlpha) {
circle.alpha = circle.targetAlpha
}
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge
}
circle.x += circle.dx + vx
circle.y += circle.dy + vy
circle.translateX +=
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / ease
circle.translateY +=
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / ease
drawCircle(circle, true)
// circle gets out of the canvas
if (
circle.x < -circle.size ||
circle.x > canvasSize.current.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.current.h + circle.size
) {
// remove the circle from the array
circles.current.splice(i, 1)
// create a new circle
const newCircle = circleParams()
drawCircle(newCircle)
}
})
rafID.current = window.requestAnimationFrame(animate)
}
return (
<div
className={cn('pointer-events-none', className)}
ref={canvasContainerRef}
aria-hidden="true"
{...props}
>
<canvas ref={canvasRef} className="size-full" />
</div>
)
}
@@ -0,0 +1,26 @@
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import * as React from 'react'
import { cn } from '@/lib/utils'
function Separator({
className,
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className
)}
{...props}
/>
)
}
export { Separator }
@@ -0,0 +1,38 @@
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from 'lucide-react'
import { useTheme } from 'next-themes'
import { Toaster as Sonner, type ToasterProps } from 'sonner'
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme()
return (
<Sonner
theme={theme as ToasterProps['theme']}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
'--border-radius': 'var(--radius)',
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }
@@ -0,0 +1,150 @@
import { motion } from 'motion/react'
import { CSSProperties, ReactElement, useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
interface Sparkle {
id: string
x: string
y: string
color: string
delay: number
scale: number
lifespan: number
}
const Sparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
return (
<motion.svg
key={id}
className="pointer-events-none absolute z-20"
initial={{ opacity: 0, left: x, top: y }}
animate={{
opacity: [0, 1, 0],
scale: [0, scale, 0],
rotate: [75, 120, 150],
}}
transition={{ duration: 0.8, repeat: Infinity, delay }}
width="21"
height="21"
viewBox="0 0 21 21"
>
<path
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
fill={color}
/>
</motion.svg>
)
}
interface SparklesTextProps {
/**
* @default <div />
* @type ReactElement
* @description
* The component to be rendered as the text
* */
as?: ReactElement
/**
* @default ""
* @type string
* @description
* The className of the text
*/
className?: string
/**
* @required
* @type ReactNode
* @description
* The content to be displayed
* */
children: React.ReactNode
/**
* @default 10
* @type number
* @description
* The count of sparkles
* */
sparklesCount?: number
/**
* @default "{first: '#9E7AFF', second: '#FE8BBB'}"
* @type string
* @description
* The colors of the sparkles
* */
colors?: {
first: string
second: string
}
}
export const SparklesText: React.FC<SparklesTextProps> = ({
children,
colors = { first: '#9E7AFF', second: '#FE8BBB' },
className,
sparklesCount = 10,
...props
}) => {
const [sparkles, setSparkles] = useState<Sparkle[]>([])
useEffect(() => {
const generateStar = (): Sparkle => {
const starX = `${Math.random() * 100}%`
const starY = `${Math.random() * 100}%`
const color = Math.random() > 0.5 ? colors.first : colors.second
const delay = Math.random() * 2
const scale = Math.random() * 1 + 0.3
const lifespan = Math.random() * 10 + 5
const id = `${starX}-${starY}-${Date.now()}`
return { id, x: starX, y: starY, color, delay, scale, lifespan }
}
const initializeStars = () => {
const newSparkles = Array.from({ length: sparklesCount }, generateStar)
setSparkles(newSparkles)
}
const updateStars = () => {
setSparkles((currentSparkles) =>
currentSparkles.map((star) => {
if (star.lifespan <= 0) {
return generateStar()
} else {
return { ...star, lifespan: star.lifespan - 0.1 }
}
})
)
}
initializeStars()
const interval = setInterval(updateStars, 100)
return () => clearInterval(interval)
}, [colors.first, colors.second, sparklesCount])
return (
<div
className={cn('text-6xl font-bold', className)}
{...props}
style={
{
'--sparkles-first-color': `${colors.first}`,
'--sparkles-second-color': `${colors.second}`,
} as CSSProperties
}
>
<span className="relative inline-block">
{sparkles.map((sparkle) => (
<Sparkle key={sparkle.id} {...sparkle} />
))}
<strong className="bg-linear-to-r from-[var(--sparkles-first-color)] to-[var(--sparkles-second-color)] bg-clip-text text-transparent">
{children}
</strong>
</span>
</div>
)
}
@@ -0,0 +1,16 @@
import { Loader2Icon } from 'lucide-react'
import { cn } from '@/lib/utils'
function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<Loader2Icon
role="status"
aria-label="Loading"
className={cn('size-4 animate-spin', className)}
{...props}
/>
)
}
export { Spinner }
@@ -0,0 +1,26 @@
import * as SwitchPrimitive from '@radix-ui/react-switch'
import * as React from 'react'
import { cn } from '@/lib/utils'
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }
@@ -0,0 +1,417 @@
import { AnimatePresence, MotionProps, Variants, motion } from 'motion/react'
import { ElementType, memo } from 'react'
import { cn } from '@/lib/utils'
type AnimationType = 'text' | 'word' | 'character' | 'line'
type AnimationVariant =
| 'fadeIn'
| 'blurIn'
| 'blurInUp'
| 'blurInDown'
| 'slideUp'
| 'slideDown'
| 'slideLeft'
| 'slideRight'
| 'scaleUp'
| 'scaleDown'
interface TextAnimateProps extends MotionProps {
/**
* The text content to animate
*/
children: string
/**
* The class name to be applied to the component
*/
className?: string
/**
* The class name to be applied to each segment
*/
segmentClassName?: string
/**
* The delay before the animation starts
*/
delay?: number
/**
* The duration of the animation
*/
duration?: number
/**
* Custom motion variants for the animation
*/
variants?: Variants
/**
* The element type to render
*/
as?: ElementType
/**
* How to split the text ("text", "word", "character")
*/
by?: AnimationType
/**
* Whether to start animation when component enters viewport
*/
startOnView?: boolean
/**
* Whether to animate only once
*/
once?: boolean
/**
* The animation preset to use
*/
animation?: AnimationVariant
/**
* Whether to enable accessibility features (default: true)
*/
accessible?: boolean
}
const staggerTimings: Record<AnimationType, number> = {
text: 0.06,
word: 0.05,
character: 0.03,
line: 0.06,
}
const defaultContainerVariants = {
hidden: { opacity: 1 },
show: {
opacity: 1,
transition: {
delayChildren: 0,
staggerChildren: 0.05,
},
},
exit: {
opacity: 0,
transition: {
staggerChildren: 0.05,
staggerDirection: -1,
},
},
}
const defaultItemVariants: Variants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
},
exit: {
opacity: 0,
},
}
const defaultItemAnimationVariants: Record<
AnimationVariant,
{ container: Variants; item: Variants }
> = {
fadeIn: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, y: 20 },
show: {
opacity: 1,
y: 0,
transition: {
duration: 0.3,
},
},
exit: {
opacity: 0,
y: 20,
transition: { duration: 0.3 },
},
},
},
blurIn: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, filter: 'blur(10px)' },
show: {
opacity: 1,
filter: 'blur(0px)',
transition: {
duration: 0.3,
},
},
exit: {
opacity: 0,
filter: 'blur(10px)',
transition: { duration: 0.3 },
},
},
},
blurInUp: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, filter: 'blur(10px)', y: 20 },
show: {
opacity: 1,
filter: 'blur(0px)',
y: 0,
transition: {
y: { duration: 0.3 },
opacity: { duration: 0.4 },
filter: { duration: 0.3 },
},
},
exit: {
opacity: 0,
filter: 'blur(10px)',
y: 20,
transition: {
y: { duration: 0.3 },
opacity: { duration: 0.4 },
filter: { duration: 0.3 },
},
},
},
},
blurInDown: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, filter: 'blur(10px)', y: -20 },
show: {
opacity: 1,
filter: 'blur(0px)',
y: 0,
transition: {
y: { duration: 0.3 },
opacity: { duration: 0.4 },
filter: { duration: 0.3 },
},
},
},
},
slideUp: {
container: defaultContainerVariants,
item: {
hidden: { y: 20, opacity: 0 },
show: {
y: 0,
opacity: 1,
transition: {
duration: 0.3,
},
},
exit: {
y: -20,
opacity: 0,
transition: {
duration: 0.3,
},
},
},
},
slideDown: {
container: defaultContainerVariants,
item: {
hidden: { y: -20, opacity: 0 },
show: {
y: 0,
opacity: 1,
transition: { duration: 0.3 },
},
exit: {
y: 20,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
slideLeft: {
container: defaultContainerVariants,
item: {
hidden: { x: 20, opacity: 0 },
show: {
x: 0,
opacity: 1,
transition: { duration: 0.3 },
},
exit: {
x: -20,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
slideRight: {
container: defaultContainerVariants,
item: {
hidden: { x: -20, opacity: 0 },
show: {
x: 0,
opacity: 1,
transition: { duration: 0.3 },
},
exit: {
x: 20,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
scaleUp: {
container: defaultContainerVariants,
item: {
hidden: { scale: 0.5, opacity: 0 },
show: {
scale: 1,
opacity: 1,
transition: {
duration: 0.3,
scale: {
type: 'spring',
damping: 15,
stiffness: 300,
},
},
},
exit: {
scale: 0.5,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
scaleDown: {
container: defaultContainerVariants,
item: {
hidden: { scale: 1.5, opacity: 0 },
show: {
scale: 1,
opacity: 1,
transition: {
duration: 0.3,
scale: {
type: 'spring',
damping: 15,
stiffness: 300,
},
},
},
exit: {
scale: 1.5,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
}
const TextAnimateBase = ({
children,
delay = 0,
duration = 0.3,
variants,
className,
segmentClassName,
as: Component = 'p',
startOnView = true,
once = false,
by = 'word',
animation = 'fadeIn',
accessible = true,
...props
}: TextAnimateProps) => {
const MotionComponent = motion.create(Component)
let segments: string[] = []
switch (by) {
case 'word':
segments = children.split(/(\s+)/)
break
case 'character':
segments = children.split('')
break
case 'line':
segments = children.split('\n')
break
case 'text':
default:
segments = [children]
break
}
const finalVariants = variants
? {
container: {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
opacity: { duration: 0.01, delay },
delayChildren: delay,
staggerChildren: duration / segments.length,
},
},
exit: {
opacity: 0,
transition: {
staggerChildren: duration / segments.length,
staggerDirection: -1,
},
},
},
item: variants,
}
: animation
? {
container: {
...defaultItemAnimationVariants[animation].container,
show: {
...defaultItemAnimationVariants[animation].container.show,
transition: {
delayChildren: delay,
staggerChildren: duration / segments.length,
},
},
exit: {
...defaultItemAnimationVariants[animation].container.exit,
transition: {
staggerChildren: duration / segments.length,
staggerDirection: -1,
},
},
},
item: defaultItemAnimationVariants[animation].item,
}
: { container: defaultContainerVariants, item: defaultItemVariants }
return (
<AnimatePresence mode="popLayout">
<MotionComponent
variants={finalVariants.container as Variants}
initial="hidden"
whileInView={startOnView ? 'show' : undefined}
animate={startOnView ? undefined : 'show'}
exit="exit"
className={cn('whitespace-pre-wrap', className)}
viewport={{ once }}
aria-label={accessible ? children : undefined}
{...props}
>
{accessible && <span className="sr-only">{children}</span>}
{segments.map((segment, i) => (
<motion.span
key={`${by}-${segment}-${i}`}
variants={finalVariants.item}
custom={i * staggerTimings[by]}
className={cn(
by === 'line' ? 'block' : 'inline-block whitespace-pre',
by === 'character' && '',
segmentClassName
)}
aria-hidden={accessible ? true : undefined}
>
{segment}
</motion.span>
))}
</MotionComponent>
</AnimatePresence>
)
}
// Export the memoized version
export const TextAnimate = memo(TextAnimateBase)
@@ -0,0 +1,55 @@
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import * as React from 'react'
import { cn } from '@/lib/utils'
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,164 @@
import { MotionProps, motion, useInView } from 'motion/react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
interface TypingAnimationProps extends MotionProps {
children?: string
words?: string[]
className?: string
duration?: number
typeSpeed?: number
deleteSpeed?: number
delay?: number
pauseDelay?: number
loop?: boolean
as?: React.ElementType
startOnView?: boolean
showCursor?: boolean
blinkCursor?: boolean
cursorStyle?: 'line' | 'block' | 'underscore'
}
export function TypingAnimation({
children,
words,
className,
duration = 100,
typeSpeed,
deleteSpeed,
delay = 0,
pauseDelay = 1000,
loop = false,
as: Component = 'span',
startOnView = true,
showCursor = true,
blinkCursor = true,
cursorStyle = 'line',
...props
}: TypingAnimationProps) {
const MotionComponent = motion.create(Component, {
forwardMotionProps: true,
})
const [displayedText, setDisplayedText] = useState<string>('')
const [currentWordIndex, setCurrentWordIndex] = useState(0)
const [currentCharIndex, setCurrentCharIndex] = useState(0)
const [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')
const elementRef = useRef<HTMLElement | null>(null)
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
const wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])
const hasMultipleWords = wordsToAnimate.length > 1
const typingSpeed = typeSpeed || duration
const deletingSpeed = deleteSpeed || typingSpeed / 2
const shouldStart = startOnView ? isInView : true
useEffect(() => {
if (!shouldStart || wordsToAnimate.length === 0) return
const timeoutDelay =
delay > 0 && displayedText === ''
? delay
: phase === 'typing'
? typingSpeed
: phase === 'deleting'
? deletingSpeed
: pauseDelay
const timeout = setTimeout(() => {
const currentWord = wordsToAnimate[currentWordIndex] || ''
const graphemes = Array.from(currentWord)
switch (phase) {
case 'typing':
if (currentCharIndex < graphemes.length) {
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))
setCurrentCharIndex(currentCharIndex + 1)
} else {
if (hasMultipleWords || loop) {
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
if (!isLastWord || loop) {
setPhase('pause')
}
}
}
break
case 'pause':
setPhase('deleting')
break
case 'deleting':
if (currentCharIndex > 0) {
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))
setCurrentCharIndex(currentCharIndex - 1)
} else {
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
setCurrentWordIndex(nextIndex)
setPhase('typing')
}
break
}
}, timeoutDelay)
return () => clearTimeout(timeout)
}, [
shouldStart,
phase,
currentCharIndex,
currentWordIndex,
displayedText,
wordsToAnimate,
hasMultipleWords,
loop,
typingSpeed,
deletingSpeed,
pauseDelay,
delay,
])
const currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')
const isComplete =
!loop &&
currentWordIndex === wordsToAnimate.length - 1 &&
currentCharIndex >= currentWordGraphemes.length &&
phase !== 'deleting'
const shouldShowCursor =
showCursor &&
!isComplete &&
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
const getCursorChar = () => {
switch (cursorStyle) {
case 'block':
return '▌'
case 'underscore':
return '_'
case 'line':
default:
return '|'
}
}
return (
<MotionComponent
ref={elementRef}
className={cn('leading-[5rem] tracking-[-0.02em]', className)}
{...props}
>
{displayedText}
{shouldShowCursor && (
<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>
{getCursorChar()}
</span>
)}
</MotionComponent>
)
}
+3
View File
@@ -0,0 +1,3 @@
export const CDN_URL = 'https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js'
export const CDN_CN_URL =
'https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js'
@@ -1,48 +0,0 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function DataMasking() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
AI
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🔒
</h3>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
</h3>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
</div>
<CodeEditor
code={`// 数据脱敏配置
// @todo
const rules = [
{ pattern: /\\d{11}/, replacement: '***-****-****' },
{ pattern: /\\d{4}-\\d{4}-\\d{4}-\\d{4}/, replacement: '****-****-****-****' }
]
pageAgent.maskData(rules)`}
/>
</div>
)
}
@@ -1,162 +0,0 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function KnowledgeInjection() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
AI
</p>
{/* Custom Instruction */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">Instruction - </h2>
<div className="p-6 bg-purple-50 dark:bg-purple-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-purple-900 dark:text-purple-300">
🎯
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
AI
</p>
<ul className="list-disc list-inside space-y-2 text-gray-500 dark:text-gray-400">
<li> AI </li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<CodeEditor
className="mb-6"
code={`// 构造函数中设置系统指令
const pageAgent = new PageAgent({
instruction: \`
#
#
-
-
-
#
\`
});`}
/>
</section>
{/* App Knowledge */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">App Knowledge - </h2>
<div className="p-6 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-blue-900 dark:text-blue-300">
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
AI
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</div>
<CodeEditor
className="mb-6"
code={`// 应用知识
pageAgent.knowledge.setAppKnowledge(\`
#
#
##
1. 2. 3. 4. 5.
#
- SKU
- SPU
-
#
- 0
- VIP会员享9.5
\`);`}
/>
</section>
{/* Page Knowledge */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">Page Knowledge - </h2>
<div className="p-6 bg-green-50 dark:bg-green-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-green-900 dark:text-green-300">
📄
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
AI
</p>
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-gray-500 dark:text-gray-400"></p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
</div>
</div>
<CodeEditor
className="mb-6"
code={`// 页面知识库
// 添加页面知识
pageAgent.knowledge.addPageKnowledge("/products", \`
#add-product-btn
.product-item
#search-input2
\`);
pageAgent.knowledge.addPageKnowledge("/orders/*", \`
.order-status
#update-status-btn
\`);
// 移除页面知识
pageAgent.knowledge.removePageKnowledge("/products");`}
/>
</section>
</div>
)
}
@@ -1,164 +0,0 @@
import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function ModelIntegration() {
const { t } = useTranslation('docs')
return (
<div>
<h1 className="text-4xl font-bold mb-6">{t('model_integration.title')}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
{t('model_integration.subtitle')}
</p>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.recommended')}</h2>
<div className="grid md:grid-cols-3 gap-4 mb-6">
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
{t('model_integration.model_gpt4_title')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{t('model_integration.model_gpt4_badge')}
</p>
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li>{t('model_integration.model_gpt4_1')}</li>
<li>{t('model_integration.model_gpt4_2')}</li>
<li>{t('model_integration.model_gpt4_3')}</li>
</ul>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
{t('model_integration.model_deepseek_title')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{t('model_integration.model_deepseek_badge')}
</p>
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li>{t('model_integration.model_deepseek_1')}</li>
<li>{t('model_integration.model_deepseek_2')}</li>
<li>{t('model_integration.model_deepseek_3')}</li>
</ul>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
{t('model_integration.model_qwen_title')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{t('model_integration.model_qwen_badge')}
</p>
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li>{t('model_integration.model_qwen_1')}</li>
<li>{t('model_integration.model_qwen_2')}</li>
<li>{t('model_integration.model_qwen_3')}</li>
</ul>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
{t('model_integration.model_gemini_title')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{t('model_integration.model_gemini_badge')}
</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.available')}</h2>
<div className="p-4 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg mb-6">
<h3 className="text-lg font-semibold mb-3 text-emerald-900 dark:text-emerald-300">
{t('model_integration.available_verified')}
</h3>
<div className="flex flex-wrap gap-2">
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
gpt-4.1-mini/4.1/5
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
grok-4/grok-code-fast
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
qwen3
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
deepseek-v3.1/3.2
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
claude-sonnet-4/4.5/haiku-4.5
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
gemini-2.5
</span>
</div>
</div>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.tips')}</h2>
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg mb-6">
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-disc pl-5">
<li>{t('model_integration.tip_1')}</li>
<li>{t('model_integration.tip_2')}</li>
<li>{t('model_integration.tip_3')}</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.security')}</h2>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-500 mb-4">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
{t('model_integration.security_warning')}
</p>
</div>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t('model_integration.security_desc')}
</p>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
<h3 className="text-lg font-semibold mb-3 text-blue-900 dark:text-blue-300">
{t('model_integration.security_backend_proxy')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
{t('model_integration.security_backend_desc')}
</p>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-none pl-0">
<li>{t('model_integration.security_method_1')}</li>
<li>{t('model_integration.security_method_2')}</li>
<li>{t('model_integration.security_method_3')}</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.configuration')}</h2>
<CodeEditor
code={`
// 百炼等其他兼容服务
const pageAgent = new PageAgent({
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: 'your-api-key',
model: 'qwen-plus'
});
// 私有部署模型
const pageAgent = new PageAgent({
baseURL: 'http://localhost:11434/v1',
apiKey: 'N/A', // Ollama 通常使用任意值
model: 'qwen3:latest'
});
// 测试接口
// @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'
`}
/>
</div>
)
}
@@ -1,81 +0,0 @@
import BetaNotice from '@/components/BetaNotice'
export default function SecurityPermissions() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
page-agent AI
</p>
<div className="space-y-6">
<section>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-3">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫
</h3>
<p className="text-gray-600 dark:text-gray-300">
AI
</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
</h3>
<p className="text-gray-600 dark:text-gray-300"> AI </p>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">URL </h2>
<div className="space-y-3">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫 URL
</h3>
<p className="text-gray-600 dark:text-gray-300"> AI 访</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
URL
</h3>
<p className="text-gray-600 dark:text-gray-300"> AI 访</p>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">Instruction </h2>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-3">
AI
</p>
<div className="space-y-2">
<div className="pl-3 border-l-2 border-red-400">
<p className="font-medium text-red-700 dark:text-red-300"></p>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
<div className="pl-3 border-l-2 border-orange-400">
<p className="font-medium text-orange-700 dark:text-orange-300"></p>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
</div>
</div>
</section>
</div>
</div>
)
}
@@ -1,38 +0,0 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function CdnSetup() {
return (
<div>
<h1 className="text-4xl font-bold mb-6">CDN </h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
CDN page-agent
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<CodeEditor
className="mb-8"
code={`
// 仅供测试使用,稳定 CDN 待定
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>
<script>
window.pageAgent.panel.show()
</script>`}
/>
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3>
<ul className="text-gray-600 dark:text-gray-300 space-y-1">
<li> 使</li>
<li> HTTPS 使</li>
<li> CSP </li>
</ul>
</div>
</div>
)
}
@@ -1,109 +0,0 @@
import CodeEditor from '@/components/CodeEditor'
export default function Configuration() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<CodeEditor
className="mb-8"
language="typescript"
code={`// config
type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
interface LLMConfig {
baseURL?: string
apiKey?: string
model?: string
temperature?: number
maxTokens?: number
maxRetries?: number
}
interface AgentConfig {
language?: "en-US" | "zh-CN"
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see [tools](../tools/index.ts)
*
* @example
* // override internal tool
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: zod.object({
* question: zod.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
// lifecycle hooks
// @todo: use event instead of hooks
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void
onBeforeTask?: (this: PageAgent) => Promise<void> | void
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
/**
* @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.
*/
onDispose?: (this: PageAgent, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* TODO: @unimplemented
* hook when action causes a new page to be opened
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
*/
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
/**
* TODO: @unimplemented
* try to navigate to a new page instead of opening a new tab/window.
* @note will unload the current page when a action tries to open a new page. so that things keep in the same tab/window.
*/
experimentalPreventNewPage?: boolean
}
interface PageControllerConfig {
interactiveBlacklist?: (Element | (() => Element))[]
interactiveWhitelist?: (Element | (() => Element))[]
include_attributes?: string[]
highlightOpacity?: number
highlightLabelOpacity?: number
viewportExpansion?: number
}
`}
/>
</div>
)
}
@@ -27,14 +27,14 @@ export default {
overview: 'Overview',
quick_start: 'Quick Start',
limitations: 'Limitations',
model_integration: 'Model Integration',
models: 'Models',
custom_tools: 'Custom Tools',
knowledge_injection: 'Knowledge Injection',
security_permissions: 'Security & Permissions',
knowledge_injection: 'Instructions',
data_masking: 'Data Masking',
cdn_setup: 'CDN Setup',
configuration: 'Configuration',
best_practices: 'Best Practices',
third_party_agent: 'Third-party Agent',
security_permissions: 'Security & Permissions',
},
}
+11 -26
View File
@@ -5,7 +5,7 @@ export default {
'page-agent is a purely web-based GUI Agent. Gives your website an AI operator in simple steps.',
what_is: 'What is page-agent?',
what_is_desc:
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent targets web developers, not scrapers or agent clients builders. Integrate it into your site to let users interact with pages through natural language.',
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent is built for web developers and web applications first. Integrate it into your site to let users interact with pages through natural language.',
features_title: 'Core Features',
feature_dom: {
title: '🧠 Smart DOM Analysis',
@@ -112,37 +112,22 @@ export default {
future_3: '• Basic visual understanding',
future_4: '• Smarter error recovery',
},
model_integration: {
title: 'Model Integration',
models: {
title: 'Models',
subtitle:
'Supports OpenAI-compatible models with tool call support, including public cloud services and private deployments.',
'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.',
recommendation_logic: 'Recommended: Lightweight models with strong ToolCall capabilities.',
recommended: 'Recommended Models',
model_gpt4_title: '⚡ gpt-4.1-mini',
model_gpt4_badge: 'Evaluation Baseline ✅',
model_gpt4_1: '• Cost-effective',
model_gpt4_2: '• Fast',
model_gpt4_3: '• High success rate',
model_deepseek_title: '💰 DeepSeek-3.2',
model_deepseek_badge: 'Economical',
model_deepseek_1: '• Much cheaper than similar models',
model_deepseek_2: '• ToolCall may error but usually auto-recovers',
model_deepseek_3: "• This site's free demo uses DeepSeek",
model_qwen_title: '🛡️ qwen3',
model_qwen_badge: 'Secure & Compliant',
model_qwen_1: '• Controllable, decent results, reasonable price',
model_qwen_2: '• ToolCall may error but usually auto-recovers',
model_qwen_3: '• Best for scenarios with detailed steps',
model_gemini_title: '⚡ gemini-2.5-flash',
model_gemini_badge: 'Highly efficient, high success rate, reasonable price',
available: 'Available Models',
available_verified: '✅ Verified Working',
available: 'Tested Models',
available_verified: 'Verified Working Models',
tips: 'Tips',
tip_1: 'Reasoning models (like GPT-5) are slower with no advantage',
tip_2:
"Non-OpenAI models don't guarantee JSON schema compliance—tool call may error but usually recovers. Higher temperature recommended",
tip_3: 'Small/nano models perform poorly',
'Models with weaker ToolCall capabilities may return incorrect formats. Common errors usually auto-recover. Higher temperature recommended',
tip_3:
'Small models or those unable to handle complex tool definitions typically perform poorly',
security: '🔐 Production Authentication',
security_warning: '⚠️ Never commit real LLM API Keys to your frontend codebase',
security_warning: '⚠️ Never commit real LLM API Keys to your frontend code',
security_desc:
'In production environments, to hide the real LLM API Keys, we recommend the following architecture:',
security_backend_proxy: 'Backend Proxy Pattern',
@@ -3,7 +3,7 @@ export default {
badge: 'GUI Agent in your webpage',
title_line1: 'The AI Operator',
title_line2: 'Living in Your Web App',
subtitle_emoji: '🪄 One line of CDN',
subtitle_emoji: '🪄One line of code',
subtitle_main: ' adds intelligent GUI Agents to your website.',
subtitle_detail: 'Users give natural language commands, AI handles the rest.',
tab_try: '🚀 Try It Now',
@@ -18,6 +18,9 @@ export default {
step1_content: 'Show your bookmarks bar',
step2_title: 'Step 2:',
step2_content: 'Drag this button to your bookmarks',
cdn_label: 'CDN Source',
cdn_international: 'International',
cdn_china: 'China Mirror',
step3_title: 'Step 3:',
step3_content: 'Click the bookmark on any site to activate',
notice_title: '⚠️ Heads Up',
@@ -26,14 +26,14 @@ export default {
overview: '概览',
quick_start: '快速开始',
limitations: '使用限制',
model_integration: '模型接入',
models: '模型',
custom_tools: '自定义工具',
knowledge_injection: '知识注入',
security_permissions: '安全与权限',
knowledge_injection: '知识注入',
data_masking: '数据脱敏',
cdn_setup: 'CDN 引入',
configuration: '配置选项',
best_practices: '最佳实践',
third_party_agent: '接入第三方 Agent',
security_permissions: '安全与权限',
},
}
@@ -111,36 +111,20 @@ export default {
future_3: '• 基础的视觉理解能力',
future_4: '• 更智能的错误恢复机制',
},
model_integration: {
title: '模型接入',
models: {
title: '模型',
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
recommendation_logic: '推荐使用 ToolCall 能力强的轻量级模型。',
recommended: '推荐模型',
model_gpt4_title: '⚡ gpt-4.1-mini',
model_gpt4_badge: '评估基准 ✅',
model_gpt4_1: '• 性价比高',
model_gpt4_2: '• 速度快',
model_gpt4_3: '• 成功率高',
model_deepseek_title: '💰 DeepSeek-3.2',
model_deepseek_badge: '经济实惠',
model_deepseek_1: '• 价格远低于同等级其他模型',
model_deepseek_2: '• ToolCall 有出错率,通常能够自动修复',
model_deepseek_3: '• 本网站提供的免费试用为 DeepSeek',
model_qwen_title: '🛡️ qwen3',
model_qwen_badge: '安全合规',
model_qwen_1: '• 可控、效果尚可,价格合理',
model_qwen_2: '• ToolCall 有出错率,通常能够自动修复',
model_qwen_3: '• 适合能给出详细步骤的场景',
model_gemini_title: '⚡ gemini-2.5-flash',
model_gemini_badge: '极其高效,成功率高,价格合理',
available: '可用模型',
available_verified: '✅ 已验证可用',
available: '已测试模型',
available_verified: '已验证可用模型',
tips: '提示',
tip_1: 'reasoning 模型(如 GPT-5),速度偏慢,没有必要',
tip_2:
'不保证 json schema 的模型(openAI 以外的几乎所有模型),tool call 有概率出错,通常能自动修复,建议 temperature 设置高一些',
tip_3: '小模型、nano 模型,效果不佳',
'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复,建议设置较高的 temperature',
tip_3: '小模型或者无法适应复杂 Tool 定义的模型,通常效果不佳',
security: '🔐 生产环境鉴权建议',
security_warning: '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码',
security_warning: '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码',
security_desc: '在实际应用中,为了隐藏真实的 LLM API Key,建议采用以下架构:',
security_backend_proxy: '后端代理转发',
security_backend_desc:
@@ -3,7 +3,7 @@ export default {
badge: 'GUI Agent in your webpage',
title_line1: '让你的 Web 应用',
title_line2: '拥有 AI 操作员',
subtitle_emoji: '🪄 一行 CDN 引入',
subtitle_emoji: '🪄一行代码',
subtitle_main: ',为你的网站添加 GUI Agent。',
subtitle_detail: '用户/答疑机器人给出文字指示,AI 帮你操作页面。',
tab_try: '🚀 立即尝试',
@@ -17,6 +17,9 @@ export default {
step1_content: '显示收藏夹栏',
step2_title: '步骤 2:',
step2_content: '拖拽下面按钮到收藏夹栏',
cdn_label: 'CDN 源',
cdn_international: '国际',
cdn_china: '国内镜像',
step3_title: '步骤 3:',
step3_content: '在其他网站点击收藏夹中的按钮即可使用',
notice_title: '⚠️ 注意',
+187 -2
View File
@@ -1,14 +1,49 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
/* 启用 class-based dark mode for Tailwind v4 */
@variant dark (.dark &);
:root {
--background: #ffffff;
--foreground: #171717;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
/* 主题色渐变 */
--theme-color-1: rgb(88, 192, 252);
--theme-color-2: rgb(189, 69, 251);
/* shadcn */
--radius: 0.625rem;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
/* class-based dark mode - 应用到 html.dark */
@@ -202,3 +237,153 @@ td {
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
/* shadcn */
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* magic ui */
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
@keyframes blink-cursor {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
--animate-aurora: aurora 8s ease-in-out infinite alternate;
@keyframes aurora {
0% {
background-position: 0% 50%;
transform: rotate(-5deg) scale(0.9);
}
25% {
background-position: 50% 100%;
transform: rotate(5deg) scale(1.1);
}
50% {
background-position: 100% 50%;
transform: rotate(-3deg) scale(0.95);
}
75% {
background-position: 50% 0%;
transform: rotate(3deg) scale(1.05);
}
100% {
background-position: 0% 50%;
transform: rotate(-5deg) scale(0.9);
}
}
--animate-shiny-text: shiny-text 8s infinite;
@keyframes shiny-text {
0%,
90%,
100% {
background-position: calc(-100% - var(--shiny-width)) 0;
}
30%,
60% {
background-position: calc(100% + var(--shiny-width)) 0;
}
}
--animate-gradient: gradient 8s linear infinite;
@keyframes gradient {
to {
background-position: var(--bg-size, 300%) 0;
}
}
--animate-background-position-spin: background-position-spin 3000ms infinite alternate;
@keyframes background-position-spin {
0% {
background-position: top center;
}
100% {
background-position: bottom center;
}
}
}
/* shadcn dark mode */
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
/* shadcn base */
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+3 -7
View File
@@ -1,19 +1,15 @@
import { createRoot } from 'react-dom/client'
import { Route, Router, Switch } from 'wouter'
import { Router } from 'wouter'
import { useHashLocation } from 'wouter/use-hash-location'
import './i18n/config'
import './i18n/types'
import { default as PagesRouter } from './router.tsx'
import { default as TestPagesRouter } from './test-pages/router.tsx'
import { default as PagesRouter } from './router'
import './index.css'
createRoot(document.getElementById('root')!).render(
<Router hook={useHashLocation}>
<Switch>
<Route path="/test-pages" component={TestPagesRouter} nest />
<Route path="/" component={PagesRouter} nest />
</Switch>
<PagesRouter />
</Router>
)
@@ -1,28 +1,38 @@
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
import { Bot, Box, MessageSquare, PlayCircle, Shield, Sparkles, Users, Zap } from 'lucide-react'
import { PageAgent } from 'page-agent'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, useSearchParams } from 'wouter'
import Footer from './components/Footer'
import Header from './components/Header'
import Footer from '../components/Footer'
import Header from '../components/Header'
import { AnimatedGradientText } from '../components/ui/animated-gradient-text'
import { Highlighter } from '../components/ui/highlighter'
import { NeonGradientCard } from '../components/ui/neon-gradient-card'
import { Particles } from '../components/ui/particles'
import { SparklesText } from '../components/ui/sparkles-text'
import { CDN_CN_URL, CDN_URL } from '../constants'
const injection = encodeURI(
"javascript:(function(){var s=document.createElement('script');s.src=`https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js?t=${Math.random()}`;s.setAttribute('crossorigin', true);s.type=`text/javascript`;s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();"
)
function getInjection(useCN?: boolean) {
const cdn = useCN ? CDN_CN_URL : CDN_URL
const injectionA = `
<a
href=${injection}
class="inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
draggable="true"
onclick="return false;"
title="Drag me to your bookmarks bar!"
>
PageAgent
</a>
const injection = encodeURI(
`javascript:(function(){var s=document.createElement('script');s.src=\`${cdn}?t=\${Math.random()}\`;s.setAttribute('crossorigin', true);s.type="text/javascript";s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();`
)
`
return `
<a
href=${injection}
class="inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
draggable="true"
onclick="return false;"
title="Drag me to your bookmarks bar!"
>
PageAgent
</a>
`
}
export default function HomePage() {
const { t, i18n } = useTranslation(['home', 'common'])
@@ -38,6 +48,7 @@ export default function HomePage() {
const isOther = params.has('try_other')
const [activeTab, setActiveTab] = useState<'try' | 'other'>(isOther ? 'other' : 'try')
const [cdnSource, setCdnSource] = useState<'international' | 'china'>('international')
const handleExecute = async () => {
if (!task.trim()) return
@@ -53,6 +64,15 @@ export default function HomePage() {
interactiveBlacklist: [document.getElementById('root')!],
language: i18n.language as any,
instructions: {
system: 'You are a helpful assistant on PageAgent website.',
getPageInstructions: (url) => {
const hint = url.includes('page-agent') ? 'This is PageAgent demo page.' : undefined
console.log('[instructions] getPageInstructions:', url, '->', hint)
return hint
},
},
// experimentalScriptExecutionTool: true,
// testing server
@@ -77,19 +97,28 @@ export default function HomePage() {
<main id="main-content">
<section className="relative px-6 py-22 lg:py-28" aria-labelledby="hero-heading">
<div className="max-w-7xl mx-auto text-center">
{/* Background Pattern */}
{/* Background Pattern + Particles */}
<div className="absolute inset-0 opacity-30" aria-hidden="true">
<div className="absolute inset-0 bg-linear-to-r from-blue-400/20 to-purple-400/20 rounded-3xl transform rotate-1"></div>
<div className="absolute inset-0 bg-linear-to-l from-purple-400/20 to-blue-400/20 rounded-3xl transform -rotate-1"></div>
</div>
<Particles
className="absolute inset-0"
quantity={80}
staticity={30}
ease={80}
color="#6366f1"
/>
<div className="relative z-10">
<div className="inline-flex items-center px-4 py-2 mb-8 text-sm font-medium text-blue-700 bg-blue-100 rounded-full dark:text-blue-300 dark:bg-blue-900/30">
<div className="inline-flex items-center px-4 py-2 mb-8 text-sm font-medium bg-white/90 dark:bg-gray-800/90 rounded-full shadow-lg border border-gray-200 dark:border-gray-700">
<span
className="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse"
aria-hidden="true"
></span>
{t('home:hero.badge')}
<AnimatedGradientText colorFrom="#3b82f6" colorTo="#8b5cf6">
{t('home:hero.badge')}
</AnimatedGradientText>
</div>
<h1
@@ -102,23 +131,29 @@ export default function HomePage() {
</h1>
<p className="text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-4xl mx-auto leading-relaxed">
<span className="bg-linear-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold">
{t('home:hero.subtitle_emoji')}
</span>
<Highlighter action="underline" color="#8b5cf6" strokeWidth={2}>
<span className="bg-linear-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold">
{t('home:hero.subtitle_emoji')}
</span>
</Highlighter>
{t('home:hero.subtitle_main')}
<br />
{t('home:hero.subtitle_detail')}
</p>
{/* Try It Now Section - Tab Card */}
<div className="mt-8 mb-6">
<div className="mb-12">
<div className="max-w-3xl mx-auto">
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<NeonGradientCard
borderSize={2}
borderRadius={20}
neonColors={{ firstColor: '#ff00aa', secondColor: '#00FFF1' }}
>
{/* Tab Headers */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setActiveTab('try')}
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 ${
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 rounded-tl-2xl ${
activeTab === 'try'
? 'bg-linear-to-r from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
@@ -128,7 +163,7 @@ export default function HomePage() {
</button>
<button
onClick={() => setActiveTab('other')}
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 ${
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 rounded-tr-2xl ${
activeTab === 'other'
? 'bg-linear-to-r from-green-50 to-blue-50 dark:from-green-900/30 dark:to-blue-900/30 text-green-700 dark:text-green-300 border-b-2 border-green-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
@@ -194,10 +229,25 @@ export default function HomePage() {
</span>{' '}
{t('home:try_other.step2_content')}
</p>
<div
className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400"
dangerouslySetInnerHTML={{ __html: injectionA }}
></div>
<div className="flex items-center justify-center gap-3">
<select
value={cdnSource}
onChange={(e) =>
setCdnSource(e.target.value as 'international' | 'china')
}
className="px-2 py-1.5 text-xs border border-gray-300 dark:border-gray-500 rounded bg-white dark:bg-gray-600 text-gray-700 dark:text-gray-200"
>
<option value="international">
{t('home:try_other.cdn_international')}
</option>
<option value="china">{t('home:try_other.cdn_china')}</option>
</select>
<div
dangerouslySetInnerHTML={{
__html: getInjection(cdnSource === 'china'),
}}
></div>
</div>
</div>
{/* Usage Instructions */}
@@ -252,7 +302,7 @@ export default function HomePage() {
</div>
)}
</div>
</div>
</NeonGradientCard>
</div>
</div>
@@ -306,10 +356,10 @@ export default function HomePage() {
role="listitem"
>
<div
className="w-12 h-12 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
className="w-14 h-14 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
aria-hidden="true"
>
<span className="text-white text-xl">📦</span>
<Box className="w-7 h-7 text-white" strokeWidth={2.5} />
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.in_page.title')}
@@ -325,10 +375,10 @@ export default function HomePage() {
role="listitem"
>
<div
className="w-12 h-12 bg-linear-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
className="w-14 h-14 bg-linear-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
aria-hidden="true"
>
<span className="text-white text-xl"></span>
<Zap className="w-7 h-7 text-white fill-white" strokeWidth={2.5} />
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.zero_backend.title')}
@@ -344,10 +394,10 @@ export default function HomePage() {
role="listitem"
>
<div
className="w-12 h-12 bg-linear-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
className="w-14 h-14 bg-linear-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
aria-hidden="true"
>
<span className="text-white text-xl">🌈</span>
<MessageSquare className="w-7 h-7 text-white" strokeWidth={2.5} />
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.accessible.title')}
@@ -363,10 +413,10 @@ export default function HomePage() {
role="listitem"
>
<div
className="w-12 h-12 bg-linear-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
className="w-14 h-14 bg-linear-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
aria-hidden="true"
>
<span className="text-white text-xl">🔒</span>
<Shield className="w-7 h-7 text-white" strokeWidth={2.5} />
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.secure_integration.title')}
@@ -383,12 +433,12 @@ export default function HomePage() {
<section className="px-6 py-20" aria-labelledby="use-cases-heading">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-16">
<h2
id="use-cases-heading"
className="text-4xl lg:text-5xl font-bold mb-6 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
<SparklesText
className="text-4xl lg:text-5xl mb-6"
colors={{ first: '#3b82f6', second: '#8b5cf6' }}
>
{t('home:use_cases.section_title')}
</h2>
</SparklesText>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
{t('home:use_cases.section_subtitle')}
</p>
@@ -396,10 +446,10 @@ export default function HomePage() {
<div className="grid lg:grid-cols-2 gap-12" role="list">
{/* Use Case 1 */}
<div className="bg-linear-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="group bg-linear-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold">1</span>
<div className="w-12 h-12 bg-linear-to-br from-blue-500 to-purple-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
<Bot className="w-6 h-6 text-white" strokeWidth={2.5} />
</div>
<div>
@@ -414,10 +464,10 @@ export default function HomePage() {
</div>
{/* Use Case 2 */}
<div className="bg-linear-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="group bg-linear-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold">2</span>
<div className="w-12 h-12 bg-linear-to-br from-green-500 to-blue-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
<Sparkles className="w-6 h-6 text-white" strokeWidth={2.5} />
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
@@ -431,10 +481,10 @@ export default function HomePage() {
</div>
{/* Use Case 3 */}
<div className="bg-linear-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="group bg-linear-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-purple-500 rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold">3</span>
<div className="w-12 h-12 bg-linear-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
<PlayCircle className="w-6 h-6 text-white fill-white/30" strokeWidth={2.5} />
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
@@ -448,10 +498,10 @@ export default function HomePage() {
</div>
{/* Use Case 4 */}
<div className="bg-linear-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="group bg-linear-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold">4</span>
<div className="w-12 h-12 bg-linear-to-br from-orange-500 to-red-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
<Users className="w-6 h-6 text-white" strokeWidth={2.5} />
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
@@ -32,20 +32,23 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
{
title: t('nav.features'),
items: [
{ title: t('nav.model_integration'), path: '/docs/features/model-integration' },
{ title: t('nav.models'), path: '/docs/features/models' },
{ title: t('nav.custom_tools'), path: '/docs/features/custom-tools' },
{ title: t('nav.knowledge_injection'), path: '/docs/features/knowledge-injection' },
{ title: t('nav.security_permissions'), path: '/docs/features/security-permissions' },
{ title: t('nav.knowledge_injection'), path: '/docs/features/custom-instructions' },
{ title: t('nav.data_masking'), path: '/docs/features/data-masking' },
],
},
{
title: t('nav.integration'),
items: [
{ title: t('nav.cdn_setup'), path: '/docs/integration/cdn-setup' },
{ title: t('nav.configuration'), path: '/docs/integration/configuration' },
{ title: t('nav.best_practices'), path: '/docs/integration/best-practices' },
{ title: t('nav.third_party_agent'), path: '/docs/integration/third-party-agent' },
{ title: '🚧 ' + t('nav.cdn_setup'), path: '/docs/integration/cdn-setup' },
{
title: '🚧 ' + t('nav.security_permissions'),
path: '/docs/integration/security-permissions',
},
{ title: '🚧 ' + t('nav.best_practices'), path: '/docs/integration/best-practices' },
],
},
]
@@ -55,7 +58,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
<div className="flex gap-8 min-w-[900px]">
{/* Sidebar */}
<aside className="w-64 shrink-0" role="complementary" aria-label="文档导航">
<div className="sticky top-8">
<div className="sticky">
<nav className="space-y-8" role="navigation" aria-label="文档章节">
{navigationSections.map((section) => (
<section key={section.title}>
@@ -0,0 +1,135 @@
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/components/CodeEditor'
export default function Instructions() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
return (
<div>
<h1 className="text-4xl font-bold mb-6">{isZh ? '知识注入' : 'Instructions'}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh
? '通过 instructions 配置,为 AI 注入系统级指导和页面级上下文,让它更好地理解你的业务场景。'
: 'Use the instructions config to inject system-level directives and page-specific context, helping the AI better understand your application.'}
</p>
{/* System Instructions */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">
{isZh ? '系统级指导 (System Instructions)' : 'System Instructions'}
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
{isZh
? '全局提示词,应用于所有任务。定义 AI 的角色、工作风格和行为边界。'
: "Global directives applied to all tasks. Define the AI's role, working style, and behavioral boundaries."}
</p>
<CodeEditor
className="mb-6"
code={`const agent = new PageAgent({
// ...other config
instructions: {
system: \`
You are a professional e-commerce assistant.
Guidelines:
- Always confirm before submitting orders
- Double-check prices and quantities
- Report errors immediately instead of retrying blindly
\`
}
})`}
/>
</section>
{/* Page Instructions */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">
{isZh ? '页面级指导 (Page Instructions)' : 'Page Instructions'}
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
{isZh
? '动态回调函数,在每个 step 执行前调用,根据当前页面 URL 返回特定提示词。适用于为不同页面提供针对性的操作引导。'
: 'A dynamic callback invoked before each step. Returns page-specific instructions based on the current URL. Useful for providing targeted guidance on different pages.'}
</p>
<CodeEditor
className="mb-6"
code={`const agent = new PageAgent({
// ...other config
instructions: {
system: 'You are an order management assistant.',
getPageInstructions: (url) => {
if (url.includes('/checkout')) {
return \`
This is the checkout page.
- Verify shipping address before proceeding
- Check if any discounts are applied
- Confirm the total amount with the user
\`
}
if (url.includes('/products')) {
return \`
This is the product listing page.
- Use filters to narrow down search results
- Check stock availability before adding to cart
\`
}
return undefined // No special instructions for other pages
}
}
})`}
/>
</section>
{/* How It Works */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">{isZh ? '工作原理' : 'How It Works'}</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '在每个执行步骤之前,page-agent 会将 instructions 拼接到用户提示词中:'
: 'Before each execution step, page-agent prepends the instructions to the user prompt:'}
</p>
<CodeEditor
language="xml"
className="mb-6"
code={`<instructions>
<system_instructions>
You are a professional e-commerce assistant.
...
</system_instructions>
<page_instructions>
This is the checkout page.
...
</page_instructions>
</instructions>
<!-- followed by agent state, history, and browser state -->`}
/>
<ul className="list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400">
<li>
{isZh
? '如果 system 为空,则不输出 <system_instructions> 标签'
: 'If system is empty, the <system_instructions> tag is omitted'}
</li>
<li>
{isZh
? '如果 getPageInstructions 返回空值,则不输出 <page_instructions> 标签'
: 'If getPageInstructions returns empty, the <page_instructions> tag is omitted'}
</li>
</ul>
</section>
</div>
)
}
@@ -0,0 +1,73 @@
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/components/CodeEditor'
export default function DataMasking() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
return (
<div>
<h1 className="text-4xl font-bold mb-6">{isZh ? '数据脱敏' : 'Data Masking'}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh
? '使用 transformPageContent 钩子在页面内容发送给 LLM 之前进行处理,可用于检查清洗效果、修改页面信息、隐藏敏感数据等。'
: 'Use the transformPageContent hook to process page content before sending to LLM. Useful for inspecting extraction results, modifying page info, and masking sensitive data.'}
</p>
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">{isZh ? '接口定义' : 'API Definition'}</h2>
<CodeEditor
className="mb-6"
code={`interface PageAgentConfig {
/**
* Transform page content before sending to LLM.
* Called after DOM extraction and simplification.
*/
transformPageContent?: (content: string) => Promise<string> | string
}`}
/>
</section>
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">
{isZh ? '常用脱敏规则' : 'Common Masking Patterns'}
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
{isZh
? '以下示例展示了如何脱敏常见的敏感信息:'
: 'The following example shows how to mask common sensitive data:'}
</p>
<CodeEditor
code={`const agent = new PageAgent({
transformPageContent: async (content) => {
// China phone number (11 digits starting with 1)
content = content.replace(/\\b(1[3-9]\\d)(\\d{4})(\\d{4})\\b/g, '$1****$3')
// Email address
content = content.replace(
/\\b([a-zA-Z0-9._%+-])[^@]*(@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})\\b/g,
'$1***$2'
)
// China ID card number (18 digits)
content = content.replace(
/\\b(\\d{6})(19|20\\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])(\\d{3}[\\dXx])\\b/g,
'$1********$5'
)
// Bank card number (16-19 digits)
content = content.replace(/\\b(\\d{4})\\d{8,11}(\\d{4})\\b/g, '$1********$2')
return content
}
})`}
/>
</section>
</div>
)
}
@@ -0,0 +1,131 @@
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/components/CodeEditor'
// Recommended models: lightweight with excellent tool call capabilities
const MODELS = {
recommended: [
'gpt-4.1-mini',
'claude-haiku-4.5',
'gemini-3-flash',
'deepseek-3.2',
'gpt-5.2',
'qwen-3-max',
],
verified: [
'qwen-3-plus',
'gpt-4.1',
'gpt-5',
'gpt-5-mini',
'grok-4',
'grok-code-fast',
'claude-sonnet-3.5',
'claude-sonnet-4.5',
'claude-opus-4.5',
'gemini-2.5',
'gemini-3-pro',
],
}
export default function Models() {
const { t } = useTranslation('docs')
const allModels = [...MODELS.recommended, ...MODELS.verified]
return (
<div className="max-w-4xl">
<h1 className="text-4xl font-bold mb-4">{t('models.title')}</h1>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">{t('models.subtitle')}</p>
{/* Models Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-3">{t('models.available')}</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t('models.recommendation_logic')}
</p>
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
<div className="flex flex-wrap gap-2">
{allModels.map((model) => {
const isRecommended = MODELS.recommended.includes(model)
return (
<div
key={model}
className={`px-3 py-1.5 rounded-md text-sm font-medium font-mono transition-colors ${
isRecommended
? 'bg-emerald-500 text-white shadow-sm'
: 'bg-white/80 dark:bg-gray-800/80 text-gray-800 dark:text-gray-200 border border-gray-300 dark:border-gray-600'
}`}
>
{model}
{isRecommended && <span className="ml-1"></span>}
</div>
)
})}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-5"> baseline models</p>
</div>
</section>
{/* Tips Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{t('models.tips')}</h2>
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
<li>{t('models.tip_2')}</li>
<li>{t('models.tip_3')}</li>
</ul>
</div>
</section>
{/* Security Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{t('models.security')}</h2>
<div className="bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-5 rounded-r-lg mb-4">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
{t('models.security_warning')}
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{t('models.security_backend_proxy')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{t('models.security_backend_desc')}
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>{t('models.security_method_1')}</li>
<li>{t('models.security_method_2')}</li>
<li>{t('models.security_method_3')}</li>
</ul>
</div>
</section>
{/* Configuration Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{t('models.configuration')}</h2>
<CodeEditor
code={`// OpenAI-compatible services (e.g., Alibaba Bailian)
const pageAgent = new PageAgent({
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: 'your-api-key',
model: 'qwen-plus'
});
// Self-hosted models (e.g., Ollama)
const pageAgent = new PageAgent({
baseURL: 'http://localhost:11434/v1',
apiKey: 'N/A', // Ollama typically accepts any value
model: 'qwen3:latest'
});
// Free testing endpoint
// Note: Rate-limited, content-filtered, subject to change. Replace with your own.
// Note: Uses official DeepSeek-chat (3.2). See DeepSeek website for terms & privacy.
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'
`}
/>
</section>
</div>
)
}
@@ -0,0 +1,82 @@
import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
import { CDN_CN_URL, CDN_URL } from '@/constants'
export default function CdnSetup() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
return (
<div>
<BetaNotice />
<h1 className="text-4xl font-bold mb-6">{isZh ? 'CDN 引入' : 'CDN Setup'}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh
? '通过 CDN 快速集成 page-agent,无需复杂的构建配置。默认使用演示模型。'
: 'Fastest way to integrate page-agent via CDN. No complex build setup needed. Demo model will be used by default.'}
</p>
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">{isZh ? 'CDN 地址' : 'CDN URLs'}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 px-4 font-semibold">{isZh ? '位置' : 'Location'}</th>
<th className="text-left py-3 px-4 font-semibold">URL</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 px-4">{isZh ? '全球' : 'Global'}</td>
<td className="py-3 px-4 font-mono text-xs break-all">{CDN_URL}</td>
</tr>
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 px-4">{isZh ? '中国镜像' : 'China Mirror'}</td>
<td className="py-3 px-4 font-mono text-xs break-all">{CDN_CN_URL}</td>
</tr>
</tbody>
</table>
</div>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">{isZh ? '快速开始' : 'Quick Start'}</h2>
<CodeEditor
className="mb-6"
code={`<script
src="${CDN_URL}"
crossorigin="true"
type="text/javascript"
></script>`}
/>
</section>
<section className="mb-8">
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
{isZh ? '注意事项' : 'Notes'}
</h3>
<ul className="text-gray-600 dark:text-gray-300 space-y-1">
<li>
{isZh ? '生产环境建议使用固定版本号' : 'Use fixed version number in production'}
</li>
<li> {isZh ? '确保 HTTPS 环境下使用' : 'Ensure HTTPS environment'}</li>
<li>
{' '}
{isZh
? '配置 CSP 策略允许脚本执行'
: 'Configure CSP policy to allow script execution'}
</li>
</ul>
</div>
</section>
</div>
)
}
@@ -0,0 +1,138 @@
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/components/CodeEditor'
export default function Configuration() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
return (
<div>
<h1 className="text-4xl font-bold mb-6">{isZh ? '配置选项' : 'Configuration'}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh
? 'PageAgent 的完整配置接口定义。'
: 'Complete configuration interface for PageAgent.'}
</p>
{/* LLM Configuration */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{isZh ? 'LLM 配置' : 'LLM Configuration'}</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '配置与大语言模型的连接参数。'
: 'Configure connection parameters for the language model.'}
</p>
<CodeEditor
className="mb-4"
language="typescript"
code={`interface LLMConfig {
baseURL?: string
apiKey?: string
model?: string
temperature?: number
maxRetries?: number
/**
* Custom fetch function for LLM API requests.
* Use this to customize headers, credentials, proxy, etc.
*/
customFetch?: typeof globalThis.fetch
}`}
/>
</section>
{/* Agent Configuration */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">
{isZh ? 'Agent 配置' : 'Agent Configuration'}
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '配置 Agent 的行为、生命周期钩子和扩展能力。'
: 'Configure agent behavior, lifecycle hooks, and extension capabilities.'}
</p>
<CodeEditor
className="mb-4"
language="typescript"
code={`interface AgentConfig {
language?: 'en-US' | 'zh-CN'
/** Custom tools to extend or override built-in tools */
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 */
getPageInstructions?: (url: string) => string | undefined | null
}
// Lifecycle hooks
onBeforeStep?: (stepCnt: number) => Promise<void> | void
onAfterStep?: (stepCnt: number, history: AgentHistory[]) => Promise<void> | void
onBeforeTask?: () => Promise<void> | void
onAfterTask?: (result: ExecutionResult) => Promise<void> | void
onDispose?: (reason?: string) => void
/**
* Transform page content before sending to LLM.
* Use cases: inspect extraction results, modify page info, mask sensitive data.
*/
transformPageContent?: (content: string) => Promise<string> | string
/** @experimental Enable JavaScript execution tool */
experimentalScriptExecutionTool?: boolean
}`}
/>
</section>
{/* PageController Configuration */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">
{isZh ? 'PageController 配置' : 'PageController Configuration'}
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '配置 DOM 提取、元素交互和视觉高亮的细节。'
: 'Configure DOM extraction, element interaction, and visual highlighting.'}
</p>
<CodeEditor
className="mb-4"
language="typescript"
code={`interface PageControllerConfig {
/** Elements to exclude from interaction */
interactiveBlacklist?: (Element | (() => Element))[]
/** Elements to force include for interaction */
interactiveWhitelist?: (Element | (() => Element))[]
/** Additional attributes to include in DOM extraction */
include_attributes?: string[]
/** Highlight overlay opacity (0-1) */
highlightOpacity?: number
/** Highlight label opacity (0-1) */
highlightLabelOpacity?: number
/** Viewport expansion in pixels (-1 for full page) */
viewportExpansion?: number
}`}
/>
</section>
{/* Complete Type */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{isZh ? '完整类型' : 'Complete Type'}</h2>
<CodeEditor
language="typescript"
code={`type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig`}
/>
</section>
</div>
)
}
@@ -0,0 +1,90 @@
import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
export default function SecurityPermissions() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
return (
<div>
<BetaNotice />
<h1 className="text-4xl font-bold mb-6">{isZh ? '安全与权限' : 'Security & Permissions'}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh
? 'page-agent 提供多种安全机制,确保 AI 操作在可控范围内进行。'
: 'page-agent provides multiple security mechanisms to ensure AI operations stay within controlled boundaries.'}
</p>
<div className="space-y-6">
<section>
<h2 className="text-2xl font-bold mb-3">
{isZh ? '元素操作黑白名单' : 'Element Interaction Allowlist/Blocklist'}
</h2>
<div className="space-y-3">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫 {isZh ? '操作黑名单' : 'Blocklist'}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{isZh
? '禁止 AI 操作敏感元素,如删除按钮、支付按钮等。'
: 'Prevent AI from interacting with sensitive elements like delete buttons, payment buttons, etc.'}
</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
{isZh ? '操作白名单' : 'Allowlist'}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{isZh
? '明确定义 AI 可以操作的元素范围。'
: 'Explicitly define which elements AI can interact with.'}
</p>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">
{isZh ? 'Instruction 安全约束' : 'Instruction Safety Constraints'}
</h2>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
{isZh ? '高危操作控制' : 'High-Risk Operation Control'}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-3">
{isZh
? '在 AI 指令中明确列举高危操作,通过两种策略进行控制:'
: 'Define high-risk operations in AI instructions and control them through two strategies:'}
</p>
<div className="space-y-2">
<div className="pl-3 border-l-2 border-red-400">
<p className="font-medium text-red-700 dark:text-red-300">
{isZh ? '完全禁止操作' : 'Completely Forbidden'}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{isZh
? '对极高风险操作明确禁止执行'
: 'Explicitly prohibit execution of extremely high-risk operations'}
</p>
</div>
<div className="pl-3 border-l-2 border-orange-400">
<p className="font-medium text-orange-700 dark:text-orange-300">
{isZh ? '需用户确认操作' : 'Requires User Confirmation'}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{isZh
? '对中等风险操作要求用户明确同意'
: 'Require explicit user consent for medium-risk operations'}
</p>
</div>
</div>
</div>
</section>
</div>
</div>
)
}
@@ -74,19 +74,6 @@ const pageAgentTool = {
</div>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white"></h3>
<CodeEditor code={`// @TODO`} language="javascript" />
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white"></h3>
<CodeEditor code={`// @TODO`} language="javascript" />
</div>
</div>
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-100 mb-2">
@@ -97,19 +84,6 @@ const pageAgentTool = {
<li> </li>
</ul>
</div>
<div className="bg-linear-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">🎉 </h3>
<p className="mb-3 text-gray-700 dark:text-gray-300">
Agent
</p>
<a
href="/docs/integration/configuration"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200"
>
</a>
</div>
</div>
)
}
@@ -6,13 +6,13 @@ export default function Overview() {
return (
<article>
{/* 头图 */}
<figure className="mb-8 rounded-xl overflow-hidden">
{/* <figure className="mb-8 rounded-xl overflow-hidden">
<img
src="https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg"
src="https://img.alicdn.com/imgextra/i3/O1CN014xLeic1gnF2iWdzjJ_!!6000000004186-49-tps-1280-353.webp"
alt="page-agent"
className="w-full h-64 object-cover"
/>
</figure>
</figure> */}
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4">{t('overview.title')}</h1>
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
import { CDN_CN_URL, CDN_URL } from '@/constants'
export default function QuickStart() {
const { t } = useTranslation('docs')
@@ -23,8 +24,9 @@ export default function QuickStart() {
<div>
<p className="text-sm font-medium mb-2">{t('quick_start.step1_cdn')}</p>
<CodeEditor
code={`// 仅供测试使用
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>`}
code={`// CDN: \t${CDN_URL}
// Mirror: \t${CDN_CN_URL}
<script src="${CDN_URL}" crossorigin="true" type="text/javascript"></script>`}
language="html"
/>
</div>
@@ -53,7 +55,7 @@ const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({
modelName: DEMO_MODEL,
model: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'zh-CN'
+27 -27
View File
@@ -1,23 +1,23 @@
import { Route, Switch } from 'wouter'
import DocsLayout from './components/DocsLayout'
import Header from './components/Header'
import HomePage from './pages/Home'
import DocsLayout from './pages/docs/Layout'
// Features pages
import CustomTools from './docs/features/custom-tools/page'
import DataMasking from './docs/features/data-masking/page'
import KnowledgeInjection from './docs/features/knowledge-injection/page'
import ModelIntegration from './docs/features/model-integration/page'
import SecurityPermissions from './docs/features/security-permissions/page'
import BestPractices from './docs/integration/best-practices/page'
import Instructions from './pages/docs/features/custom-instructions/page'
import CustomTools from './pages/docs/features/custom-tools/page'
import DataMasking from './pages/docs/features/data-masking/page'
import Models from './pages/docs/features/models/page'
// Integration pages
import CdnSetup from './docs/integration/cdn-setup/page'
import Configuration from './docs/integration/configuration/page'
import ThirdPartyAgent from './docs/integration/third-party-agent/page'
import Limitations from './docs/introduction/limitations/page'
import BestPractices from './pages/docs/integration/best-practices/page'
import CdnSetup from './pages/docs/integration/cdn-setup/page'
import Configuration from './pages/docs/integration/configuration/page'
import SecurityPermissions from './pages/docs/integration/security-permissions/page'
import ThirdPartyAgent from './pages/docs/integration/third-party-agent/page'
// Introduction pages
import Overview from './docs/introduction/overview/page'
import QuickStart from './docs/introduction/quick-start/page'
import HomePage from './page'
import Limitations from './pages/docs/introduction/limitations/page'
import Overview from './pages/docs/introduction/overview/page'
import QuickStart from './pages/docs/introduction/quick-start/page'
export default function Router() {
return (
@@ -53,15 +53,6 @@ export default function Router() {
</div>
</Route>
<Route path="/docs/features/security-permissions">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<SecurityPermissions />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/custom-tools">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
@@ -80,20 +71,20 @@ export default function Router() {
</div>
</Route>
<Route path="/docs/features/knowledge-injection">
<Route path="/docs/features/custom-instructions">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<KnowledgeInjection />
<Instructions />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/model-integration">
<Route path="/docs/features/models">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<ModelIntegration />
<Models />
</DocsLayout>
</div>
</Route>
@@ -107,6 +98,15 @@ export default function Router() {
</div>
</Route>
<Route path="/docs/integration/security-permissions">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<SecurityPermissions />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/configuration">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />

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