Compare commits

...

43 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
113 changed files with 2353 additions and 1859 deletions
+21 -27
View File
@@ -6,32 +6,11 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Please follow our community communication guidelines when filing issues. Thanks for your interest in improving the project! Before submitting, please read our guidelines.
- Be polite and respectful. 感谢您对改进项目的兴趣!提交前请阅读我们的指南。
- Prefer constructive, actionable feedback over venting.
- Include clear reproduction steps and environment details.
- Consider contributing a fix if possible — PRs are welcome.
提交 Issue 时请遵守社区沟通规范: - [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
- 保持礼貌与尊重。 - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
- 提供建设性、可行动的反馈,避免情绪化表达。
- 请包含清晰的复现步骤与环境信息。
- 如果可以,欢迎直接提交修复 PR。
See: CODE_OF_CONDUCT.md / CODE_OF_CONDUCT_zh.md, CONTRIBUTING.md
- type: checkboxes
id: community
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true
- label: I will share constructive, actionable details. / 我会提供建设性、可行动的细节。
required: true
- label: I have read the Code of Conduct. / 我已阅读行为准则。
required: true
- type: textarea - type: textarea
id: description id: description
@@ -48,7 +27,7 @@ body:
render: typescript render: typescript
placeholder: Minimal reproduction code placeholder: Minimal reproduction code
validations: validations:
required: true required: false
- type: input - type: input
id: browser id: browser
@@ -56,12 +35,27 @@ body:
label: Browser label: Browser
placeholder: 'Chrome 120, Firefox 119, etc.' placeholder: 'Chrome 120, Firefox 119, etc.'
validations: validations:
required: true required: false
- type: input - type: input
id: version id: version
attributes: attributes:
label: version label: version
placeholder: '0.0.0' placeholder: '0.0.0'
validations:
required: false
- type: checkboxes
id: community
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
required: true
- label: I have read the Code of Conduct. / 我已阅读行为准则。
required: true
validations: validations:
required: true required: true
-3
View File
@@ -12,6 +12,3 @@ contact_links:
- name: Code of Conduct / 行为准则 - name: Code of Conduct / 行为准则
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md
about: Community expectations and standards. 社区行为期望与标准。 about: Community expectations and standards. 社区行为期望与标准。
- name: 行为准则(中文)
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT_zh.md
about: 中文版社区行为准则。
+22 -33
View File
@@ -6,19 +6,30 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Please follow our community communication guidelines when proposing features. Thanks for your interest in improving the project! Before submitting, please read our guidelines.
- Be polite and respectful. 感谢您对改进项目的兴趣!提交前请阅读我们的指南。
- Explain the problem first, then propose solutions.
- Prefer constructive, actionable suggestions.
- Contributions are encouraged — consider opening a PR.
提交功能建议时请遵守社区沟通规范: - [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
- 保持礼貌与尊重。 - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
- 先阐述问题,再提出解决方案。
- 提供建设性、可行动的建议。
- 鼓励代码贡献,欢迎直接提交 PR。
See: CODE_OF_CONDUCT.md / CODE_OF_CONDUCT_zh.md, CONTRIBUTING.md - type: textarea
id: description
attributes:
label: Feature Description / 功能描述
description: Describe the problem, solution, and any API changes. / 描述问题、解决方案以及 API 变更。
placeholder: |
**Problem**:
What problem does this solve?
**Solution**:
How should this work?
**Proposed API**:
```typescript
// code here
```
validations:
required: true
- type: checkboxes - type: checkboxes
id: community id: community
@@ -32,27 +43,5 @@ body:
required: true required: true
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。 - label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
required: true required: true
- type: textarea
id: problem
attributes:
label: Problem
placeholder: What problem does this solve?
validations: validations:
required: true required: true
- type: textarea
id: solution
attributes:
label: Solution
placeholder: How should this work?
validations:
required: true
- type: textarea
id: api
attributes:
label: Proposed API
render: typescript
validations:
required: false
+5 -8
View File
@@ -5,9 +5,12 @@ Brief description of changes.
## Type ## Type
- [ ] Bug fix - [ ] Bug fix
- [ ] New feature - [ ] Feature / Improvement
- [ ] Breaking change - [ ] Refactor
- [ ] Documentation - [ ] Documentation
- [ ] Website
- [ ] Demo / Testing
- [ ] Breaking change
## Testing ## Testing
@@ -19,10 +22,4 @@ Closes #(issue)
## Requirements / 要求 ## Requirements / 要求
- [ ] I will be polite and respectful. / 我会保持礼貌与尊重。
- [ ] My comments and replies are constructive and actionable. / 我的评论与回复具有建设性。
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。 - [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
## Contributing / 贡献
Constructive suggestions and code contributions are encouraged. If this PR originated from a discussion or issue, please link it above. 欢迎建设性意见与代码贡献;如源自讨论或 Issue,请在上方关联链接。
+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
+1 -1
View File
@@ -32,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
+5
View File
@@ -27,3 +27,8 @@ 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>
+6
View File
@@ -30,6 +30,12 @@ The development progress and future plans for PageAgent.
- [ ] **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] **i18n of the website**
- [x] Chinese version - [x] Chinese version
+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,
-45
View File
@@ -1,45 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="https://img.alicdn.com/imgextra/i3/O1CN01uo9VaC20mamWNM2Zn_!!6000000006892-55-tps-64-64.svg"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PageAgent - AI-powered UI Agent in Your Webpage</title>
<meta
name="description"
content="PageAgent.js: One-line CDN import, add intelligent UI Agent to any website. Modern web AI automation with minimal integration."
/>
<meta
name="keywords"
content="PageAgent, AI Agent, Web Automation, UI Automation, Frontend, CDN, JavaScript, React, Vite, LLM"
/>
<meta name="theme-color" content="#58c0fc" />
<meta name="color-scheme" content="light dark" />
<meta name="author" content="PageAgent.js Team" />
<meta property="og:title" content="PageAgent.js - AI-powered UI Agent" />
<meta
property="og:description"
content="One-line CDN import, add intelligent UI Agent to any website."
/>
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="zh_CN" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./pages/main.tsx"></script>
<script>
// Dynamically update html lang attribute based on i18n detection
const updateHtmlLang = () => {
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
document.documentElement.lang = lang
}
updateHtmlLang()
window.addEventListener('storage', updateHtmlLang)
</script>
</body>
</html>
+845 -950
View File
File diff suppressed because it is too large Load Diff
+18 -60
View File
@@ -1,35 +1,12 @@
{ {
"name": "page-agent", "name": "root",
"private": false, "private": true,
"version": "0.0.4", "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": {
@@ -38,56 +15,37 @@
}, },
"homepage": "https://alibaba.github.io/page-agent/", "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",
"i18next": "^25.6.0",
"i18next-browser-languagedetector": "^8.2.0",
"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",
"react-i18next": "^16.1.4",
"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
@@ -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"
}
}
@@ -6,7 +6,7 @@ export default function BetaNotice() {
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">
@@ -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
@@ -54,7 +54,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
<div className="max-w-7xl mx-auto px-6 py-8 overflow-x-auto"> <div className="max-w-7xl mx-auto px-6 py-8 overflow-x-auto">
<div className="flex gap-8 min-w-[900px]"> <div className="flex gap-8 min-w-[900px]">
{/* Sidebar */} {/* Sidebar */}
<aside className="w-64 flex-shrink-0" role="complementary" aria-label="文档导航"> <aside className="w-64 shrink-0" role="complementary" aria-label="文档导航">
<div className="sticky top-8"> <div className="sticky top-8">
<nav className="space-y-8" role="navigation" aria-label="文档章节"> <nav className="space-y-8" role="navigation" aria-label="文档章节">
{navigationSections.map((section) => ( {navigationSections.map((section) => (
+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
@@ -56,7 +56,7 @@ export default function ThemeSwitcher() {
style={{ style={{
backgroundColor: theme === 'dark' ? '#1e293b' : '#e0f2fe', backgroundColor: theme === 'dark' ? '#1e293b' : '#e0f2fe',
}} }}
aria-label={theme === 'light' ? '切换到深色模式' : '切换到浅色模式'} aria-label={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
role="switch" role="switch"
aria-checked={theme === 'dark'} aria-checked={theme === 'dark'}
> >
+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,7 +1,8 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function CustomTools() { export default function CustomTools() {
const { t } = useTranslation('docs') const { t } = useTranslation('docs')
@@ -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 (
@@ -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 (
@@ -1,7 +1,8 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function ModelIntegration() { export default function ModelIntegration() {
const { t } = useTranslation('docs') const { t } = useTranslation('docs')
@@ -106,6 +107,32 @@ export default function ModelIntegration() {
</ul> </ul>
</div> </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> <h2 className="text-2xl font-bold mb-3">{t('model_integration.configuration')}</h2>
<CodeEditor <CodeEditor
@@ -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 (
@@ -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 (
@@ -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()
@@ -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,4 +1,4 @@
import CodeEditor from '@pages/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
export default function ThirdPartyAgentPage() { export default function ThirdPartyAgentPage() {
return ( return (
@@ -48,25 +48,25 @@ 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 text-gray-900 dark:text-white">🤖 </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 text-gray-900 dark:text-white">📋 </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 text-gray-900 dark:text-white">🎯 </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 text-gray-900 dark:text-white">🔧 </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">
"重启服务器" "重启服务器"
@@ -98,7 +98,7 @@ 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 text-gray-900 dark:text-white">🎉 </h3> <h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">🎉 </h3>
<p className="mb-3 text-gray-700 dark:text-gray-300"> <p className="mb-3 text-gray-700 dark:text-gray-300">
Agent Agent
@@ -174,7 +174,7 @@ export default function Overview() {
<ul className="space-y-4 mb-8"> <ul className="space-y-4 mb-8">
<li className="flex items-start space-x-3"> <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 flex-shrink-0"> <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 1
</span> </span>
<div className="text-gray-700 dark:text-gray-300"> <div className="text-gray-700 dark:text-gray-300">
@@ -182,7 +182,7 @@ export default function Overview() {
</div> </div>
</li> </li>
<li className="flex items-start space-x-3"> <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 flex-shrink-0"> <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 2
</span> </span>
<div className="text-gray-700 dark:text-gray-300"> <div className="text-gray-700 dark:text-gray-300">
@@ -190,7 +190,7 @@ export default function Overview() {
</div> </div>
</li> </li>
<li className="flex items-start space-x-3"> <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 flex-shrink-0"> <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 3
</span> </span>
<div className="text-gray-700 dark:text-gray-300"> <div className="text-gray-700 dark:text-gray-300">
@@ -198,7 +198,7 @@ export default function Overview() {
</div> </div>
</li> </li>
<li className="flex items-start space-x-3"> <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 flex-shrink-0"> <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 4
</span> </span>
<div className="text-gray-700 dark:text-gray-300"> <div className="text-gray-700 dark:text-gray-300">
@@ -1,7 +1,8 @@
import BetaNotice from '@pages/components/BetaNotice'
import CodeEditor from '@pages/components/CodeEditor'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function QuickStart() { export default function QuickStart() {
const { t } = useTranslation('docs') const { t } = useTranslation('docs')
@@ -23,7 +24,7 @@ export default function QuickStart() {
<p className="text-sm font-medium mb-2">{t('quick_start.step1_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>
@@ -2,14 +2,14 @@ export default {
overview: { overview: {
title: 'Overview', title: 'Overview',
subtitle: subtitle:
'page-agent is a pure web-based GUI Agent. Simple integration gives your website an AI operator.', '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: 'What is page-agent?',
what_is_desc: what_is_desc:
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent targets web developers, not scrapers or generic agent builders. Integrate it into your site to let users interact with pages through natural language.', 'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent 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', features_title: 'Core Features',
feature_dom: { feature_dom: {
title: '🧠 Smart DOM Analysis', title: '🧠 Smart DOM Analysis',
desc: 'DOM-based analysis with high-intensity dehydration. No visual recognition needed—pure text for precise operations.', desc: 'DOM-based analysis with high-intensity dehydration. No visual recognition needed. Pure text for fast and precise operations.',
}, },
feature_secure: { feature_secure: {
title: '🔒 Secure & Controllable', title: '🔒 Secure & Controllable',
@@ -141,6 +141,16 @@ export default {
tip_2: tip_2:
"Non-OpenAI models don't guarantee JSON schema compliance—tool call may error but usually recovers. Higher temperature recommended", "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', 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', configuration: 'Configuration',
}, },
custom_tools: { custom_tools: {
@@ -1,8 +1,8 @@
export default { export default {
hero: { hero: {
badge: 'GUI Agent in your webpage', badge: 'GUI Agent in your webpage',
title_line1: 'An AI Operator', title_line1: 'The AI Operator',
title_line2: 'Living in Your Web Apps', title_line2: 'Living in Your Web App',
subtitle_emoji: '🪄 One line of CDN', subtitle_emoji: '🪄 One line of CDN',
subtitle_main: ' adds intelligent GUI Agents to your website.', subtitle_main: ' adds intelligent GUI Agents to your website.',
subtitle_detail: 'Users give natural language commands, AI handles the rest.', subtitle_detail: 'Users give natural language commands, AI handles the rest.',
@@ -10,7 +10,8 @@ export default {
tab_other: '🌐 Try on Other Sites', tab_other: '🌐 Try on Other Sites',
input_placeholder: 'Describe what you want AI to do...', input_placeholder: 'Describe what you want AI to do...',
execute_button: 'Run', execute_button: 'Run',
default_task: 'Navigate to docs, find data masking section, and summarize in markdown', default_task:
'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown',
}, },
try_other: { try_other: {
step1_title: 'Step 1:', step1_title: 'Step 1:',
@@ -31,16 +32,16 @@ export default {
}, },
}, },
benefits: { benefits: {
no_backend: 'Zero Backend', no_backend: 'Pure Front-end Solution',
private_model: 'Your Own Models', private_model: 'Your Own Models',
data_masking: 'Built-in Privacy', data_masking: 'Built-in Privacy',
open_source: 'MIT Open Source', open_source: 'MIT Open Source',
}, },
features: { features: {
section_title: 'Why PageAgent', section_title: 'Why PageAgent',
dom_understanding: { in_page: {
title: 'Smart DOM Analysis', title: 'In-page Solution',
desc: 'Analyzes page structure without screenshots or vision models. Pure text understanding for fast, accurate automation.', desc: 'Runs entirely within your page. No browser extensions, no headless browsers, and no backend required.',
}, },
secure_integration: { secure_integration: {
title: 'Secure by Design', title: 'Secure by Design',
@@ -48,7 +49,7 @@ export default {
}, },
zero_backend: { zero_backend: {
title: 'Zero Backend Setup', title: 'Zero Backend Setup',
desc: 'Just drop in a script tag. Works with any LLM provider—OpenAI, Anthropic, or your own models.', desc: 'Just drop in a script. Works with any LLM provider—OpenAI, Anthropic, or your own models.',
}, },
accessible: { accessible: {
title: 'Natural Language UI', title: 'Natural Language UI',
@@ -4,7 +4,7 @@ export default {
slogan: 'GUI Agent in your webpage', slogan: 'GUI Agent in your webpage',
nav_docs: '文档', nav_docs: '文档',
nav_source: 'GitHub', nav_source: 'GitHub',
mobile_menu: '打开导航', mobile_menu: '打开导航',
}, },
footer: { footer: {
copyright: '© 2025 page-agent. All rights reserved.', copyright: '© 2025 page-agent. All rights reserved.',
@@ -1,10 +1,11 @@
export default { export default {
overview: { overview: {
title: 'Overview', title: 'Overview',
subtitle: 'page-agent 是一个完全基于Web技术的 GUI Agent,简单引入,让你的网站拥有 AI 操作员。', subtitle:
'page-agent 是一个完全基于 Web 技术的 GUI Agent,简单几步,让你的网站拥有 AI 操作员。',
what_is: '什么是 page-agent', what_is: '什么是 page-agent',
what_is_desc: what_is_desc:
'page-agent 是一个页面内嵌式 GUI Agent。与传统的浏览器自动化工具不同,page-agent 面向网站开发者,而非爬虫或通用Agent开发者将 Agent 集成到你的网站中,让用户可以通过自然语言与页面进行交互。', 'page-agent 是一个页面内嵌式 GUI Agent。与传统的浏览器自动化工具不同,page-agent 面向网站开发者,而非爬虫或Agent客户端开发者将 Agent 集成到你的网站中,让用户可以通过自然语言与页面进行交互。',
features_title: '核心特性', features_title: '核心特性',
feature_dom: { feature_dom: {
title: '🧠 智能 DOM 理解', title: '🧠 智能 DOM 理解',
@@ -112,7 +113,7 @@ export default {
}, },
model_integration: { model_integration: {
title: '模型接入', title: '模型接入',
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型包括公有云服务和私有部署方案。', subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
recommended: '推荐模型', recommended: '推荐模型',
model_gpt4_title: '⚡ gpt-4.1-mini', model_gpt4_title: '⚡ gpt-4.1-mini',
model_gpt4_badge: '评估基准 ✅', model_gpt4_badge: '评估基准 ✅',
@@ -122,22 +123,31 @@ export default {
model_deepseek_title: '💰 DeepSeek-3.2', model_deepseek_title: '💰 DeepSeek-3.2',
model_deepseek_badge: '经济实惠', model_deepseek_badge: '经济实惠',
model_deepseek_1: '• 价格远低于同等级其他模型', model_deepseek_1: '• 价格远低于同等级其他模型',
model_deepseek_2: '• ToolCall 有出错率通常能够自动修复', model_deepseek_2: '• ToolCall 有出错率,通常能够自动修复',
model_deepseek_3: '• 本网站提供的免费试用为 DeepSeek', model_deepseek_3: '• 本网站提供的免费试用为 DeepSeek',
model_qwen_title: '🛡️ qwen3', model_qwen_title: '🛡️ qwen3',
model_qwen_badge: '安全合规', model_qwen_badge: '安全合规',
model_qwen_1: '• 可控、效果尚可价格合理', model_qwen_1: '• 可控、效果尚可,价格合理',
model_qwen_2: '• ToolCall 有出错率通常能够自动修复', model_qwen_2: '• ToolCall 有出错率,通常能够自动修复',
model_qwen_3: '• 适合能给出详细步骤的场景', model_qwen_3: '• 适合能给出详细步骤的场景',
model_gemini_title: '⚡ gemini-2.5-flash', model_gemini_title: '⚡ gemini-2.5-flash',
model_gemini_badge: '极其高效成功率高价格合理', model_gemini_badge: '极其高效,成功率高,价格合理',
available: '可用模型', available: '可用模型',
available_verified: '✅ 已验证可用', available_verified: '✅ 已验证可用',
tips: '提示', tips: '提示',
tip_1: 'reasoning 模型如 GPT-5),速度偏慢没有必要', tip_1: 'reasoning 模型(如 GPT-5),速度偏慢,没有必要',
tip_2: tip_2:
'不保证 json schema 的模型openAI 以外的几乎所有模型),tool call 有概率出错通常能自动修复建议 temperature 设置高一些', '不保证 json schema 的模型(openAI 以外的几乎所有模型),tool call 有概率出错,通常能自动修复,建议 temperature 设置高一些',
tip_3: '小模型、nano 模型效果不佳', 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: '配置方式', configuration: '配置方式',
}, },
custom_tools: { custom_tools: {
@@ -4,13 +4,13 @@ export default {
title_line1: '让你的 Web 应用', title_line1: '让你的 Web 应用',
title_line2: '拥有 AI 操作员', title_line2: '拥有 AI 操作员',
subtitle_emoji: '🪄 一行 CDN 引入', subtitle_emoji: '🪄 一行 CDN 引入',
subtitle_main: ',为你的网站添加智能 UI Agent。', subtitle_main: ',为你的网站添加 GUI Agent。',
subtitle_detail: '用户/答疑机器人给出文字指示,AI 帮你操作页面。', subtitle_detail: '用户/答疑机器人给出文字指示,AI 帮你操作页面。',
tab_try: '🚀 立即尝试', tab_try: '🚀 立即尝试',
tab_other: '🌐 其他网页尝试', tab_other: '🌐 其他网页尝试',
input_placeholder: '输入您想要 AI 执行的任务...', input_placeholder: '输入您想要 AI 执行的任务...',
execute_button: '执行', execute_button: '执行',
default_task: '导航栏中进入文档页,打开数据脱敏相关的文档,帮我总结成 markdown', default_task: '导航栏中进入文档页,打开“快速开始”相关的文档,帮我总结成 markdown',
}, },
try_other: { try_other: {
step1_title: '步骤 1:', step1_title: '步骤 1:',
@@ -31,16 +31,16 @@ export default {
}, },
}, },
benefits: { benefits: {
no_backend: '无需后端', no_backend: '纯前端方案',
private_model: '支持私有模型', private_model: '支持私有模型',
data_masking: '无痛脱敏', data_masking: '无痛脱敏',
open_source: 'MIT 开源', open_source: 'MIT 开源',
}, },
features: { features: {
section_title: '核心特性', section_title: '核心特性',
dom_understanding: { in_page: {
title: '智能 DOM 理解', title: '纯页面内方案',
desc: '基于 DOM 分析,高强度脱水。无需视觉识别,纯文本实现精准操作。', desc: '完全运行在你的页面内。不需要浏览器插件、不需要无头浏览器,不需要后端。',
}, },
secure_integration: { secure_integration: {
title: '安全可控集成', title: '安全可控集成',
@@ -48,7 +48,7 @@ export default {
}, },
zero_backend: { zero_backend: {
title: '零后端部署', title: '零后端部署',
desc: 'CDN 直接引入,自定义 LLM 接入点。从 OpenAI 到 qwen3,完全由你掌控。', desc: '前端脚本引入,自定义 LLM 接入点。从 OpenAI 到 qwen3,完全由你掌控。',
}, },
accessible: { accessible: {
title: '普惠智能交互', title: '普惠智能交互',
@@ -192,3 +192,13 @@ td {
border-bottom-color: rgba(255, 255, 255, 0.2); 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 */
}
@@ -1,15 +1,14 @@
/* eslint-disable react-dom/no-dangerously-set-innerhtml */ /* eslint-disable react-dom/no-dangerously-set-innerhtml */
import { PageAgent } from 'page-agent'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' 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 = `
@@ -18,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>
@@ -44,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.
@@ -59,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)
@@ -68,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 */}
@@ -77,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">
@@ -92,7 +94,7 @@ export default function HomePage() {
<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"
> >
{t('home:hero.title_line1')} {t('home:hero.title_line1')}
<br /> <br />
@@ -100,7 +102,7 @@ export default function HomePage() {
</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">
{t('home:hero.subtitle_emoji')} {t('home:hero.subtitle_emoji')}
</span> </span>
{t('home:hero.subtitle_main')} {t('home:hero.subtitle_main')}
@@ -118,7 +120,7 @@ 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'
}`} }`}
> >
@@ -128,7 +130,7 @@ export default function HomePage() {
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'
}`} }`}
> >
@@ -152,7 +154,7 @@ 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')} {t('home:hero.execute_button')}
@@ -216,27 +218,27 @@ export default function HomePage() {
</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')} {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>
{t('home:try_other.notice_items.item2')} {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')} {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')} {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')} {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')}{' '} {t('home:try_other.notice_items.item6_prefix')}{' '}
<Link <Link
href="/docs/introduction/limitations" href="/docs/introduction/limitations"
@@ -300,49 +302,30 @@ 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">
{t('home:features.dom_understanding.title')} {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">
{t('home:features.dom_understanding.desc')} {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"
>
<span className="text-white text-xl">🔒</span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.secure_integration.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.secure_integration.desc')}
</p>
</article>
{/* Feature 3 */}
<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"
role="listitem"
>
<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"
aria-hidden="true" aria-hidden="true"
> >
<span className="text-white text-xl"></span> <span className="text-white text-xl"></span>
@@ -355,16 +338,16 @@ export default function HomePage() {
</p> </p>
</article> </article>
{/* Feature 4 */} {/* Feature 3 */}
<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-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-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-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 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.accessible.title')} {t('home:features.accessible.title')}
@@ -373,6 +356,25 @@ export default function HomePage() {
{t('home:features.accessible.desc')} {t('home:features.accessible.desc')}
</p> </p>
</article> </article>
{/* Feature 4 */}
<article
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"
>
<div
className="w-12 h-12 bg-linear-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
aria-hidden="true"
>
<span className="text-white text-xl">🔒</span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.secure_integration.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.secure_integration.desc')}
</p>
</article>
</div> </div>
</div> </div>
</section> </section>
@@ -383,7 +385,7 @@ 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')} {t('home:use_cases.section_title')}
</h2> </h2>
@@ -394,9 +396,9 @@ export default function HomePage() {
<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"> <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>
@@ -412,9 +414,9 @@ export default function HomePage() {
</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"> <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>
@@ -429,9 +431,9 @@ export default function HomePage() {
</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"> <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>
@@ -446,9 +448,9 @@ export default function HomePage() {
</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"> <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>
@@ -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,12 +123,12 @@ 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,
} },
})) }))
} }
@@ -116,7 +136,7 @@ export default function ComplexTestPage() {
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) {
@@ -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">
@@ -193,8 +212,11 @@ export default function ComplexTestPage() {
</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>
@@ -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