Compare commits

...

53 Commits

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

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



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

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

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

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

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

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

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

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

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

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

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

Updates `i18next` from 25.6.0 to 25.6.2
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.6.0...v25.6.2)

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 19:50:43 +00:00
Simon 0f697c21bd chore: update lock 2025-10-24 21:47:25 +08:00
Simon e0d6d0d51b docs: plan 2025-10-24 17:50:57 +08:00
Simon d92ba7d882 fix: update aria-label for theme switcher button 2025-10-24 17:21:39 +08:00
Simon 3e8e584065 fix: demo video cn 2025-10-24 17:06:32 +08:00
Simon 9bdd565feb fix: demo video cn 2025-10-24 17:05:32 +08:00
Simon b0a31e777b fix: demo video
Updated demo video source in README.
2025-10-24 17:03:56 +08:00
Simon 78a54d0d3d docs: 演示视频 2025-10-24 16:56:26 +08:00
Simon 7f04f3564a fix: gemini response format 2025-10-24 16:54:05 +08:00
Simon 22a25b17f5 Merge pull request #36 from gaomeng1900/main
docs update
2025-10-24 01:21:01 +08:00
Simon 8a63c1b716 feat: Enhance code highlighting 2025-10-24 00:50:51 +08:00
Simon ab710bcaf5 stye: optimize mobile header with icon navigation 2025-10-24 00:07:15 +08:00
Simon df449166d5 docs: wording 2025-10-23 21:20:48 +08:00
Simon da822c5fe1 docs: wording 2025-10-23 20:48:57 +08:00
Simon 78412323a7 docs: wording 2025-10-23 20:45:03 +08:00
Simon ad06cd1869 docs: wording 2025-10-23 20:42:27 +08:00
Simon 86682c3a1e docs: wording 2025-10-23 20:41:12 +08:00
Simon fee3bbdf85 docs: Authentication suggestion 2025-10-23 20:25:55 +08:00
Simon 4d3c878809 feat: add experimentalScriptExecutionTool 2025-10-23 19:59:17 +08:00
Simon e704398c43 feat: add Google Analytics tracking script 2025-10-23 16:16:20 +08:00
Simon 56d20904b9 docs: demo 2025-10-23 16:07:59 +08:00
Simon bdddcb860e chore: add Open Graph meta tags for better sharing 2025-10-23 15:43:12 +08:00
Simon e6e8ebf4e3 docs: wording 2025-10-23 15:37:22 +08:00
Simon 9b1131ce13 docs: wording 2025-10-23 15:32:56 +08:00
Simon 462d76dbb6 docs: update CDN links to version 0.0.4 2025-10-23 15:27:31 +08:00
Simon 3791d1e157 chore: wording 2025-10-23 15:17:05 +08:00
Simon 2e3355f52c fix: update language setting to use i18n configuration 2025-10-22 23:07:23 +08:00
Simon b85a82785f chore: bump version to 0.0.4 2025-10-22 23:02:49 +08:00
Simon 2314d28e60 fix: mobile panel styles 2025-10-22 23:02:17 +08:00
Simon 58475c63da feat: enhance mobile navigation and update i18n labels 2025-10-22 22:58:52 +08:00
Simon 55f07d971a docs: update i18n status in ROADMAP 2025-10-22 22:41:48 +08:00
Simon cd84269427 feat: i18n for website 2025-10-22 22:35:25 +08:00
Simon fedeb57f48 chore: add homepage 2025-10-22 19:04:19 +08:00
Simon 386167e99b chore: wording 2025-10-22 17:48:06 +08:00
Simon 190d264bda docs: update ROADMAP 2025-10-22 17:18:39 +08:00
Simon 7af3b580d5 docs: updating ROADMAP 2025-10-22 17:16:15 +08:00
Simon 4af927f218 fix: action 2025-10-22 16:59:10 +08:00
117 changed files with 4221 additions and 2305 deletions
+22 -28
View File
@@ -6,32 +6,11 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Please follow our community communication guidelines when filing issues. Thanks for your interest in improving the project! Before submitting, please read our guidelines.
- Be polite and respectful. 感谢您对改进项目的兴趣!提交前请阅读我们的指南。
- Prefer constructive, actionable feedback over venting.
- Include clear reproduction steps and environment details.
- Consider contributing a fix if possible — PRs are welcome.
提交 Issue 时请遵守社区沟通规范: - [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
- 保持礼貌与尊重。 - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
- 提供建设性、可行动的反馈,避免情绪化表达。
- 请包含清晰的复现步骤与环境信息。
- 如果可以,欢迎直接提交修复 PR。
See: CODE_OF_CONDUCT.md / CODE_OF_CONDUCT_zh.md, CONTRIBUTING.md
- type: checkboxes
id: community
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true
- label: I will share constructive, actionable details. / 我会提供建设性、可行动的细节。
required: true
- label: I have read the Code of Conduct. / 我已阅读行为准则。
required: true
- type: textarea - type: textarea
id: description id: description
@@ -48,7 +27,7 @@ body:
render: typescript render: typescript
placeholder: Minimal reproduction code placeholder: Minimal reproduction code
validations: validations:
required: true required: false
- type: input - type: input
id: browser id: browser
@@ -56,7 +35,7 @@ body:
label: Browser label: Browser
placeholder: 'Chrome 120, Firefox 119, etc.' placeholder: 'Chrome 120, Firefox 119, etc.'
validations: validations:
required: true required: false
- type: input - type: input
id: version id: version
@@ -64,4 +43,19 @@ body:
label: version label: version
placeholder: '0.0.0' placeholder: '0.0.0'
validations: validations:
required: true required: false
- type: checkboxes
id: community
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
required: true
- label: I have read the Code of Conduct. / 我已阅读行为准则。
required: true
validations:
required: true
-3
View File
@@ -12,6 +12,3 @@ contact_links:
- name: Code of Conduct / 行为准则 - name: Code of Conduct / 行为准则
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md
about: Community expectations and standards. 社区行为期望与标准。 about: Community expectations and standards. 社区行为期望与标准。
- name: 行为准则(中文)
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT_zh.md
about: 中文版社区行为准则。
+23 -34
View File
@@ -6,19 +6,30 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Please follow our community communication guidelines when proposing features. Thanks for your interest in improving the project! Before submitting, please read our guidelines.
- Be polite and respectful. 感谢您对改进项目的兴趣!提交前请阅读我们的指南。
- Explain the problem first, then propose solutions.
- Prefer constructive, actionable suggestions.
- Contributions are encouraged — consider opening a PR.
提交功能建议时请遵守社区沟通规范: - [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
- 保持礼貌与尊重。 - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
- 先阐述问题,再提出解决方案。
- 提供建设性、可行动的建议。
- 鼓励代码贡献,欢迎直接提交 PR。
See: CODE_OF_CONDUCT.md / CODE_OF_CONDUCT_zh.md, CONTRIBUTING.md - type: textarea
id: description
attributes:
label: Feature Description / 功能描述
description: Describe the problem, solution, and any API changes. / 描述问题、解决方案以及 API 变更。
placeholder: |
**Problem**:
What problem does this solve?
**Solution**:
How should this work?
**Proposed API**:
```typescript
// code here
```
validations:
required: true
- type: checkboxes - type: checkboxes
id: community id: community
@@ -32,27 +43,5 @@ body:
required: true required: true
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。 - label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
required: true required: true
- type: textarea
id: problem
attributes:
label: Problem
placeholder: What problem does this solve?
validations: validations:
required: true required: true
- type: textarea
id: solution
attributes:
label: Solution
placeholder: How should this work?
validations:
required: true
- type: textarea
id: api
attributes:
label: Proposed API
render: typescript
validations:
required: false
+5 -8
View File
@@ -5,9 +5,12 @@ Brief description of changes.
## Type ## Type
- [ ] Bug fix - [ ] Bug fix
- [ ] New feature - [ ] Feature / Improvement
- [ ] Breaking change - [ ] Refactor
- [ ] Documentation - [ ] Documentation
- [ ] Website
- [ ] Demo / Testing
- [ ] Breaking change
## Testing ## Testing
@@ -19,10 +22,4 @@ Closes #(issue)
## Requirements / 要求 ## Requirements / 要求
- [ ] I will be polite and respectful. / 我会保持礼貌与尊重。
- [ ] My comments and replies are constructive and actionable. / 我的评论与回复具有建设性。
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。 - [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
## Contributing / 贡献
Constructive suggestions and code contributions are encouraged. If this PR originated from a discussion or issue, please link it above. 欢迎建设性意见与代码贡献;如源自讨论或 Issue,请在上方关联链接。
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v4 uses: actions/upload-pages-artifact@v4
with: with:
path: './dist' path: './packages/website/dist'
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
+5 -2
View File
@@ -19,9 +19,12 @@ jobs:
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 20 node-version: 20
cache: 'npm'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -29,4 +32,4 @@ jobs:
run: npm run build:lib run: npm run build:lib
- name: Publish to npm - name: Publish to npm
run: npm publish run: npm publish --workspace=page-agent
+6 -1
View File
@@ -26,4 +26,9 @@ dist-ssr
# #
.env .env
# Copied files for npm publish (generated by prepublishOnly)
packages/page-agent/README.md
packages/page-agent/README-zh.md
packages/page-agent/LICENSE
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"editor.fontLigatures": true, "editor.fontLigatures": true,
"cSpell.words": ["HITL", "opensource", "retryable"], "cSpell.words": ["HITL", "innerhtml", "opensource", "retryable", "wouter"],
"markdownlint.config": { "markdownlint.config": {
// "comment": "Relaxed rules", // "comment": "Relaxed rules",
"default": true, "default": true,
+53 -32
View File
@@ -2,36 +2,56 @@
## Project Overview ## Project Overview
This is a dual-architecture project with **two separate parts**: This is a **monorepo** with npm workspaces containing **two main packages**:
1. **Core Library** (`src/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation 1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
2. **Demo&docs Website** (`pages/`) - React documentation and landing page 2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
## Development Commands ## Development Commands
### Core Commands ### Core Commands
```bash ```bash
npm start # Start React website development server npm start # Start website dev server
npm run build # Build both library AND website npm run dev # Same as start
npm run build:lib # Build pure JS library only (src/ → dist/lib/) npm run build # Build all packages
npm run build:lib:watch # Library development with auto-rebuild npm run build:lib # Build page-agent library only
npm run lint # ESLint with TypeScript strict rules 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 & Critical Patterns
### Dual Build System ### Monorepo Structure
- **Website build**: `vite.config.js` → React SPA with hash routing → `dist/` ```
- **Library build**: `vite.lib.config.js` → UMD/ES modules → `dist/lib/` packages/
- **Entry points**: `src/entry.ts` (library), `pages/main.tsx` (website) ├── page-agent/ # npm: "page-agent"
│ ├── src/ # Core library source
│ ├── vite.config.js # Library build (ES + UMD)
│ └── package.json
└── website/ # npm: "@page-agent/website" (private)
├── src/ # Website source (formerly pages/)
├── index.html
├── vite.config.js # Website build
└── package.json
```
### Module Boundaries (Critical) ### Module Boundaries (Critical)
- **Core library** (`src/`): NEVER import from `pages/` - must remain pure JavaScript - **Core library** (`packages/page-agent/`): NEVER import from website - must remain pure JavaScript
- **Website** (`pages/`): CAN import from `src/` via `@/` alias for demos - **Website** (`packages/website/`): CAN import from `page-agent` for demos. Alias `@/` `website/src/`
- **Import aliases**: `@/``src/`, `@pages/``pages/`
### DOM Pipeline ### DOM Pipeline
@@ -73,7 +93,7 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
## File Organization ## File Organization
### Core Library (`src/`) ### Core Library (`packages/page-agent/src/`)
- `entry.ts` - CDN/UMD entry point with auto-initialization - `entry.ts` - CDN/UMD entry point with auto-initialization
- `PageAgent.ts` - **Main AI agent class** orchestrating DOM operations - `PageAgent.ts` - **Main AI agent class** orchestrating DOM operations
@@ -85,7 +105,7 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
- `dom/` - HTML serialization and page analysis utilities - `dom/` - HTML serialization and page analysis utilities
- `config/` - Configuration constants and settings - `config/` - Configuration constants and settings
### Website (`pages/`) ### Website (`packages/website/src/`)
- `main.tsx` - Site entry with hash routing setup - `main.tsx` - Site entry with hash routing setup
- `router.tsx` - **Manual route definitions** (requires explicit registration) - `router.tsx` - **Manual route definitions** (requires explicit registration)
@@ -97,21 +117,20 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
### New Documentation Page ### New Documentation Page
1. Create `pages/docs/<section>/<slug>/page.tsx` 1. Create `packages/website/src/docs/<section>/<slug>/page.tsx`
2. Add route to `pages/router.tsx` with `<Header /> + <DocsLayout>` wrapper 2. Add route to `packages/website/src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
3. Add navigation item to `DocsLayout.tsx` 3. Add navigation item to `DocsLayout.tsx`
### New Agent Tool ### New Agent Tool
1. Implement under `src/tools/` 1. Implement under `packages/page-agent/src/tools/`
2. Export via `src/tools/index.ts` 2. Export via `packages/page-agent/src/tools/index.ts`
3. Wire into `PageAgent.ts` if needed 3. Wire into `PageAgent.ts` if needed
### New UI Component ### New UI Component
1. Create in `src/ui/` with colocated CSS modules 1. Create in `packages/page-agent/src/ui/` with colocated CSS modules
2. Use event bus for PageAgent communication 2. Use event bus for PageAgent communication
3. Test via `pages/test-pages/`
## Code Standards ## Code Standards
@@ -136,26 +155,28 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
## Critical Files to Understand ## Critical Files to Understand
- `pages/router.tsx` - Central routing definition (manual registration required) - `packages/page-agent/src/PageAgent.ts` - Core AI agent class with DOM manipulation
- `pages/components/DocsLayout.tsx` - Navigation structure - `packages/page-agent/src/dom/dom_tree/index.js` - DOM extraction engine
- `src/PageAgent.ts` - Core AI agent class with DOM manipulation - `packages/page-agent/src/utils/bus.ts` - Type-safe event bus system
- `src/dom/dom_tree/index.js` - DOM extraction engine - `packages/page-agent/src/entry.ts` - Library entry point for CDN usage
- `src/utils/bus.ts` - Type-safe event bus system - `packages/page-agent/vite.config.js` - Library build configuration
- `src/entry.ts` - Library entry point for CDN usage
- `vite.config.js` / `vite.lib.config.js` - Dual build configuration - `packages/website/src/router.tsx` - Central routing definition (manual registration required)
- `packages/website/src/components/DocsLayout.tsx` - Navigation structure
- `packages/website/vite.config.js` - Website build configuration
## Debugging Common Issues ## Debugging Common Issues
### Blank Documentation Pages ### Blank Documentation Pages
1. Verify route exists in `pages/router.tsx` 1. Verify route exists in `packages/website/src/router.tsx`
2. Check component import path 2. Check component import path
3. Verify CSS isn't hiding content (check dark mode classes) 3. Verify CSS isn't hiding content (check dark mode classes)
4. Test with minimal component first 4. Test with minimal component first
### Library Integration Issues ### Library Integration Issues
1. Check `dist/lib/page-agent.umd.js` builds correctly 1. Check `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly
2. Test CDN injection with query params 2. Test CDN injection with query params
3. Verify event bus communications are properly typed 3. Verify event bus communications are properly typed
4. Use `pages/test-pages/` for isolated testing 4. Use `packages/website/src/test-pages/` for isolated testing
+5 -3
View File
@@ -22,10 +22,12 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
### Project Structure ### Project Structure
This project has **two separate parts**: This is a **monorepo** with npm workspaces containing **two main packages**:
- **Core Library** (`src/`) - Pure JavaScript AI agent library 1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
- **Documentation Website** (`pages/`) - React web app for landing page and docs 2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
We use a simplified monorepo solution with native npm-workspace. No fancy tooling. Hoisting is required.
## 🤝 How to Contribute ## 🤝 How to Contribute
+5 -5
View File
@@ -4,13 +4,13 @@
[![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent) [![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
**让你的网页支持 AI 自动化。** 纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
运行在页面内的 UI agent. 使用自然语言操作 Web 应用。
🌐 [English](./README.md) | **中文** 🌐 [English](./README.md) | **中文**
👉 [🚀 **Demo**](https://alibaba.github.io/page-agent/) | [📖 **Documentation**](https://alibaba.github.io/page-agent/#/docs/introduction/overview) 👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
<video id="demo-video" src="https://github.com/user-attachments/assets/141bbb01-8022-4d1f-919d-9efc9a1dc1cf" width="640" crossorigin muted autoplay loop></video>
--- ---
@@ -33,7 +33,7 @@
```html ```html
<!-- 临时 CDN URL. 未来会变更 --> <!-- 临时 CDN URL. 未来会变更 -->
<script <script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js" src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
crossorigin="true" crossorigin="true"
type="text/javascript" type="text/javascript"
></script> ></script>
+6 -6
View File
@@ -4,19 +4,19 @@
[![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent) [![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
**Transform your webpage into an AI-powered application with a single script tag.** The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
An in-page UI agent in javascript. Control web interfaces with natural language.
🌐 **English** | [中文](./README-zh.md) 🌐 **English** | [中文](./README-zh.md)
👉 [🚀 **Demo**](https://alibaba.github.io/page-agent/) | [📖 **Documentation**](https://alibaba.github.io/page-agent/#/docs/introduction/overview) 👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
<video id="demo-video" src="https://github.com/user-attachments/assets/de8d1964-8bde-494f-a52f-2975469557a5" width="640" crossorigin muted autoplay loop></video>
--- ---
## ✨ Features ## ✨ Features
- **🎯 Easy Integration** - **🎯 Easy Integration** - Transform your webpage into an agent with a single script tag.
- **🔐 Client-Side Processing** - **🔐 Client-Side Processing**
- **🧠 DOM Extraction** - **🧠 DOM Extraction**
- **💬 Natural Language Interface** - **💬 Natural Language Interface**
@@ -33,7 +33,7 @@ An in-page UI agent in javascript. Control web interfaces with natural language.
```html ```html
<!-- temporary CDN URL. May change in the future --> <!-- temporary CDN URL. May change in the future -->
<script <script
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js" src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
crossorigin="true" crossorigin="true"
type="text/javascript" type="text/javascript"
></script> ></script>
+28 -9
View File
@@ -4,32 +4,49 @@ The development progress and future plans for PageAgent.
## 🚀 Current Works ## 🚀 Current Works
- [x] **MVP** - Core functionality implemented - [x] **MVP**
- [x] **SPA interaction** - Single Page Application support - Core functionality implemented.
- [x] **SPA interaction**
- [x] **Reasoning and (short) memory** - [x] **Reasoning and (short) memory**
- [x] **Multi model provider integration and testing** - [x] **Multi model provider integration and testing**
- [x] **UI with HITL** - Human-in-the-loop user interface - [x] **UI with HITL**
- Human-in-the-loop user interface. Agent can ask user questions.
- [x] **Landing and doc pages** - [x] **Landing and doc pages**
- [x] **Remove ai-sdk** - Only one function is being used - [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** - [x] **Robust LLM output**
- Auto-fix incomplete output format of DeepSeek and QWen.
- [x] **Working homepage with live LLM API** - [x] **Working homepage with live LLM API**
- [x] **~~free~~ CDN** - [x] **~~free~~ CDN**
- [x] **Free evaluation plan?** - [x] **Free evaluation plan**
- [x] **Custom actions and HITL** - [x] **Custom actions and HITL**
- [ ] **Hooks and Events** - [ ] **Hooks and Events**
- [x] **lifecycle hooks** - [x] **lifecycle hooks**
- [ ] **lifecycle events** - [ ] **lifecycle events**
- [ ] **❗Pause and intervene** - [ ] **❗Pause and intervene**
- Current pause does not allow user intervention.
- [ ] **❗Hijack `page_open/page_change/page_unload` behavior** - [ ] **❗Hijack `page_open/page_change/page_unload` behavior**
- [ ] **Custom knowledge base and instructions** - [ ] **Custom knowledge base and instructions**
- [ ] **Black/white-list safeguard** - [ ] **Black/white-list safeguard**
- [ ] **Data-masking** - [ ] **Data-masking**
- [ ] **Improve Memory**
- Current phrasing can cause logic-loop for some models.
- Test adding `Action` to memory.
- [ ] **Tools for more complex tasks**
- todo list
- file sys
- [ ] **Optimize for popular UI frameworks** - [ ] **Optimize for popular UI frameworks**
- [x] **i18n of the website**
- [x] Chinese version
- [x] English version
- [ ] **Testing suits** - [ ] **Testing suits**
- [ ] **Support custom llm fetch** - [ ] **Support custom llm fetch**
- [ ] **Refactor: Separate Agent and Page-Controller** - Agent should run w/o dom - [ ] **Refactor: Separate `Agent` and `Page-Controller`**
- Agent should be able to run w/o dom.
- Actions should be able to be injected through iframe.
♻️ Following browser-use's update and contribute back ♻️ Following browser-use's update and contribute back.
## 📋 Pending Features ## 📋 Pending Features
@@ -39,4 +56,6 @@ The development progress and future plans for PageAgent.
## 🤔 To Be Decided ## 🤔 To Be Decided
- [ ] **Cross-origin multi-page?** - Tricky - [ ] **Cross-origin multi-page?**
- Tricky
- Need some kind of "memory rally"
+2 -2
View File
@@ -8,7 +8,7 @@ import globals from 'globals'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
export default defineConfig([ export default defineConfig([
globalIgnores(['dist', 'test-pages']), globalIgnores(['**/dist', '**/test-pages', '**/node_modules']),
{ {
plugins: { plugins: {
'react-hooks': reactHooks, 'react-hooks': reactHooks,
@@ -37,7 +37,7 @@ export default defineConfig([
], ],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: ['./tsconfig.json'], project: ['./packages/*/tsconfig.json'],
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
ecmaVersion: 2020, ecmaVersion: 2020,
-32
View File
@@ -1,32 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="https://img.alicdn.com/imgextra/i3/O1CN01uo9VaC20mamWNM2Zn_!!6000000006892-55-tps-64-64.svg"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PageAgent - 网页内的交互助手</title>
<meta
name="description"
content="PageAgent.js:一行 CDN 引入,为任何网站添加智能 UI Agent。现代网页 AI 自动化,极简集成,开发者专用。"
/>
<meta
name="keywords"
content="PageAgent, AI Agent, Web Automation, UI 自动化, 前端, CDN, JavaScript, React, Vite"
/>
<meta name="theme-color" content="#58c0fc" />
<meta name="color-scheme" content="light dark" />
<meta name="author" content="PageAgent.js Team" />
<meta property="og:title" content="PageAgent.js" />
<meta property="og:description" content="一行 CDN 引入,为任何网站添加智能 UI Agent。" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="zh_CN" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./pages/main.tsx"></script>
</body>
</html>
+937 -940
View File
File diff suppressed because it is too large Load Diff
+19 -57
View File
@@ -1,89 +1,51 @@
{ {
"name": "page-agent", "name": "root",
"private": false, "private": true,
"version": "0.0.3", "version": "0.0.6",
"type": "module", "type": "module",
"main": "./dist/lib/page-agent.js", "workspaces": [
"module": "./dist/lib/page-agent.js", "packages/*"
"types": "./dist/lib/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageAgent.d.ts",
"import": "./dist/lib/page-agent.js",
"default": "./dist/lib/page-agent.js"
}
},
"files": [
"dist/lib/",
"README.md",
"LICENSE",
"NOTICE"
],
"description": "AI-powered UI agent for web applications - add intelligent automation to any webpage with a single script tag",
"keywords": [
"ai",
"automation",
"ui-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"intelligent-ui"
], ],
"description": "AI-powered UI agent for web applications",
"author": "Simon<gaomeng1900>", "author": "Simon<gaomeng1900>",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/alibaba/page-agent.git" "url": "https://github.com/alibaba/page-agent.git"
}, },
"homepage": "https://alibaba.github.io/page-agent/",
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0",
"npm": ">=10.0.0"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "npm run dev --workspace=@page-agent/website",
"start": "vite", "start": "npm run dev --workspace=@page-agent/website",
"build": "tsc -b && vite build && npm run build:lib && npm run build:umd", "build": "npm run build --workspaces --if-present",
"build:lib": "MODE=lib vite build", "build:lib": "npm run build --workspace=page-agent",
"build:lib:watch": "MODE=lib vite build --watch", "version": "node scripts/sync-version.js",
"build:umd": "MODE=umd vite build",
"lint": "eslint .", "lint": "eslint .",
"prepare": "husky" "prepare": "husky"
}, },
"dependencies": {
"ai-motion": "^0.4.7",
"chalk": "^5.6.2",
"zod": "^4.1.12"
},
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.1.0", "@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0", "@commitlint/config-conventional": "^20.0.0",
"@eslint/js": "^9.37.0", "@eslint/js": "^9.37.0",
"@microsoft/api-extractor": "^7.53.1",
"@tailwindcss/vite": "^4.1.14",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"eslint": "^9.37.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.0.6", "eslint-plugin-react-dom": "^2.3.9",
"eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.23", "eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-x": "^2.0.6", "eslint-plugin-react-x": "^2.3.9",
"globals": "^16.4.0", "globals": "^16.4.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.2.4", "lint-staged": "^16.2.4",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.14",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.46.0", "typescript-eslint": "^8.46.0",
"unplugin-dts": "^1.0.0-beta.6", "vite": "^7.1.9"
"vite": "^7.1.9",
"vite-plugin-css-injected-by-js": "^3.5.2",
"wouter": "^3.7.1"
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [ "*.{js,ts,cjs,cts,mjs,mts}": [
-5
View File
@@ -11,15 +11,10 @@ declare module '*.md?raw' {
export default content export default content
} }
/**
* for local dev and umd demo
*/
declare global { declare global {
interface Window { interface Window {
pageAgent?: PageAgent pageAgent?: PageAgent
PageAgent: typeof PageAgent PageAgent: typeof PageAgent
__PAGE_AGENT_IDS__: string[] __PAGE_AGENT_IDS__: string[]
} }
} }
+59
View File
@@ -0,0 +1,59 @@
{
"name": "page-agent",
"private": false,
"version": "0.0.6",
"type": "module",
"main": "./dist/lib/page-agent.js",
"module": "./dist/lib/page-agent.js",
"types": "./dist/lib/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageAgent.d.ts",
"import": "./dist/lib/page-agent.js",
"default": "./dist/lib/page-agent.js"
}
},
"files": [
"dist/",
"README.md",
"LICENSE"
],
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
"keywords": [
"ai",
"automation",
"ui-agent",
"GUI-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"web-automation",
"GUI-simulation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "MODE=lib vite build && MODE=umd vite build",
"build:lib": "MODE=lib vite build",
"build:umd": "MODE=umd vite build",
"build:watch": "MODE=lib vite build --watch",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"ai-motion": "^0.4.7",
"chalk": "^5.6.2",
"zod": "^4.1.12"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.55.1",
"unplugin-dts": "^1.0.0-beta.6",
"vite-plugin-css-injected-by-js": "^3.5.2"
}
}
@@ -125,6 +125,10 @@ export class PageAgent extends EventTarget {
} }
} }
if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript')
}
patchReact(this) patchReact(this)
window.addEventListener('beforeunload', (e) => { window.addEventListener('beforeunload', (e) => {
@@ -1,8 +1,7 @@
import type { AgentHistory, ExecutionResult, PageAgent } from '@/PageAgent' import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent'
import type { DomConfig } from '@/dom' import type { DomConfig } from '../dom'
import type { SupportedLanguage } from '@/i18n' import type { SupportedLanguage } from '../i18n'
import type { PageAgentTool } from '@/tools' import type { PageAgentTool } from '../tools'
import { import {
DEFAULT_API_KEY, DEFAULT_API_KEY,
DEFAULT_BASE_URL, DEFAULT_BASE_URL,
@@ -72,6 +71,14 @@ export interface AgentConfig {
// page behavior hooks // page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/** /**
* TODO: @unimplemented * TODO: @unimplemented
* hook when action causes a new page to be opened * hook when action causes a new page to be opened
@@ -1,11 +1,11 @@
import { VIEWPORT_EXPANSION } from '@/config/constants' import { VIEWPORT_EXPANSION } from '../config/constants'
import domTree from '@/dom/dom_tree/index' import domTree from './dom_tree/index'
import { import {
ElementDomNode, ElementDomNode,
FlatDomTree, FlatDomTree,
InteractiveElementDomNode, InteractiveElementDomNode,
TextDomNode, TextDomNode,
} from '@/dom/dom_tree/type' } from './dom_tree/type'
export interface DomConfig { export interface DomConfig {
interactiveBlacklist?: (Element | (() => Element))[] interactiveBlacklist?: (Element | (() => Element))[]
@@ -1,8 +1,7 @@
/** /**
* OpenAI Client implementation * OpenAI Client implementation
*/ */
import type { MacroToolInput } from '@/PageAgent' import type { MacroToolInput } from '../PageAgent'
import { InvokeError, InvokeErrorType } from './errors' import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types' import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types'
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils' import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
@@ -31,10 +31,9 @@
* - 使 tool call * - 使 tool call
* - tool 使 tool call * - tool 使 tool call
*/ */
import type { LLMConfig } from '@/config' import type { LLMConfig } from '../config'
import { parseLLMConfig } from '@/config' import { parseLLMConfig } from '../config'
import { EventBus, getEventBus } from '@/utils/bus' import { EventBus, getEventBus } from '../utils/bus'
import { OpenAIClient } from './OpenAILenientClient' import { OpenAIClient } from './OpenAILenientClient'
import { InvokeError } from './errors' import { InvokeError } from './errors'
import type { InvokeResult, LLMClient, Message, Tool } from './types' import type { InvokeResult, LLMClient, Message, Tool } from './types'
@@ -4,8 +4,7 @@
import chalk from 'chalk' import chalk from 'chalk'
import { z } from 'zod' import { z } from 'zod'
import type { MacroToolInput } from '@/PageAgent' import type { MacroToolInput } from '../PageAgent'
import { InvokeError, InvokeErrorType } from './errors' import { InvokeError, InvokeErrorType } from './errors'
import type { Tool } from './types' import type { Tool } from './types'
@@ -42,6 +41,7 @@ export function lenientParseMacroToolCall(
// check // check
switch (choice.finish_reason) { switch (choice.finish_reason) {
case 'tool_calls': case 'tool_calls':
case 'function_call': // gemini
case 'stop': // will try a robust parse case 'stop': // will try a robust parse
// ✅ Normal // ✅ Normal
break break
@@ -1,4 +1,4 @@
import type { PageAgent } from '@/PageAgent' import type { PageAgent } from '../PageAgent'
const clearFunctions = [] as (() => void)[] const clearFunctions = [] as (() => void)[]
@@ -1,4 +1,4 @@
import type { PageAgent } from '@/PageAgent' import type { PageAgent } from '../PageAgent'
// Find common React root elements and add data-page-agent-not-interactive attribute // Find common React root elements and add data-page-agent-not-interactive attribute
export function patchReact(pageAgent: PageAgent) { export function patchReact(pageAgent: PageAgent) {
@@ -4,8 +4,7 @@
*/ */
import zod, { type z } from 'zod' import zod, { type z } from 'zod'
import type { PageAgent } from '@/PageAgent' import type { PageAgent } from '../PageAgent'
import { import {
clickElement, clickElement,
getElementByIndex, getElementByIndex,
@@ -215,6 +214,27 @@ tools.set(
}) })
) )
tools.set(
'execute_javascript',
tool({
description:
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
inputSchema: zod.object({
script: zod.string(),
}),
execute: async function (this: PageAgent, input) {
try {
// Wrap script in async function to support await
const asyncFunction = eval(`(async () => { ${input.script} })`)
const result = await asyncFunction()
return `✅ Executed JavaScript. Result: ${result}` + (await getSystemInfo())
} catch (error) {
return `❌ Error executing JavaScript: ${error}` + (await getSystemInfo())
}
},
})
)
// @todo get_dropdown_options // @todo get_dropdown_options
// @todo select_dropdown_option // @todo select_dropdown_option
// @todo send_keys // @todo send_keys
@@ -33,8 +33,7 @@
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 480px) { @media (max-width: 480px) {
width: calc(100vw - 40px); width: calc(100vw - 40px);
left: 20px; --width: calc(100vw - 40px);
transform: none;
} }
.background { .background {
@@ -1,8 +1,7 @@
import type { PageAgent } from '@/PageAgent' import type { PageAgent } from '../PageAgent'
import type { I18n } from '@/i18n' import type { I18n } from '../i18n'
import { truncate } from '@/utils' import { truncate } from '../utils'
import type { EventBus } from '@/utils/bus' import type { EventBus } from '../utils/bus'
import { type Step, UIState } from './UIState' import { type Step, UIState } from './UIState'
import styles from './Panel.module.css' import styles from './Panel.module.css'
@@ -1,6 +1,6 @@
import { Motion } from 'ai-motion' import { Motion } from 'ai-motion'
import { isPageDark } from '@/utils/checkDarkMode' import { isPageDark } from '../utils/checkDarkMode'
import styles from './SimulatorMask.module.css' import styles from './SimulatorMask.module.css'
import cursorStyles from './cursor.module.css' import cursorStyles from './cursor.module.css'
@@ -1,7 +1,7 @@
/** /**
* Type-safe event bus for decoupling PageAgent and Panel * Type-safe event bus for decoupling PageAgent and Panel
*/ */
import type { Step } from '@/ui/UIState' import type { Step } from '../ui/UIState'
/** /**
* Event mapping definitions * Event mapping definitions
@@ -69,10 +69,7 @@ class EventBus extends EventTarget {
/** /**
* Listen to built-in events * Listen to built-in events
*/ */
on<T extends keyof PageAgentEventMap>( on<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
event: T,
handler: EventHandler<T & keyof PageAgentEventMap>
): void {
const wrappedHandler = (e: Event) => { const wrappedHandler = (e: Event) => {
const customEvent = e as CustomEvent const customEvent = e as CustomEvent
const params = customEvent.detail?.[0] const params = customEvent.detail?.[0]
@@ -84,10 +81,7 @@ class EventBus extends EventTarget {
/** /**
* Listen to built-in events (one-time) * Listen to built-in events (one-time)
*/ */
once<T extends keyof PageAgentEventMap>( once<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
event: T,
handler: EventHandler<T & keyof PageAgentEventMap>
): void {
const wrappedHandler = (e: Event) => { const wrappedHandler = (e: Event) => {
const customEvent = e as CustomEvent const customEvent = e as CustomEvent
const params = customEvent.detail?.[0] const params = customEvent.detail?.[0]
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"noEmit": false,
"outDir": "./dist",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
},
"include": ["src", "env.d.ts"]
}
@@ -1,9 +1,4 @@
// @ts-check // @ts-check
// ============================================================================
// Export Configuration Based on MODE Environment Variable
// ============================================================================
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react-swc'
import chalk from 'chalk' import chalk from 'chalk'
import 'dotenv/config' import 'dotenv/config'
import process from 'node:process' import process from 'node:process'
@@ -13,44 +8,18 @@ import { fileURLToPath } from 'url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(fileURLToPath(import.meta.url))
const __dirname = dirname(__filename)
// Website Config (React Documentation Site)
/** @type {import('vite').UserConfig} */
const websiteConfig = {
// https://vite.dev/config/
base: './',
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@pages': resolve(__dirname, 'pages'),
},
},
define: {
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),
'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
},
}
// ============================================================================ // ============================================================================
// Library Config (ES Module for NPM Package) // Library Config (ES Module for NPM Package)
// ============================================================================ // ============================================================================
/** @type {import('vite').UserConfig} */ /** @type {import('vite').UserConfig} */
const libConfig = { const libConfig = {
// Library build configuration clearScreen: false,
plugins: [ plugins: [
dts({ tsconfigPath: './tsconfig.json', bundleTypes: true }), dts({ tsconfigPath: './tsconfig.json', bundleTypes: true }),
cssInjectedByJsPlugin({ relativeCSSInjection: true }), cssInjectedByJsPlugin({ relativeCSSInjection: true }),
], ],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
publicDir: false, publicDir: false,
esbuild: { esbuild: {
keepNames: true, keepNames: true,
@@ -66,7 +35,6 @@ const libConfig = {
rollupOptions: { rollupOptions: {
external: ['ai', 'ai-motion', 'chalk', 'zod'], external: ['ai', 'ai-motion', 'chalk', 'zod'],
}, },
// minify: 'terser',
minify: false, minify: false,
sourcemap: true, sourcemap: true,
cssCodeSplit: true, cssCodeSplit: true,
@@ -81,13 +49,7 @@ const libConfig = {
// ============================================================================ // ============================================================================
/** @type {import('vite').UserConfig} */ /** @type {import('vite').UserConfig} */
const umdConfig = { const umdConfig = {
// Library build configuration
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })], plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
publicDir: false, publicDir: false,
esbuild: { esbuild: {
keepNames: true, keepNames: true,
@@ -109,19 +71,15 @@ const umdConfig = {
// ============================================================================ // ============================================================================
// ============================================================================
const MODE = process.env.MODE const MODE = process.env.MODE
console.log(chalk.cyan(`📦 Build mode: ${chalk.bold(MODE || 'website')}`)) console.log(chalk.cyan(`📦 Build mode: ${chalk.bold(MODE || 'lib')}`))
let config let config
if (MODE === 'lib') { if (MODE === 'umd') {
config = libConfig
} else if (MODE === 'umd') {
config = umdConfig config = umdConfig
} else { } else {
config = websiteConfig config = libConfig
} }
export default defineConfig(config) export default defineConfig(config)
+1
View File
@@ -0,0 +1 @@
# Landing Page & Docs
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
+60
View File
@@ -0,0 +1,60 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="https://img.alicdn.com/imgextra/i2/O1CN012eGDRI1X6nnMt9clU_!!6000000002875-49-tps-64-64.webp"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PageAgent - The GUI Agent Living in Your Webpage</title>
<meta
name="description"
content="PageAgent.js: Intelligent GUI Agent for any website. Modern web AI automation with minimal integration."
/>
<meta
name="keywords"
content="PageAgent, AI Agent, GUI Agent, Web Automation, GUI Automation, Frontend, CDN, JavaScript, React, Vite, LLM"
/>
<meta
property="og:image"
content="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
/>
<meta property="og:url" content="https://alibaba.github.io/page-agent" />
<meta property="og:type" content="website" />
<meta name="theme-color" content="#58c0fc" />
<meta name="color-scheme" content="light dark" />
<meta name="author" content="PageAgent.js Team" />
<meta property="og:title" content="PageAgent.js - AI-powered GUI Agent" />
<meta property="og:description" content="The GUI Agent living in your website." />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="zh_CN" />
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-HCGRJTN3HM"></script>
<script>
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments)
}
gtag('js', new Date())
gtag('config', 'G-HCGRJTN3HM')
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.tsx"></script>
<script>
// Dynamically update html lang attribute based on i18n detection
const updateHtmlLang = () => {
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
document.documentElement.lang = lang
}
updateHtmlLang()
window.addEventListener('storage', updateHtmlLang)
</script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
{
"name": "@page-agent/website",
"private": true,
"version": "0.0.6",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"page-agent": "*"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.14",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"i18next": "^25.6.0",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^16.1.4",
"tailwindcss": "^4.1.14",
"wouter": "^3.7.1"
}
}
@@ -1,17 +1,19 @@
import { useTranslation } from 'react-i18next'
export default function BetaNotice() { export default function BetaNotice() {
const { t } = useTranslation('common')
return ( return (
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-8"> <div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-8">
<div className="flex items-start"> <div className="flex items-start">
<div className="flex-shrink-0"> <div className="shrink-0">
<span className="text-xl">🚧</span> <span className="text-xl">🚧</span>
</div> </div>
<div className="ml-3"> <div className="ml-3">
<h3 className="text-sm font-medium text-orange-800 dark:text-orange-200 mb-1"> <h3 className="text-sm font-medium text-orange-800 dark:text-orange-200 mb-1">
Beta {t('beta_notice.title')}
</h3> </h3>
<p className="text-sm text-orange-700 dark:text-orange-300"> <p className="text-sm text-orange-700 dark:text-orange-300">{t('beta_notice.content')}</p>
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -28,7 +28,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
// 使用 Tailwind 的 dark: 前缀实现自动主题切换 // 使用 Tailwind 的 dark: 前缀实现自动主题切换
const containerClasses = const containerClasses =
'bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-gray-300 dark:border-gray-700' 'bg-linear-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-gray-300 dark:border-gray-700'
const headerClasses = 'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700' const headerClasses = 'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700'
const headerTextClasses = 'text-gray-700 dark:text-gray-300' const headerTextClasses = 'text-gray-700 dark:text-gray-300'
const languageTextClasses = 'text-gray-600 dark:text-gray-400' const languageTextClasses = 'text-gray-600 dark:text-gray-400'
@@ -72,7 +72,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
<div className="flex"> <div className="flex">
{/* 行号 */} {/* 行号 */}
{showLineNumbers && ( {showLineNumbers && (
<div className={`flex-shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}> <div className={`shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
<div className="text-xs font-mono leading-6"> <div className="text-xs font-mono leading-6">
{lines.map((line, lineIdx) => { {lines.map((line, lineIdx) => {
const lineNum = lineIdx + 1 const lineNum = lineIdx + 1
@@ -0,0 +1,98 @@
import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, useLocation } from 'wouter'
interface DocsLayoutProps {
children: ReactNode
}
interface NavItem {
title: string
path: string
}
interface NavSection {
title: string
items: NavItem[]
}
export default function DocsLayout({ children }: DocsLayoutProps) {
const { t } = useTranslation('common')
const [location] = useLocation()
const navigationSections: NavSection[] = [
{
title: t('nav.introduction'),
items: [
{ title: t('nav.overview'), path: '/docs/introduction/overview' },
{ title: t('nav.quick_start'), path: '/docs/introduction/quick-start' },
{ title: t('nav.limitations'), path: '/docs/introduction/limitations' },
],
},
{
title: t('nav.features'),
items: [
{ title: t('nav.model_integration'), path: '/docs/features/model-integration' },
{ 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.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' },
],
},
]
return (
<div className="max-w-7xl mx-auto px-6 py-8 overflow-x-auto">
<div className="flex gap-8 min-w-[900px]">
{/* Sidebar */}
<aside className="w-64 shrink-0" role="complementary" aria-label="文档导航">
<div className="sticky top-8">
<nav className="space-y-8" role="navigation" aria-label="文档章节">
{navigationSections.map((section) => (
<section key={section.title}>
<h3 className="font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-3">
{section.title}
</h3>
<ul className="space-y-2" role="list">
{section.items.map((item) => {
const isActive = location === item.path
return (
<li key={item.path}>
<Link
href={item.path}
className={`block px-3 py-2 rounded-lg transition-colors duration-200 ${
isActive
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-medium'
: 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
aria-current={isActive ? 'page' : undefined}
>
{item.title}
</Link>
</li>
)
})}
</ul>
</section>
))}
</nav>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 min-w-0" id="main-content" role="main">
<div className="prose prose-lg dark:prose-invert max-w-none">{children}</div>
</main>
</div>
</div>
)
}
@@ -1,4 +1,8 @@
import { useTranslation } from 'react-i18next'
export default function Footer() { export default function Footer() {
const { t } = useTranslation('common')
return ( return (
<footer <footer
className="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700" className="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700"
@@ -6,14 +10,14 @@ export default function Footer() {
> >
<div className="max-w-7xl mx-auto px-6 py-6"> <div className="max-w-7xl mx-auto px-6 py-6">
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0"> <div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<p className="text-foreground/80 text-sm">© 2025 page-agent. All rights reserved.</p> <p className="text-gray-600 dark:text-gray-300 text-sm">{t('footer.copyright')}</p>
<div className="flex items-center space-x-6"> <div className="flex items-center space-x-6">
<a <a
href="https://github.com/alibaba/page-agent" href="https://github.com/alibaba/page-agent"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-foreground/80 hover:text-foreground transition-colors duration-200" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200"
aria-label="访问 GitHub 仓库" aria-label={t('footer.github_label')}
> >
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <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" /> <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" />
+142
View File
@@ -0,0 +1,142 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'wouter'
import LanguageSwitcher from './LanguageSwitcher'
import ThemeSwitcher from './ThemeSwitcher'
import { BookIcon, CloseIcon, GithubIcon, MenuIcon } from './icons'
export default function Header() {
const { t } = useTranslation('common')
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
<>
<header
className="relative z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
role="banner"
>
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between gap-2">
{/* Logo */}
<Link
href="/"
className="flex items-center gap-2 sm:gap-3 group shrink-0"
aria-label={t('header.logo_alt')}
onClick={() => setMobileMenuOpen(false)}
>
<img
src="https://img.alicdn.com/imgextra/i2/O1CN01HB8ylu1uozANEMZw2_!!6000000006085-49-tps-128-128.webp"
alt="PageAgent Logo"
className="w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200"
/>
<div>
<span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white block leading-tight">
page-agent
</span>
<p className="hidden sm:block text-xs text-gray-600 dark:text-gray-300">
{t('header.slogan')}
</p>
</div>
</Link>
{/* Mobile Icon Navigation (横向滚动) */}
<nav
className="md:hidden flex items-center gap-1 overflow-x-auto scrollbar-hide flex-1"
role="navigation"
aria-label="Mobile navigation"
>
<Link
href="/docs/introduction/overview"
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
aria-label={t('header.nav_docs')}
>
<BookIcon className="w-5 h-5" />
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
aria-label={t('header.nav_source')}
>
<GithubIcon className="w-5 h-5" />
</a>
</nav>
{/* Desktop Navigation */}
<nav
className="hidden md:flex items-center space-x-6"
role="navigation"
aria-label={t('header.nav_docs')}
>
<Link
href="/docs/introduction/overview"
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
>
<BookIcon />
{t('header.nav_docs')}
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
aria-label={t('header.nav_source')}
>
<GithubIcon />
{t('header.nav_source')}
</a>
<ThemeSwitcher />
<LanguageSwitcher />
</nav>
{/* Mobile menu button */}
<button
type="button"
className="md:hidden p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200 shrink-0"
aria-label={t('header.mobile_menu')}
aria-expanded={mobileMenuOpen}
aria-controls="mobile-menu"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
</button>
</div>
{/* Mobile Navigation */}
{mobileMenuOpen && (
<nav
id="mobile-menu"
className="md:hidden pt-4 pb-2 space-y-3 border-t border-gray-200 dark:border-gray-700 mt-4"
role="navigation"
>
<Link
href="/docs/introduction/overview"
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
onClick={() => setMobileMenuOpen(false)}
>
<BookIcon className="w-5 h-5" />
{t('header.nav_docs')}
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
aria-label={t('header.nav_source')}
>
<GithubIcon className="w-5 h-5" />
{t('header.nav_source')}
</a>
<div className="flex items-center gap-3 px-3 py-2">
<ThemeSwitcher />
<LanguageSwitcher />
</div>
</nav>
)}
</div>
</header>
</>
)
}
@@ -0,0 +1,126 @@
.syntax {
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
font-family: monospace;
color: #171717;
}
:global(.dark) .syntax {
color: #e0e0e0;
}
/* JavaScript/TypeScript 关键字 */
.keyword {
color: #d73a49;
font-weight: 600;
}
:global(.dark) .keyword {
color: #ff6b6b;
}
/* TypeScript 特定关键字 (interface, type, enum, etc.) */
.tsKeyword {
color: #af00db;
font-weight: 600;
}
:global(.dark) .tsKeyword {
color: #c792ea;
}
/* TypeScript 内置类型 */
.type {
color: #267f99;
font-weight: 500;
}
:global(.dark) .type {
color: #4ec9b0;
}
/* 字符串 */
.string {
color: #1d6eca;
}
:global(.dark) .string {
color: #4fc3f7;
}
/* 数字 */
.number {
color: #00c583;
}
:global(.dark) .number {
color: #66bb6a;
}
/* 布尔值和字面量 (true, false, null, undefined) */
.literal {
color: #0000ff;
font-weight: 500;
}
:global(.dark) .literal {
color: #569cd6;
}
/* 注释 */
.comment {
color: #6a737d;
font-style: italic;
}
:global(.dark) .comment {
color: #9e9e9e;
}
/* 装饰器 (@decorator) */
.decorator {
color: #e0aa00;
font-weight: 500;
}
:global(.dark) .decorator {
color: #dcdcaa;
}
/* 箭头函数 (=>) */
.arrow {
color: #d73a49;
font-weight: bold;
}
:global(.dark) .arrow {
color: #ff6b6b;
}
/* 标识符(变量名、函数名等) */
.identifier {
color: #171717;
}
:global(.dark) .identifier {
color: #e0e0e0;
}
/* 属性访问 (.property) */
.property {
color: #0550ae;
}
:global(.dark) .property {
color: #9cdcfe;
}
/* 运算符 */
.operator {
color: #5a5a5a;
}
:global(.dark) .operator {
color: #d4d4d4;
}
@@ -0,0 +1,176 @@
/**
* js 语法高亮组件,适合在文章中演示代码片段
*/
import React from 'react'
import styles from './HighlightSyntax.module.css'
interface HighlightSyntaxProps {
code: string
}
// JavaScript/TypeScript 关键字
const keywords =
'async|await|function|const|let|var|if|else|for|while|return|try|catch|finally|class|extends|from|import|export|default|undefined|throw|break|continue|switch|case|do|with|yield|delete|typeof|void|static|get|set|super|debugger'
// TypeScript 特定关键字
const tsKeywords =
'interface|type|enum|namespace|module|declare|abstract|implements|public|private|protected|readonly|as|satisfies|infer|keyof|is'
// 布尔值和空值
const literals = 'true|false|null|undefined|NaN|Infinity'
// TypeScript 内置类型
const tsTypes =
'string|number|boolean|any|unknown|never|void|object|symbol|bigint|Array|Promise|Record|Partial|Required|Readonly|Pick|Omit|Exclude|Extract|NonNullable|ReturnType|Parameters|ConstructorParameters|InstanceType|ThisType|Uppercase|Lowercase|Capitalize|Uncapitalize'
// 辅助函数:转义 HTML 特殊字符
function escapeHtml(text: string): string {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
// 语法高亮函数,先提取 token 再转义和高亮
function highlightSyntax(code: string): string {
// 构建正则模式,包含更多 token 类型(在原始文本上匹配)
const pattern = new RegExp(
'(' +
// 1. 字符串(双引号、单引号、模板字符串)
'"([^"\\\\]|\\\\.)*"|' +
"'([^'\\\\]|\\\\.)*'|" +
'`([^`\\\\]|\\\\.)*`|' +
// 2. 注释(单行和多行)
'//[^\\n]*|' +
'/\\*[\\s\\S]*?\\*/|' +
// 3. 装饰器
'@[a-zA-Z_$][\\w$]*|' +
// 4. 数字(包括小数、十六进制、科学计数法)
'\\b0[xX][0-9a-fA-F]+\\b|' +
'\\b\\d+\\.?\\d*(?:[eE][+-]?\\d+)?\\b|' +
// 5. TypeScript/JavaScript 关键字
'\\b(?:' +
keywords +
'|' +
tsKeywords +
'|' +
literals +
')\\b|' +
// 6. TypeScript 内置类型
'\\b(?:' +
tsTypes +
')\\b|' +
// 7. 箭头函数
'=>|' +
// 8. 函数调用(函数名后跟括号)
'\\b[a-zA-Z_$][\\w$]*(?=\\()|' +
// 9. 属性访问
'\\.[a-zA-Z_$][\\w$]*|' +
// 10. 运算符和特殊符号
'[+\\-*/%&|^!~<>=?:]+|' +
'[{}\\[\\]();,.]' +
')',
'g'
)
const tokens: string[] = []
let lastIndex = 0
let match: RegExpExecArray | null
while ((match = pattern.exec(code)) !== null) {
if (match.index > lastIndex) {
const gap = code.slice(lastIndex, match.index)
// 将间隙按空白符分割,保留空白符
tokens.push(...gap.split(/(\s+)/))
}
tokens.push(match[0])
lastIndex = pattern.lastIndex
}
if (lastIndex < code.length) {
tokens.push(...code.slice(lastIndex).split(/(\s+)/))
}
const highlighted = tokens
.map((token) => {
// 空白符直接返回
if (/^\s+$/.test(token)) {
return token
}
// 1. 注释(单行和多行)
if (/^\/\/.*$/.test(token) || /^\/\*[\s\S]*?\*\/$/.test(token)) {
return `<span class="${styles.comment}">${escapeHtml(token)}</span>`
}
// 2. 字符串
if (
/^"([^"\\]|\\.)*"$/.test(token) ||
/^'([^'\\]|\\.)*'$/.test(token) ||
/^`([^`\\]|\\.)*`$/.test(token)
) {
return `<span class="${styles.string}">${escapeHtml(token)}</span>`
}
// 3. 数字
if (/^(0[xX][0-9a-fA-F]+|\d+\.?\d*(?:[eE][+-]?\d+)?)$/.test(token)) {
return `<span class="${styles.number}">${escapeHtml(token)}</span>`
}
// 4. 布尔值和特殊字面量
if (new RegExp(`^(?:${literals})$`).test(token)) {
return `<span class="${styles.literal}">${escapeHtml(token)}</span>`
}
// 5. JavaScript/TypeScript 关键字
if (new RegExp(`^(?:${keywords})$`).test(token)) {
return `<span class="${styles.keyword}">${escapeHtml(token)}</span>`
}
// 6. TypeScript 特定关键字
if (new RegExp(`^(?:${tsKeywords})$`).test(token)) {
return `<span class="${styles.tsKeyword}">${escapeHtml(token)}</span>`
}
// 7. TypeScript 内置类型
if (new RegExp(`^(?:${tsTypes})$`).test(token)) {
return `<span class="${styles.type}">${escapeHtml(token)}</span>`
}
// 8. 装饰器
if (/^@[a-zA-Z_$][\w$]*$/.test(token)) {
return `<span class="${styles.decorator}">${escapeHtml(token)}</span>`
}
// 9. 箭头函数
if (token === '=>') {
return `<span class="${styles.arrow}">${escapeHtml(token)}</span>`
}
// 10. 函数调用和标识符
if (/^[a-zA-Z_$][\w$]*$/.test(token)) {
return `<span class="${styles.identifier}">${escapeHtml(token)}</span>`
}
// 11. 属性访问
if (/^\.[a-zA-Z_$][\w$]*$/.test(token)) {
return `<span class="${styles.property}">${escapeHtml(token)}</span>`
}
// 12. 运算符
if (/^[+\-*/%&|^!~<>=?:]+$/.test(token)) {
return `<span class="${styles.operator}">${escapeHtml(token)}</span>`
}
// 13. 其他符号,需要转义
return escapeHtml(token)
})
.join('')
return highlighted
}
const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {
const htmlContent = highlightSyntax(code)
// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml
return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />
}
export default HighlightSyntaxClient
@@ -0,0 +1,115 @@
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function LanguageSwitcher() {
const { i18n, t } = useTranslation('common')
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const currentLang = i18n.language
const languages = [
{ code: 'zh-CN', label: '中文' },
{ code: 'en-US', label: 'English' },
]
const currentLanguage = languages.find((lang) => lang.code === currentLang) || languages[0]
const handleLanguageChange = (langCode: string) => {
i18n.changeLanguage(langCode)
setIsOpen(false)
}
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [isOpen])
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300"
aria-label={t('language.switch_label')}
aria-expanded={isOpen}
aria-haspopup="true"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
/>
</svg>
<span>{currentLanguage.label}</span>
<svg
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div
className="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50"
role="menu"
aria-orientation="vertical"
>
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => handleLanguageChange(lang.code)}
className={`w-full flex items-center gap-2 px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${
currentLang === lang.code
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300'
}`}
role="menuitem"
>
<span>{lang.label}</span>
{currentLang === lang.code && (
<svg
className="w-4 h-4 ml-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
))}
</div>
)}
</div>
)
}
@@ -0,0 +1,133 @@
import { useEffect, useState } from 'react'
type Theme = 'light' | 'dark'
export default function ThemeSwitcher() {
const [theme, setTheme] = useState<Theme>(() => {
// 初始化时读取保存的主题
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme') as Theme | null
if (savedTheme === 'light' || savedTheme === 'dark') {
return savedTheme
}
// 默认跟随系统
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
return 'light'
})
useEffect(() => {
// 应用主题
if (theme === 'dark') {
document.documentElement.classList.add('dark')
document.documentElement.style.colorScheme = 'dark'
} else {
document.documentElement.classList.remove('dark')
document.documentElement.style.colorScheme = 'light'
}
// 保存到 localStorage
localStorage.setItem('theme', theme)
}, [theme])
// 监听系统主题变化
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
// 只有在用户未手动设置时才自动跟随系统
const savedTheme = localStorage.getItem('theme')
if (!savedTheme) {
setTheme(e.matches ? 'dark' : 'light')
}
}
mediaQuery.addEventListener('change', handleSystemThemeChange)
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange)
}, [])
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
}
return (
<button
onClick={toggleTheme}
className="relative inline-flex items-center h-8 w-16 rounded-full transition-colors duration-300 ease-in-out focus:outline-none"
style={{
backgroundColor: theme === 'dark' ? '#1e293b' : '#e0f2fe',
}}
aria-label={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
role="switch"
aria-checked={theme === 'dark'}
>
{/* 滑块 */}
<span
className="inline-block h-6 w-6 transform rounded-full transition-all duration-300 ease-in-out shadow-md"
style={{
backgroundColor: theme === 'dark' ? '#475569' : '#fbbf24',
transform: theme === 'dark' ? 'translateX(2.25rem)' : 'translateX(0.25rem)',
}}
>
{/* 图标 */}
<span className="flex items-center justify-center h-full w-full">
{theme === 'light' ? (
// 太阳图标
<svg
className="w-4 h-4 text-white"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>
) : (
// 月亮图标
<svg
className="w-4 h-4 text-slate-200"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
)}
</span>
</span>
{/* 背景装饰 */}
<span
className="absolute inset-0 flex items-center justify-between px-2 pointer-events-none"
aria-hidden="true"
>
{/* 左侧太阳(浅色模式时显示) */}
<span
className={`transition-opacity duration-300 ${
theme === 'light' ? 'opacity-0' : 'opacity-40'
}`}
>
<svg className="w-4 h-4 text-sky-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>
</span>
{/* 右侧月亮(深色模式时显示) */}
<span
className={`transition-opacity duration-300 ${
theme === 'dark' ? 'opacity-0' : 'opacity-40'
}`}
>
<svg className="w-4 h-4 text-slate-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
</span>
</span>
</button>
)
}
+75
View File
@@ -0,0 +1,75 @@
// 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>
)
}
@@ -1,21 +1,24 @@
import BetaNotice from '@pages/components/BetaNotice' import { useTranslation } from 'react-i18next'
import CodeEditor from '@pages/components/CodeEditor'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function CustomTools() { export default function CustomTools() {
const { t } = useTranslation('docs')
return ( return (
<div> <div>
<h1 className="text-4xl font-bold mb-6"></h1> <h1 className="text-4xl font-bold mb-6">{t('custom_tools.title')}</h1>
<p className="text-xl text-foreground/80 mb-8 leading-relaxed"> <p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
AI Agent 使 Zod AI {t('custom_tools.subtitle')}
</p> </p>
<div className="space-y-8"> <div className="space-y-8">
<section> <section>
<h2 className="text-2xl font-bold mb-4"></h2> <h2 className="text-2xl font-bold mb-4">{t('custom_tools.registration')}</h2>
<p className="text-foreground/80 mb-4"> <p className="text-gray-600 dark:text-gray-300 mb-4">
namedescriptioninput schema execute {t('custom_tools.registration_desc')}
</p> </p>
<CodeEditor <CodeEditor
@@ -49,13 +52,12 @@ const pageAgent = new PageAgent({customTools})
</section> </section>
<section> <section>
<h2 className="text-2xl font-bold mb-4"></h2> <h2 className="text-2xl font-bold mb-4">{t('custom_tools.page_filter')}</h2>
<BetaNotice /> <BetaNotice />
<p className="text-foreground/80 mb-4"> <p className="text-gray-600 dark:text-gray-300 mb-4">
<code className="bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded">pageFilter</code>{' '} {t('custom_tools.page_filter_desc')}
</p> </p>
<CodeEditor <CodeEditor
@@ -82,16 +84,16 @@ const pageAgent = new PageAgent({customTools})
</section> </section>
<section> <section>
<h2 className="text-2xl font-bold mb-4"></h2> <h2 className="text-2xl font-bold mb-4">{t('custom_tools.best_practices')}</h2>
<div className="space-y-4"> <div className="space-y-4">
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg"> <div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-300 mb-2"> <h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-300 mb-2">
{t('custom_tools.bp_performance')}
</h3> </h3>
<ul className="text-foreground/80 space-y-1 text-sm"> <ul className="text-gray-600 dark:text-gray-300 space-y-1 text-sm">
<li> 使 pageFilter </li> <li>{t('custom_tools.bp_1')}</li>
<li> execute </li> <li>{t('custom_tools.bp_2')}</li>
<li> </li> <li>{t('custom_tools.bp_3')}</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -1,5 +1,5 @@
import BetaNotice from '@pages/components/BetaNotice' import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
export default function DataMasking() { export default function DataMasking() {
return ( return (
@@ -8,7 +8,7 @@ export default function DataMasking() {
<BetaNotice /> <BetaNotice />
<p className="text-xl text-foreground/80 mb-6 leading-relaxed"> <p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
AI AI
</p> </p>
@@ -19,14 +19,18 @@ export default function DataMasking() {
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300"> <h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🔒 🔒
</h3> </h3>
<p className="text-foreground/80"></p> <p className="text-gray-600 dark:text-gray-300">
</p>
</div> </div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg"> <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 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
</h3> </h3>
<p className="text-foreground/80"></p> <p className="text-gray-600 dark:text-gray-300">
</p>
</div> </div>
</div> </div>
@@ -1,5 +1,5 @@
import BetaNotice from '@pages/components/BetaNotice' import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
export default function KnowledgeInjection() { export default function KnowledgeInjection() {
return ( return (
@@ -8,7 +8,7 @@ export default function KnowledgeInjection() {
<BetaNotice /> <BetaNotice />
<p className="text-xl text-foreground/80 mb-8 leading-relaxed"> <p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
AI AI
</p> </p>
@@ -20,8 +20,10 @@ export default function KnowledgeInjection() {
<h3 className="text-xl font-semibold mb-3 text-purple-900 dark:text-purple-300"> <h3 className="text-xl font-semibold mb-3 text-purple-900 dark:text-purple-300">
🎯 🎯
</h3> </h3>
<p className="text-foreground/80 mb-4"> AI </p> <p className="text-gray-600 dark:text-gray-300 mb-4">
<ul className="list-disc list-inside space-y-2 text-foreground/70"> AI
</p>
<ul className="list-disc list-inside space-y-2 text-gray-500 dark:text-gray-400">
<li> AI </li> <li> AI </li>
<li></li> <li></li>
<li></li> <li></li>
@@ -57,13 +59,13 @@ const pageAgent = new PageAgent({
<h3 className="text-xl font-semibold mb-3 text-blue-900 dark:text-blue-300"> <h3 className="text-xl font-semibold mb-3 text-blue-900 dark:text-blue-300">
</h3> </h3>
<p className="text-foreground/80 mb-4"> <p className="text-gray-600 dark:text-gray-300 mb-4">
AI AI
</p> </p>
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4> <h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-foreground/70 space-y-1"> <ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li></li> <li></li>
<li></li> <li></li>
<li></li> <li></li>
@@ -71,7 +73,7 @@ const pageAgent = new PageAgent({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4> <h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-foreground/70 space-y-1"> <ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li></li> <li></li>
<li></li> <li></li>
<li></li> <li></li>
@@ -111,21 +113,25 @@ pageAgent.knowledge.setAppKnowledge(\`
<h3 className="text-xl font-semibold mb-3 text-green-900 dark:text-green-300"> <h3 className="text-xl font-semibold mb-3 text-green-900 dark:text-green-300">
📄 📄
</h3> </h3>
<p className="text-foreground/80 mb-4"> <p className="text-gray-600 dark:text-gray-300 mb-4">
AI AI
</p> </p>
<div className="grid md:grid-cols-3 gap-4"> <div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4> <h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-foreground/70"></p> <p className="text-sm text-gray-500 dark:text-gray-400"></p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4> <h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-foreground/70"></p> <p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4> <h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-foreground/70"></p> <p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,164 @@
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,4 +1,4 @@
import BetaNotice from '@pages/components/BetaNotice' import BetaNotice from '@/components/BetaNotice'
export default function SecurityPermissions() { export default function SecurityPermissions() {
return ( return (
@@ -7,7 +7,7 @@ export default function SecurityPermissions() {
<BetaNotice /> <BetaNotice />
<p className="text-xl text-foreground/80 mb-8 leading-relaxed"> <p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
page-agent AI page-agent AI
</p> </p>
@@ -19,13 +19,15 @@ export default function SecurityPermissions() {
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300"> <h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫 🚫
</h3> </h3>
<p className="text-foreground/80"> AI </p> <p className="text-gray-600 dark:text-gray-300">
AI
</p>
</div> </div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg"> <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 className="text-lg font-semibold text-green-900 dark:text-green-300">
</h3> </h3>
<p className="text-foreground/80"> AI </p> <p className="text-gray-600 dark:text-gray-300"> AI </p>
</div> </div>
</div> </div>
</section> </section>
@@ -37,13 +39,13 @@ export default function SecurityPermissions() {
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300"> <h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫 URL 🚫 URL
</h3> </h3>
<p className="text-foreground/80"> AI 访</p> <p className="text-gray-600 dark:text-gray-300"> AI 访</p>
</div> </div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg"> <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 className="text-lg font-semibold text-green-900 dark:text-green-300">
URL URL
</h3> </h3>
<p className="text-foreground/80"> AI 访</p> <p className="text-gray-600 dark:text-gray-300"> AI 访</p>
</div> </div>
</div> </div>
</section> </section>
@@ -54,17 +56,21 @@ export default function SecurityPermissions() {
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300"> <h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3> </h3>
<p className="text-foreground/80 mb-3"> <p className="text-gray-600 dark:text-gray-300 mb-3">
AI AI
</p> </p>
<div className="space-y-2"> <div className="space-y-2">
<div className="pl-3 border-l-2 border-red-400"> <div className="pl-3 border-l-2 border-red-400">
<p className="font-medium text-red-700 dark:text-red-300"></p> <p className="font-medium text-red-700 dark:text-red-300"></p>
<p className="text-sm text-foreground/70"></p> <p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div> </div>
<div className="pl-3 border-l-2 border-orange-400"> <div className="pl-3 border-l-2 border-orange-400">
<p className="font-medium text-orange-700 dark:text-orange-300"></p> <p className="font-medium text-orange-700 dark:text-orange-300"></p>
<p className="text-sm text-foreground/70"></p> <p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -1,5 +1,5 @@
import BetaNotice from '@pages/components/BetaNotice' import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
export default function BestPractices() { export default function BestPractices() {
return ( return (
@@ -19,7 +19,9 @@ export default function BestPractices() {
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300"> <h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
API API
</h3> </h3>
<p className="text-foreground/80 mb-3"> AI </p> <p className="text-gray-600 dark:text-gray-300 mb-3">
AI
</p>
<CodeEditor <CodeEditor
code={`// 推荐:合并操作 code={`// 推荐:合并操作
@@ -36,7 +38,9 @@ await pageAgent.execute('点击提交按钮');`}
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300"> <h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🎯 🎯
</h3> </h3>
<p className="text-foreground/80">使</p> <p className="text-gray-600 dark:text-gray-300">
使
</p>
</div> </div>
</div> </div>
@@ -45,12 +49,12 @@ await pageAgent.execute('点击提交按钮');`}
<div className="space-y-3 mb-6"> <div className="space-y-3 mb-6">
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border-l-4 border-red-500"> <div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border-l-4 border-red-500">
<h3 className="font-semibold mb-1 text-red-900 dark:text-red-300"></h3> <h3 className="font-semibold mb-1 text-red-900 dark:text-red-300"></h3>
<p className="text-foreground/80"></p> <p className="text-gray-600 dark:text-gray-300"></p>
</div> </div>
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border-l-4 border-yellow-500"> <div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border-l-4 border-yellow-500">
<h3 className="font-semibold mb-1 text-yellow-900 dark:text-yellow-300"></h3> <h3 className="font-semibold mb-1 text-yellow-900 dark:text-yellow-300"></h3>
<p className="text-foreground/80"></p> <p className="text-gray-600 dark:text-gray-300"></p>
</div> </div>
</div> </div>
@@ -1,5 +1,5 @@
import BetaNotice from '@pages/components/BetaNotice' import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
export default function CdnSetup() { export default function CdnSetup() {
return ( return (
@@ -16,7 +16,7 @@ export default function CdnSetup() {
className="mb-8" className="mb-8"
code={` code={`
// 仅供测试使用,稳定 CDN 待定 // 仅供测试使用,稳定 CDN 待定
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js" crossorigin="true" type="text/javascript"></script> <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> <script>
window.pageAgent.panel.show() window.pageAgent.panel.show()
@@ -27,7 +27,7 @@ export default function CdnSetup() {
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300"> <h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3> </h3>
<ul className="text-foreground/80 space-y-1"> <ul className="text-gray-600 dark:text-gray-300 space-y-1">
<li> 使</li> <li> 使</li>
<li> HTTPS 使</li> <li> HTTPS 使</li>
<li> CSP </li> <li> CSP </li>
@@ -1,4 +1,4 @@
import CodeEditor from '@pages/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
export default function Configuration() { export default function Configuration() {
return ( return (
@@ -70,6 +70,14 @@ interface AgentConfig {
// page behavior hooks // page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/** /**
* TODO: @unimplemented * TODO: @unimplemented
* hook when action causes a new page to be opened * hook when action causes a new page to be opened
@@ -1,17 +1,17 @@
import CodeEditor from '@pages/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
export default function ThirdPartyAgentPage() { export default function ThirdPartyAgentPage() {
return ( return (
<div> <div>
<h1 className="text-4xl font-bold mb-6"> Agent</h1> <h1 className="text-4xl font-bold mb-6"> Agent</h1>
<p className="mb-6 leading-relaxed"> <p className="mb-6 leading-relaxed text-gray-600 dark:text-gray-300">
pageAgent Agent Agent pageAgent Agent Agent
</p> </p>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-6"> <div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">💡 </h3> <h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">💡 </h3>
<p className="text-blue-800 dark:text-blue-200"> <p className="text-blue-800 dark:text-blue-200">
Agent "嘴巴""眼睛""手" "嘴巴""眼睛""手"
</p> </p>
</div> </div>
@@ -48,26 +48,26 @@ const pageAgentTool = {
<h2 className="text-2xl font-bold mb-4"></h2> <h2 className="text-2xl font-bold mb-4"></h2>
<div className="grid md:grid-cols-2 gap-4 mb-6"> <div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg"> <div className="bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2">🤖 </h4> <h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🤖 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
"帮我提交工单" "帮我提交工单"
</p> </p>
</div> </div>
<div className="bg-gradient-to-br from-green-50 to-blue-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg"> <div className="bg-linear-to-br from-green-50 to-blue-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2">📋 </h4> <h4 className="font-semibold mb-2 text-gray-900 dark:text-white">📋 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
"完成客户入职" "完成客户入职"
</p> </p>
</div> </div>
<div className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg"> <div className="bg-linear-to-br from-purple-50 to-pink-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2">🎯 </h4> <h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🎯 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
"预订会议室" "预订会议室"
</p> </p>
</div> </div>
<div className="bg-gradient-to-br from-orange-50 to-red-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg"> <div className="bg-linear-to-br from-orange-50 to-red-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2">🔧 </h4> <h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🔧 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
"重启服务器" "重启服务器"
</p> </p>
@@ -77,12 +77,12 @@ const pageAgentTool = {
<h2 className="text-2xl font-bold mb-4"></h2> <h2 className="text-2xl font-bold mb-4"></h2>
<div className="space-y-4 mb-6"> <div className="space-y-4 mb-6">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg"> <div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-lg font-semibold mb-2"></h3> <h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white"></h3>
<CodeEditor code={`// @TODO`} language="javascript" /> <CodeEditor code={`// @TODO`} language="javascript" />
</div> </div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg"> <div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-lg font-semibold mb-2"></h3> <h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white"></h3>
<CodeEditor code={`// @TODO`} language="javascript" /> <CodeEditor code={`// @TODO`} language="javascript" />
</div> </div>
</div> </div>
@@ -98,9 +98,11 @@ const pageAgentTool = {
</ul> </ul>
</div> </div>
<div className="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg"> <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">🎉 </h3> <h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">🎉 </h3>
<p className="mb-3"> Agent </p> <p className="mb-3 text-gray-700 dark:text-gray-300">
Agent
</p>
<a <a
href="/docs/integration/configuration" 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" className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200"
@@ -1,153 +1,141 @@
import { useTranslation } from 'react-i18next'
export default function LimitationsPage() { export default function LimitationsPage() {
const { t } = useTranslation('docs')
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">使</h1> <h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">
<p className="text-xl text-gray-600 dark:text-gray-300"> {t('limitations.title')}
page-agent </h1>
</p> <p className="text-xl text-gray-600 dark:text-gray-300">{t('limitations.subtitle')}</p>
</div> </div>
<div className="prose prose-lg dark:prose-invert max-w-none"> <div className="prose prose-lg dark:prose-invert max-w-none">
<h2 className="text-2xl font-bold mb-3"></h2> <h2 className="text-2xl font-bold mb-3">{t('limitations.page_support')}</h2>
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-400 p-4 mb-6"> <div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-400 p-4 mb-6">
<h3 className="font-semibold text-blue-800 dark:text-blue-200 mb-2"></h3> <h3 className="font-semibold text-blue-800 dark:text-blue-200 mb-2">
{t('limitations.spa_limit_title')}
</h3>
<ul className="text-blue-700 dark:text-blue-300 space-y-2"> <ul className="text-blue-700 dark:text-blue-300 space-y-2">
<li> <li>{t('limitations.spa_limit_1')}</li>
<strong>SPA</strong> <li>{t('limitations.spa_limit_2')}</li>
</li> <li>{t('limitations.spa_limit_3')}</li>
<li>
<strong></strong>
</li>
<li>
<strong></strong> page-agent
</li>
</ul> </ul>
</div> </div>
<h2 className="text-2xl font-bold mb-3"></h2> <h2 className="text-2xl font-bold mb-3">{t('limitations.interaction_limits')}</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6"> <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<h3 className="font-semibold mb-4"></h3> <h3 className="font-semibold mb-4">{t('limitations.supported_ops')}</h3>
<div className="grid md:grid-cols-2 gap-4 mb-6"> <div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center text-green-600 dark:text-green-400"> <div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_click')}</span>
</div> </div>
<div className="flex items-center text-green-600 dark:text-green-400"> <div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_input')}</span>
</div> </div>
<div className="flex items-center text-green-600 dark:text-green-400"> <div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_scroll')}</span>
</div> </div>
<div className="flex items-center text-green-600 dark:text-green-400"> <div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_submit')}</span>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center text-green-600 dark:text-green-400"> <div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_select')}</span>
</div> </div>
<div className="flex items-center text-green-600 dark:text-green-400"> <div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_focus')}</span>
</div> </div>
</div> </div>
</div> </div>
<h3 className="font-semibold mb-4"></h3> <h3 className="font-semibold mb-4">{t('limitations.unsupported_ops')}</h3>
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center text-red-600 dark:text-red-400"> <div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span>hover</span> <span>{t('limitations.op_hover')}</span>
</div> </div>
<div className="flex items-center text-red-600 dark:text-red-400"> <div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_drag')}</span>
</div> </div>
<div className="flex items-center text-red-600 dark:text-red-400"> <div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_context')}</span>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center text-red-600 dark:text-red-400"> <div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_draw')}</span>
</div> </div>
<div className="flex items-center text-red-600 dark:text-red-400"> <div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_keyboard')}</span>
</div> </div>
<div className="flex items-center text-red-600 dark:text-red-400"> <div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span> <span className="mr-2"></span>
<span></span> <span>{t('limitations.op_position')}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<h2 className="text-2xl font-bold mb-3"></h2> <h2 className="text-2xl font-bold mb-3">{t('limitations.understanding_limits')}</h2>
<div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-400 p-4 mb-6"> <div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-400 p-4 mb-6">
<h3 className="font-semibold text-red-800 dark:text-red-200 mb-2"></h3> <h3 className="font-semibold text-red-800 dark:text-red-200 mb-2">
<p className="text-red-700 dark:text-red-300 mb-3"> {t('limitations.no_vision_title')}
page-agent DOM <strong></strong> </h3>
<p className="text-red-700 dark:text-red-300 mb-3">{t('limitations.no_vision_desc')}</p>
</p>
<ul className="text-red-700 dark:text-red-300 space-y-1"> <ul className="text-red-700 dark:text-red-300 space-y-1">
<li> <li>{t('limitations.no_vision_1')}</li>
<strong></strong> <li>{t('limitations.no_vision_2')}</li>
</li> <li>{t('limitations.no_vision_3')}</li>
<li> <li>{t('limitations.no_vision_4')}</li>
<strong>Canvas </strong> Canvas
</li>
<li>
<strong>WebGL 3D </strong> 3D
</li>
<li>
<strong>SVG </strong> SVG
</li>
</ul> </ul>
</div> </div>
<h2 className="text-2xl font-bold mb-3"></h2> <h2 className="text-2xl font-bold mb-3">{t('limitations.website_requirements')}</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6"> <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h3 className="font-semibold mb-2"></h3> <h3 className="font-semibold mb-2">{t('limitations.req_semantic_title')}</h3>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-gray-600 dark:text-gray-300">
DOM {t('limitations.req_semantic_desc')}
accessibility AI
</p> </p>
</div> </div>
<div> <div>
<h3 className="font-semibold mb-2">UI/UX</h3> <h3 className="font-semibold mb-2">{t('limitations.req_ux_title')}</h3>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-gray-600 dark:text-gray-300">{t('limitations.req_ux_desc')}</p>
AI
</p>
</div> </div>
<div> <div>
<h3 className="font-semibold mb-2"></h3> <h3 className="font-semibold mb-2">{t('limitations.req_env_title')}</h3>
<p className="text-gray-600 dark:text-gray-300">modern browser</p> <p className="text-gray-600 dark:text-gray-300">{t('limitations.req_env_desc')}</p>
</div> </div>
</div> </div>
</div> </div>
<h2></h2> <h2>{t('limitations.future')}</h2>
<div className="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400 p-4"> <div className="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400 p-4">
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2"></h3> <h3 className="font-semibold text-green-800 dark:text-green-200 mb-2">
{t('limitations.future_title')}
</h3>
<ul className="text-green-700 dark:text-green-300 space-y-1"> <ul className="text-green-700 dark:text-green-300 space-y-1">
<li> </li> <li>{t('limitations.future_1')}</li>
<li> </li> <li>{t('limitations.future_2')}</li>
<li> </li> <li>{t('limitations.future_3')}</li>
<li> </li> <li>{t('limitations.future_4')}</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -0,0 +1,212 @@
import { useTranslation } from 'react-i18next'
export default function Overview() {
const { t } = useTranslation('docs')
return (
<article>
{/* 头图 */}
<figure className="mb-8 rounded-xl overflow-hidden">
<img
src="https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg"
alt="page-agent"
className="w-full h-64 object-cover"
/>
</figure>
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4">{t('overview.title')}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-4 leading-relaxed">
{t('overview.subtitle')}
</p>
{/* Status Badges */}
<div className="flex flex-wrap gap-2 items-center">
<a
href="https://www.npmjs.com/package/page-agent"
target="_blank"
rel="noopener noreferrer"
>
<img src="https://badge.fury.io/js/page-agent.svg" alt="npm version" />
</a>
<a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener noreferrer">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="MIT License" />
</a>
<a href="http://www.typescriptlang.org/" target="_blank" rel="noopener noreferrer">
<img
src="https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg"
alt="TypeScript"
/>
</a>
<a
href="https://www.npmjs.com/package/page-agent"
target="_blank"
rel="noopener noreferrer"
>
<img src="https://img.shields.io/npm/dt/page-agent.svg" alt="Downloads" />
</a>
<a
href="https://bundlephobia.com/package/page-agent"
target="_blank"
rel="noopener noreferrer"
>
<img src="https://img.shields.io/bundlephobia/minzip/page-agent" alt="Bundle Size" />
</a>
<a href="https://github.com/alibaba/page-agent" target="_blank" rel="noopener noreferrer">
<img
src="https://img.shields.io/github/stars/alibaba/page-agent.svg"
alt="GitHub stars"
/>
</a>
</div>
</div>
<section>
<h2 className="text-2xl font-bold mb-4">{t('overview.what_is')}</h2>
<p className="text-gray-600 dark:text-gray-300 mb-8 leading-relaxed ">
{t('overview.what_is_desc')}
</p>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">{t('overview.features_title')}</h2>
<div className="grid md:grid-cols-2 gap-4 mb-8" role="list">
<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">
{t('overview.feature_dom.title')}
</h3>
<p className="text-gray-700 dark:text-gray-300">{t('overview.feature_dom.desc')}</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">
{t('overview.feature_secure.title')}
</h3>
<p className="text-gray-700 dark:text-gray-300">{t('overview.feature_secure.desc')}</p>
</div>
<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('overview.feature_backend.title')}
</h3>
<p className="text-gray-700 dark:text-gray-300">{t('overview.feature_backend.desc')}</p>
</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('overview.feature_accessible.title')}
</h3>
<p className="text-gray-700 dark:text-gray-300">
{t('overview.feature_accessible.desc')}
</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-4">{t('overview.vs_browser_use')}</h2>
<div className="overflow-x-auto mb-8">
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
<thead>
<tr className="bg-gray-50 dark:bg-gray-800">
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
{t('overview.table_feature')}
</th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
page-agent
</th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
browser-use
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_deployment')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_deployment_pa')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_deployment_bu')}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_scope')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scope_pa')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scope_bu')}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_user')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_user_pa')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_user_bu')}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_scenario')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scenario_pa')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scenario_bu')}
</td>
</tr>
</tbody>
</table>
</div>
<h2 className="text-2xl font-bold mb-4">{t('overview.use_cases_title')}</h2>
<ul className="space-y-4 mb-8">
<li className="flex items-start space-x-3">
<span className="w-6 h-6 min-w-6 bg-blue-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
1
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case1_title')}</strong> {t('overview.use_case1_desc')}
</div>
</li>
<li className="flex items-start space-x-3">
<span className="w-6 h-6 min-w-6 bg-green-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
2
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case2_title')}</strong> {t('overview.use_case2_desc')}
</div>
</li>
<li className="flex items-start space-x-3">
<span className="w-6 h-6 min-w-6 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
3
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case3_title')}</strong> {t('overview.use_case3_desc')}
</div>
</li>
<li className="flex items-start space-x-3">
<span className="w-6 h-6 min-w-6 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
4
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case4_title')}</strong> {t('overview.use_case4_desc')}
</div>
</li>
</ul>
</section>
</article>
)
}
@@ -1,31 +1,35 @@
import BetaNotice from '@pages/components/BetaNotice' import { useTranslation } from 'react-i18next'
import CodeEditor from '@pages/components/CodeEditor'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function QuickStart() { export default function QuickStart() {
const { t } = useTranslation('docs')
return ( return (
<div> <div>
<h1 className="text-4xl font-bold mb-6">Quick Start</h1> <h1 className="text-4xl font-bold mb-6">{t('quick_start.title')}</h1>
<p className=" mb-6 leading-relaxed"> page-agent </p> <p className=" mb-6 leading-relaxed">{t('quick_start.subtitle')}</p>
<h2 className="text-2xl font-bold mb-3"></h2> <h2 className="text-2xl font-bold mb-3">{t('quick_start.installation')}</h2>
<div className="space-y-4 mb-6"> <div className="space-y-4 mb-6">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg"> <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 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
1. {t('quick_start.step1_title')}
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<p className="text-sm font-medium mb-2">CDN </p> <p className="text-sm font-medium mb-2">{t('quick_start.step1_cdn')}</p>
<CodeEditor <CodeEditor
code={`// 仅供测试使用 code={`// 仅供测试使用
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js" crossorigin="true" type="text/javascript"></script>`} <script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>`}
language="html" language="html"
/> />
</div> </div>
<div> <div>
<p className="text-sm font-medium mb-2">NPM </p> <p className="text-sm font-medium mb-2">{t('quick_start.step1_npm')}</p>
<CodeEditor <CodeEditor
code={`// npm install page-agent code={`// npm install page-agent
import PageAgent from 'page-agent'`} import PageAgent from 'page-agent'`}
@@ -37,7 +41,7 @@ import PageAgent from 'page-agent'`}
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg"> <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"> <h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
2. {t('quick_start.step2_title')}
</h3> </h3>
<CodeEditor <CodeEditor
code={`// 仅供测试使用,生产环境需要配置 LLM 接入点,本工具不提供 LLM 服务 code={`// 仅供测试使用,生产环境需要配置 LLM 接入点,本工具不提供 LLM 服务
@@ -60,7 +64,7 @@ const agent = new PageAgent({
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg"> <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 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
3. 使 {t('quick_start.step3_title')}
</h3> </h3>
<CodeEditor <CodeEditor
code={`// 程序化执行自然语言指令 code={`// 程序化执行自然语言指令
+74
View File
@@ -0,0 +1,74 @@
# 国际化配置说明
本项目使用 `react-i18next` 实现国际化支持。
## 目录结构
```
pages/i18n/
├── config.ts # i18next 配置和初始化
├── types.ts # TypeScript 类型声明
├── locales/
│ ├── zh-CN/ # 中文翻译
│ │ ├── common.json # 通用组件(Header, Footer等)
│ │ ├── home.json # 首页
│ │ └── docs.json # 文档页(待完善)
│ └── en-US/ # 英文翻译
│ ├── common.json
│ ├── home.json
│ └── docs.json
└── README.md # 本文件
```
## 使用方法
### 在组件中使用
```tsx
import { useTranslation } from 'react-i18next'
function MyComponent() {
const { t } = useTranslation('common') // 指定命名空间
return <h1>{t('header.nav_docs')}</h1>
}
```
### 使用多个命名空间
```tsx
const { t } = useTranslation(['home', 'common'])
// 使用时指定命名空间
t('home:hero.title')
t('common:header.nav_docs')
```
## 语言切换
用户可以通过以下方式切换语言:
1. **自动检测**:首次访问根据浏览器语言自动选择
2. **手动切换**:点击页面右上角的语言切换按钮
3. **持久化**:语言选择保存在 `localStorage` 中,刷新后保持
## 添加新翻译
1. 在对应的 JSON 文件中添加翻译条目(如 `zh-CN/home.json`
2. 在对应的英文文件中添加翻译(如 `en-US/home.json`
3. 在组件中使用 `t('namespace:key')` 获取翻译
## TypeScript 支持
`types.ts` 文件提供了类型声明,使得翻译 key 具有:
- 自动补全
- 编译期类型检查
- 防止拼写错误
## 待完成
- [ ] 文档页翻译(`docs.json`
- [ ] DocsLayout 导航结构国际化
- [ ] 404 页面国际化
+45
View File
@@ -0,0 +1,45 @@
import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
import commonEn from './locales/en-US/common'
import docsEn from './locales/en-US/docs'
import homeEn from './locales/en-US/home'
import commonZh from './locales/zh-CN/common'
import docsZh from './locales/zh-CN/docs'
import homeZh from './locales/zh-CN/home'
const resources = {
'zh-CN': {
common: commonZh,
home: homeZh,
docs: docsZh,
},
'en-US': {
common: commonEn,
home: homeEn,
docs: docsEn,
},
}
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en-US',
defaultNS: 'common',
// 语言检测配置
detection: {
// localStorage 优先(用户手动选择),其次检测浏览器语言
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
interpolation: {
escapeValue: false, // React 已经做了 XSS 防护
},
})
export default i18n
@@ -0,0 +1,40 @@
export default {
header: {
logo_alt: 'page-agent home',
slogan: 'GUI Agent in your webpage',
nav_docs: 'Docs',
nav_source: 'GitHub',
mobile_menu: 'Open navigation',
},
footer: {
copyright: '© 2025 page-agent. All rights reserved.',
github_label: 'Visit GitHub repository',
},
beta_notice: {
title: 'Beta Stage',
content:
'Current features are incomplete and the API may change at any time. Please do not use in production environments before the official release.',
},
language: {
zh: '中文',
en: 'English',
switch_label: 'Switch language',
},
nav: {
introduction: 'Introduction',
features: 'Features',
integration: 'Integration',
overview: 'Overview',
quick_start: 'Quick Start',
limitations: 'Limitations',
model_integration: 'Model Integration',
custom_tools: 'Custom Tools',
knowledge_injection: 'Knowledge Injection',
security_permissions: 'Security & Permissions',
data_masking: 'Data Masking',
cdn_setup: 'CDN Setup',
configuration: 'Configuration',
best_practices: 'Best Practices',
third_party_agent: 'Third-party Agent',
},
}
@@ -0,0 +1,172 @@
export default {
overview: {
title: 'Overview',
subtitle:
'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.',
features_title: 'Core Features',
feature_dom: {
title: '🧠 Smart DOM Analysis',
desc: 'DOM-based analysis with high-intensity dehydration. No visual recognition needed. Pure text for fast and precise operations.',
},
feature_secure: {
title: '🔒 Secure & Controllable',
desc: 'Supports operation allowlists, data masking protection. Inject custom knowledge to make AI work by your rules.',
},
feature_backend: {
title: '⚡ Zero Backend',
desc: 'CDN or NPM import with custom LLM endpoints.',
},
feature_accessible: {
title: '♿ Accessible Intelligence',
desc: 'Provides natural language interface for complex B2B systems and admin panels. Makes software easy for everyone.',
},
vs_browser_use: 'vs. browser-use',
table_feature: 'Feature',
table_deployment: 'Deployment',
table_deployment_pa: 'Embedded component',
table_deployment_bu: 'External tool',
table_scope: 'Scope',
table_scope_pa: 'Current page',
table_scope_bu: 'Entire browser',
table_user: 'Target Users',
table_user_pa: 'Web developers',
table_user_bu: 'Scraper/Agent developers',
table_scenario: 'Use Case',
table_scenario_pa: 'UX enhancement',
table_scenario_bu: 'Automation tasks',
use_cases_title: 'Use Cases',
use_case1_title: 'Connect Support Bots:',
use_case1_desc:
"Turn your support assistant into a full agent. Customer service bots no longer just say 'Please click the settings button then click...'—they operate for users directly.",
use_case2_title: 'Modernize Legacy Apps:',
use_case2_desc:
'One line of code transforms old apps into agents. Product experts help users navigate complex B2B software. Reduce support costs and improve satisfaction.',
use_case3_title: 'Interactive Training:',
use_case3_desc:
"Demonstrate workflows in real-time. Let AI show the complete process of 'how to submit an expense report.'",
use_case4_title: 'Accessibility:',
use_case4_desc:
'Provide natural language interaction for visually impaired and elderly users. Connect screen readers or voice assistants to make software accessible to everyone.',
get_started_title: '🚀 Get Started',
get_started_desc:
'Ready to add an AI operator to your website? Check our quick start guide for integration in minutes.',
get_started_button: 'Quick Start →',
},
quick_start: {
title: 'Quick Start',
subtitle: 'Integrate page-agent in minutes.',
installation: 'Installation Steps',
step1_title: '1. Import Options',
step1_cdn: 'CDN Import',
step1_npm: 'NPM Install',
step2_title: '2. Initialize Configuration',
step3_title: '3. Start Using',
},
limitations: {
title: 'Limitations',
subtitle: "Understand page-agent's current capabilities and technical constraints",
page_support: 'Page Support Limitations',
spa_limit_title: 'Single Page Application Limits',
spa_limit_1: '• SPA only: Currently operates within a single page',
spa_limit_2: '• Multi-page relay in design: Cannot execute continuous tasks across pages yet',
spa_limit_3: '• Requires integration: Cannot operate on sites without page-agent',
interaction_limits: 'Interaction Limitations',
supported_ops: 'Supported Operations',
op_click: 'Click',
op_input: 'Text input',
op_scroll: 'Scroll',
op_submit: 'Form submit',
op_select: 'Select',
op_focus: 'Focus',
unsupported_ops: 'Unsupported Operations',
op_hover: 'Mouse hover',
op_drag: 'Drag & drop',
op_context: 'Right-click menu',
op_draw: 'Drawing',
op_keyboard: 'Keyboard shortcuts',
op_position: 'Position-based control',
understanding_limits: 'Understanding Limitations',
no_vision_title: 'No Visual Recognition',
no_vision_desc:
'page-agent operates based on DOM structure with no visual recognition. Cannot understand:',
no_vision_1: '• Image content: Cannot recognize text, icons, or visual elements in images',
no_vision_2: '• Canvas: Cannot understand graphics drawn on Canvas',
no_vision_3: '• WebGL 3D: Cannot operate elements in 3D scenes',
no_vision_4: '• SVG graphics: Cannot understand visual content and paths in SVG',
website_requirements: 'Website Requirements',
req_semantic_title: 'Semantic & Usability',
req_semantic_desc:
'All operations rely on semantic tags and attributes. Poor semantic structure or lack of accessibility features may affect AI understanding accuracy.',
req_ux_title: 'UI/UX',
req_ux_desc:
'Counter-intuitive interaction rules, visual-only operation hints, complex mouse interactions, or rapidly appearing/disappearing elements can affect AI understanding and operation.',
req_env_title: 'Environment',
req_env_desc: 'modern browser',
future: 'Future Plans',
future_title: 'Coming Soon',
future_1: '• Multi-page relay capabilities',
future_2: '• Richer mouse interaction support',
future_3: '• Basic visual understanding',
future_4: '• Smarter error recovery',
},
model_integration: {
title: 'Model Integration',
subtitle:
'Supports OpenAI-compatible models with tool call support, including public cloud services and private deployments.',
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',
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',
security: '🔐 Production Authentication',
security_warning: '⚠️ Never commit real LLM API Keys to your frontend codebase',
security_desc:
'In production environments, to hide the real LLM API Keys, we recommend the following architecture:',
security_backend_proxy: 'Backend Proxy Pattern',
security_backend_desc:
'Set up a backend LLM proxy endpoint that uses the same authentication method as other APIs in your website, such as:',
security_method_1: '• Session/Cookie-based authentication',
security_method_2: '• OIDC (OpenID Connect) single sign-on',
security_method_3: '• Temporary Access Key or JWT Token',
configuration: 'Configuration',
},
custom_tools: {
title: 'Custom Tools',
subtitle:
'Extend AI Agent capabilities by registering custom tools. Use Zod to define strict input schemas for safe business logic calls.',
registration: 'Tool Registration',
registration_desc:
'Each custom tool requires four core properties: name, description, input schema, and execute function.',
page_filter: 'Page Filter',
page_filter_desc:
'Control tool visibility on specific pages via the pageFilter property to enhance security and UX.',
best_practices: 'Best Practices',
bp_performance: '⚡ Performance Optimization',
bp_1: '• Use pageFilter to reduce unnecessary tool loading',
bp_2: '• Implement appropriate caching in execute functions',
bp_3: '• Avoid long-running sync operations in tools',
},
}
@@ -0,0 +1,79 @@
export default {
hero: {
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_main: ' adds intelligent GUI Agents to your website.',
subtitle_detail: 'Users give natural language commands, AI handles the rest.',
tab_try: '🚀 Try It Now',
tab_other: '🌐 Try on Other Sites',
input_placeholder: 'Describe what you want AI to do...',
execute_button: 'Run',
default_task:
'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown',
},
try_other: {
step1_title: 'Step 1:',
step1_content: 'Show your bookmarks bar',
step2_title: 'Step 2:',
step2_content: 'Drag this button to your bookmarks',
step3_title: 'Step 3:',
step3_content: 'Click the bookmark on any site to activate',
notice_title: '⚠️ Heads Up',
notice_items: {
item1: 'Demo only—link may expire without notice',
item2: 'This free demo uses DeepSeek API (see their terms and privacy policy)',
item3: 'Some sites block script injection (CSP policies)',
item4: 'Works on single-page apps only—reload required after navigation',
item5: 'Text-only understanding—no image recognition or drag-and-drop',
item6_prefix: 'Full limitations in',
item6_link: 'Docs',
},
},
benefits: {
no_backend: 'Pure Front-end Solution',
private_model: 'Your Own Models',
data_masking: 'Built-in Privacy',
open_source: 'MIT Open Source',
},
features: {
section_title: 'Why PageAgent',
in_page: {
title: 'In-page Solution',
desc: 'Runs entirely within your page. No browser extensions, no headless browsers, and no backend required.',
},
secure_integration: {
title: 'Secure by Design',
desc: 'Control what AI can access with allowlists, data masking, and custom knowledge injection. Your rules, your data.',
},
zero_backend: {
title: 'Zero Backend Setup',
desc: 'Just drop in a script. Works with any LLM provider—OpenAI, Anthropic, or your own models.',
},
accessible: {
title: 'Natural Language UI',
desc: 'Transform complex admin panels into chat interfaces. Make powerful tools accessible to everyone, not just experts.',
},
},
use_cases: {
section_title: 'Where It Shines',
section_subtitle: 'From simple forms to complex workflows, AI understands and executes',
case1: {
title: 'Supercharge Support Bots',
desc: 'Stop telling users where to click—let AI do it for them. Turn your chatbot from a guide into an operator that actually completes tasks.',
},
case2: {
title: 'Modernize Legacy Apps',
desc: 'Add AI superpowers to old software without rebuilding. One script tag transforms complex enterprise tools into chat-driven interfaces.',
},
case3: {
title: 'Interactive Walkthroughs',
desc: "Show, don't tell. Let AI demonstrate workflows in real-time—perfect for onboarding or training new users on complex systems.",
},
case4: {
title: 'Accessibility First',
desc: 'Make web apps accessible through natural language. Perfect for screen readers, voice control, or users who find traditional interfaces challenging.',
},
},
}
@@ -0,0 +1,39 @@
export default {
header: {
logo_alt: 'page-agent 首页',
slogan: 'GUI Agent in your webpage',
nav_docs: '文档',
nav_source: 'GitHub',
mobile_menu: '打开导航栏',
},
footer: {
copyright: '© 2025 page-agent. All rights reserved.',
github_label: '访问 GitHub 仓库',
},
beta_notice: {
title: 'Beta 阶段',
content: '当前功能未完成,接口可能随时变更。正式版本发布前请勿用于生产环境。',
},
language: {
zh: '中文',
en: 'English',
switch_label: '切换语言',
},
nav: {
introduction: '介绍',
features: '功能特性',
integration: '集成指南',
overview: '概览',
quick_start: '快速开始',
limitations: '使用限制',
model_integration: '模型接入',
custom_tools: '自定义工具',
knowledge_injection: '知识库注入',
security_permissions: '安全与权限',
data_masking: '数据脱敏',
cdn_setup: 'CDN 引入',
configuration: '配置选项',
best_practices: '最佳实践',
third_party_agent: '接入第三方 Agent',
},
}
@@ -0,0 +1,168 @@
export default {
overview: {
title: 'Overview',
subtitle:
'page-agent 是一个完全基于 Web 技术的 GUI Agent,简单几步,让你的网站拥有 AI 操作员。',
what_is: '什么是 page-agent',
what_is_desc:
'page-agent 是一个页面内嵌式 GUI Agent。与传统的浏览器自动化工具不同,page-agent 面向网站开发者,而非爬虫或Agent客户端开发者;将 Agent 集成到你的网站中,让用户可以通过自然语言与页面进行交互。',
features_title: '核心特性',
feature_dom: {
title: '🧠 智能 DOM 理解',
desc: '基于 DOM 分析,高强度脱水。无需视觉识别,纯文本实现精准操作。',
},
feature_secure: {
title: '🔒 安全可控',
desc: '支持操作黑白名单、数据脱敏保护。注入自定义知识库,让 AI 按你的规则工作。',
},
feature_backend: {
title: '⚡ 零后端部署',
desc: 'CDN 或 NPM 引入,自定义 LLM 接入点。',
},
feature_accessible: {
title: '♿ 普惠智能',
desc: '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。',
},
vs_browser_use: '与 browser-use 的区别',
table_feature: '特性',
table_deployment: '部署方式',
table_deployment_pa: '页面内嵌组件',
table_deployment_bu: '外部工具',
table_scope: '操作范围',
table_scope_pa: '当前页面',
table_scope_bu: '整个浏览器',
table_user: '目标用户',
table_user_pa: '网站开发者',
table_user_bu: '爬虫/Agent 开发者',
table_scenario: '使用场景',
table_scenario_pa: '用户体验增强',
table_scenario_bu: '自动化任务',
use_cases_title: '应用场景',
use_case1_title: '对接答疑机器人:',
use_case1_desc:
'把你的答疑助手变成全能Agent。客服机器人不再只说「请先点击设置按钮然后点击...」,而是直接帮用户现场操作。',
use_case2_title: '交互升级/智能化改造:',
use_case2_desc:
'一行代码,老应用变身Agent,产品专家帮用户操作复杂 B 端软件。降低人工支持成本,提高用户满意度。',
use_case3_title: '产品教学:',
use_case3_desc:
'向用户演示交互过程,边做边教。例如让AI演示「如何提交报销申请」的完整操作流程。',
use_case4_title: '无障碍支持:',
use_case4_desc:
'为视障用户、老年用户提供自然语言交互,对接屏幕阅读器或语音助理,让软件人人可用。',
get_started_title: '🚀 开始使用',
get_started_desc:
'准备好为你的网站添加 AI 操作员了吗?查看我们的快速开始指南,几分钟内完成集成。',
get_started_button: '快速开始 →',
},
quick_start: {
title: 'Quick Start',
subtitle: '几分钟内完成 page-agent 的集成。',
installation: '安装步骤',
step1_title: '1. 引入方式',
step1_cdn: 'CDN 引入',
step1_npm: 'NPM 安装',
step2_title: '2. 初始化配置',
step3_title: '3. 开始使用',
},
limitations: {
title: '使用限制',
subtitle: '了解 page-agent 当前的功能边界和技术限制',
page_support: '页面支持限制',
spa_limit_title: '单页应用限制',
spa_limit_1: '• 仅支持单页应用(SPA):目前只能在单个页面内进行操作',
spa_limit_2: '• 多页接力功能正在设计中:暂时无法跨页面执行连续任务',
spa_limit_3: '• 无法操作未接入该能力的网站:需要目标网站主动集成 page-agent',
interaction_limits: '交互行为限制',
supported_ops: '支持的操作',
op_click: '点击操作',
op_input: '文本输入',
op_scroll: '页面滚动',
op_submit: '表单提交',
op_select: '选择操作',
op_focus: '焦点切换',
unsupported_ops: '不支持的操作',
op_hover: '鼠标悬停(hover',
op_drag: '拖拽操作',
op_context: '右键菜单',
op_draw: '图形绘制',
op_keyboard: '键盘快捷键',
op_position: '基于点击区域或鼠标位置的控制',
understanding_limits: '网页理解限制',
no_vision_title: '无视觉能力',
no_vision_desc: 'page-agent 基于 DOM 结构进行理解和操作,没有视觉识别能力,无法理解以下内容:',
no_vision_1: '• 图片内容:无法识别图片中的文字、图标或视觉元素',
no_vision_2: '• Canvas 画布:无法理解 Canvas 中绘制的图形和内容',
no_vision_3: '• WebGL 3D 内容:无法操作 3D 场景中的元素',
no_vision_4: '• SVG 图形:无法理解 SVG 中的视觉内容和路径',
website_requirements: '被操作网站要求',
req_semantic_title: '语义化和易用性',
req_semantic_desc:
'所有操作都基于 DOM 元素的语义化标签和属性。如果页面结构不够语义化,或者没有任何 accessibility 特性,可能影响 AI 的理解准确性。',
req_ux_title: 'UI/UX',
req_ux_desc:
'反常识的交互规则、基于视觉的操作提示、复杂的鼠标交互、快速出现快速消失的元素等,都会影响 AI 的理解和操作。',
req_env_title: '环境要求',
req_env_desc: 'modern browser',
future: '未来规划',
future_title: '即将支持',
future_1: '• 多页面接力操作能力',
future_2: '• 更丰富的鼠标交互支持',
future_3: '• 基础的视觉理解能力',
future_4: '• 更智能的错误恢复机制',
},
model_integration: {
title: '模型接入',
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
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: '✅ 已验证可用',
tips: '提示',
tip_1: 'reasoning 模型(如 GPT-5),速度偏慢,没有必要',
tip_2:
'不保证 json schema 的模型(openAI 以外的几乎所有模型),tool call 有概率出错,通常能自动修复,建议 temperature 设置高一些',
tip_3: '小模型、nano 模型,效果不佳',
security: '🔐 生产环境鉴权建议',
security_warning: '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码库',
security_desc: '在实际应用中,为了隐藏真实的 LLM API Key,建议采用以下架构:',
security_backend_proxy: '后端代理转发',
security_backend_desc:
'在后端搭建一个 LLM 流量转发接口,该接口使用与你网站上其他接口相同的鉴权方式,例如:',
security_method_1: '• Session/Cookie 会话认证',
security_method_2: '• OIDC (OpenID Connect) 单点登录',
security_method_3: '• 临时 Access Key 或 JWT Token',
configuration: '配置方式',
},
custom_tools: {
title: '自定义工具',
subtitle:
'通过注册自定义工具,扩展 AI Agent 的能力边界。使用 Zod 定义严格的输入接口,让 AI 安全调用你的业务逻辑。',
registration: '工具注册',
registration_desc:
'每个自定义工具需要定义四个核心属性:name、description、input schema 和 execute 函数。',
page_filter: '页面过滤器',
page_filter_desc: '通过 pageFilter 属性控制工具在哪些页面可见,提升安全性和用户体验。',
best_practices: '最佳实践',
bp_performance: '⚡ 性能优化',
bp_1: '• 使用 pageFilter 减少不必要的工具加载',
bp_2: '• 在 execute 函数中实现适当的缓存机制',
bp_3: '• 避免在工具中执行耗时的同步操作',
},
}
@@ -0,0 +1,78 @@
export default {
hero: {
badge: 'GUI Agent in your webpage',
title_line1: '让你的 Web 应用',
title_line2: '拥有 AI 操作员',
subtitle_emoji: '🪄 一行 CDN 引入',
subtitle_main: ',为你的网站添加 GUI Agent。',
subtitle_detail: '用户/答疑机器人给出文字指示,AI 帮你操作页面。',
tab_try: '🚀 立即尝试',
tab_other: '🌐 其他网页尝试',
input_placeholder: '输入您想要 AI 执行的任务...',
execute_button: '执行',
default_task: '从导航栏中进入文档页,打开“快速开始”相关的文档,帮我总结成 markdown',
},
try_other: {
step1_title: '步骤 1:',
step1_content: '显示收藏夹栏',
step2_title: '步骤 2:',
step2_content: '拖拽下面按钮到收藏夹栏',
step3_title: '步骤 3:',
step3_content: '在其他网站点击收藏夹中的按钮即可使用',
notice_title: '⚠️ 注意',
notice_items: {
item1: '仅做技术评估,链接定期失效',
item2: '使用 DeepSeek 模型,参考 DeepSeek 用户协议和隐私政策',
item3: '部分网站屏蔽了链接嵌入,将无反应',
item4: '仅支持单页应用,页面跳转后需要重新注入',
item5: '仅识别文本,不识别图像,不支持拖拽等复杂交互',
item6_prefix: '详细使用限制参照',
item6_link: '《文档》',
},
},
benefits: {
no_backend: '纯前端方案',
private_model: '支持私有模型',
data_masking: '无痛脱敏',
open_source: 'MIT 开源',
},
features: {
section_title: '核心特性',
in_page: {
title: '纯页面内方案',
desc: '完全运行在你的页面内。不需要浏览器插件、不需要无头浏览器,不需要后端。',
},
secure_integration: {
title: '安全可控集成',
desc: '支持操作黑白名单、数据脱敏保护。注入自定义知识库,让 AI 按你的规则工作。',
},
zero_backend: {
title: '零后端部署',
desc: '前端脚本引入,自定义 LLM 接入点。从 OpenAI 到 qwen3,完全由你掌控。',
},
accessible: {
title: '普惠智能交互',
desc: '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。',
},
},
use_cases: {
section_title: '应用场景',
section_subtitle: '从简单的表单填写到复杂的业务流程,AI 都能理解并执行',
case1: {
title: '对接答疑机器人',
desc: '把你的答疑助手变成全能Agent。客服机器人不再只说「请先点击设置按钮然后点击...」,而是直接帮用户现场操作。',
},
case2: {
title: '交互升级/智能化改造',
desc: '一行代码,老应用变身Agent,产品专家帮用户操作复杂 B 端软件。降低人工支持成本,提高用户满意度。',
},
case3: {
title: '产品教学',
desc: '向用户演示交互过程,边做边教。例如让AI演示「如何提交报销申请」的完整操作流程。',
},
case4: {
title: '无障碍支持',
desc: '为视障用户、老年用户提供自然语言交互,对接屏幕阅读器或语音助理,让软件人人可用。',
},
},
}
+16
View File
@@ -0,0 +1,16 @@
import 'react-i18next'
import type commonZh from './locales/zh-CN/common'
import type docsZh from './locales/zh-CN/docs'
import type homeZh from './locales/zh-CN/home'
declare module 'react-i18next' {
interface CustomTypeOptions {
defaultNS: 'common'
resources: {
common: typeof commonZh
home: typeof homeZh
docs: typeof docsZh
}
}
}
+204
View File
@@ -0,0 +1,204 @@
@import 'tailwindcss';
/* 启用 class-based dark mode for Tailwind v4 */
@variant dark (.dark &);
:root {
--background: #ffffff;
--foreground: #171717;
/* 主题色渐变 */
--theme-color-1: rgb(88, 192, 252);
--theme-color-2: rgb(189, 69, 251);
}
/* class-based dark mode - 应用到 html.dark */
html.dark,
:root.dark {
--background: #0a0a0a;
--foreground: #ededed;
}
/* 同时支持系统偏好 */
/* @media (prefers-color-scheme: dark) {
html:not(.light),
:root:not(.light) {
--background: #0a0a0a;
--foreground: #ededed;
}
} */
/* 添加 Tailwind 自定义颜色 */
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
/* 确保文档页面标题在暗色模式下可见 - 只针对 prose 内的标题 */
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
color: rgba(23, 23, 23, 0.85);
}
.dark .prose h1,
.dark .prose h2,
.dark .prose h3,
.dark .prose h4,
.dark .prose h5,
.dark .prose h6 {
color: rgba(255, 255, 255, 0.9);
}
table,
th,
td {
color: #171717;
}
.dark table,
.dark th,
.dark td {
color: #ededed;
}
/* 文档页深色模式优化 */
.dark .prose {
color: rgba(255, 255, 255, 0.7);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose {
color: rgba(255, 255, 255, 0.7);
}
} */
.dark .dark\:prose-invert {
--tw-prose-body: rgba(255, 255, 255, 0.7);
--tw-prose-headings: rgba(255, 255, 255, 0.95);
--tw-prose-lead: rgba(255, 255, 255, 0.7);
--tw-prose-links: rgba(147, 197, 253, 0.9);
--tw-prose-bold: rgba(255, 255, 255, 0.9);
--tw-prose-counters: rgba(255, 255, 255, 0.6);
--tw-prose-bullets: rgba(255, 255, 255, 0.5);
--tw-prose-hr: rgba(255, 255, 255, 0.2);
--tw-prose-quotes: rgba(255, 255, 255, 0.8);
--tw-prose-quote-borders: rgba(255, 255, 255, 0.3);
--tw-prose-captions: rgba(255, 255, 255, 0.6);
--tw-prose-code: rgba(255, 255, 255, 0.9);
--tw-prose-pre-code: rgba(255, 255, 255, 0.95);
--tw-prose-pre-bg: rgba(0, 0, 0, 0.5);
--tw-prose-th-borders: rgba(255, 255, 255, 0.3);
--tw-prose-td-borders: rgba(255, 255, 255, 0.2);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .dark\:prose-invert {
--tw-prose-body: rgba(255, 255, 255, 0.7);
--tw-prose-headings: rgba(255, 255, 255, 0.95);
--tw-prose-lead: rgba(255, 255, 255, 0.7);
--tw-prose-links: rgba(147, 197, 253, 0.9);
--tw-prose-bold: rgba(255, 255, 255, 0.9);
--tw-prose-counters: rgba(255, 255, 255, 0.6);
--tw-prose-bullets: rgba(255, 255, 255, 0.5);
--tw-prose-hr: rgba(255, 255, 255, 0.2);
--tw-prose-quotes: rgba(255, 255, 255, 0.8);
--tw-prose-quote-borders: rgba(255, 255, 255, 0.3);
--tw-prose-captions: rgba(255, 255, 255, 0.6);
--tw-prose-code: rgba(255, 255, 255, 0.9);
--tw-prose-pre-code: rgba(255, 255, 255, 0.95);
--tw-prose-pre-bg: rgba(0, 0, 0, 0.5);
--tw-prose-th-borders: rgba(255, 255, 255, 0.3);
--tw-prose-td-borders: rgba(255, 255, 255, 0.2);
}
} */
/* 标题更清晰 */
.dark .prose h1,
.dark .prose h2,
.dark .prose h3,
.dark .prose h4 {
color: rgba(255, 255, 255, 0.95);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose h1,
:root:not(.light) .prose h2,
:root:not(.light) .prose h3,
:root:not(.light) .prose h4 {
color: rgba(255, 255, 255, 0.95);
}
} */
/* 链接更清晰 */
.dark .prose a {
color: rgba(147, 197, 253, 0.9);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose a {
color: rgba(147, 197, 253, 0.9);
}
} */
/* 代码块背景更黑 */
.dark .prose pre {
background-color: rgba(0, 0, 0, 0.6);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose pre {
background-color: rgba(0, 0, 0, 0.6);
}
} */
/* 表格样式 */
.dark .prose table {
color: rgba(255, 255, 255, 0.7);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose table {
color: rgba(255, 255, 255, 0.7);
}
} */
.dark .prose thead {
color: rgba(255, 255, 255, 0.9);
border-bottom-color: rgba(255, 255, 255, 0.3);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose thead {
color: rgba(255, 255, 255, 0.9);
border-bottom-color: rgba(255, 255, 255, 0.3);
}
} */
.dark .prose tbody tr {
border-bottom-color: rgba(255, 255, 255, 0.2);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose tbody tr {
border-bottom-color: rgba(255, 255, 255, 0.2);
}
} */
/* 隐藏滚动条,但保持滚动功能 */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
@@ -2,6 +2,8 @@ import { createRoot } from 'react-dom/client'
import { Route, Router, Switch } from 'wouter' import { Route, Router, Switch } from 'wouter'
import { useHashLocation } from 'wouter/use-hash-location' import { useHashLocation } from 'wouter/use-hash-location'
import './i18n/config'
import './i18n/types'
import { default as PagesRouter } from './router.tsx' import { default as PagesRouter } from './router.tsx'
import { default as TestPagesRouter } from './test-pages/router.tsx' import { default as TestPagesRouter } from './test-pages/router.tsx'
+113 -95
View File
@@ -1,14 +1,14 @@
/* eslint-disable react-dom/no-dangerously-set-innerhtml */ /* eslint-disable react-dom/no-dangerously-set-innerhtml */
import { useState } from 'react' import { PageAgent } from 'page-agent'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, useSearchParams } from 'wouter' import { Link, useSearchParams } from 'wouter'
import { PageAgent } from '@/PageAgent.js'
import Footer from './components/Footer' import Footer from './components/Footer'
import Header from './components/Header' import Header from './components/Header'
const injection = encodeURI( const injection = encodeURI(
"javascript:(function(){var s=document.createElement('script');s.src=`https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/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);})();" "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);})();"
) )
const injectionA = ` const injectionA = `
@@ -17,7 +17,7 @@ const injectionA = `
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" 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" draggable="true"
onclick="return false;" onclick="return false;"
title="拖拽我到收藏夹栏" title="Drag me to your bookmarks bar!"
> >
PageAgent PageAgent
</a> </a>
@@ -25,7 +25,14 @@ const injectionA = `
` `
export default function HomePage() { export default function HomePage() {
const [task, setTask] = useState('进入文档页,打开数据脱敏相关的文档,帮我总结成 markdown') const { t, i18n } = useTranslation(['home', 'common'])
const [task, setTask] = useState(() => t('home:hero.default_task'))
// Update task when language changes
const defaultTask = t('home:hero.default_task')
useEffect(() => {
setTask(defaultTask)
}, [defaultTask])
const [params, setParams] = useSearchParams() const [params, setParams] = useSearchParams()
const isOther = params.has('try_other') const isOther = params.has('try_other')
@@ -36,14 +43,17 @@ export default function HomePage() {
if (!task.trim()) return if (!task.trim()) return
let pageAgent: PageAgent let pageAgent: PageAgent
const win = window as any
if (window.pageAgent && !window.pageAgent.disposed) { if (win.pageAgent && !win.pageAgent.disposed) {
pageAgent = window.pageAgent pageAgent = win.pageAgent
} else { } else {
pageAgent = new PageAgent({ pageAgent = new PageAgent({
// 把 react 根元素排除掉,挂了很多冒泡时间导致假阳 // 把 react 根元素排除掉,挂了很多冒泡时间导致假阳
interactiveBlacklist: [document.getElementById('root')!], interactiveBlacklist: [document.getElementById('root')!],
language: 'zh-CN', language: i18n.language as any,
// experimentalScriptExecutionTool: true,
// testing server // testing server
// @note: rate limit. prompt limit. // @note: rate limit. prompt limit.
@@ -51,7 +61,7 @@ export default function HomePage() {
// baseURL: DEMO_BASE_URL, // baseURL: DEMO_BASE_URL,
// apiKey: DEMO_API_KEY, // apiKey: DEMO_API_KEY,
}) })
window.pageAgent = pageAgent win.pageAgent = pageAgent
} }
const result = await pageAgent.execute(task) const result = await pageAgent.execute(task)
@@ -60,7 +70,7 @@ export default function HomePage() {
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800"> <div className="min-h-screen bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800">
<Header /> <Header />
{/* Hero Section */} {/* Hero Section */}
@@ -69,8 +79,8 @@ export default function HomePage() {
<div className="max-w-7xl mx-auto text-center"> <div className="max-w-7xl mx-auto text-center">
{/* Background Pattern */} {/* Background Pattern */}
<div className="absolute inset-0 opacity-30" aria-hidden="true"> <div className="absolute inset-0 opacity-30" aria-hidden="true">
<div className="absolute inset-0 bg-gradient-to-r from-blue-400/20 to-purple-400/20 rounded-3xl transform rotate-1"></div> <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-gradient-to-l from-purple-400/20 to-blue-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> </div>
<div className="relative z-10"> <div className="relative z-10">
@@ -79,25 +89,25 @@ export default function HomePage() {
className="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse" className="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse"
aria-hidden="true" aria-hidden="true"
></span> ></span>
UI Agent in your webpage {t('home:hero.badge')}
</div> </div>
<h1 <h1
id="hero-heading" id="hero-heading"
className="text-5xl lg:text-7xl font-bold mb-8 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent" className="text-5xl lg:text-7xl font-bold mb-8 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent pb-1"
> >
Web {t('home:hero.title_line1')}
<br /> <br />
AI {t('home:hero.title_line2')}
</h1> </h1>
<p className="text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-4xl mx-auto leading-relaxed"> <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-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold"> <span className="bg-linear-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold">
🪄 CDN {t('home:hero.subtitle_emoji')}
</span> </span>
UI Agent {t('home:hero.subtitle_main')}
<br /> <br />
/AI {t('home:hero.subtitle_detail')}
</p> </p>
{/* Try It Now Section - Tab Card */} {/* Try It Now Section - Tab Card */}
@@ -110,21 +120,21 @@ export default function HomePage() {
onClick={() => setActiveTab('try')} 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 ${
activeTab === 'try' activeTab === 'try'
? 'bg-gradient-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' ? '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' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
}`} }`}
> >
🚀 {t('home:hero.tab_try')}
</button> </button>
<button <button
onClick={() => setActiveTab('other')} 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 ${
activeTab === 'other' activeTab === 'other'
? 'bg-gradient-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' ? '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' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
}`} }`}
> >
🌐 {t('home:hero.tab_other')}
</button> </button>
</div> </div>
@@ -136,7 +146,7 @@ export default function HomePage() {
<input <input
value={task} value={task}
onChange={(e) => setTask(e.target.value)} onChange={(e) => setTask(e.target.value)}
placeholder="输入您想要 AI 执行的任务..." placeholder={t('home:hero.input_placeholder')}
className="w-full px-4 py-3 pr-20 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm mb-0" className="w-full px-4 py-3 pr-20 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm mb-0"
data-page-agent-not-interactive data-page-agent-not-interactive
/> />
@@ -144,10 +154,10 @@ export default function HomePage() {
onClick={handleExecute} onClick={handleExecute}
// disabled // disabled
// disabled={!task.trim()} // disabled={!task.trim()}
className="absolute right-2 top-2 px-5 py-1.5 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-md hover:shadow-md transform hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none text-sm" className="absolute right-2 top-2 px-5 py-1.5 bg-linear-to-r from-blue-600 to-purple-600 text-white font-medium rounded-md hover:shadow-md transform hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none text-sm"
data-page-agent-not-interactive data-page-agent-not-interactive
> >
{t('home:hero.execute_button')}
</button> </button>
</div> </div>
</div> </div>
@@ -160,7 +170,10 @@ export default function HomePage() {
{/* Keyboard Shortcut Hint */} {/* Keyboard Shortcut Hint */}
<div className="bg-blue-50 dark:bg-gray-700 p-4 rounded-lg"> <div className="bg-blue-50 dark:bg-gray-700 p-4 rounded-lg">
<p className="text-gray-700 dark:text-gray-300 text-sm mb-3"> <p className="text-gray-700 dark:text-gray-300 text-sm mb-3">
<span className="font-semibold"> 1:</span> <span className="font-semibold">
{t('home:try_other.step1_title')}
</span>{' '}
{t('home:try_other.step1_content')}
</p> </p>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<kbd className="px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono"> <kbd className="px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono">
@@ -176,8 +189,10 @@ export default function HomePage() {
{/* Draggable Bookmarklet */} {/* Draggable Bookmarklet */}
<div className="bg-green-50 dark:bg-gray-700 p-4 rounded-lg"> <div className="bg-green-50 dark:bg-gray-700 p-4 rounded-lg">
<p className="text-gray-700 dark:text-gray-300 text-sm mb-3"> <p className="text-gray-700 dark:text-gray-300 text-sm mb-3">
<span className="font-semibold"> 2:</span>{' '} <span className="font-semibold">
{t('home:try_other.step2_title')}
</span>{' '}
{t('home:try_other.step2_content')}
</p> </p>
<div <div
className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400" className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400"
@@ -188,8 +203,10 @@ export default function HomePage() {
{/* Usage Instructions */} {/* Usage Instructions */}
<div className="bg-purple-50 dark:bg-gray-700 p-4 rounded-lg"> <div className="bg-purple-50 dark:bg-gray-700 p-4 rounded-lg">
<p className="text-gray-700 dark:text-gray-300 text-sm"> <p className="text-gray-700 dark:text-gray-300 text-sm">
<span className="font-semibold"> 3:</span>{' '} <span className="font-semibold">
使 {t('home:try_other.step3_title')}
</span>{' '}
{t('home:try_other.step3_content')}
</p> </p>
</div> </div>
</div> </div>
@@ -197,37 +214,37 @@ export default function HomePage() {
{/* 右侧:注意事项 */} {/* 右侧:注意事项 */}
<div className="bg-yellow-50 dark:bg-gray-700 p-4 rounded-lg"> <div className="bg-yellow-50 dark:bg-gray-700 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 dark:text-white mb-3 text-sm"> <h4 className="font-semibold text-gray-900 dark:text-white mb-3 text-sm">
{t('home:try_other.notice_title')}
</h4> </h4>
<ul className="space-y-2 text-sm text-gray-700 dark:text-gray-300"> <ul className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li className="flex items-start text-left"> <li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span> <span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item1')}
</li> </li>
<li className="flex items-start text-left"> <li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span> <span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
使 DeepSeek DeepSeek {t('home:try_other.notice_items.item2')}
</li> </li>
<li className="flex items-start text-left"> <li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span> <span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item3')}
</li> </li>
<li className="flex items-start text-left"> <li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span> <span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item4')}
</li> </li>
<li className="flex items-start text-left"> <li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span> <span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item5')}
</li> </li>
<li className="flex items-start text-left"> <li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 flex-shrink-0 "></span> <span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
使{' '} {t('home:try_other.notice_items.item6_prefix')}{' '}
<Link <Link
href="/docs/introduction/limitations" href="/docs/introduction/limitations"
className="text-blue-600 dark:text-blue-400 hover:underline" className="text-blue-600 dark:text-blue-400 hover:underline"
> >
{t('home:try_other.notice_items.item6_link')}
</Link> </Link>
</li> </li>
</ul> </ul>
@@ -248,28 +265,28 @@ export default function HomePage() {
className="w-2 h-2 bg-green-500 rounded-full mr-2" className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true" aria-hidden="true"
></span> ></span>
{t('home:benefits.no_backend')}
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<span <span
className="w-2 h-2 bg-green-500 rounded-full mr-2" className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true" aria-hidden="true"
></span> ></span>
{t('home:benefits.private_model')}
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<span <span
className="w-2 h-2 bg-green-500 rounded-full mr-2" className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true" aria-hidden="true"
></span> ></span>
{t('home:benefits.data_masking')}
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<span <span
className="w-2 h-2 bg-green-500 rounded-full mr-2" className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true" aria-hidden="true"
></span> ></span>
DOM {t('home:benefits.open_source')}
</li> </li>
</ul> </ul>
</div> </div>
@@ -285,75 +302,77 @@ export default function HomePage() {
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8" role="list"> <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8" role="list">
{/* Feature 1 */} {/* Feature 1 */}
<article <article
className="group p-8 bg-gradient-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700" className="group p-8 bg-linear-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem" role="listitem"
> >
<div <div
className="w-12 h-12 bg-gradient-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-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"
aria-hidden="true" aria-hidden="true"
> >
<span className="text-white text-xl">🧠</span> <span className="text-white text-xl">📦</span>
</div> </div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white"> <h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
DOM {t('home:features.in_page.title')}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed"> <p className="text-gray-600 dark:text-gray-300 leading-relaxed">
DOM {t('home:features.in_page.desc')}
</p> </p>
</article> </article>
{/* Feature 2 */} {/* Feature 2 */}
<article <article
className="group p-8 bg-gradient-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700" className="group p-8 bg-linear-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem" role="listitem"
> >
<div <div
className="w-12 h-12 bg-gradient-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-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"
aria-hidden="true" aria-hidden="true"
> >
<span className="text-white text-xl">🔒</span> <span className="text-white text-xl"></span>
</div> </div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white"> <h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.zero_backend.title')}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed"> <p className="text-gray-600 dark:text-gray-300 leading-relaxed">
AI {t('home:features.zero_backend.desc')}
</p> </p>
</article> </article>
{/* Feature 3 */} {/* Feature 3 */}
<article <article
className="group p-8 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700" className="group p-8 bg-linear-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem" role="listitem"
> >
<div <div
className="w-12 h-12 bg-gradient-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-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"
aria-hidden="true" aria-hidden="true"
> >
<span className="text-white text-xl"></span> <span className="text-white text-xl">🌈</span>
</div> </div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white"></h3> <h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.accessible.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed"> <p className="text-gray-600 dark:text-gray-300 leading-relaxed">
CDN LLM OpenAI qwen3 {t('home:features.accessible.desc')}
</p> </p>
</article> </article>
{/* Feature 4 */} {/* Feature 4 */}
<article <article
className="group p-8 bg-gradient-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700" className="group p-8 bg-linear-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem" role="listitem"
> >
<div <div
className="w-12 h-12 bg-gradient-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-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"
aria-hidden="true" aria-hidden="true"
> >
<span className="text-white text-xl"></span> <span className="text-white text-xl">🔒</span>
</div> </div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white"> <h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.secure_integration.title')}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed"> <p className="text-gray-600 dark:text-gray-300 leading-relaxed">
B端系统 {t('home:features.secure_integration.desc')}
</p> </p>
</article> </article>
</div> </div>
@@ -366,81 +385,80 @@ export default function HomePage() {
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 <h2
id="use-cases-heading" id="use-cases-heading"
className="text-4xl lg:text-5xl mb-6 text-gray-900 dark:text-white" 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"
> >
{t('home:use_cases.section_title')}
</h2> </h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"> <p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
AI {t('home:use_cases.section_subtitle')}
</p> </p>
</div> </div>
<div className="grid lg:grid-cols-2 gap-12" role="list"> <div className="grid lg:grid-cols-2 gap-12" role="list">
{/* Use Case 1 */} {/* Use Case 1 */}
<div className="bg-gradient-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl"> <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="flex items-start space-x-4 h-20"> <div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0"> <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> <span className="text-white font-bold">1</span>
</div> </div>
<div> <div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white"> <h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case1.title')}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-gray-600 dark:text-gray-300">
Agent"请先点击设置按钮然后点击..." {t('home:use_cases.case1.desc')}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{/* Use Case 2 */} {/* Use Case 2 */}
<div className="bg-gradient-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl"> <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="flex items-start space-x-4 h-20"> <div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0"> <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> <span className="text-white font-bold">2</span>
</div> </div>
<div> <div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white"> <h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
/ {t('home:use_cases.case2.title')}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-gray-600 dark:text-gray-300">
Agent B {t('home:use_cases.case2.desc')}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{/* Use Case 3 */} {/* Use Case 3 */}
<div className="bg-gradient-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl"> <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="flex items-start space-x-4 h-20"> <div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-purple-500 rounded-lg flex items-center justify-center flex-shrink-0"> <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> <span className="text-white font-bold">3</span>
</div> </div>
<div> <div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white"> <h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case3.title')}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-gray-600 dark:text-gray-300">
AI演示"如何提交报销申请" {t('home:use_cases.case3.desc')}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{/* Use Case 4 */} {/* Use Case 4 */}
<div className="bg-gradient-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl"> <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="flex items-start space-x-4 h-20"> <div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center flex-shrink-0"> <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> <span className="text-white font-bold">4</span>
</div> </div>
<div> <div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white"> <h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case4.title')}
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-gray-600 dark:text-gray-300">
{t('home:use_cases.case4.desc')}
</p> </p>
</div> </div>
</div> </div>
@@ -19,19 +19,31 @@ interface WizardStep {
export default function ComplexTestPage() { export default function ComplexTestPage() {
const [currentStep, setCurrentStep] = useState(1) const [currentStep, setCurrentStep] = useState(1)
const [cartItems, setCartItems] = useState<CartItem[]>([ const [cartItems, setCartItems] = useState<CartItem[]>([
{ id: 1, name: 'iPhone 15 Pro', price: 7999, quantity: 1, image: 'https://picsum.photos/100/100?random=1' }, {
{ id: 2, name: 'MacBook Air', price: 8999, quantity: 1, image: 'https://picsum.photos/100/100?random=2' } id: 1,
name: 'iPhone 15 Pro',
price: 7999,
quantity: 1,
image: 'https://picsum.photos/100/100?random=1',
},
{
id: 2,
name: 'MacBook Air',
price: 8999,
quantity: 1,
image: 'https://picsum.photos/100/100?random=2',
},
]) ])
const [wizardData, setWizardData] = useState({ const [wizardData, setWizardData] = useState({
personalInfo: { name: '', email: '', phone: '' }, personalInfo: { name: '', email: '', phone: '' },
address: { street: '', city: '', zipCode: '' }, address: { street: '', city: '', zipCode: '' },
payment: { cardNumber: '', expiryDate: '', cvv: '' } payment: { cardNumber: '', expiryDate: '', cvv: '' },
}) })
const [wizardSteps, setWizardSteps] = useState<WizardStep[]>([ const [wizardSteps, setWizardSteps] = useState<WizardStep[]>([
{ id: 1, title: '个人信息', description: '填写基本信息', completed: false }, { id: 1, title: '个人信息', description: '填写基本信息', completed: false },
{ id: 2, title: '收货地址', description: '填写收货地址', completed: false }, { id: 2, title: '收货地址', description: '填写收货地址', completed: false },
{ id: 3, title: '支付方式', description: '选择支付方式', completed: false }, { id: 3, title: '支付方式', description: '选择支付方式', completed: false },
{ id: 4, title: '确认订单', description: '确认订单信息', completed: false } { id: 4, title: '确认订单', description: '确认订单信息', completed: false },
]) ])
const [showConfirmDialog, setShowConfirmDialog] = useState(false) const [showConfirmDialog, setShowConfirmDialog] = useState(false)
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
@@ -43,15 +55,13 @@ export default function ComplexTestPage() {
removeItem(id) removeItem(id)
return return
} }
setCartItems(prev => setCartItems((prev) =>
prev.map(item => prev.map((item) => (item.id === id ? { ...item, quantity: newQuantity } : item))
item.id === id ? { ...item, quantity: newQuantity } : item
)
) )
} }
const removeItem = (id: number) => { const removeItem = (id: number) => {
setCartItems(prev => prev.filter(item => item.id !== id)) setCartItems((prev) => prev.filter((item) => item.id !== id))
} }
const addItem = () => { const addItem = () => {
@@ -60,9 +70,9 @@ export default function ComplexTestPage() {
name: `新产品 ${cartItems.length + 1}`, name: `新产品 ${cartItems.length + 1}`,
price: Math.floor(Math.random() * 5000) + 1000, price: Math.floor(Math.random() * 5000) + 1000,
quantity: 1, quantity: 1,
image: `https://picsum.photos/100/100?random=${Date.now()}` image: `https://picsum.photos/100/100?random=${Date.now()}`,
} }
setCartItems(prev => [...prev, newItem]) setCartItems((prev) => [...prev, newItem])
} }
const getTotalPrice = () => { const getTotalPrice = () => {
@@ -73,11 +83,23 @@ export default function ComplexTestPage() {
const validateStep = (step: number): boolean => { const validateStep = (step: number): boolean => {
switch (step) { switch (step) {
case 1: case 1:
return !!(wizardData.personalInfo.name && wizardData.personalInfo.email && wizardData.personalInfo.phone) return !!(
wizardData.personalInfo.name &&
wizardData.personalInfo.email &&
wizardData.personalInfo.phone
)
case 2: case 2:
return !!(wizardData.address.street && wizardData.address.city && wizardData.address.zipCode) return !!(
wizardData.address.street &&
wizardData.address.city &&
wizardData.address.zipCode
)
case 3: case 3:
return !!(wizardData.payment.cardNumber && wizardData.payment.expiryDate && wizardData.payment.cvv) return !!(
wizardData.payment.cardNumber &&
wizardData.payment.expiryDate &&
wizardData.payment.cvv
)
default: default:
return true return true
} }
@@ -92,10 +114,8 @@ export default function ComplexTestPage() {
// 更新步骤完成状态 // 更新步骤完成状态
if (step > currentStep) { if (step > currentStep) {
setWizardSteps(prev => setWizardSteps((prev) =>
prev.map(s => prev.map((s) => (s.id === currentStep ? { ...s, completed: true } : s))
s.id === currentStep ? { ...s, completed: true } : s
)
) )
} }
@@ -103,21 +123,21 @@ export default function ComplexTestPage() {
} }
const handleInputChange = (section: string, field: string, value: string) => { const handleInputChange = (section: string, field: string, value: string) => {
setWizardData(prev => ({ setWizardData((prev) => ({
...prev, ...prev,
[section]: { [section]: {
...prev[section as keyof typeof prev], ...prev[section as keyof typeof prev],
[field]: value [field]: value,
} },
})) }))
} }
const handleSubmitOrder = async () => { const handleSubmitOrder = async () => {
setIsProcessing(true) setIsProcessing(true)
// 模拟处理时间 // 模拟处理时间
await new Promise(resolve => setTimeout(resolve, 3000)) await new Promise((resolve) => setTimeout(resolve, 3000))
// 模拟随机失败 // 模拟随机失败
if (Math.random() < 0.2) { if (Math.random() < 0.2) {
setIsProcessing(false) setIsProcessing(false)
@@ -135,9 +155,9 @@ export default function ComplexTestPage() {
setWizardData({ setWizardData({
personalInfo: { name: '', email: '', phone: '' }, personalInfo: { name: '', email: '', phone: '' },
address: { street: '', city: '', zipCode: '' }, address: { street: '', city: '', zipCode: '' },
payment: { cardNumber: '', expiryDate: '', cvv: '' } payment: { cardNumber: '', expiryDate: '', cvv: '' },
}) })
setWizardSteps(prev => prev.map(s => ({ ...s, completed: false }))) setWizardSteps((prev) => prev.map((s) => ({ ...s, completed: false })))
setOrderComplete(false) setOrderComplete(false)
setShowConfirmDialog(false) setShowConfirmDialog(false)
} }
@@ -162,7 +182,10 @@ export default function ComplexTestPage() {
> >
</button> </button>
<Link href="/test-pages" className="block w-full bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-md transition-colors text-center"> <Link
href="/test-pages"
className="block w-full bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-md transition-colors text-center"
>
</Link> </Link>
</div> </div>
@@ -176,12 +199,8 @@ export default function ComplexTestPage() {
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-6xl mx-auto px-4"> <div className="max-w-6xl mx-auto px-4">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"> <h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"></h1>
<p className="text-gray-600 dark:text-gray-300"></p>
</h1>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
@@ -191,10 +210,13 @@ export default function ComplexTestPage() {
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
({cartItems.length}) ({cartItems.length})
</h3> </h3>
<div className="space-y-4 mb-6"> <div className="space-y-4 mb-6">
{cartItems.map(item => ( {cartItems.map((item) => (
<div key={item.id} className="flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-600 rounded-lg"> <div
key={item.id}
className="flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-600 rounded-lg"
>
<img <img
src={item.image} src={item.image}
alt={item.name} alt={item.name}
@@ -215,9 +237,7 @@ export default function ComplexTestPage() {
> >
- -
</button> </button>
<span className="text-sm font-medium w-8 text-center"> <span className="text-sm font-medium w-8 text-center">{item.quantity}</span>
{item.quantity}
</span>
<button <button
onClick={() => updateQuantity(item.id, item.quantity + 1)} onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="w-6 h-6 flex items-center justify-center bg-gray-200 dark:bg-gray-600 rounded text-sm hover:bg-gray-300 dark:hover:bg-gray-500" className="w-6 h-6 flex items-center justify-center bg-gray-200 dark:bg-gray-600 rounded text-sm hover:bg-gray-300 dark:hover:bg-gray-500"
@@ -265,16 +285,18 @@ export default function ComplexTestPage() {
step.completed step.completed
? 'bg-green-500 text-white' ? 'bg-green-500 text-white'
: step.id === currentStep : step.id === currentStep
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400' : 'bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400'
}`} }`}
> >
{step.completed ? '✓' : step.id} {step.completed ? '✓' : step.id}
</button> </button>
{index < wizardSteps.length - 1 && ( {index < wizardSteps.length - 1 && (
<div className={`w-16 h-1 mx-2 ${ <div
step.completed ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-600' className={`w-16 h-1 mx-2 ${
}`} /> step.completed ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-600'
}`}
/>
)} )}
</div> </div>
))} ))}
@@ -395,7 +417,9 @@ export default function ComplexTestPage() {
<input <input
type="text" type="text"
value={wizardData.payment.expiryDate} value={wizardData.payment.expiryDate}
onChange={(e) => handleInputChange('payment', 'expiryDate', e.target.value)} onChange={(e) =>
handleInputChange('payment', 'expiryDate', e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="MM/YY" placeholder="MM/YY"
/> />
@@ -419,25 +443,35 @@ export default function ComplexTestPage() {
{currentStep === 4 && ( {currentStep === 4 && (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-4"></h4> <h4 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h4>
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg"> <div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 dark:text-white mb-2"></h5> <h5 className="font-medium text-gray-900 dark:text-white mb-2">
</h5>
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
{wizardData.personalInfo.name} | {wizardData.personalInfo.email} | {wizardData.personalInfo.phone} {wizardData.personalInfo.name} | {wizardData.personalInfo.email} |{' '}
{wizardData.personalInfo.phone}
</p> </p>
</div> </div>
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg"> <div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 dark:text-white mb-2"></h5> <h5 className="font-medium text-gray-900 dark:text-white mb-2">
</h5>
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
{wizardData.address.street}, {wizardData.address.city} {wizardData.address.zipCode} {wizardData.address.street}, {wizardData.address.city}{' '}
{wizardData.address.zipCode}
</p> </p>
</div> </div>
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg"> <div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 dark:text-white mb-2"></h5> <h5 className="font-medium text-gray-900 dark:text-white mb-2">
</h5>
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-gray-600 dark:text-gray-300">
**** **** **** {wizardData.payment.cardNumber.slice(-4)} **** **** **** {wizardData.payment.cardNumber.slice(-4)}
</p> </p>
@@ -457,7 +491,7 @@ export default function ComplexTestPage() {
> >
</button> </button>
{currentStep < 4 ? ( {currentStep < 4 ? (
<button <button
onClick={() => goToStep(currentStep + 1)} onClick={() => goToStep(currentStep + 1)}
@@ -505,9 +539,25 @@ export default function ComplexTestPage() {
> >
{isProcessing ? ( {isProcessing ? (
<> <>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
... ...
</> </>
@@ -17,7 +17,7 @@ export default function ErrorTestPage() {
username: '', username: '',
password: '', password: '',
email: '', email: '',
file: null as File | null file: null as File | null,
}) })
const errorScenarios: ErrorScenario[] = [ const errorScenarios: ErrorScenario[] = [
@@ -25,32 +25,32 @@ export default function ErrorTestPage() {
id: 'network-error', id: 'network-error',
title: '网络连接错误', title: '网络连接错误',
description: '模拟网络连接失败,测试重试机制', description: '模拟网络连接失败,测试重试机制',
type: 'network' type: 'network',
}, },
{ {
id: 'validation-error', id: 'validation-error',
title: '表单验证错误', title: '表单验证错误',
description: '模拟表单验证失败,测试错误提示', description: '模拟表单验证失败,测试错误提示',
type: 'validation' type: 'validation',
}, },
{ {
id: 'permission-error', id: 'permission-error',
title: '权限不足错误', title: '权限不足错误',
description: '模拟权限验证失败,测试权限处理', description: '模拟权限验证失败,测试权限处理',
type: 'permission' type: 'permission',
}, },
{ {
id: 'timeout-error', id: 'timeout-error',
title: '请求超时错误', title: '请求超时错误',
description: '模拟请求超时,测试超时处理', description: '模拟请求超时,测试超时处理',
type: 'timeout' type: 'timeout',
}, },
{ {
id: 'server-error', id: 'server-error',
title: '服务器内部错误', title: '服务器内部错误',
description: '模拟服务器500错误,测试错误恢复', description: '模拟服务器500错误,测试错误恢复',
type: 'server' type: 'server',
} },
] ]
const simulateError = async (scenario: ErrorScenario): Promise<void> => { const simulateError = async (scenario: ErrorScenario): Promise<void> => {
@@ -59,7 +59,7 @@ export default function ErrorTestPage() {
setSuccess(null) setSuccess(null)
// 模拟网络延迟 // 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000)) await new Promise((resolve) => setTimeout(resolve, 1000 + Math.random() * 2000))
switch (scenario.type) { switch (scenario.type) {
case 'network': case 'network':
@@ -91,7 +91,7 @@ export default function ErrorTestPage() {
case 'timeout': case 'timeout':
// 模拟超时 // 模拟超时
await new Promise(resolve => setTimeout(resolve, 8000)) await new Promise((resolve) => setTimeout(resolve, 8000))
throw new Error('请求超时:服务器响应时间过长,请稍后重试') throw new Error('请求超时:服务器响应时间过长,请稍后重试')
case 'server': case 'server':
@@ -117,7 +117,7 @@ export default function ErrorTestPage() {
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : '未知错误' const errorMessage = err instanceof Error ? err.message : '未知错误'
setError(errorMessage) setError(errorMessage)
setRetryCount(prev => prev + 1) setRetryCount((prev) => prev + 1)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@@ -154,7 +154,7 @@ export default function ErrorTestPage() {
} }
// 模拟上传过程 // 模拟上传过程
await new Promise(resolve => setTimeout(resolve, 2000)) await new Promise((resolve) => setTimeout(resolve, 2000))
// 模拟随机失败 // 模拟随机失败
if (Math.random() < 0.3) { if (Math.random() < 0.3) {
@@ -178,12 +178,18 @@ export default function ErrorTestPage() {
const getErrorIcon = (type: string) => { const getErrorIcon = (type: string) => {
switch (type) { switch (type) {
case 'network': return '🌐' case 'network':
case 'validation': return '⚠️' return '🌐'
case 'permission': return '🔒' case 'validation':
case 'timeout': return '' return '⚠️'
case 'server': return '🔧' case 'permission':
default: return '' return '🔒'
case 'timeout':
return '⏰'
case 'server':
return '🔧'
default:
return '❌'
} }
} }
@@ -191,9 +197,7 @@ export default function ErrorTestPage() {
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-4xl mx-auto px-4"> <div className="max-w-4xl mx-auto px-4">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"> <h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"></h1>
</h1>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-gray-600 dark:text-gray-300">
Agent Agent
</p> </p>
@@ -205,18 +209,18 @@ export default function ErrorTestPage() {
{error && ( {error && (
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg p-4 mb-4"> <div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg p-4 mb-4">
<div className="flex items-start"> <div className="flex items-start">
<div className="flex-shrink-0"> <div className="shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /> <path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg> </svg>
</div> </div>
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200"> <h3 className="text-sm font-medium text-red-800 dark:text-red-200"></h3>
<p className="mt-1 text-sm text-red-700 dark:text-red-300">{error}</p>
</h3>
<p className="mt-1 text-sm text-red-700 dark:text-red-300">
{error}
</p>
{retryCount > 0 && ( {retryCount > 0 && (
<p className="mt-2 text-xs text-red-600 dark:text-red-400"> <p className="mt-2 text-xs text-red-600 dark:text-red-400">
{retryCount} {retryCount >= 3 && '(已达最大重试次数)'} {retryCount} {retryCount >= 3 && '(已达最大重试次数)'}
@@ -228,7 +232,11 @@ export default function ErrorTestPage() {
className="ml-3 text-red-400 hover:text-red-600 dark:hover:text-red-300" className="ml-3 text-red-400 hover:text-red-600 dark:hover:text-red-300"
> >
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" /> <path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -238,25 +246,31 @@ export default function ErrorTestPage() {
{success && ( {success && (
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-4 mb-4"> <div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-4 mb-4">
<div className="flex items-start"> <div className="flex items-start">
<div className="flex-shrink-0"> <div className="shrink-0">
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /> <path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg> </svg>
</div> </div>
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-green-800 dark:text-green-200"> <h3 className="text-sm font-medium text-green-800 dark:text-green-200">
</h3> </h3>
<p className="mt-1 text-sm text-green-700 dark:text-green-300"> <p className="mt-1 text-sm text-green-700 dark:text-green-300">{success}</p>
{success}
</p>
</div> </div>
<button <button
onClick={clearMessages} onClick={clearMessages}
className="ml-3 text-green-400 hover:text-green-600 dark:hover:text-green-300" className="ml-3 text-green-400 hover:text-green-600 dark:hover:text-green-300"
> >
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" /> <path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -268,9 +282,7 @@ export default function ErrorTestPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 错误场景测试 */} {/* 错误场景测试 */}
<div className="space-y-6"> <div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white"> <h2 className="text-xl font-semibold text-gray-900 dark:text-white"></h2>
</h2>
{errorScenarios.map((scenario) => ( {errorScenarios.map((scenario) => (
<div key={scenario.id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <div key={scenario.id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
@@ -309,9 +321,7 @@ export default function ErrorTestPage() {
{/* 表单验证测试 */} {/* 表单验证测试 */}
<div className="space-y-6"> <div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white"> <h2 className="text-xl font-semibold text-gray-900 dark:text-white"></h2>
</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
@@ -325,7 +335,7 @@ export default function ErrorTestPage() {
<input <input
type="text" type="text"
value={formData.username} value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))} onChange={(e) => setFormData((prev) => ({ ...prev, username: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入用户名" placeholder="请输入用户名"
/> />
@@ -337,7 +347,7 @@ export default function ErrorTestPage() {
<input <input
type="password" type="password"
value={formData.password} value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))} onChange={(e) => setFormData((prev) => ({ ...prev, password: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入密码" placeholder="请输入密码"
/> />
@@ -349,13 +359,15 @@ export default function ErrorTestPage() {
<input <input
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))} onChange={(e) => setFormData((prev) => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入邮箱地址" placeholder="请输入邮箱地址"
/> />
</div> </div>
<button <button
onClick={() => handleScenarioTest(errorScenarios.find(s => s.type === 'validation')!)} onClick={() =>
handleScenarioTest(errorScenarios.find((s) => s.type === 'validation')!)
}
disabled={isLoading} disabled={isLoading}
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors" className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors"
> >
@@ -376,14 +388,17 @@ export default function ErrorTestPage() {
</label> </label>
<input <input
type="file" type="file"
onChange={(e) => setFormData(prev => ({ ...prev, file: e.target.files?.[0] || null }))} onChange={(e) =>
setFormData((prev) => ({ ...prev, file: e.target.files?.[0] || null }))
}
accept="image/*,.pdf" accept="image/*,.pdf"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/> />
</div> </div>
{formData.file && ( {formData.file && (
<div className="text-sm text-gray-600 dark:text-gray-300"> <div className="text-sm text-gray-600 dark:text-gray-300">
: {formData.file.name} ({(formData.file.size / 1024 / 1024).toFixed(2)} MB) : {formData.file.name} ({(formData.file.size / 1024 / 1024).toFixed(2)}{' '}
MB)
</div> </div>
)} )}
<button <button
@@ -412,9 +427,25 @@ export default function ErrorTestPage() {
{isLoading && ( {isLoading && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 flex items-center space-x-4"> <div className="bg-white dark:bg-gray-800 rounded-lg p-6 flex items-center space-x-4">
<svg className="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> className="animate-spin h-8 w-8 text-blue-600"
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
<span className="text-gray-900 dark:text-white">...</span> <span className="text-gray-900 dark:text-white">...</span>
</div> </div>

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