Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e04a131f1 | |||
| b487a7900b | |||
| 78b49c8b7a | |||
| 949e511dcf | |||
| d9faca5d01 | |||
| 398c6d0869 | |||
| 54d478df1e | |||
| 43c80f5418 | |||
| b186ffcaa6 | |||
| 778bbda4b8 | |||
| f5900bd096 | |||
| 09772d79bd | |||
| bfa6a5347e | |||
| da5cf44012 | |||
| 1666c08e58 | |||
| 0c0c76fdd8 | |||
| 6b0b1218ac | |||
| d70f652285 | |||
| c3c01b63fb | |||
| 463d010c24 | |||
| 4e52f17b9f | |||
| 5237edfcca | |||
| 79dcf9d9e4 | |||
| 09544205ed | |||
| 2c74c3170a | |||
| 9cee8f4174 | |||
| d05b43cf50 | |||
| 3fe6ae6862 | |||
| 66be1e56e3 | |||
| 0fd4eaf9af |
+20
-15
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
🌐 [English](./README.md) | **中文**
|
🌐 [English](./README.md) | **中文**
|
||||||
|
|
||||||
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
||||||
|
|
||||||
<video id="demo-video" src="https://github.com/user-attachments/assets/141bbb01-8022-4d1f-919d-9efc9a1dc1cf" width="640" crossorigin muted autoplay loop></video>
|
<video id="demo-video" src="https://github.com/user-attachments/assets/141bbb01-8022-4d1f-919d-9efc9a1dc1cf" width="640" crossorigin muted autoplay loop></video>
|
||||||
|
|
||||||
@@ -24,22 +24,10 @@
|
|||||||
|
|
||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
👉 [**Roadmap**](./ROADMAP.md)
|
👉 [**Roadmap**](https://github.com/alibaba/page-agent/issues/96)
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
### CDN 集成
|
|
||||||
|
|
||||||
```html
|
|
||||||
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js
|
|
||||||
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
|
|
||||||
<script
|
|
||||||
src="https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js"
|
|
||||||
crossorigin="true"
|
|
||||||
type="text/javascript"
|
|
||||||
></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### NPM 安装
|
### NPM 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -57,7 +45,7 @@ const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm
|
|||||||
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||||
|
|
||||||
const agent = new PageAgent({
|
const agent = new PageAgent({
|
||||||
modelName: DEMO_MODEL,
|
model: DEMO_MODEL,
|
||||||
baseURL: DEMO_BASE_URL,
|
baseURL: DEMO_BASE_URL,
|
||||||
apiKey: DEMO_API_KEY,
|
apiKey: DEMO_API_KEY,
|
||||||
language: 'zh-CN',
|
language: 'zh-CN',
|
||||||
@@ -66,6 +54,23 @@ const agent = new PageAgent({
|
|||||||
await agent.execute('点击登录按钮')
|
await agent.execute('点击登录按钮')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CDN 集成
|
||||||
|
|
||||||
|
Fastest way to try PageAgent is to include it via CDN. Demo model will be used by default.
|
||||||
|
|
||||||
|
| Location | URL |
|
||||||
|
| -------- | ----------------------------------------------------------------------------- |
|
||||||
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js |
|
||||||
|
| China | https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js |
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script
|
||||||
|
src="https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js"
|
||||||
|
crossorigin="true"
|
||||||
|
type="text/javascript"
|
||||||
|
></script>
|
||||||
|
```
|
||||||
|
|
||||||
## 🏗️ 架构设计
|
## 🏗️ 架构设计
|
||||||
|
|
||||||
PageAgent adopts a simplified monorepo structure:
|
PageAgent adopts a simplified monorepo structure:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
|
|||||||
|
|
||||||
🌐 **English** | [中文](./README-zh.md)
|
🌐 **English** | [中文](./README-zh.md)
|
||||||
|
|
||||||
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
||||||
|
|
||||||
<video id="demo-video" src="https://github.com/user-attachments/assets/de8d1964-8bde-494f-a52f-2975469557a5" width="640" crossorigin muted autoplay loop></video>
|
<video id="demo-video" src="https://github.com/user-attachments/assets/de8d1964-8bde-494f-a52f-2975469557a5" width="640" crossorigin muted autoplay loop></video>
|
||||||
|
|
||||||
@@ -24,22 +24,10 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
|
|||||||
|
|
||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
👉 [**Roadmap**](./ROADMAP.md)
|
👉 [**Roadmap**](https://github.com/alibaba/page-agent/issues/96)
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### CDN Integration
|
|
||||||
|
|
||||||
```html
|
|
||||||
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js
|
|
||||||
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
|
|
||||||
<script
|
|
||||||
src="https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js"
|
|
||||||
crossorigin="true"
|
|
||||||
type="text/javascript"
|
|
||||||
></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### NPM Installation
|
### NPM Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -57,7 +45,7 @@ const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm
|
|||||||
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||||
|
|
||||||
const agent = new PageAgent({
|
const agent = new PageAgent({
|
||||||
modelName: DEMO_MODEL,
|
model: DEMO_MODEL,
|
||||||
baseURL: DEMO_BASE_URL,
|
baseURL: DEMO_BASE_URL,
|
||||||
apiKey: DEMO_API_KEY,
|
apiKey: DEMO_API_KEY,
|
||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
@@ -66,6 +54,23 @@ const agent = new PageAgent({
|
|||||||
await agent.execute('Click the login button')
|
await agent.execute('Click the login button')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CDN Integration
|
||||||
|
|
||||||
|
Fastest way to try PageAgent is to include it via CDN. Demo model will be used by default.
|
||||||
|
|
||||||
|
| Location | URL |
|
||||||
|
| -------- | ----------------------------------------------------------------------------- |
|
||||||
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js |
|
||||||
|
| China | https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js |
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js"
|
||||||
|
crossorigin="true"
|
||||||
|
type="text/javascript"
|
||||||
|
></script>
|
||||||
|
```
|
||||||
|
|
||||||
## 🏗️ Structure
|
## 🏗️ Structure
|
||||||
|
|
||||||
PageAgent adopts a simplified monorepo structure:
|
PageAgent adopts a simplified monorepo structure:
|
||||||
|
|||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
# 🗺️ PageAgent Roadmap
|
|
||||||
|
|
||||||
The development progress and future plans for PageAgent.
|
|
||||||
|
|
||||||
## 🚀 Current Works
|
|
||||||
|
|
||||||
- [x] **MVP**
|
|
||||||
- Core functionality implemented.
|
|
||||||
- [x] **SPA interaction**
|
|
||||||
- [x] **Reasoning and (short) memory**
|
|
||||||
- [x] **Multi model provider integration and testing**
|
|
||||||
- [x] **UI with HITL**
|
|
||||||
- Human-in-the-loop user interface. Agent can ask user questions.
|
|
||||||
- [x] **Landing and doc pages**
|
|
||||||
- [x] **Remove `ai-sdk`**
|
|
||||||
- Only one function of AI-ADK is being used.
|
|
||||||
- Our agent memory and thinking mechanism does not suite ai-sdk.
|
|
||||||
- [x] **Robust LLM output**
|
|
||||||
- Auto-fix incomplete output format of DeepSeek and QWen.
|
|
||||||
- [x] **Working homepage with live LLM API**
|
|
||||||
- [x] **~~free~~ CDN**
|
|
||||||
- [x] **Free evaluation plan**
|
|
||||||
- [x] **Custom actions and HITL**
|
|
||||||
- [ ] **Hooks and Events**
|
|
||||||
- [x] **lifecycle hooks**
|
|
||||||
- [ ] **lifecycle events**
|
|
||||||
- [ ] **User takeover**
|
|
||||||
- [#64](https://github.com/alibaba/page-agent/issues/64)
|
|
||||||
- [ ] **❗Hijack `page_open/page_change/page_unload` behavior**
|
|
||||||
- [ ] **Custom knowledge base and instructions**
|
|
||||||
- [#45](https://github.com/alibaba/page-agent/issues/45)
|
|
||||||
- [ ] **Black/white-list safeguard**
|
|
||||||
- [ ] **Data-masking**
|
|
||||||
- [#44](https://github.com/alibaba/page-agent/issues/44)
|
|
||||||
- [ ] **Improve Memory**
|
|
||||||
- [#66](https://github.com/alibaba/page-agent/issues/66)
|
|
||||||
- [#65](https://github.com/alibaba/page-agent/issues/65)
|
|
||||||
- [ ] **Optimize for popular UI frameworks**
|
|
||||||
- [x] **i18n of the website**
|
|
||||||
- [x] Chinese version
|
|
||||||
- [x] English version
|
|
||||||
- [x] **Refactor: Separate `Agent` and `PageController`**
|
|
||||||
- [ ] **Chrome-ext wrapper for multi-page tasks**
|
|
||||||
|
|
||||||
♻️ Following browser-use's update and contribute back.
|
|
||||||
|
|
||||||
## 📋 Pending Features
|
|
||||||
|
|
||||||
- [ ] **Tools for more complex tasks**
|
|
||||||
- todo list
|
|
||||||
- file sys
|
|
||||||
- [x] **Support custom llm fetch**
|
|
||||||
- [ ] **Testing suits**
|
|
||||||
- [ ] **Same-origin multi-page-app rally**
|
|
||||||
- [ ] **Local MCP proxy**
|
|
||||||
|
|
||||||
## 🤔 To Be Decided
|
|
||||||
|
|
||||||
- [ ] **Cross-origin multi-page?**
|
|
||||||
- Tricky
|
|
||||||
- Need some kind of "memory rally"
|
|
||||||
+1
-6
@@ -8,12 +8,7 @@ import globals from 'globals'
|
|||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores([
|
globalIgnores(['**/dist', '**/node_modules', 'packages/website/src/components/ui']),
|
||||||
'**/dist',
|
|
||||||
'**/test-pages',
|
|
||||||
'**/node_modules',
|
|
||||||
'packages/website/src/components/ui',
|
|
||||||
]),
|
|
||||||
{
|
{
|
||||||
plugins: {
|
plugins: {
|
||||||
'react-hooks': reactHooks,
|
'react-hooks': reactHooks,
|
||||||
|
|||||||
Generated
+205
-225
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
@@ -16,26 +16,26 @@
|
|||||||
"packages/website"
|
"packages/website"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.2.0",
|
"@commitlint/cli": "^20.3.0",
|
||||||
"@commitlint/config-conventional": "^20.2.0",
|
"@commitlint/config-conventional": "^20.3.0",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@microsoft/api-extractor": "^7.55.2",
|
"@microsoft/api-extractor": "^7.55.2",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.1",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-dom": "^2.3.13",
|
"eslint-plugin-react-dom": "^2.5.1",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
"eslint-plugin-react-x": "^2.3.13",
|
"eslint-plugin-react-x": "^2.5.1",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.2.4",
|
"lint-staged": "^16.2.4",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.50.1",
|
"typescript-eslint": "^8.52.0",
|
||||||
"unplugin-dts": "^1.0.0-beta.6",
|
"unplugin-dts": "^1.0.0-beta.6",
|
||||||
"vite": "^7.3.0",
|
"vite": "^7.3.0",
|
||||||
"vite-plugin-css-injected-by-js": "^3.5.2"
|
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||||
@@ -76,7 +76,6 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -317,15 +316,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@commitlint/cli": {
|
"node_modules/@commitlint/cli": {
|
||||||
"version": "20.2.0",
|
"version": "20.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.3.0.tgz",
|
||||||
"integrity": "sha512-l37HkrPZ2DZy26rKiTUvdq/LZtlMcxz+PeLv9dzK9NzoFGuJdOQyYU7IEkEQj0pO++uYue89wzOpZ0hcTtoqUA==",
|
"integrity": "sha512-HXO8YVfqdBK+MnlX2zqNrv6waGYPs6Ysjm5W2Y0GMagWXwiIKx7C8dcIX9ca+QdHq4WA0lcMnZLQ0pzQh1piZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@commitlint/format": "^20.2.0",
|
"@commitlint/format": "^20.2.0",
|
||||||
"@commitlint/lint": "^20.2.0",
|
"@commitlint/lint": "^20.3.0",
|
||||||
"@commitlint/load": "^20.2.0",
|
"@commitlint/load": "^20.3.0",
|
||||||
"@commitlint/read": "^20.2.0",
|
"@commitlint/read": "^20.2.0",
|
||||||
"@commitlint/types": "^20.2.0",
|
"@commitlint/types": "^20.2.0",
|
||||||
"tinyexec": "^1.0.0",
|
"tinyexec": "^1.0.0",
|
||||||
@@ -339,9 +338,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@commitlint/config-conventional": {
|
"node_modules/@commitlint/config-conventional": {
|
||||||
"version": "20.2.0",
|
"version": "20.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.3.0.tgz",
|
||||||
"integrity": "sha512-MsRac+yNIbTB4Q/psstKK4/ciVzACHicSwz+04Sxve+4DW+PiJeTjU0JnS4m/oOnulrXYN+yBPlKaBSGemRfgQ==",
|
"integrity": "sha512-g1OXVl6E2v0xF1Ru2RpxQ+Vfy7XUcUsCmLKzGUrhFLS4hSNykje0QSy6djBtzOiOBQCepBrmIlqx/gRlzrSh5A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -423,15 +422,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@commitlint/lint": {
|
"node_modules/@commitlint/lint": {
|
||||||
"version": "20.2.0",
|
"version": "20.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.3.0.tgz",
|
||||||
"integrity": "sha512-cQEEB+jlmyQbyiji/kmh8pUJSDeUmPiWq23kFV0EtW3eM+uAaMLMuoTMajbrtWYWQpPzOMDjYltQ8jxHeHgITg==",
|
"integrity": "sha512-X19HOGU5nRo6i9DIY0kG0mhgtvpn1UGO1D6aLX1ILLyeqSM5yJyMcrRqNj8SLgeSeUDODhLY9QYsBIG0LdNHkA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@commitlint/is-ignored": "^20.2.0",
|
"@commitlint/is-ignored": "^20.2.0",
|
||||||
"@commitlint/parse": "^20.2.0",
|
"@commitlint/parse": "^20.2.0",
|
||||||
"@commitlint/rules": "^20.2.0",
|
"@commitlint/rules": "^20.3.0",
|
||||||
"@commitlint/types": "^20.2.0"
|
"@commitlint/types": "^20.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -439,9 +438,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@commitlint/load": {
|
"node_modules/@commitlint/load": {
|
||||||
"version": "20.2.0",
|
"version": "20.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.3.0.tgz",
|
||||||
"integrity": "sha512-iAK2GaBM8sPFTSwtagI67HrLKHIUxQc2BgpgNc/UMNme6LfmtHpIxQoN1TbP+X1iz58jq32HL1GbrFTCzcMi6g==",
|
"integrity": "sha512-amkdVZTXp5R65bsRXRSCwoNXbJHR2aAIY/RGFkoyd63t8UEwqEgT3f0MgeLqYw4hwXyq+TYXKdaW133E29pnGQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -521,9 +520,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@commitlint/rules": {
|
"node_modules/@commitlint/rules": {
|
||||||
"version": "20.2.0",
|
"version": "20.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.3.0.tgz",
|
||||||
"integrity": "sha512-27rHGpeAjnYl/A+qUUiYDa7Yn1WIjof/dFJjYW4gA1Ug+LUGa1P0AexzGZ5NBxTbAlmDgaxSZkLLxtLVqtg8PQ==",
|
"integrity": "sha512-TGgXN/qBEhbzVD13crE1l7YSMJRrbPbUL0OBZALbUM5ER36RZmiZRu2ud2W/AA7HO9YLBRbyx6YVi2t/2Be0yQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1016,9 +1015,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1058,16 +1057,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-react/ast": {
|
"node_modules/@eslint-react/ast": {
|
||||||
"version": "2.3.13",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-2.5.1.tgz",
|
||||||
"integrity": "sha512-OP2rOhHYLx2nfd9uA9uACKZJN9z9rX9uuAMx4PjT75JNOdYr1GgqWQZcYCepyJ+gmVNCyiXcLXuyhavqxCSM8Q==",
|
"integrity": "sha512-l+pEb3kS/RhFN+8ZOhL5+P5+Yj7iw+ta7+abUBNH+5PMyiixlnAJlJ7Wy+rsTGxjUJtzEff3CzSr06V7qwJuwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-react/eff": "2.3.13",
|
"@eslint-react/eff": "2.5.1",
|
||||||
"@typescript-eslint/types": "^8.49.0",
|
"@typescript-eslint/types": "^8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "^8.49.0",
|
"@typescript-eslint/typescript-estree": "^8.51.0",
|
||||||
"@typescript-eslint/utils": "^8.49.0",
|
"@typescript-eslint/utils": "^8.51.0",
|
||||||
"string-ts": "^2.3.1"
|
"string-ts": "^2.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1079,19 +1078,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-react/core": {
|
"node_modules/@eslint-react/core": {
|
||||||
"version": "2.3.13",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-2.5.1.tgz",
|
||||||
"integrity": "sha512-4bWBE+1kApuxJKIrLJH2FuFtCbM4fXfDs6Ou8MNamGoX6hdynlntssvaMZTd/lk/L8dt01H/3btr7xBX4+4BNA==",
|
"integrity": "sha512-48YQYhjHga0TAv7FI0uGGT+X3SITec89fJLKSTqIEa7jTLpXTV56B+B/V/IcfZ0s8I0lNVEmtHFTXaibAxmfyw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-react/ast": "2.3.13",
|
"@eslint-react/ast": "2.5.1",
|
||||||
"@eslint-react/eff": "2.3.13",
|
"@eslint-react/eff": "2.5.1",
|
||||||
"@eslint-react/shared": "2.3.13",
|
"@eslint-react/shared": "2.5.1",
|
||||||
"@eslint-react/var": "2.3.13",
|
"@eslint-react/var": "2.5.1",
|
||||||
"@typescript-eslint/scope-manager": "^8.49.0",
|
"@typescript-eslint/scope-manager": "^8.51.0",
|
||||||
"@typescript-eslint/types": "^8.49.0",
|
"@typescript-eslint/types": "^8.51.0",
|
||||||
"@typescript-eslint/utils": "^8.49.0",
|
"@typescript-eslint/utils": "^8.51.0",
|
||||||
"birecord": "^0.1.1",
|
"birecord": "^0.1.1",
|
||||||
"ts-pattern": "^5.9.0"
|
"ts-pattern": "^5.9.0"
|
||||||
},
|
},
|
||||||
@@ -1104,9 +1103,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-react/eff": {
|
"node_modules/@eslint-react/eff": {
|
||||||
"version": "2.3.13",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-2.5.1.tgz",
|
||||||
"integrity": "sha512-byXsssozwh3VaiqcOonAKQgLXgpMVNSxBWFjdfbNhW7+NttorSt950qtiw+P7A9JoRab1OuGYk4MDY5UVBno8Q==",
|
"integrity": "sha512-henDz7sTB67Q+YPb+Kiy0UpD7eHLvDtB2nOrRtHLTatS/LFr7wVr4J2muQPkPzY9KFF8pv811nrwW58FuDphPg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1114,16 +1113,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-react/shared": {
|
"node_modules/@eslint-react/shared": {
|
||||||
"version": "2.3.13",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-2.5.1.tgz",
|
||||||
"integrity": "sha512-ESE7dVeOXtem3K6BD6k2wJaFt35kPtTT9SWCL99LFk7pym4OEGoMxPcyB2R7PMWiVudwl63BmiOgQOdaFYPONg==",
|
"integrity": "sha512-JSTnpZdBu57ZvrJkBwUZXgkBW8iPiQfSiQMYHEjVFR2NHWFgcOd9qYJLX7mJLrdcw/j+GgRQeOAGok0a0xcxGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-react/eff": "2.3.13",
|
"@eslint-react/eff": "2.5.1",
|
||||||
"@typescript-eslint/utils": "^8.49.0",
|
"@typescript-eslint/utils": "^8.51.0",
|
||||||
"ts-pattern": "^5.9.0",
|
"ts-pattern": "^5.9.0",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
@@ -1134,17 +1133,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-react/var": {
|
"node_modules/@eslint-react/var": {
|
||||||
"version": "2.3.13",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-2.5.1.tgz",
|
||||||
"integrity": "sha512-BozBfUZkzzobD6x/M8XERAnZQ3UvZPsD49zTGFKKU9M/bgsM78HwzxAPLkiu88W55v3sO/Kqf8fQTXT4VEeZ/g==",
|
"integrity": "sha512-O3rbdK8pHKZ4NYPD39Zq7zhWu76dvhyeroqG8nQzf667F/FdHGpGhsCPJOeIU5L/ooYO/3/Qz2vJD1pB5JeROQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-react/ast": "2.3.13",
|
"@eslint-react/ast": "2.5.1",
|
||||||
"@eslint-react/eff": "2.3.13",
|
"@eslint-react/eff": "2.5.1",
|
||||||
"@typescript-eslint/scope-manager": "^8.49.0",
|
"@typescript-eslint/scope-manager": "^8.51.0",
|
||||||
"@typescript-eslint/types": "^8.49.0",
|
"@typescript-eslint/types": "^8.51.0",
|
||||||
"@typescript-eslint/utils": "^8.49.0",
|
"@typescript-eslint/utils": "^8.51.0",
|
||||||
"ts-pattern": "^5.9.0"
|
"ts-pattern": "^5.9.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3459,9 +3458,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.1.tgz",
|
||||||
"integrity": "sha512-Xarx55ow0R8oC7ViL5fPmDsg1EBa1dVhyZFVbFXNtPPJyW2w9bJADIla8YFSaNG9N06XfcklA9O9vmw4noNxkQ==",
|
"integrity": "sha512-6B13DCWDfAfh4AEJ43gRgeCSAQmlKG5LHqHzHc0lbUwgBy0rX7o41US+46Fd4XiXBx+JDGEz3NBadCbUls0dUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3562,7 +3561,6 @@
|
|||||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -3573,7 +3571,6 @@
|
|||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -3584,26 +3581,25 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz",
|
||||||
"integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==",
|
"integrity": "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
"@typescript-eslint/scope-manager": "8.50.1",
|
"@typescript-eslint/scope-manager": "8.52.0",
|
||||||
"@typescript-eslint/type-utils": "8.50.1",
|
"@typescript-eslint/type-utils": "8.52.0",
|
||||||
"@typescript-eslint/utils": "8.50.1",
|
"@typescript-eslint/utils": "8.52.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.1",
|
"@typescript-eslint/visitor-keys": "8.52.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.5",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3613,7 +3609,7 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.50.1",
|
"@typescript-eslint/parser": "^8.52.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
@@ -3629,18 +3625,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz",
|
||||||
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
|
"integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.50.1",
|
"@typescript-eslint/scope-manager": "8.52.0",
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.52.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.1",
|
"@typescript-eslint/typescript-estree": "8.52.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.1",
|
"@typescript-eslint/visitor-keys": "8.52.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3655,15 +3650,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.52.0.tgz",
|
||||||
"integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==",
|
"integrity": "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.50.1",
|
"@typescript-eslint/tsconfig-utils": "^8.52.0",
|
||||||
"@typescript-eslint/types": "^8.50.1",
|
"@typescript-eslint/types": "^8.52.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3677,14 +3672,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.52.0.tgz",
|
||||||
"integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==",
|
"integrity": "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.52.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.1"
|
"@typescript-eslint/visitor-keys": "8.52.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3695,9 +3690,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.52.0.tgz",
|
||||||
"integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==",
|
"integrity": "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3712,17 +3707,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.52.0.tgz",
|
||||||
"integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==",
|
"integrity": "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.52.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.1",
|
"@typescript-eslint/typescript-estree": "8.52.0",
|
||||||
"@typescript-eslint/utils": "8.50.1",
|
"@typescript-eslint/utils": "8.52.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.4.3",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3737,9 +3732,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.52.0.tgz",
|
||||||
"integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==",
|
"integrity": "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3751,21 +3746,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.52.0.tgz",
|
||||||
"integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==",
|
"integrity": "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.50.1",
|
"@typescript-eslint/project-service": "8.52.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.50.1",
|
"@typescript-eslint/tsconfig-utils": "8.52.0",
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.52.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.1",
|
"@typescript-eslint/visitor-keys": "8.52.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.4.3",
|
||||||
"minimatch": "^9.0.4",
|
"minimatch": "^9.0.5",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.7.3",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3805,16 +3800,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.52.0.tgz",
|
||||||
"integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==",
|
"integrity": "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
"@typescript-eslint/scope-manager": "8.50.1",
|
"@typescript-eslint/scope-manager": "8.52.0",
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.52.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.1"
|
"@typescript-eslint/typescript-estree": "8.52.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3829,13 +3824,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.52.0.tgz",
|
||||||
"integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==",
|
"integrity": "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.52.0",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3898,7 +3893,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -4096,7 +4090,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -4519,7 +4512,6 @@
|
|||||||
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
|
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"env-paths": "^2.2.1",
|
"env-paths": "^2.2.1",
|
||||||
"import-fresh": "^3.3.0",
|
"import-fresh": "^3.3.0",
|
||||||
@@ -4733,7 +4725,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@@ -4798,7 +4789,6 @@
|
|||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -4870,20 +4860,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-dom": {
|
"node_modules/eslint-plugin-react-dom": {
|
||||||
"version": "2.3.13",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-dom/-/eslint-plugin-react-dom-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react-dom/-/eslint-plugin-react-dom-2.5.1.tgz",
|
||||||
"integrity": "sha512-O9jglTOnnuyfJcSxjeVc8lqIp5kuS9/0MLLCHlOTH8ZjIifHHxUr6GZ2fd4la9y0FsoEYXEO7DBIMjWx2vCwjg==",
|
"integrity": "sha512-7x3k0bQwtTOSxjReH11buYL6aSMopBzSaWr2bU/LPOuy1dDxvqE6HC2Y6QPEyZjLJ1ppUSKwRmcX85uiF0jHAA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-react/ast": "2.3.13",
|
"@eslint-react/ast": "2.5.1",
|
||||||
"@eslint-react/core": "2.3.13",
|
"@eslint-react/core": "2.5.1",
|
||||||
"@eslint-react/eff": "2.3.13",
|
"@eslint-react/eff": "2.5.1",
|
||||||
"@eslint-react/shared": "2.3.13",
|
"@eslint-react/shared": "2.5.1",
|
||||||
"@eslint-react/var": "2.3.13",
|
"@eslint-react/var": "2.5.1",
|
||||||
"@typescript-eslint/scope-manager": "^8.49.0",
|
"@typescript-eslint/scope-manager": "^8.51.0",
|
||||||
"@typescript-eslint/types": "^8.49.0",
|
"@typescript-eslint/types": "^8.51.0",
|
||||||
"@typescript-eslint/utils": "^8.49.0",
|
"@typescript-eslint/utils": "^8.51.0",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
"string-ts": "^2.3.1",
|
"string-ts": "^2.3.1",
|
||||||
"ts-pattern": "^5.9.0"
|
"ts-pattern": "^5.9.0"
|
||||||
@@ -4927,25 +4917,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-x": {
|
"node_modules/eslint-plugin-react-x": {
|
||||||
"version": "2.3.13",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-2.5.1.tgz",
|
||||||
"integrity": "sha512-+m+V/5VLMxgx0VsFUUyflMNLQG0WFYspsfv0XJFqx7me3A2b3P20QatNDHQCYswz0PRbRFqinTPukPRhZh68ag==",
|
"integrity": "sha512-w3KtEG8M7sFRh/psUmep3H+i2O/5Nn9wI8GXjlqzwp8zLb4ax66Ue+1oecLTzY/0j/M1xRfB3hzk5oio54RwLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-react/ast": "2.3.13",
|
"@eslint-react/ast": "2.5.1",
|
||||||
"@eslint-react/core": "2.3.13",
|
"@eslint-react/core": "2.5.1",
|
||||||
"@eslint-react/eff": "2.3.13",
|
"@eslint-react/eff": "2.5.1",
|
||||||
"@eslint-react/shared": "2.3.13",
|
"@eslint-react/shared": "2.5.1",
|
||||||
"@eslint-react/var": "2.3.13",
|
"@eslint-react/var": "2.5.1",
|
||||||
"@typescript-eslint/scope-manager": "^8.49.0",
|
"@typescript-eslint/scope-manager": "^8.51.0",
|
||||||
"@typescript-eslint/type-utils": "^8.49.0",
|
"@typescript-eslint/type-utils": "^8.51.0",
|
||||||
"@typescript-eslint/types": "^8.49.0",
|
"@typescript-eslint/types": "^8.51.0",
|
||||||
"@typescript-eslint/utils": "^8.49.0",
|
"@typescript-eslint/utils": "^8.51.0",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
"is-immutable-type": "^5.0.1",
|
"is-immutable-type": "^5.0.1",
|
||||||
"string-ts": "^2.3.1",
|
"string-ts": "^2.3.1",
|
||||||
"ts-api-utils": "^2.1.0",
|
"ts-api-utils": "^2.4.0",
|
||||||
"ts-pattern": "^5.9.0"
|
"ts-pattern": "^5.9.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5320,13 +5310,13 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "12.23.26",
|
"version": "12.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.24.0.tgz",
|
||||||
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
|
"integrity": "sha512-ggTMRkIDPc76lHmM+dRT1MmVfFV6t/y+jkWjWuzR7FG5xRvnAAl/5wFPjzSkLE8Nu5E5uIQRCNxmIXtWJVo6XQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"motion-dom": "^12.23.23",
|
"motion-dom": "^12.24.0",
|
||||||
"motion-utils": "^12.23.6",
|
"motion-utils": "^12.23.28",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -5572,7 +5562,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4"
|
"@babel/runtime": "^7.28.4"
|
||||||
},
|
},
|
||||||
@@ -6534,12 +6523,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion": {
|
"node_modules/motion": {
|
||||||
"version": "12.23.26",
|
"version": "12.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.26.tgz",
|
"resolved": "https://registry.npmjs.org/motion/-/motion-12.24.0.tgz",
|
||||||
"integrity": "sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==",
|
"integrity": "sha512-FAnpl/DhCFct3p+T2hCjAY95w+3EggUk3b8Ql4jQ6mmdRbEaGD1000goqWeEYVoD7mqx9H0As9ORmRADD4LQAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"framer-motion": "^12.23.26",
|
"framer-motion": "^12.24.0",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -6560,18 +6549,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion-dom": {
|
"node_modules/motion-dom": {
|
||||||
"version": "12.23.23",
|
"version": "12.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.24.0.tgz",
|
||||||
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
|
"integrity": "sha512-RD2kZkFd/GH4fITI8IJvypGgn0vIu5vkrJaXIAkYqORGs5P0CKDHKNvswmoY1H+tbUAOPSh6VtUqoAmc/3Gvig==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"motion-utils": "^12.23.6"
|
"motion-utils": "^12.23.28"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion-utils": {
|
"node_modules/motion-utils": {
|
||||||
"version": "12.23.6",
|
"version": "12.23.28",
|
||||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.28.tgz",
|
||||||
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
"integrity": "sha512-0W6cWd5Okoyf8jmessVK3spOmbyE0yTdNKujHctHH9XdAE4QDuZ1/LjSXC68rrhsJU+TkzXURC5OdSWh9ibOwQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
@@ -6887,7 +6876,6 @@
|
|||||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -6930,7 +6918,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -6940,7 +6927,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -6949,13 +6935,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-i18next": {
|
"node_modules/react-i18next": {
|
||||||
"version": "16.5.0",
|
"version": "16.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.1.tgz",
|
||||||
"integrity": "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==",
|
"integrity": "sha512-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.6",
|
"@babel/runtime": "^7.28.4",
|
||||||
"html-parse-stringify": "^3.0.1",
|
"html-parse-stringify": "^3.0.1",
|
||||||
"use-sync-external-store": "^1.6.0"
|
"use-sync-external-store": "^1.6.0"
|
||||||
},
|
},
|
||||||
@@ -7067,7 +7053,6 @@
|
|||||||
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -7189,9 +7174,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-icons": {
|
"node_modules/simple-icons": {
|
||||||
"version": "16.2.0",
|
"version": "16.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-16.4.0.tgz",
|
||||||
"integrity": "sha512-SCV4cAX8O2M2WQ3QJm+3k4QdcUIJiBiwKNViQiOwPjWbFWgypzelviW+Bp0A2ij6r/DIEZpTTDcLgD/BE4aexQ==",
|
"integrity": "sha512-8CKtCvx1Zq3L0CBsR4RR1MjGCXkXbzdspwl2yCxs8oWkstbzj2+DatRKDee/tuj3Ffd/2CDzwEky9RgG2yggew==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -7462,7 +7447,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -7494,9 +7478,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -7584,7 +7568,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -7594,16 +7577,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.50.1",
|
"version": "8.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.52.0.tgz",
|
||||||
"integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==",
|
"integrity": "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.52.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.52.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.1",
|
"@typescript-eslint/typescript-estree": "8.52.0",
|
||||||
"@typescript-eslint/utils": "8.50.1"
|
"@typescript-eslint/utils": "8.52.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -7794,7 +7777,6 @@
|
|||||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -7898,7 +7880,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8138,11 +8119,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.2.1",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||||
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
|
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -8162,32 +8142,32 @@
|
|||||||
},
|
},
|
||||||
"packages/llms": {
|
"packages/llms": {
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/page-agent": {
|
"packages/page-agent": {
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/llms": "0.0.17",
|
"@page-agent/llms": "0.0.20",
|
||||||
"@page-agent/page-controller": "0.0.17",
|
"@page-agent/page-controller": "0.0.20",
|
||||||
"@page-agent/ui": "0.0.17",
|
"@page-agent/ui": "0.0.20",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/page-controller": {
|
"packages/page-controller": {
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ai-motion": "^0.4.7"
|
"ai-motion": "^0.4.7"
|
||||||
@@ -8195,7 +8175,7 @@
|
|||||||
},
|
},
|
||||||
"packages/website": {
|
"packages/website": {
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
@@ -8205,10 +8185,10 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"motion": "^12.23.26",
|
"motion": "^12.24.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.2.0",
|
"simple-icons": "^16.4.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
@@ -8221,7 +8201,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-i18next": "^16.5.0",
|
"react-i18next": "^16.5.1",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"wouter": "^3.9.0"
|
"wouter": "^3.9.0"
|
||||||
|
|||||||
+7
-7
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
@@ -34,26 +34,26 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.2.0",
|
"@commitlint/cli": "^20.3.0",
|
||||||
"@commitlint/config-conventional": "^20.2.0",
|
"@commitlint/config-conventional": "^20.3.0",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@microsoft/api-extractor": "^7.55.2",
|
"@microsoft/api-extractor": "^7.55.2",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.1",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-dom": "^2.3.13",
|
"eslint-plugin-react-dom": "^2.5.1",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
"eslint-plugin-react-x": "^2.3.13",
|
"eslint-plugin-react-x": "^2.5.1",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.2.4",
|
"lint-staged": "^16.2.4",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.50.1",
|
"typescript-eslint": "^8.52.0",
|
||||||
"unplugin-dts": "^1.0.0-beta.6",
|
"unplugin-dts": "^1.0.0-beta.6",
|
||||||
"vite": "^7.3.0",
|
"vite": "^7.3.0",
|
||||||
"vite-plugin-css-injected-by-js": "^3.5.2"
|
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-llms.js",
|
"main": "./dist/lib/page-agent-llms.js",
|
||||||
"module": "./dist/lib/page-agent-llms.js",
|
"module": "./dist/lib/page-agent-llms.js",
|
||||||
@@ -38,6 +38,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export class OpenAIClient implements LLMClient {
|
|||||||
modelPatch({
|
modelPatch({
|
||||||
model: this.config.model,
|
model: this.config.model,
|
||||||
temperature: this.config.temperature,
|
temperature: this.config.temperature,
|
||||||
max_tokens: this.config.maxTokens,
|
|
||||||
messages,
|
messages,
|
||||||
|
|
||||||
tools: openaiTools,
|
tools: openaiTools,
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export class OpenAIClient implements LLMClient {
|
|||||||
modelPatch({
|
modelPatch({
|
||||||
model: this.config.model,
|
model: this.config.model,
|
||||||
temperature: this.config.temperature,
|
temperature: this.config.temperature,
|
||||||
max_tokens: this.config.maxTokens,
|
|
||||||
messages,
|
messages,
|
||||||
|
|
||||||
tools: openaiTools,
|
tools: openaiTools,
|
||||||
@@ -48,6 +47,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
})
|
})
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Network error
|
// Network error
|
||||||
|
console.error(error)
|
||||||
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
|
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,3 @@ export const DEFAULT_BASE_URL: string =
|
|||||||
|
|
||||||
export const LLM_MAX_RETRIES = 2
|
export const LLM_MAX_RETRIES = 2
|
||||||
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
|
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
|
||||||
export const DEFAULT_MAX_TOKENS = 16000
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import { OpenAIClient } from './OpenAILenientClient'
|
|||||||
import {
|
import {
|
||||||
DEFAULT_API_KEY,
|
DEFAULT_API_KEY,
|
||||||
DEFAULT_BASE_URL,
|
DEFAULT_BASE_URL,
|
||||||
DEFAULT_MAX_TOKENS,
|
|
||||||
DEFAULT_MODEL_NAME,
|
DEFAULT_MODEL_NAME,
|
||||||
DEFAULT_TEMPERATURE,
|
DEFAULT_TEMPERATURE,
|
||||||
LLM_MAX_RETRIES,
|
LLM_MAX_RETRIES,
|
||||||
@@ -69,7 +68,6 @@ export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
|||||||
apiKey: config.apiKey ?? DEFAULT_API_KEY,
|
apiKey: config.apiKey ?? DEFAULT_API_KEY,
|
||||||
model: config.model ?? DEFAULT_MODEL_NAME,
|
model: config.model ?? DEFAULT_MODEL_NAME,
|
||||||
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
||||||
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
||||||
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
||||||
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export interface LLMConfig {
|
|||||||
model?: string
|
model?: string
|
||||||
|
|
||||||
temperature?: number
|
temperature?: number
|
||||||
maxTokens?: number
|
|
||||||
maxRetries?: number
|
maxRetries?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "page-agent",
|
"name": "page-agent",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent.js",
|
"main": "./dist/esm/page-agent.js",
|
||||||
"module": "./dist/esm/page-agent.js",
|
"module": "./dist/esm/page-agent.js",
|
||||||
@@ -45,9 +45,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.2.1",
|
"zod": "^4.3.5",
|
||||||
"@page-agent/llms": "0.0.17",
|
"@page-agent/llms": "0.0.20",
|
||||||
"@page-agent/page-controller": "0.0.17",
|
"@page-agent/page-controller": "0.0.20",
|
||||||
"@page-agent/ui": "0.0.17"
|
"@page-agent/ui": "0.0.20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,9 +263,7 @@ export class PageAgent extends EventTarget {
|
|||||||
const tools = this.tools
|
const tools = this.tools
|
||||||
|
|
||||||
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
||||||
return zod.object({
|
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
|
||||||
[toolName]: tool.inputSchema,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const actionSchema = zod.union(
|
const actionSchema = zod.union(
|
||||||
@@ -363,25 +361,49 @@ export class PageAgent extends EventTarget {
|
|||||||
return systemPrompt
|
return systemPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get instructions from config and format as XML block
|
||||||
|
*/
|
||||||
|
async #getInstructions(): Promise<string> {
|
||||||
|
const { instructions } = this.config
|
||||||
|
if (!instructions) return ''
|
||||||
|
|
||||||
|
const systemInstructions = instructions.system?.trim()
|
||||||
|
const url = await this.pageController.getCurrentUrl()
|
||||||
|
let pageInstructions: string | undefined
|
||||||
|
|
||||||
|
if (instructions.getPageInstructions) {
|
||||||
|
try {
|
||||||
|
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
chalk.red('[PageAgent] Failed to execute getPageInstructions callback:'),
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!systemInstructions && !pageInstructions) return ''
|
||||||
|
|
||||||
|
let result = '<instructions>\n'
|
||||||
|
|
||||||
|
if (systemInstructions) {
|
||||||
|
result += `<system_instructions>\n${systemInstructions}\n</system_instructions>\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageInstructions) {
|
||||||
|
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
result += '</instructions>\n\n'
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
async #assembleUserPrompt(): Promise<string> {
|
async #assembleUserPrompt(): Promise<string> {
|
||||||
let prompt = ''
|
let prompt = ''
|
||||||
|
|
||||||
// <agent_history>
|
// <instructions> (optional)
|
||||||
// - <step_>
|
prompt += await this.#getInstructions()
|
||||||
|
|
||||||
prompt += '<agent_history>\n'
|
|
||||||
|
|
||||||
this.history.forEach((history, index) => {
|
|
||||||
prompt += `<step_${index + 1}>
|
|
||||||
Evaluation of Previous Step: ${history.brain.evaluation_previous_goal}
|
|
||||||
Memory: ${history.brain.memory}
|
|
||||||
Next Goal: ${history.brain.next_goal}
|
|
||||||
Action Results: ${history.action.output}
|
|
||||||
</step_${index + 1}>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|
||||||
prompt += '</agent_history>\n\n'
|
|
||||||
|
|
||||||
// <agent_state>
|
// <agent_state>
|
||||||
// - <user_request>
|
// - <user_request>
|
||||||
@@ -399,6 +421,23 @@ export class PageAgent extends EventTarget {
|
|||||||
</agent_state>
|
</agent_state>
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// <agent_history>
|
||||||
|
// - <step_>
|
||||||
|
|
||||||
|
prompt += '\n<agent_history>\n'
|
||||||
|
|
||||||
|
this.history.forEach((history, index) => {
|
||||||
|
prompt += `<step_${index + 1}>
|
||||||
|
Evaluation of Previous Step: ${history.brain.evaluation_previous_goal}
|
||||||
|
Memory: ${history.brain.memory}
|
||||||
|
Next Goal: ${history.brain.next_goal}
|
||||||
|
Action Results: ${history.action.output}
|
||||||
|
</step_${index + 1}>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
prompt += '</agent_history>\n\n'
|
||||||
|
|
||||||
// <browser_state>
|
// <browser_state>
|
||||||
|
|
||||||
prompt += await this.#getBrowserState()
|
prompt += await this.#getBrowserState()
|
||||||
@@ -434,7 +473,11 @@ export class PageAgent extends EventTarget {
|
|||||||
await this.pageController.updateTree()
|
await this.pageController.updateTree()
|
||||||
this.mask.wrapper.style.pointerEvents = 'auto'
|
this.mask.wrapper.style.pointerEvents = 'auto'
|
||||||
|
|
||||||
const simplifiedHTML = await this.pageController.getSimplifiedHTML()
|
let simplifiedHTML = await this.pageController.getSimplifiedHTML()
|
||||||
|
|
||||||
|
if (this.config.transformPageContent) {
|
||||||
|
simplifiedHTML = await this.config.transformPageContent(simplifiedHTML)
|
||||||
|
}
|
||||||
|
|
||||||
let prompt = trimLines(`<browser_state>
|
let prompt = trimLines(`<browser_state>
|
||||||
Current Page: [${pageTitle}](${pageUrl})
|
Current Page: [${pageTitle}](${pageUrl})
|
||||||
|
|||||||
@@ -42,8 +42,27 @@ export interface AgentConfig {
|
|||||||
*/
|
*/
|
||||||
customTools?: Record<string, PageAgentTool | null>
|
customTools?: Record<string, PageAgentTool | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instructions to guide the agent's behavior
|
||||||
|
*/
|
||||||
|
instructions?: {
|
||||||
|
/**
|
||||||
|
* Global system-level instructions, applied to all tasks
|
||||||
|
*/
|
||||||
|
system?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic page-level instructions callback
|
||||||
|
* Called before each step to get instructions for the current page
|
||||||
|
* @param url - Current page URL (window.location.href)
|
||||||
|
* @returns Instructions string, or undefined/null to skip
|
||||||
|
*/
|
||||||
|
getPageInstructions?: (url: string) => string | undefined | null
|
||||||
|
}
|
||||||
|
|
||||||
// lifecycle hooks
|
// lifecycle hooks
|
||||||
// @todo: use event instead of hooks
|
// @todo: use event instead of hooks
|
||||||
|
// @todo: remove `this` binding, pass agent as explicit parameter instead
|
||||||
|
|
||||||
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
|
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
|
||||||
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void
|
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void
|
||||||
@@ -53,6 +72,7 @@ export interface AgentConfig {
|
|||||||
/**
|
/**
|
||||||
* @note this hook can block the disposal process
|
* @note this hook can block the disposal process
|
||||||
* @note when dispose caused by page unload, reason will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut.
|
* @note when dispose caused by page unload, reason will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut.
|
||||||
|
* @todo remove `this` binding, pass agent as explicit parameter instead
|
||||||
*/
|
*/
|
||||||
onDispose?: (this: PageAgent, reason?: string) => void
|
onDispose?: (this: PageAgent, reason?: string) => void
|
||||||
|
|
||||||
@@ -66,10 +86,27 @@ export interface AgentConfig {
|
|||||||
*/
|
*/
|
||||||
experimentalScriptExecutionTool?: boolean
|
experimentalScriptExecutionTool?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform page content before sending to LLM.
|
||||||
|
* Called after DOM extraction and simplification, before LLM invocation.
|
||||||
|
* Use cases: inspect extraction results, modify page info, mask sensitive data.
|
||||||
|
*
|
||||||
|
* @param content - Simplified page content that will be sent to LLM
|
||||||
|
* @returns Transformed content
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Mask phone numbers
|
||||||
|
* transformPageContent: async (content) => {
|
||||||
|
* return content.replace(/1[3-9]\d{9}/g, '***********')
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
transformPageContent?: (content: string) => Promise<string> | string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: @unimplemented
|
* TODO: @unimplemented
|
||||||
* hook when action causes a new page to be opened
|
* hook when action causes a new page to be opened
|
||||||
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
|
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
|
||||||
|
* @todo remove `this` binding, pass agent as explicit parameter instead
|
||||||
*/
|
*/
|
||||||
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
|
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-controller.js",
|
"main": "./dist/lib/page-controller.js",
|
||||||
"module": "./dist/lib/page-controller.js",
|
"module": "./dist/lib/page-controller.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-ui.js",
|
"main": "./dist/lib/page-agent-ui.js",
|
||||||
"module": "./dist/lib/page-agent-ui.js",
|
"module": "./dist/lib/page-agent-ui.js",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class I18n {
|
|||||||
|
|
||||||
constructor(language: SupportedLanguage = 'en-US') {
|
constructor(language: SupportedLanguage = 'en-US') {
|
||||||
this.language = language in locales ? language : 'en-US'
|
this.language = language in locales ? language : 'en-US'
|
||||||
this.translations = locales[language]
|
this.translations = locales[this.language]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 类型安全的翻译方法
|
// 类型安全的翻译方法
|
||||||
|
|||||||
+16
-21
@@ -37,14 +37,17 @@ Available Magic UI components: https://magicui.design/docs/components
|
|||||||
Located in `src/components/ui/`:
|
Located in `src/components/ui/`:
|
||||||
|
|
||||||
**From shadcn/ui:**
|
**From shadcn/ui:**
|
||||||
|
|
||||||
- `alert`, `badge`, `button`, `separator`, `sonner`, `switch`, `tooltip`
|
- `alert`, `badge`, `button`, `separator`, `sonner`, `switch`, `tooltip`
|
||||||
|
|
||||||
**From Magic UI:**
|
**From Magic UI:**
|
||||||
|
|
||||||
- `animated-gradient-text`, `animated-shiny-text`, `aurora-text`
|
- `animated-gradient-text`, `animated-shiny-text`, `aurora-text`
|
||||||
- `hyper-text`, `magic-card`, `neon-gradient-card`, `particles`
|
- `hyper-text`, `magic-card`, `neon-gradient-card`, `particles`
|
||||||
- `sparkles-text`, `text-animate`, `typing-animation`
|
- `sparkles-text`, `text-animate`, `typing-animation`
|
||||||
|
|
||||||
**Custom:**
|
**Custom:**
|
||||||
|
|
||||||
- `highlighter`, `kbd`, `spinner`
|
- `highlighter`, `kbd`, `spinner`
|
||||||
|
|
||||||
### Styling Rules
|
### Styling Rules
|
||||||
@@ -58,17 +61,17 @@ Located in `src/components/ui/`:
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
|
├── pages/
|
||||||
|
│ ├── Home.tsx # Homepage
|
||||||
|
│ └── docs/
|
||||||
|
│ ├── Layout.tsx # Documentation sidebar
|
||||||
|
│ └── [section]/[topic]/page.tsx
|
||||||
├── components/
|
├── components/
|
||||||
│ ├── ui/ # shadcn/ui + Magic UI components
|
│ ├── ui/ # shadcn/ui + Magic UI components
|
||||||
│ ├── Header.tsx # Site header
|
│ ├── Header.tsx # Site header
|
||||||
│ ├── Footer.tsx # Site footer
|
│ └── Footer.tsx # Site footer
|
||||||
│ └── DocsLayout.tsx # Documentation sidebar
|
|
||||||
├── docs/ # Documentation pages
|
|
||||||
│ └── [section]/[topic]/page.tsx
|
|
||||||
├── test-pages/ # Library integration tests
|
|
||||||
├── i18n/ # Internationalization
|
├── i18n/ # Internationalization
|
||||||
├── router.tsx # Central routing
|
├── router.tsx # Central routing
|
||||||
├── page.tsx # Homepage
|
|
||||||
└── main.tsx # App entry
|
└── main.tsx # App entry
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -76,14 +79,9 @@ src/
|
|||||||
|
|
||||||
### Documentation Page
|
### Documentation Page
|
||||||
|
|
||||||
1. Create `src/docs/<section>/<slug>/page.tsx`
|
1. Create `src/pages/docs/<section>/<slug>/page.tsx`
|
||||||
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
||||||
3. Add navigation item to `DocsLayout.tsx`
|
3. Add navigation item to `pages/docs/Layout.tsx`
|
||||||
|
|
||||||
### Test Page
|
|
||||||
|
|
||||||
1. Create `src/test-pages/<name>.tsx`
|
|
||||||
2. Add route to `src/test-pages/router.tsx`
|
|
||||||
|
|
||||||
## Routing
|
## Routing
|
||||||
|
|
||||||
@@ -93,18 +91,16 @@ Uses hash-based routing for static hosting:
|
|||||||
import { Router } from 'wouter'
|
import { Router } from 'wouter'
|
||||||
import { useHashLocation } from 'wouter/use-hash-location'
|
import { useHashLocation } from 'wouter/use-hash-location'
|
||||||
|
|
||||||
<Router hook={useHashLocation}>
|
;<Router hook={useHashLocation}>{/* routes */}</Router>
|
||||||
{/* routes */}
|
|
||||||
</Router>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
| ----------------- | ----------------------- |
|
||||||
| `components.json` | shadcn/ui configuration |
|
| `components.json` | shadcn/ui configuration |
|
||||||
| `vite.config.js` | Vite build settings |
|
| `vite.config.js` | Vite build settings |
|
||||||
| `tsconfig.json` | TypeScript config |
|
| `tsconfig.json` | TypeScript config |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -112,4 +108,3 @@ import { useHashLocation } from 'wouter/use-hash-location'
|
|||||||
npm start # Dev server (from root)
|
npm start # Dev server (from root)
|
||||||
npm run build:website # Build website (from root)
|
npm run build:website # Build website (from root)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.17",
|
"version": "0.0.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-i18next": "^16.5.0",
|
"react-i18next": "^16.5.1",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"wouter": "^3.9.0"
|
"wouter": "^3.9.0"
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"motion": "^12.23.26",
|
"motion": "^12.24.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.2.0",
|
"simple-icons": "^16.4.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import BetaNotice from '@/components/BetaNotice'
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
|
||||||
|
|
||||||
export default function DataMasking() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-4xl font-bold mb-6">数据脱敏</h1>
|
|
||||||
|
|
||||||
<BetaNotice />
|
|
||||||
|
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
|
|
||||||
保护敏感数据,确保 AI 处理过程中的数据安全。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-3">脱敏策略</h2>
|
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
|
||||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
|
|
||||||
🔒 自动脱敏
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
自动识别并脱敏手机号、身份证号、银行卡号等敏感信息。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
|
|
||||||
⚙️ 自定义规则
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
支持自定义脱敏规则,适应不同业务场景的数据保护需求。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
code={`// 数据脱敏配置
|
|
||||||
// @todo
|
|
||||||
const rules = [
|
|
||||||
{ pattern: /\\d{11}/, replacement: '***-****-****' },
|
|
||||||
{ pattern: /\\d{4}-\\d{4}-\\d{4}-\\d{4}/, replacement: '****-****-****-****' }
|
|
||||||
]
|
|
||||||
pageAgent.maskData(rules)`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import BetaNotice from '@/components/BetaNotice'
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
|
||||||
|
|
||||||
export default function KnowledgeInjection() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-4xl font-bold mb-6">知识库注入</h1>
|
|
||||||
|
|
||||||
<BetaNotice />
|
|
||||||
|
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
|
||||||
通过多层次的知识注入,让 AI 深度理解你的业务场景和应用逻辑,实现更精准的自动化操作。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Custom Instruction */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-3xl font-bold mb-6">Instruction - 系统指令</h2>
|
|
||||||
|
|
||||||
<div className="p-6 bg-purple-50 dark:bg-purple-900/20 rounded-lg mb-6">
|
|
||||||
<h3 className="text-xl font-semibold mb-3 text-purple-900 dark:text-purple-300">
|
|
||||||
🎯 系统级指令
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
||||||
为 AI 设定全局行为准则和工作风格。
|
|
||||||
</p>
|
|
||||||
<ul className="list-disc list-inside space-y-2 text-gray-500 dark:text-gray-400">
|
|
||||||
<li>定义 AI 的工作风格和交互方式</li>
|
|
||||||
<li>设置安全边界和操作限制</li>
|
|
||||||
<li>指定错误处理和异常情况的应对策略</li>
|
|
||||||
<li>配置输出格式和反馈机制</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
className="mb-6"
|
|
||||||
code={`// 构造函数中设置系统指令
|
|
||||||
const pageAgent = new PageAgent({
|
|
||||||
instruction: \`
|
|
||||||
# 角色定义
|
|
||||||
你是专业的电商运营助手。
|
|
||||||
|
|
||||||
# 工作风格
|
|
||||||
- 谨慎:操作前确认
|
|
||||||
- 准确:确保正确性
|
|
||||||
- 高效:优化流程
|
|
||||||
|
|
||||||
# 错误处理
|
|
||||||
遇到错误时暂停并报告。
|
|
||||||
\`
|
|
||||||
});`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* App Knowledge */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-3xl font-bold mb-6">App Knowledge - 应用知识</h2>
|
|
||||||
|
|
||||||
<div className="p-6 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
|
|
||||||
<h3 className="text-xl font-semibold mb-3 text-blue-900 dark:text-blue-300">
|
|
||||||
� 业务领域知识
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
||||||
注入应用的核心业务知识,包括产品介绍、操作流程、术语定义等,让 AI 理解业务上下文。
|
|
||||||
</p>
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-semibold text-blue-800 dark:text-blue-200">产品知识</h4>
|
|
||||||
<ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
|
||||||
<li>产品功能和特性介绍</li>
|
|
||||||
<li>用户角色和权限体系</li>
|
|
||||||
<li>业务规则和约束条件</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-semibold text-blue-800 dark:text-blue-200">操作指南</h4>
|
|
||||||
<ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
|
||||||
<li>标准操作流程定义</li>
|
|
||||||
<li>异常情况处理方案</li>
|
|
||||||
<li>术语和概念解释</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
className="mb-6"
|
|
||||||
code={`// 应用知识
|
|
||||||
pageAgent.knowledge.setAppKnowledge(\`
|
|
||||||
# 产品介绍
|
|
||||||
电商管理系统:面向中小企业的一站式解决方案。
|
|
||||||
|
|
||||||
# 操作流程
|
|
||||||
## 商品上架
|
|
||||||
1. 进入商品管理页面 2. 点击新增商品 3. 填写基本信息 4. 设置库存 5. 提交审核
|
|
||||||
|
|
||||||
# 术语解释
|
|
||||||
- SKU:库存量单位
|
|
||||||
- SPU:标准产品单位
|
|
||||||
- 运费模板:物流费用计算规则
|
|
||||||
|
|
||||||
# 业务规则
|
|
||||||
- 库存为0时自动下架
|
|
||||||
- VIP会员享9.5折
|
|
||||||
\`);`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Page Knowledge */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-3xl font-bold mb-6">Page Knowledge - 页面知识</h2>
|
|
||||||
|
|
||||||
<div className="p-6 bg-green-50 dark:bg-green-900/20 rounded-lg mb-6">
|
|
||||||
<h3 className="text-xl font-semibold mb-3 text-green-900 dark:text-green-300">
|
|
||||||
📄 页面级精准指导
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
||||||
为特定页面提供精确的操作指导和元素说明,让 AI 准确理解页面结构和交互逻辑。
|
|
||||||
</p>
|
|
||||||
<div className="grid md:grid-cols-3 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-semibold text-green-800 dark:text-green-200">元素标注</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">为页面元素添加语义化描述</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-semibold text-green-800 dark:text-green-200">交互说明</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
定义元素的交互行为和预期结果
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-semibold text-green-800 dark:text-green-200">页面逻辑</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
说明页面的业务逻辑和状态变化
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
className="mb-6"
|
|
||||||
code={`// 页面知识库
|
|
||||||
// 添加页面知识
|
|
||||||
pageAgent.knowledge.addPageKnowledge("/products", \`
|
|
||||||
商品列表页面,包含搜索、筛选、批量操作功能。
|
|
||||||
#add-product-btn:新增商品按钮
|
|
||||||
.product-item:商品列表项
|
|
||||||
#search-input:搜索框,最少2个字符
|
|
||||||
\`);
|
|
||||||
|
|
||||||
pageAgent.knowledge.addPageKnowledge("/orders/*", \`
|
|
||||||
订单详情页面。
|
|
||||||
.order-status:订单状态标签
|
|
||||||
#update-status-btn:状态更新按钮
|
|
||||||
\`);
|
|
||||||
|
|
||||||
// 移除页面知识
|
|
||||||
pageAgent.knowledge.removePageKnowledge("/products");`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import BetaNotice from '@/components/BetaNotice'
|
|
||||||
|
|
||||||
export default function SecurityPermissions() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-4xl font-bold mb-6">安全与权限</h1>
|
|
||||||
|
|
||||||
<BetaNotice />
|
|
||||||
|
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
|
||||||
page-agent 提供四种安全机制,确保 AI 操作在可控范围内进行。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-bold mb-3">元素操作黑白名单</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
|
|
||||||
🚫 操作黑名单
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
禁止 AI 操作敏感元素,如删除按钮、支付按钮等。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
|
|
||||||
✅ 操作白名单
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">明确定义 AI 可以操作的元素范围。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-bold mb-3">URL 黑白名单</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
|
|
||||||
🚫 URL 黑名单
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">禁止 AI 访问敏感页面和危险链接。</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
|
|
||||||
✅ URL 白名单
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">限制 AI 只能访问预定义的安全页面。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-bold mb-3">Instruction 安全约束</h2>
|
|
||||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
|
|
||||||
⚠️ 高危操作控制
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-3">
|
|
||||||
在 AI 指令中明确列举高危操作,通过两种策略进行控制:
|
|
||||||
</p>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="pl-3 border-l-2 border-red-400">
|
|
||||||
<p className="font-medium text-red-700 dark:text-red-300">完全禁止操作</p>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
对极高风险操作明确禁止执行
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="pl-3 border-l-2 border-orange-400">
|
|
||||||
<p className="font-medium text-orange-700 dark:text-orange-300">需用户确认操作</p>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
对中等风险操作要求用户明确同意
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import BetaNotice from '@/components/BetaNotice'
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
|
||||||
import { CDN_CN_URL, CDN_URL } from '@/constants'
|
|
||||||
|
|
||||||
export default function CdnSetup() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-4xl font-bold mb-6">CDN 引入</h1>
|
|
||||||
|
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
|
|
||||||
通过 CDN 快速集成 page-agent,无需复杂的构建配置。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-3">快速开始</h2>
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
className="mb-8"
|
|
||||||
code={`
|
|
||||||
// CDN: \t${CDN_URL}
|
|
||||||
// Mirror: \t${CDN_CN_URL}
|
|
||||||
<script
|
|
||||||
src="${CDN_URL}"
|
|
||||||
crossorigin="true"
|
|
||||||
type="text/javascript"
|
|
||||||
></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// window.pageAgent
|
|
||||||
</script>`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
|
|
||||||
⚠️ 注意事项
|
|
||||||
</h3>
|
|
||||||
<ul className="text-gray-600 dark:text-gray-300 space-y-1">
|
|
||||||
<li>• 生产环境建议使用固定版本号</li>
|
|
||||||
<li>• 确保 HTTPS 环境下使用</li>
|
|
||||||
<li>• 配置 CSP 策略允许脚本执行</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import CodeEditor from '@/components/CodeEditor'
|
|
||||||
|
|
||||||
export default function Configuration() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-4xl font-bold mb-6">配置选项</h1>
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
className="mb-8"
|
|
||||||
language="typescript"
|
|
||||||
code={`// config
|
|
||||||
type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
|
|
||||||
|
|
||||||
interface LLMConfig {
|
|
||||||
baseURL?: string
|
|
||||||
apiKey?: string
|
|
||||||
model?: string
|
|
||||||
temperature?: number
|
|
||||||
maxTokens?: number
|
|
||||||
maxRetries?: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom fetch function for LLM API requests.
|
|
||||||
* Use this to customize headers, credentials, proxy, etc.
|
|
||||||
* The response should follow OpenAI API format.
|
|
||||||
*/
|
|
||||||
customFetch?: typeof globalThis.fetch
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AgentConfig {
|
|
||||||
language?: "en-US" | "zh-CN"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom tools to extend PageAgent capabilities
|
|
||||||
* @experimental
|
|
||||||
* @note You can also override or remove internal tools by using the same name.
|
|
||||||
* @see [tools](../tools/index.ts)
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // override internal tool
|
|
||||||
* import { tool } from 'page-agent'
|
|
||||||
* const customTools = {
|
|
||||||
* ask_user: tool({
|
|
||||||
* description:
|
|
||||||
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
|
|
||||||
* inputSchema: zod.object({
|
|
||||||
* question: zod.string(),
|
|
||||||
* }),
|
|
||||||
* execute: async function (this: PageAgent, input) {
|
|
||||||
* const answer = await do_some_thing(input.question)
|
|
||||||
* return "✅ Received user answer: " + answer
|
|
||||||
* },
|
|
||||||
* })
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // remove internal tool
|
|
||||||
* const customTools = {
|
|
||||||
* ask_user: null // never ask user questions
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
customTools?: Record<string, PageAgentTool | null>
|
|
||||||
|
|
||||||
// lifecycle hooks
|
|
||||||
// @todo: use event instead of hooks
|
|
||||||
|
|
||||||
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
|
|
||||||
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void
|
|
||||||
onBeforeTask?: (this: PageAgent) => Promise<void> | void
|
|
||||||
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @note this hook can block the disposal process
|
|
||||||
* @note when dispose caused by page unload, "reason" will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut.
|
|
||||||
*/
|
|
||||||
onDispose?: (this: PageAgent, reason?: string) => void
|
|
||||||
|
|
||||||
// page behavior hooks
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @experimental
|
|
||||||
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
|
|
||||||
* @note Can cause unpredictable side effects.
|
|
||||||
* @note May bypass some safe guards and data-masking mechanisms.
|
|
||||||
*/
|
|
||||||
experimentalScriptExecutionTool?: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: @unimplemented
|
|
||||||
* hook when action causes a new page to be opened
|
|
||||||
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
|
|
||||||
*/
|
|
||||||
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: @unimplemented
|
|
||||||
* try to navigate to a new page instead of opening a new tab/window.
|
|
||||||
* @note will unload the current page when a action tries to open a new page. so that things keep in the same tab/window.
|
|
||||||
*/
|
|
||||||
experimentalPreventNewPage?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageControllerConfig {
|
|
||||||
interactiveBlacklist?: (Element | (() => Element))[]
|
|
||||||
interactiveWhitelist?: (Element | (() => Element))[]
|
|
||||||
include_attributes?: string[]
|
|
||||||
highlightOpacity?: number
|
|
||||||
highlightLabelOpacity?: number
|
|
||||||
viewportExpansion?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -27,14 +27,14 @@ export default {
|
|||||||
overview: 'Overview',
|
overview: 'Overview',
|
||||||
quick_start: 'Quick Start',
|
quick_start: 'Quick Start',
|
||||||
limitations: 'Limitations',
|
limitations: 'Limitations',
|
||||||
model_integration: 'Model Integration',
|
models: 'Models',
|
||||||
custom_tools: 'Custom Tools',
|
custom_tools: 'Custom Tools',
|
||||||
knowledge_injection: 'Knowledge Injection',
|
knowledge_injection: 'Instructions',
|
||||||
security_permissions: 'Security & Permissions',
|
|
||||||
data_masking: 'Data Masking',
|
data_masking: 'Data Masking',
|
||||||
cdn_setup: 'CDN Setup',
|
cdn_setup: 'CDN Setup',
|
||||||
configuration: 'Configuration',
|
configuration: 'Configuration',
|
||||||
best_practices: 'Best Practices',
|
best_practices: 'Best Practices',
|
||||||
third_party_agent: 'Third-party Agent',
|
third_party_agent: 'Third-party Agent',
|
||||||
|
security_permissions: 'Security & Permissions',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default {
|
|||||||
'page-agent is a purely web-based GUI Agent. Gives your website an AI operator in simple steps.',
|
'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 agent clients builders. Integrate it into your site to let users interact with pages through natural language.',
|
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent is built for web developers and web applications first. Integrate it into your site to let users interact with pages through natural language.',
|
||||||
features_title: 'Core Features',
|
features_title: 'Core Features',
|
||||||
feature_dom: {
|
feature_dom: {
|
||||||
title: '🧠 Smart DOM Analysis',
|
title: '🧠 Smart DOM Analysis',
|
||||||
@@ -112,8 +112,8 @@ export default {
|
|||||||
future_3: '• Basic visual understanding',
|
future_3: '• Basic visual understanding',
|
||||||
future_4: '• Smarter error recovery',
|
future_4: '• Smarter error recovery',
|
||||||
},
|
},
|
||||||
model_integration: {
|
models: {
|
||||||
title: 'Model Integration',
|
title: 'Models',
|
||||||
subtitle:
|
subtitle:
|
||||||
'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.',
|
'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.',
|
||||||
recommendation_logic: 'Recommended: Lightweight models with strong ToolCall capabilities.',
|
recommendation_logic: 'Recommended: Lightweight models with strong ToolCall capabilities.',
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ export default {
|
|||||||
overview: '概览',
|
overview: '概览',
|
||||||
quick_start: '快速开始',
|
quick_start: '快速开始',
|
||||||
limitations: '使用限制',
|
limitations: '使用限制',
|
||||||
model_integration: '模型接入',
|
models: '模型',
|
||||||
custom_tools: '自定义工具',
|
custom_tools: '自定义工具',
|
||||||
knowledge_injection: '知识库注入',
|
knowledge_injection: '知识注入',
|
||||||
security_permissions: '安全与权限',
|
|
||||||
data_masking: '数据脱敏',
|
data_masking: '数据脱敏',
|
||||||
cdn_setup: 'CDN 引入',
|
cdn_setup: 'CDN 引入',
|
||||||
configuration: '配置选项',
|
configuration: '配置选项',
|
||||||
best_practices: '最佳实践',
|
best_practices: '最佳实践',
|
||||||
third_party_agent: '接入第三方 Agent',
|
third_party_agent: '接入第三方 Agent',
|
||||||
|
security_permissions: '安全与权限',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ export default {
|
|||||||
future_3: '• 基础的视觉理解能力',
|
future_3: '• 基础的视觉理解能力',
|
||||||
future_4: '• 更智能的错误恢复机制',
|
future_4: '• 更智能的错误恢复机制',
|
||||||
},
|
},
|
||||||
model_integration: {
|
models: {
|
||||||
title: '模型接入',
|
title: '模型',
|
||||||
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
|
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
|
||||||
recommendation_logic: '推荐使用 ToolCall 能力强的轻量级模型。',
|
recommendation_logic: '推荐使用 ToolCall 能力强的轻量级模型。',
|
||||||
recommended: '推荐模型',
|
recommended: '推荐模型',
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { Route, Router, Switch } from 'wouter'
|
import { Router } from 'wouter'
|
||||||
import { useHashLocation } from 'wouter/use-hash-location'
|
import { useHashLocation } from 'wouter/use-hash-location'
|
||||||
|
|
||||||
import './i18n/config'
|
import './i18n/config'
|
||||||
import './i18n/types'
|
import './i18n/types'
|
||||||
import { default as PagesRouter } from './router'
|
import { default as PagesRouter } from './router'
|
||||||
import { default as TestPagesRouter } from './test-pages/router'
|
|
||||||
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<Router hook={useHashLocation}>
|
<Router hook={useHashLocation}>
|
||||||
<Switch>
|
<PagesRouter />
|
||||||
<Route path="/test-pages" component={TestPagesRouter} nest />
|
|
||||||
<Route path="/" component={PagesRouter} nest />
|
|
||||||
</Switch>
|
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ 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 Footer from './components/Footer'
|
import Footer from '../components/Footer'
|
||||||
import Header from './components/Header'
|
import Header from '../components/Header'
|
||||||
import { AnimatedGradientText } from './components/ui/animated-gradient-text'
|
import { AnimatedGradientText } from '../components/ui/animated-gradient-text'
|
||||||
import { Highlighter } from './components/ui/highlighter'
|
import { Highlighter } from '../components/ui/highlighter'
|
||||||
import { NeonGradientCard } from './components/ui/neon-gradient-card'
|
import { NeonGradientCard } from '../components/ui/neon-gradient-card'
|
||||||
import { Particles } from './components/ui/particles'
|
import { Particles } from '../components/ui/particles'
|
||||||
import { SparklesText } from './components/ui/sparkles-text'
|
import { SparklesText } from '../components/ui/sparkles-text'
|
||||||
import { CDN_CN_URL, CDN_URL } from './constants'
|
import { CDN_CN_URL, CDN_URL } from '../constants'
|
||||||
|
|
||||||
function getInjection(useCN?: boolean) {
|
function getInjection(useCN?: boolean) {
|
||||||
const cdn = useCN ? CDN_CN_URL : CDN_URL
|
const cdn = useCN ? CDN_CN_URL : CDN_URL
|
||||||
@@ -64,6 +64,15 @@ export default function HomePage() {
|
|||||||
interactiveBlacklist: [document.getElementById('root')!],
|
interactiveBlacklist: [document.getElementById('root')!],
|
||||||
language: i18n.language as any,
|
language: i18n.language as any,
|
||||||
|
|
||||||
|
instructions: {
|
||||||
|
system: 'You are a helpful assistant on PageAgent website.',
|
||||||
|
getPageInstructions: (url) => {
|
||||||
|
const hint = url.includes('page-agent') ? 'This is PageAgent demo page.' : undefined
|
||||||
|
console.log('[instructions] getPageInstructions:', url, '->', hint)
|
||||||
|
return hint
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// experimentalScriptExecutionTool: true,
|
// experimentalScriptExecutionTool: true,
|
||||||
|
|
||||||
// testing server
|
// testing server
|
||||||
+9
-6
@@ -32,20 +32,23 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
|
|||||||
{
|
{
|
||||||
title: t('nav.features'),
|
title: t('nav.features'),
|
||||||
items: [
|
items: [
|
||||||
{ title: t('nav.model_integration'), path: '/docs/features/model-integration' },
|
{ title: t('nav.models'), path: '/docs/features/model-integration' },
|
||||||
{ title: t('nav.custom_tools'), path: '/docs/features/custom-tools' },
|
{ title: t('nav.custom_tools'), path: '/docs/features/custom-tools' },
|
||||||
{ title: t('nav.knowledge_injection'), path: '/docs/features/knowledge-injection' },
|
{ title: t('nav.knowledge_injection'), path: '/docs/features/custom-instructions' },
|
||||||
{ title: t('nav.security_permissions'), path: '/docs/features/security-permissions' },
|
|
||||||
{ title: t('nav.data_masking'), path: '/docs/features/data-masking' },
|
{ title: t('nav.data_masking'), path: '/docs/features/data-masking' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('nav.integration'),
|
title: t('nav.integration'),
|
||||||
items: [
|
items: [
|
||||||
{ title: t('nav.cdn_setup'), path: '/docs/integration/cdn-setup' },
|
|
||||||
{ title: t('nav.configuration'), path: '/docs/integration/configuration' },
|
{ title: t('nav.configuration'), path: '/docs/integration/configuration' },
|
||||||
{ title: t('nav.best_practices'), path: '/docs/integration/best-practices' },
|
|
||||||
{ title: t('nav.third_party_agent'), path: '/docs/integration/third-party-agent' },
|
{ title: t('nav.third_party_agent'), path: '/docs/integration/third-party-agent' },
|
||||||
|
{ title: '🚧 ' + t('nav.cdn_setup'), path: '/docs/integration/cdn-setup' },
|
||||||
|
{
|
||||||
|
title: '🚧 ' + t('nav.security_permissions'),
|
||||||
|
path: '/docs/integration/security-permissions',
|
||||||
|
},
|
||||||
|
{ title: '🚧 ' + t('nav.best_practices'), path: '/docs/integration/best-practices' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -55,7 +58,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
|
|||||||
<div className="flex gap-8 min-w-[900px]">
|
<div className="flex gap-8 min-w-[900px]">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-64 shrink-0" role="complementary" aria-label="文档导航">
|
<aside className="w-64 shrink-0" role="complementary" aria-label="文档导航">
|
||||||
<div className="sticky top-8">
|
<div className="sticky">
|
||||||
<nav className="space-y-8" role="navigation" aria-label="文档章节">
|
<nav className="space-y-8" role="navigation" aria-label="文档章节">
|
||||||
{navigationSections.map((section) => (
|
{navigationSections.map((section) => (
|
||||||
<section key={section.title}>
|
<section key={section.title}>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
|
export default function Instructions() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const isZh = i18n.language === 'zh-CN'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl font-bold mb-6">{isZh ? '知识注入' : 'Instructions'}</h1>
|
||||||
|
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
||||||
|
{isZh
|
||||||
|
? '通过 instructions 配置,为 AI 注入系统级指导和页面级上下文,让它更好地理解你的业务场景。'
|
||||||
|
: 'Use the instructions config to inject system-level directives and page-specific context, helping the AI better understand your application.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* System Instructions */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-bold mb-6">
|
||||||
|
{isZh ? '系统级指导 (System Instructions)' : 'System Instructions'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
|
{isZh
|
||||||
|
? '全局提示词,应用于所有任务。定义 AI 的角色、工作风格和行为边界。'
|
||||||
|
: "Global directives applied to all tasks. Define the AI's role, working style, and behavioral boundaries."}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
|
className="mb-6"
|
||||||
|
code={`const agent = new PageAgent({
|
||||||
|
// ...other config
|
||||||
|
instructions: {
|
||||||
|
system: \`
|
||||||
|
You are a professional e-commerce assistant.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
- Always confirm before submitting orders
|
||||||
|
- Double-check prices and quantities
|
||||||
|
- Report errors immediately instead of retrying blindly
|
||||||
|
\`
|
||||||
|
}
|
||||||
|
})`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Page Instructions */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-bold mb-6">
|
||||||
|
{isZh ? '页面级指导 (Page Instructions)' : 'Page Instructions'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
|
{isZh
|
||||||
|
? '动态回调函数,在每个 step 执行前调用,根据当前页面 URL 返回特定提示词。适用于为不同页面提供针对性的操作引导。'
|
||||||
|
: 'A dynamic callback invoked before each step. Returns page-specific instructions based on the current URL. Useful for providing targeted guidance on different pages.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
|
className="mb-6"
|
||||||
|
code={`const agent = new PageAgent({
|
||||||
|
// ...other config
|
||||||
|
instructions: {
|
||||||
|
system: 'You are an order management assistant.',
|
||||||
|
|
||||||
|
getPageInstructions: (url) => {
|
||||||
|
if (url.includes('/checkout')) {
|
||||||
|
return \`
|
||||||
|
This is the checkout page.
|
||||||
|
- Verify shipping address before proceeding
|
||||||
|
- Check if any discounts are applied
|
||||||
|
- Confirm the total amount with the user
|
||||||
|
\`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.includes('/products')) {
|
||||||
|
return \`
|
||||||
|
This is the product listing page.
|
||||||
|
- Use filters to narrow down search results
|
||||||
|
- Check stock availability before adding to cart
|
||||||
|
\`
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined // No special instructions for other pages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* How It Works */}
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-bold mb-6">{isZh ? '工作原理' : 'How It Works'}</h2>
|
||||||
|
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '在每个执行步骤之前,page-agent 会将 instructions 拼接到用户提示词中:'
|
||||||
|
: 'Before each execution step, page-agent prepends the instructions to the user prompt:'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
|
language="xml"
|
||||||
|
className="mb-6"
|
||||||
|
code={`<instructions>
|
||||||
|
<system_instructions>
|
||||||
|
You are a professional e-commerce assistant.
|
||||||
|
...
|
||||||
|
</system_instructions>
|
||||||
|
<page_instructions>
|
||||||
|
This is the checkout page.
|
||||||
|
...
|
||||||
|
</page_instructions>
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
<!-- followed by agent state, history, and browser state -->`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ul className="list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400">
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '如果 system 为空,则不输出 <system_instructions> 标签'
|
||||||
|
: 'If system is empty, the <system_instructions> tag is omitted'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '如果 getPageInstructions 返回空值,则不输出 <page_instructions> 标签'
|
||||||
|
: 'If getPageInstructions returns empty, the <page_instructions> tag is omitted'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
|
export default function DataMasking() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const isZh = i18n.language === 'zh-CN'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl font-bold mb-6">{isZh ? '数据脱敏' : 'Data Masking'}</h1>
|
||||||
|
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
||||||
|
{isZh
|
||||||
|
? '使用 transformPageContent 钩子在页面内容发送给 LLM 之前进行处理,可用于检查清洗效果、修改页面信息、隐藏敏感数据等。'
|
||||||
|
: 'Use the transformPageContent hook to process page content before sending to LLM. Useful for inspecting extraction results, modifying page info, and masking sensitive data.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-bold mb-6">{isZh ? '接口定义' : 'API Definition'}</h2>
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
|
className="mb-6"
|
||||||
|
code={`interface PageAgentConfig {
|
||||||
|
/**
|
||||||
|
* Transform page content before sending to LLM.
|
||||||
|
* Called after DOM extraction and simplification.
|
||||||
|
*/
|
||||||
|
transformPageContent?: (content: string) => Promise<string> | string
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-3xl font-bold mb-6">
|
||||||
|
{isZh ? '常用脱敏规则' : 'Common Masking Patterns'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
|
{isZh
|
||||||
|
? '以下示例展示了如何脱敏常见的敏感信息:'
|
||||||
|
: 'The following example shows how to mask common sensitive data:'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
|
code={`const agent = new PageAgent({
|
||||||
|
transformPageContent: async (content) => {
|
||||||
|
// China phone number (11 digits starting with 1)
|
||||||
|
content = content.replace(/\\b(1[3-9]\\d)(\\d{4})(\\d{4})\\b/g, '$1****$3')
|
||||||
|
|
||||||
|
// Email address
|
||||||
|
content = content.replace(
|
||||||
|
/\\b([a-zA-Z0-9._%+-])[^@]*(@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})\\b/g,
|
||||||
|
'$1***$2'
|
||||||
|
)
|
||||||
|
|
||||||
|
// China ID card number (18 digits)
|
||||||
|
content = content.replace(
|
||||||
|
/\\b(\\d{6})(19|20\\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])(\\d{3}[\\dXx])\\b/g,
|
||||||
|
'$1********$5'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bank card number (16-19 digits)
|
||||||
|
content = content.replace(/\\b(\\d{4})\\d{8,11}(\\d{4})\\b/g, '$1********$2')
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
})`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+15
-17
@@ -27,16 +27,14 @@ export default function ModelIntegration() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
<h1 className="text-4xl font-bold mb-4">{t('model_integration.title')}</h1>
|
<h1 className="text-4xl font-bold mb-4">{t('models.title')}</h1>
|
||||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">
|
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">{t('models.subtitle')}</p>
|
||||||
{t('model_integration.subtitle')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Models Section */}
|
{/* Models Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-3">{t('model_integration.available')}</h2>
|
<h2 className="text-2xl font-semibold mb-3">{t('models.available')}</h2>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{t('model_integration.recommendation_logic')}
|
{t('models.recommendation_logic')}
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
|
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -63,41 +61,41 @@ export default function ModelIntegration() {
|
|||||||
|
|
||||||
{/* Tips Section */}
|
{/* Tips Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{t('model_integration.tips')}</h2>
|
<h2 className="text-2xl font-semibold mb-4">{t('models.tips')}</h2>
|
||||||
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
||||||
<li>{t('model_integration.tip_2')}</li>
|
<li>{t('models.tip_2')}</li>
|
||||||
<li>{t('model_integration.tip_3')}</li>
|
<li>{t('models.tip_3')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Security Section */}
|
{/* Security Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{t('model_integration.security')}</h2>
|
<h2 className="text-2xl font-semibold mb-4">{t('models.security')}</h2>
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-5 rounded-r-lg mb-4">
|
<div className="bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-5 rounded-r-lg mb-4">
|
||||||
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
|
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
|
||||||
{t('model_integration.security_warning')}
|
{t('models.security_warning')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800">
|
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800">
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
{t('model_integration.security_backend_proxy')}
|
{t('models.security_backend_proxy')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
{t('model_integration.security_backend_desc')}
|
{t('models.security_backend_desc')}
|
||||||
</p>
|
</p>
|
||||||
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||||
<li>{t('model_integration.security_method_1')}</li>
|
<li>{t('models.security_method_1')}</li>
|
||||||
<li>{t('model_integration.security_method_2')}</li>
|
<li>{t('models.security_method_2')}</li>
|
||||||
<li>{t('model_integration.security_method_3')}</li>
|
<li>{t('models.security_method_3')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Configuration Section */}
|
{/* Configuration Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{t('model_integration.configuration')}</h2>
|
<h2 className="text-2xl font-semibold mb-4">{t('models.configuration')}</h2>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`// OpenAI-compatible services (e.g., Alibaba Bailian)
|
code={`// OpenAI-compatible services (e.g., Alibaba Bailian)
|
||||||
const pageAgent = new PageAgent({
|
const pageAgent = new PageAgent({
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { CDN_CN_URL, CDN_URL } from '@/constants'
|
||||||
|
|
||||||
|
export default function CdnSetup() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const isZh = i18n.language === 'zh-CN'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<BetaNotice />
|
||||||
|
|
||||||
|
<h1 className="text-4xl font-bold mb-6">{isZh ? 'CDN 引入' : 'CDN Setup'}</h1>
|
||||||
|
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
||||||
|
{isZh
|
||||||
|
? '通过 CDN 快速集成 page-agent,无需复杂的构建配置。默认使用演示模型。'
|
||||||
|
: 'Fastest way to integrate page-agent via CDN. No complex build setup needed. Demo model will be used by default.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section className="mb-8">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">{isZh ? 'CDN 地址' : 'CDN URLs'}</h2>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto mb-6">
|
||||||
|
<table className="w-full border-collapse text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<th className="text-left py-3 px-4 font-semibold">{isZh ? '位置' : 'Location'}</th>
|
||||||
|
<th className="text-left py-3 px-4 font-semibold">URL</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-b border-gray-100 dark:border-gray-800">
|
||||||
|
<td className="py-3 px-4">{isZh ? '全球' : 'Global'}</td>
|
||||||
|
<td className="py-3 px-4 font-mono text-xs break-all">{CDN_URL}</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-gray-100 dark:border-gray-800">
|
||||||
|
<td className="py-3 px-4">{isZh ? '中国镜像' : 'China Mirror'}</td>
|
||||||
|
<td className="py-3 px-4 font-mono text-xs break-all">{CDN_CN_URL}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-8">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">{isZh ? '快速开始' : 'Quick Start'}</h2>
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
|
className="mb-6"
|
||||||
|
code={`<script
|
||||||
|
src="${CDN_URL}"
|
||||||
|
crossorigin="true"
|
||||||
|
type="text/javascript"
|
||||||
|
></script>`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-8">
|
||||||
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
|
||||||
|
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
|
||||||
|
⚠️ {isZh ? '注意事项' : 'Notes'}
|
||||||
|
</h3>
|
||||||
|
<ul className="text-gray-600 dark:text-gray-300 space-y-1">
|
||||||
|
<li>
|
||||||
|
• {isZh ? '生产环境建议使用固定版本号' : 'Use fixed version number in production'}
|
||||||
|
</li>
|
||||||
|
<li>• {isZh ? '确保 HTTPS 环境下使用' : 'Ensure HTTPS environment'}</li>
|
||||||
|
<li>
|
||||||
|
•{' '}
|
||||||
|
{isZh
|
||||||
|
? '配置 CSP 策略允许脚本执行'
|
||||||
|
: 'Configure CSP policy to allow script execution'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
|
export default function Configuration() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const isZh = i18n.language === 'zh-CN'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl font-bold mb-6">{isZh ? '配置选项' : 'Configuration'}</h1>
|
||||||
|
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
||||||
|
{isZh
|
||||||
|
? 'PageAgent 的完整配置接口定义。'
|
||||||
|
: 'Complete configuration interface for PageAgent.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* LLM Configuration */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? 'LLM 配置' : 'LLM Configuration'}</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '配置与大语言模型的连接参数。'
|
||||||
|
: 'Configure connection parameters for the language model.'}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
className="mb-4"
|
||||||
|
language="typescript"
|
||||||
|
code={`interface LLMConfig {
|
||||||
|
baseURL?: string
|
||||||
|
apiKey?: string
|
||||||
|
model?: string
|
||||||
|
temperature?: number
|
||||||
|
maxRetries?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom fetch function for LLM API requests.
|
||||||
|
* Use this to customize headers, credentials, proxy, etc.
|
||||||
|
*/
|
||||||
|
customFetch?: typeof globalThis.fetch
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Agent Configuration */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">
|
||||||
|
{isZh ? 'Agent 配置' : 'Agent Configuration'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '配置 Agent 的行为、生命周期钩子和扩展能力。'
|
||||||
|
: 'Configure agent behavior, lifecycle hooks, and extension capabilities.'}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
className="mb-4"
|
||||||
|
language="typescript"
|
||||||
|
code={`interface AgentConfig {
|
||||||
|
language?: 'en-US' | 'zh-CN'
|
||||||
|
|
||||||
|
/** Custom tools to extend or override built-in tools */
|
||||||
|
customTools?: Record<string, PageAgentTool | null>
|
||||||
|
|
||||||
|
/** Instructions to guide the agent's behavior */
|
||||||
|
instructions?: {
|
||||||
|
/** Global system-level instructions, applied to all tasks */
|
||||||
|
system?: string
|
||||||
|
|
||||||
|
/** Dynamic page-level instructions callback */
|
||||||
|
getPageInstructions?: (url: string) => string | undefined | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle hooks
|
||||||
|
onBeforeStep?: (stepCnt: number) => Promise<void> | void
|
||||||
|
onAfterStep?: (stepCnt: number, history: AgentHistory[]) => Promise<void> | void
|
||||||
|
onBeforeTask?: () => Promise<void> | void
|
||||||
|
onAfterTask?: (result: ExecutionResult) => Promise<void> | void
|
||||||
|
onDispose?: (reason?: string) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform page content before sending to LLM.
|
||||||
|
* Use cases: inspect extraction results, modify page info, mask sensitive data.
|
||||||
|
*/
|
||||||
|
transformPageContent?: (content: string) => Promise<string> | string
|
||||||
|
|
||||||
|
/** @experimental Enable JavaScript execution tool */
|
||||||
|
experimentalScriptExecutionTool?: boolean
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* PageController Configuration */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">
|
||||||
|
{isZh ? 'PageController 配置' : 'PageController Configuration'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '配置 DOM 提取、元素交互和视觉高亮的细节。'
|
||||||
|
: 'Configure DOM extraction, element interaction, and visual highlighting.'}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
className="mb-4"
|
||||||
|
language="typescript"
|
||||||
|
code={`interface PageControllerConfig {
|
||||||
|
/** Elements to exclude from interaction */
|
||||||
|
interactiveBlacklist?: (Element | (() => Element))[]
|
||||||
|
|
||||||
|
/** Elements to force include for interaction */
|
||||||
|
interactiveWhitelist?: (Element | (() => Element))[]
|
||||||
|
|
||||||
|
/** Additional attributes to include in DOM extraction */
|
||||||
|
include_attributes?: string[]
|
||||||
|
|
||||||
|
/** Highlight overlay opacity (0-1) */
|
||||||
|
highlightOpacity?: number
|
||||||
|
|
||||||
|
/** Highlight label opacity (0-1) */
|
||||||
|
highlightLabelOpacity?: number
|
||||||
|
|
||||||
|
/** Viewport expansion in pixels (-1 for full page) */
|
||||||
|
viewportExpansion?: number
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Complete Type */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '完整类型' : 'Complete Type'}</h2>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
|
|
||||||
|
export default function SecurityPermissions() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const isZh = i18n.language === 'zh-CN'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<BetaNotice />
|
||||||
|
|
||||||
|
<h1 className="text-4xl font-bold mb-6">{isZh ? '安全与权限' : 'Security & Permissions'}</h1>
|
||||||
|
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
||||||
|
{isZh
|
||||||
|
? 'page-agent 提供多种安全机制,确保 AI 操作在可控范围内进行。'
|
||||||
|
: 'page-agent provides multiple security mechanisms to ensure AI operations stay within controlled boundaries.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold mb-3">
|
||||||
|
{isZh ? '元素操作黑白名单' : 'Element Interaction Allowlist/Blocklist'}
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||||
|
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
|
||||||
|
🚫 {isZh ? '操作黑名单' : 'Blocklist'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{isZh
|
||||||
|
? '禁止 AI 操作敏感元素,如删除按钮、支付按钮等。'
|
||||||
|
: 'Prevent AI from interacting with sensitive elements like delete buttons, payment buttons, etc.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
|
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
|
||||||
|
✅ {isZh ? '操作白名单' : 'Allowlist'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{isZh
|
||||||
|
? '明确定义 AI 可以操作的元素范围。'
|
||||||
|
: 'Explicitly define which elements AI can interact with.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold mb-3">
|
||||||
|
{isZh ? 'Instruction 安全约束' : 'Instruction Safety Constraints'}
|
||||||
|
</h2>
|
||||||
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
||||||
|
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
|
||||||
|
⚠️ {isZh ? '高危操作控制' : 'High-Risk Operation Control'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-3">
|
||||||
|
{isZh
|
||||||
|
? '在 AI 指令中明确列举高危操作,通过两种策略进行控制:'
|
||||||
|
: 'Define high-risk operations in AI instructions and control them through two strategies:'}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="pl-3 border-l-2 border-red-400">
|
||||||
|
<p className="font-medium text-red-700 dark:text-red-300">
|
||||||
|
{isZh ? '完全禁止操作' : 'Completely Forbidden'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{isZh
|
||||||
|
? '对极高风险操作明确禁止执行'
|
||||||
|
: 'Explicitly prohibit execution of extremely high-risk operations'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="pl-3 border-l-2 border-orange-400">
|
||||||
|
<p className="font-medium text-orange-700 dark:text-orange-300">
|
||||||
|
{isZh ? '需用户确认操作' : 'Requires User Confirmation'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{isZh
|
||||||
|
? '对中等风险操作要求用户明确同意'
|
||||||
|
: 'Require explicit user consent for medium-risk operations'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
-26
@@ -74,19 +74,6 @@ const pageAgentTool = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-4">最佳实践</h2>
|
|
||||||
<div className="space-y-4 mb-6">
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">错误处理</h3>
|
|
||||||
<CodeEditor code={`// @TODO`} language="javascript" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">权限控制</h3>
|
|
||||||
<CodeEditor code={`// @TODO`} language="javascript" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
|
||||||
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-100 mb-2">
|
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-100 mb-2">
|
||||||
⚠️ 注意事项
|
⚠️ 注意事项
|
||||||
@@ -97,19 +84,6 @@ const pageAgentTool = {
|
|||||||
<li>• 敏感操作建议要求人工确认</li>
|
<li>• 敏感操作建议要求人工确认</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-linear-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">🎉 开始集成</h3>
|
|
||||||
<p className="mb-3 text-gray-700 dark:text-gray-300">
|
|
||||||
通过这种方式,你的 Agent 系统就能真正成为用户的智能助手。
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="/docs/integration/configuration"
|
|
||||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200"
|
|
||||||
>
|
|
||||||
查看配置选项 →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+2
-2
@@ -6,13 +6,13 @@ export default function Overview() {
|
|||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
{/* 头图 */}
|
{/* 头图 */}
|
||||||
<figure className="mb-8 rounded-xl overflow-hidden">
|
{/* <figure className="mb-8 rounded-xl overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src="https://img.alicdn.com/imgextra/i3/O1CN014xLeic1gnF2iWdzjJ_!!6000000004186-49-tps-1280-353.webp"
|
src="https://img.alicdn.com/imgextra/i3/O1CN014xLeic1gnF2iWdzjJ_!!6000000004186-49-tps-1280-353.webp"
|
||||||
alt="page-agent"
|
alt="page-agent"
|
||||||
className="w-full h-64 object-cover"
|
className="w-full h-64 object-cover"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure> */}
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-4xl font-bold mb-4">{t('overview.title')}</h1>
|
<h1 className="text-4xl font-bold mb-4">{t('overview.title')}</h1>
|
||||||
+1
-1
@@ -55,7 +55,7 @@ const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm
|
|||||||
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||||
|
|
||||||
const agent = new PageAgent({
|
const agent = new PageAgent({
|
||||||
modelName: DEMO_MODEL,
|
model: DEMO_MODEL,
|
||||||
baseURL: DEMO_BASE_URL,
|
baseURL: DEMO_BASE_URL,
|
||||||
apiKey: DEMO_API_KEY,
|
apiKey: DEMO_API_KEY,
|
||||||
language: 'zh-CN'
|
language: 'zh-CN'
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import { Route, Switch } from 'wouter'
|
import { Route, Switch } from 'wouter'
|
||||||
|
|
||||||
import DocsLayout from './components/DocsLayout'
|
|
||||||
import Header from './components/Header'
|
import Header from './components/Header'
|
||||||
|
import HomePage from './pages/Home'
|
||||||
|
import DocsLayout from './pages/docs/Layout'
|
||||||
// Features pages
|
// Features pages
|
||||||
import CustomTools from './docs/features/custom-tools/page'
|
import Instructions from './pages/docs/features/custom-instructions/page'
|
||||||
import DataMasking from './docs/features/data-masking/page'
|
import CustomTools from './pages/docs/features/custom-tools/page'
|
||||||
import KnowledgeInjection from './docs/features/knowledge-injection/page'
|
import DataMasking from './pages/docs/features/data-masking/page'
|
||||||
import ModelIntegration from './docs/features/model-integration/page'
|
import ModelIntegration from './pages/docs/features/model-integration/page'
|
||||||
import SecurityPermissions from './docs/features/security-permissions/page'
|
|
||||||
import BestPractices from './docs/integration/best-practices/page'
|
|
||||||
// Integration pages
|
// Integration pages
|
||||||
import CdnSetup from './docs/integration/cdn-setup/page'
|
import BestPractices from './pages/docs/integration/best-practices/page'
|
||||||
import Configuration from './docs/integration/configuration/page'
|
import CdnSetup from './pages/docs/integration/cdn-setup/page'
|
||||||
import ThirdPartyAgent from './docs/integration/third-party-agent/page'
|
import Configuration from './pages/docs/integration/configuration/page'
|
||||||
import Limitations from './docs/introduction/limitations/page'
|
import SecurityPermissions from './pages/docs/integration/security-permissions/page'
|
||||||
|
import ThirdPartyAgent from './pages/docs/integration/third-party-agent/page'
|
||||||
// Introduction pages
|
// Introduction pages
|
||||||
import Overview from './docs/introduction/overview/page'
|
import Limitations from './pages/docs/introduction/limitations/page'
|
||||||
import QuickStart from './docs/introduction/quick-start/page'
|
import Overview from './pages/docs/introduction/overview/page'
|
||||||
import HomePage from './page'
|
import QuickStart from './pages/docs/introduction/quick-start/page'
|
||||||
|
|
||||||
export default function Router() {
|
export default function Router() {
|
||||||
return (
|
return (
|
||||||
@@ -53,15 +53,6 @@ export default function Router() {
|
|||||||
</div>
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/docs/features/security-permissions">
|
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
|
||||||
<Header />
|
|
||||||
<DocsLayout>
|
|
||||||
<SecurityPermissions />
|
|
||||||
</DocsLayout>
|
|
||||||
</div>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/docs/features/custom-tools">
|
<Route path="/docs/features/custom-tools">
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||||
<Header />
|
<Header />
|
||||||
@@ -80,11 +71,11 @@ export default function Router() {
|
|||||||
</div>
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/docs/features/knowledge-injection">
|
<Route path="/docs/features/custom-instructions">
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||||
<Header />
|
<Header />
|
||||||
<DocsLayout>
|
<DocsLayout>
|
||||||
<KnowledgeInjection />
|
<Instructions />
|
||||||
</DocsLayout>
|
</DocsLayout>
|
||||||
</div>
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
@@ -107,6 +98,15 @@ export default function Router() {
|
|||||||
</div>
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/docs/integration/security-permissions">
|
||||||
|
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||||
|
<Header />
|
||||||
|
<DocsLayout>
|
||||||
|
<SecurityPermissions />
|
||||||
|
</DocsLayout>
|
||||||
|
</div>
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="/docs/integration/configuration">
|
<Route path="/docs/integration/configuration">
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
# Page Use Agent 测试页面
|
|
||||||
|
|
||||||
这个目录包含了一系列专门设计的测试页面,用于验证 Page Use Agent 的各种能力。每个页面都模拟了真实 Web 应用中的常见交互模式和边界情况。
|
|
||||||
|
|
||||||
## 测试页面列表
|
|
||||||
|
|
||||||
### 1. 表单测试页面 (`/test-pages/form`)
|
|
||||||
**测试目标:** 表单填写、验证和提交能力
|
|
||||||
|
|
||||||
**包含功能:**
|
|
||||||
- 各种输入类型(文本、邮箱、密码、数字、日期、电话、URL)
|
|
||||||
- 下拉选择框和复选框
|
|
||||||
- 实时表单验证
|
|
||||||
- 异步提交和错误处理
|
|
||||||
- 重置表单功能
|
|
||||||
|
|
||||||
**测试任务示例:**
|
|
||||||
- 填写完整的用户注册表单并提交
|
|
||||||
- 故意输入错误信息触发验证错误
|
|
||||||
- 测试密码确认功能
|
|
||||||
- 尝试提交空表单查看错误提示
|
|
||||||
|
|
||||||
### 2. 导航测试页面 (`/test-pages/navigation`)
|
|
||||||
**测试目标:** 复杂导航和交互元素处理
|
|
||||||
|
|
||||||
**包含功能:**
|
|
||||||
- 顶部导航栏和下拉菜单
|
|
||||||
- 面包屑导航
|
|
||||||
- 标签页切换
|
|
||||||
- 模态框弹窗
|
|
||||||
- 通知系统
|
|
||||||
- 用户菜单
|
|
||||||
|
|
||||||
**测试任务示例:**
|
|
||||||
- 点击产品下拉菜单选择不同选项
|
|
||||||
- 切换不同的标签页查看内容
|
|
||||||
- 打开和关闭模态框
|
|
||||||
- 点击面包屑导航
|
|
||||||
- 添加新通知并标记为已读
|
|
||||||
|
|
||||||
### 3. 列表测试页面 (`/test-pages/list`)
|
|
||||||
**测试目标:** 列表操作、搜索、过滤和分页
|
|
||||||
|
|
||||||
**包含功能:**
|
|
||||||
- 产品列表展示(网格和列表视图)
|
|
||||||
- 搜索功能
|
|
||||||
- 类别过滤
|
|
||||||
- 排序功能
|
|
||||||
- 分页导航
|
|
||||||
- 加载状态和骨架屏
|
|
||||||
|
|
||||||
**测试任务示例:**
|
|
||||||
- 搜索特定产品名称
|
|
||||||
- 按价格排序产品列表
|
|
||||||
- 切换网格和列表视图
|
|
||||||
- 使用分页浏览不同页面
|
|
||||||
- 按类别过滤产品
|
|
||||||
|
|
||||||
### 4. 复杂交互测试页面 (`/test-pages/complex`)
|
|
||||||
**测试目标:** 多步骤流程和状态管理
|
|
||||||
|
|
||||||
**包含功能:**
|
|
||||||
- 购物车管理(添加、删除、修改数量)
|
|
||||||
- 多步骤向导流程
|
|
||||||
- 步骤验证和导航
|
|
||||||
- 订单确认流程
|
|
||||||
- 异步提交处理
|
|
||||||
|
|
||||||
**测试任务示例:**
|
|
||||||
- 完成完整的购买流程
|
|
||||||
- 在向导中前进和后退
|
|
||||||
- 修改购物车中的商品数量
|
|
||||||
- 添加新商品到购物车
|
|
||||||
- 提交订单并处理可能的错误
|
|
||||||
|
|
||||||
### 5. 错误处理测试页面 (`/test-pages/errors`)
|
|
||||||
**测试目标:** 错误识别和重试机制
|
|
||||||
|
|
||||||
**包含功能:**
|
|
||||||
- 网络连接错误模拟
|
|
||||||
- 表单验证错误
|
|
||||||
- 权限不足错误
|
|
||||||
- 请求超时错误
|
|
||||||
- 服务器内部错误
|
|
||||||
- 文件上传错误处理
|
|
||||||
|
|
||||||
**测试任务示例:**
|
|
||||||
- 触发网络错误并重试
|
|
||||||
- 提交不完整表单查看验证错误
|
|
||||||
- 测试权限验证(用户名需为"admin")
|
|
||||||
- 上传超大文件触发错误
|
|
||||||
- 处理各种错误场景的重试逻辑
|
|
||||||
|
|
||||||
### 6. 异步操作测试页面 (`/test-pages/async`)
|
|
||||||
**测试目标:** 等待和异步操作处理
|
|
||||||
|
|
||||||
**包含功能:**
|
|
||||||
- 文件上传进度条
|
|
||||||
- 实时数据更新
|
|
||||||
- 数据加载骨架屏
|
|
||||||
- 长时间运行任务
|
|
||||||
- 进度跟踪和日志显示
|
|
||||||
|
|
||||||
**测试任务示例:**
|
|
||||||
- 启动文件上传并等待完成
|
|
||||||
- 开启实时数据更新功能
|
|
||||||
- 加载数据并等待所有项目完成
|
|
||||||
- 执行长时间任务并监控进度
|
|
||||||
- 处理上传失败的重试
|
|
||||||
|
|
||||||
## 测试任务集合
|
|
||||||
|
|
||||||
### 基础操作测试
|
|
||||||
1. **导航测试**
|
|
||||||
- 前往表单测试页面
|
|
||||||
- 返回测试页面首页
|
|
||||||
- 前往导航测试页面
|
|
||||||
|
|
||||||
2. **表单填写测试**
|
|
||||||
- 填写用户注册表单的所有必填字段
|
|
||||||
- 提交表单并等待结果
|
|
||||||
- 重置表单并重新填写
|
|
||||||
|
|
||||||
3. **搜索和过滤测试**
|
|
||||||
- 在列表页面搜索"Apple"
|
|
||||||
- 按价格降序排列产品
|
|
||||||
- 过滤显示"手机"类别的产品
|
|
||||||
|
|
||||||
### 中级交互测试
|
|
||||||
4. **购物流程测试**
|
|
||||||
- 前往复杂交互页面
|
|
||||||
- 添加商品到购物车
|
|
||||||
- 完成多步骤购买流程
|
|
||||||
- 填写个人信息、地址和支付信息
|
|
||||||
- 提交订单
|
|
||||||
|
|
||||||
5. **导航和菜单测试**
|
|
||||||
- 点击产品下拉菜单选择"手机"
|
|
||||||
- 切换到"订单管理"标签页
|
|
||||||
- 打开模态框并关闭
|
|
||||||
- 添加新的面包屑导航
|
|
||||||
|
|
||||||
6. **异步操作测试**
|
|
||||||
- 启动文件上传
|
|
||||||
- 开启实时数据更新
|
|
||||||
- 执行长时间任务并等待完成
|
|
||||||
|
|
||||||
### 高级错误处理测试
|
|
||||||
7. **错误恢复测试**
|
|
||||||
- 触发网络连接错误
|
|
||||||
- 重试失败的操作
|
|
||||||
- 处理表单验证错误
|
|
||||||
- 测试权限验证(用户名输入"admin")
|
|
||||||
|
|
||||||
8. **边界情况测试**
|
|
||||||
- 提交空表单查看错误
|
|
||||||
- 上传不支持的文件类型
|
|
||||||
- 在向导中跳过必填步骤
|
|
||||||
- 处理超时错误
|
|
||||||
|
|
||||||
### 综合场景测试
|
|
||||||
9. **完整用户流程**
|
|
||||||
- 浏览产品列表
|
|
||||||
- 搜索并过滤产品
|
|
||||||
- 添加产品到购物车
|
|
||||||
- 完成购买流程
|
|
||||||
- 处理可能出现的错误
|
|
||||||
|
|
||||||
10. **压力和边界测试**
|
|
||||||
- 快速连续点击按钮
|
|
||||||
- 在加载过程中尝试其他操作
|
|
||||||
- 测试各种错误恢复场景
|
|
||||||
- 验证所有异步操作的完成
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
### 对于 Agent 开发者
|
|
||||||
- 每个页面都包含了详细的状态指示器和反馈信息
|
|
||||||
- 错误信息清晰明确,便于 Agent 理解和处理
|
|
||||||
- 异步操作都有明确的完成标志
|
|
||||||
- 所有交互元素都有适当的可访问性标记
|
|
||||||
|
|
||||||
### 对于测试人员
|
|
||||||
- 可以按照测试任务逐一验证 Agent 的能力
|
|
||||||
- 每个页面都是独立的,可以单独测试
|
|
||||||
- 包含了各种真实场景的模拟
|
|
||||||
- 错误场景是随机的,确保测试的真实性
|
|
||||||
|
|
||||||
### 技术特性
|
|
||||||
- 使用 React + TypeScript 构建
|
|
||||||
- 响应式设计,支持不同屏幕尺寸
|
|
||||||
- 深色模式支持
|
|
||||||
- 无需外部依赖,完全自包含
|
|
||||||
- 模拟真实的网络延迟和错误
|
|
||||||
|
|
||||||
## 扩展建议
|
|
||||||
|
|
||||||
如需添加新的测试场景,建议考虑以下方面:
|
|
||||||
- 特定行业的业务流程
|
|
||||||
- 更复杂的数据可视化交互
|
|
||||||
- 多媒体内容处理
|
|
||||||
- 实时协作功能
|
|
||||||
- 移动端特有的交互模式
|
|
||||||
|
|
||||||
每个新页面都应该:
|
|
||||||
- 有明确的测试目标
|
|
||||||
- 包含多种难度级别的任务
|
|
||||||
- 提供清晰的状态反馈
|
|
||||||
- 模拟真实的用户场景
|
|
||||||
@@ -1,543 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { Link } from 'wouter'
|
|
||||||
|
|
||||||
interface UploadProgress {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
progress: number
|
|
||||||
status: 'uploading' | 'completed' | 'error'
|
|
||||||
speed: string
|
|
||||||
timeRemaining: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataItem {
|
|
||||||
id: number
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
timestamp: string
|
|
||||||
status: 'loading' | 'loaded' | 'error'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AsyncTestPage() {
|
|
||||||
const [uploads, setUploads] = useState<UploadProgress[]>([])
|
|
||||||
const [dataItems, setDataItems] = useState<DataItem[]>([])
|
|
||||||
const [isLoadingData, setIsLoadingData] = useState(false)
|
|
||||||
const [realTimeData, setRealTimeData] = useState<string[]>([])
|
|
||||||
const [isRealTimeActive, setIsRealTimeActive] = useState(false)
|
|
||||||
const [longRunningTask, setLongRunningTask] = useState<{
|
|
||||||
isRunning: boolean
|
|
||||||
progress: number
|
|
||||||
currentStep: string
|
|
||||||
logs: string[]
|
|
||||||
}>({
|
|
||||||
isRunning: false,
|
|
||||||
progress: 0,
|
|
||||||
currentStep: '',
|
|
||||||
logs: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
// 模拟实时数据更新
|
|
||||||
useEffect(() => {
|
|
||||||
let interval: NodeJS.Timeout
|
|
||||||
if (isRealTimeActive) {
|
|
||||||
interval = setInterval(() => {
|
|
||||||
const newData = `数据更新 ${new Date().toLocaleTimeString()}: ${Math.floor(Math.random() * 1000)}`
|
|
||||||
setRealTimeData((prev) => [newData, ...prev.slice(0, 9)]) // 保持最新10条
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (interval) clearInterval(interval)
|
|
||||||
}
|
|
||||||
}, [isRealTimeActive])
|
|
||||||
|
|
||||||
// 模拟文件上传
|
|
||||||
const simulateFileUpload = (fileName: string) => {
|
|
||||||
const uploadId = Date.now().toString()
|
|
||||||
const newUpload: UploadProgress = {
|
|
||||||
id: uploadId,
|
|
||||||
name: fileName,
|
|
||||||
progress: 0,
|
|
||||||
status: 'uploading',
|
|
||||||
speed: '0 KB/s',
|
|
||||||
timeRemaining: '计算中...',
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploads((prev) => [...prev, newUpload])
|
|
||||||
|
|
||||||
// 模拟上传进度
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setUploads((prev) =>
|
|
||||||
prev.map((upload) => {
|
|
||||||
if (upload.id === uploadId) {
|
|
||||||
const newProgress = Math.min(upload.progress + Math.random() * 15, 100)
|
|
||||||
const speed = `${(Math.random() * 500 + 100).toFixed(0)} KB/s`
|
|
||||||
const timeRemaining =
|
|
||||||
newProgress >= 100 ? '完成' : `${Math.ceil((100 - newProgress) / 10)}秒`
|
|
||||||
|
|
||||||
// 模拟随机失败
|
|
||||||
if (newProgress > 50 && Math.random() < 0.1) {
|
|
||||||
clearInterval(interval)
|
|
||||||
return {
|
|
||||||
...upload,
|
|
||||||
status: 'error' as const,
|
|
||||||
speed: '0 KB/s',
|
|
||||||
timeRemaining: '失败',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newProgress >= 100) {
|
|
||||||
clearInterval(interval)
|
|
||||||
return {
|
|
||||||
...upload,
|
|
||||||
progress: 100,
|
|
||||||
status: 'completed' as const,
|
|
||||||
speed,
|
|
||||||
timeRemaining,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...upload,
|
|
||||||
progress: newProgress,
|
|
||||||
speed,
|
|
||||||
timeRemaining,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return upload
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟数据加载
|
|
||||||
const loadData = async () => {
|
|
||||||
setIsLoadingData(true)
|
|
||||||
setDataItems([])
|
|
||||||
|
|
||||||
// 创建骨架屏数据
|
|
||||||
const skeletonItems: DataItem[] = Array.from({ length: 6 }, (_, i) => ({
|
|
||||||
id: i,
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
timestamp: '',
|
|
||||||
status: 'loading',
|
|
||||||
}))
|
|
||||||
setDataItems(skeletonItems)
|
|
||||||
|
|
||||||
// 逐个加载数据项
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800 + Math.random() * 1000))
|
|
||||||
|
|
||||||
setDataItems((prev) =>
|
|
||||||
prev.map((item) => {
|
|
||||||
if (item.id === i) {
|
|
||||||
// 模拟随机加载失败
|
|
||||||
if (Math.random() < 0.15) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
status: 'error',
|
|
||||||
title: '加载失败',
|
|
||||||
content: '数据加载失败,请重试',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
status: 'loaded',
|
|
||||||
title: `数据项 ${i + 1}`,
|
|
||||||
content: `这是第 ${i + 1} 个数据项的内容,包含了一些示例文本用于展示加载效果。`,
|
|
||||||
timestamp: new Date().toLocaleString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoadingData(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟长时间运行的任务
|
|
||||||
const startLongRunningTask = async () => {
|
|
||||||
setLongRunningTask({
|
|
||||||
isRunning: true,
|
|
||||||
progress: 0,
|
|
||||||
currentStep: '初始化任务...',
|
|
||||||
logs: ['任务开始'],
|
|
||||||
})
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{ name: '初始化任务...', duration: 2000 },
|
|
||||||
{ name: '连接服务器...', duration: 1500 },
|
|
||||||
{ name: '验证权限...', duration: 1000 },
|
|
||||||
{ name: '下载数据...', duration: 3000 },
|
|
||||||
{ name: '处理数据...', duration: 2500 },
|
|
||||||
{ name: '生成报告...', duration: 2000 },
|
|
||||||
{ name: '保存结果...', duration: 1000 },
|
|
||||||
{ name: '清理资源...', duration: 500 },
|
|
||||||
]
|
|
||||||
|
|
||||||
for (let i = 0; i < steps.length; i++) {
|
|
||||||
const step = steps[i]
|
|
||||||
|
|
||||||
setLongRunningTask((prev) => ({
|
|
||||||
...prev,
|
|
||||||
currentStep: step.name,
|
|
||||||
logs: [...prev.logs, `开始: ${step.name}`],
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 模拟步骤执行时间
|
|
||||||
const startTime = Date.now()
|
|
||||||
while (Date.now() - startTime < step.duration) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
||||||
const elapsed = Date.now() - startTime
|
|
||||||
const stepProgress = Math.min((elapsed / step.duration) * 100, 100)
|
|
||||||
const totalProgress = ((i + stepProgress / 100) / steps.length) * 100
|
|
||||||
|
|
||||||
setLongRunningTask((prev) => ({
|
|
||||||
...prev,
|
|
||||||
progress: totalProgress,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
setLongRunningTask((prev) => ({
|
|
||||||
...prev,
|
|
||||||
logs: [...prev.logs, `完成: ${step.name}`],
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 模拟随机失败
|
|
||||||
if (i === 3 && Math.random() < 0.2) {
|
|
||||||
setLongRunningTask((prev) => ({
|
|
||||||
...prev,
|
|
||||||
isRunning: false,
|
|
||||||
currentStep: '任务失败',
|
|
||||||
logs: [...prev.logs, '错误: 数据下载失败,请重试'],
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLongRunningTask((prev) => ({
|
|
||||||
...prev,
|
|
||||||
isRunning: false,
|
|
||||||
progress: 100,
|
|
||||||
currentStep: '任务完成',
|
|
||||||
logs: [...prev.logs, '任务成功完成!'],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearUploads = () => {
|
|
||||||
setUploads([])
|
|
||||||
}
|
|
||||||
|
|
||||||
const retryFailedUpload = (uploadId: string) => {
|
|
||||||
const failedUpload = uploads.find((u) => u.id === uploadId)
|
|
||||||
if (failedUpload) {
|
|
||||||
setUploads((prev) => prev.filter((u) => u.id !== uploadId))
|
|
||||||
simulateFileUpload(failedUpload.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const retryDataLoad = (itemId: number) => {
|
|
||||||
setDataItems((prev) =>
|
|
||||||
prev.map((item) => {
|
|
||||||
if (item.id === itemId) {
|
|
||||||
return { ...item, status: 'loading', title: '', content: '', timestamp: '' }
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setDataItems((prev) =>
|
|
||||||
prev.map((item) => {
|
|
||||||
if (item.id === itemId) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
status: 'loaded',
|
|
||||||
title: `数据项 ${itemId + 1}`,
|
|
||||||
content: `这是重新加载的第 ${itemId + 1} 个数据项的内容。`,
|
|
||||||
timestamp: new Date().toLocaleString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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="mb-8">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
||||||
{/* 文件上传进度 */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
文件上传进度
|
|
||||||
</h2>
|
|
||||||
<div className="space-x-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => simulateFileUpload(`文件_${Date.now()}.pdf`)}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm"
|
|
||||||
>
|
|
||||||
开始上传
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={clearUploads}
|
|
||||||
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors text-sm"
|
|
||||||
>
|
|
||||||
清空列表
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{uploads.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
||||||
点击"开始上传"来模拟文件上传
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
uploads.map((upload) => (
|
|
||||||
<div
|
|
||||||
key={upload.id}
|
|
||||||
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="font-medium text-gray-900 dark:text-white">
|
|
||||||
{upload.name}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`text-sm ${
|
|
||||||
upload.status === 'completed'
|
|
||||||
? 'text-green-600 dark:text-green-400'
|
|
||||||
: upload.status === 'error'
|
|
||||||
? 'text-red-600 dark:text-red-400'
|
|
||||||
: 'text-blue-600 dark:text-blue-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{upload.status === 'completed'
|
|
||||||
? '✓ 完成'
|
|
||||||
: upload.status === 'error'
|
|
||||||
? '✗ 失败'
|
|
||||||
: '上传中...'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
|
|
||||||
<div
|
|
||||||
className={`h-2 rounded-full transition-all duration-300 ${
|
|
||||||
upload.status === 'completed'
|
|
||||||
? 'bg-green-500'
|
|
||||||
: upload.status === 'error'
|
|
||||||
? 'bg-red-500'
|
|
||||||
: 'bg-blue-500'
|
|
||||||
}`}
|
|
||||||
style={{ width: `${upload.progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
<span>{upload.progress.toFixed(1)}%</span>
|
|
||||||
<span>{upload.speed}</span>
|
|
||||||
<span>{upload.timeRemaining}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{upload.status === 'error' && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => retryFailedUpload(upload.id)}
|
|
||||||
className="mt-2 px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-sm transition-colors"
|
|
||||||
>
|
|
||||||
重试上传
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 实时数据更新 */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
实时数据更新
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsRealTimeActive(!isRealTimeActive)}
|
|
||||||
className={`px-4 py-2 rounded-md transition-colors text-sm ${
|
|
||||||
isRealTimeActive
|
|
||||||
? 'bg-red-600 hover:bg-red-700 text-white'
|
|
||||||
: 'bg-green-600 hover:bg-green-700 text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isRealTimeActive ? '停止更新' : '开始更新'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
|
||||||
{realTimeData.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
||||||
点击"开始更新"来查看实时数据
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
realTimeData.map((data) => (
|
|
||||||
<div
|
|
||||||
key={data}
|
|
||||||
className={`p-3 rounded-lg border transition-all duration-300 ${
|
|
||||||
data === realTimeData[0]
|
|
||||||
? 'bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700'
|
|
||||||
: 'bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="text-sm text-gray-900 dark:text-white">{data}</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 数据加载和长时间任务 */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* 数据加载骨架屏 */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
数据加载测试
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={loadData}
|
|
||||||
disabled={isLoadingData}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{isLoadingData ? '加载中...' : '加载数据'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{dataItems.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4"
|
|
||||||
>
|
|
||||||
{item.status === 'loading' ? (
|
|
||||||
<div className="animate-pulse">
|
|
||||||
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-3/4 mb-2"></div>
|
|
||||||
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-full mb-1"></div>
|
|
||||||
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-2/3"></div>
|
|
||||||
</div>
|
|
||||||
) : item.status === 'error' ? (
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-red-600 dark:text-red-400 mb-2">
|
|
||||||
{item.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-red-500 dark:text-red-400 mb-2">
|
|
||||||
{item.content}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => retryDataLoad(item.id)}
|
|
||||||
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-sm transition-colors"
|
|
||||||
>
|
|
||||||
重试
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
|
|
||||||
{item.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
|
||||||
{item.content}
|
|
||||||
</p>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{item.timestamp}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 长时间运行任务 */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">长时间任务</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={startLongRunningTask}
|
|
||||||
disabled={longRunningTask.isRunning}
|
|
||||||
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-400 text-white rounded-md transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{longRunningTask.isRunning ? '执行中...' : '开始任务'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{longRunningTask.progress > 0 && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{longRunningTask.currentStep}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
{longRunningTask.progress.toFixed(1)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-purple-500 h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${longRunningTask.progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{longRunningTask.logs.length > 0 && (
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 max-h-48 overflow-y-auto">
|
|
||||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-2">
|
|
||||||
执行日志:
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{longRunningTask.logs.map((log, logIdx) => {
|
|
||||||
const logKey = `${logIdx + 1}-${log.substring(0, 30)}`
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={logKey}
|
|
||||||
className="text-sm text-gray-600 dark:text-gray-300 font-mono"
|
|
||||||
>
|
|
||||||
{log}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 返回链接 */}
|
|
||||||
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
|
|
||||||
← 返回测试页面列表
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,582 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { Link } from 'wouter'
|
|
||||||
|
|
||||||
interface CartItem {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
price: number
|
|
||||||
quantity: number
|
|
||||||
image: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WizardStep {
|
|
||||||
id: number
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
completed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ComplexTestPage() {
|
|
||||||
const [currentStep, setCurrentStep] = useState(1)
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
const [wizardData, setWizardData] = useState({
|
|
||||||
personalInfo: { name: '', email: '', phone: '' },
|
|
||||||
address: { street: '', city: '', zipCode: '' },
|
|
||||||
payment: { cardNumber: '', expiryDate: '', cvv: '' },
|
|
||||||
})
|
|
||||||
const [wizardSteps, setWizardSteps] = useState<WizardStep[]>([
|
|
||||||
{ id: 1, title: '个人信息', description: '填写基本信息', completed: false },
|
|
||||||
{ id: 2, title: '收货地址', description: '填写收货地址', completed: false },
|
|
||||||
{ id: 3, title: '支付方式', description: '选择支付方式', completed: false },
|
|
||||||
{ id: 4, title: '确认订单', description: '确认订单信息', completed: false },
|
|
||||||
])
|
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
|
||||||
const [orderComplete, setOrderComplete] = useState(false)
|
|
||||||
|
|
||||||
// 购物车操作
|
|
||||||
const updateQuantity = (id: number, newQuantity: number) => {
|
|
||||||
if (newQuantity <= 0) {
|
|
||||||
removeItem(id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setCartItems((prev) =>
|
|
||||||
prev.map((item) => (item.id === id ? { ...item, quantity: newQuantity } : item))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeItem = (id: number) => {
|
|
||||||
setCartItems((prev) => prev.filter((item) => item.id !== id))
|
|
||||||
}
|
|
||||||
|
|
||||||
const addItem = () => {
|
|
||||||
const newItem: CartItem = {
|
|
||||||
id: Date.now(),
|
|
||||||
name: `新产品 ${cartItems.length + 1}`,
|
|
||||||
price: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
quantity: 1,
|
|
||||||
image: `https://picsum.photos/100/100?random=${Date.now()}`,
|
|
||||||
}
|
|
||||||
setCartItems((prev) => [...prev, newItem])
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTotalPrice = () => {
|
|
||||||
return cartItems.reduce((total, item) => total + item.price * item.quantity, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 向导步骤验证
|
|
||||||
const validateStep = (step: number): boolean => {
|
|
||||||
switch (step) {
|
|
||||||
case 1:
|
|
||||||
return !!(
|
|
||||||
wizardData.personalInfo.name &&
|
|
||||||
wizardData.personalInfo.email &&
|
|
||||||
wizardData.personalInfo.phone
|
|
||||||
)
|
|
||||||
case 2:
|
|
||||||
return !!(
|
|
||||||
wizardData.address.street &&
|
|
||||||
wizardData.address.city &&
|
|
||||||
wizardData.address.zipCode
|
|
||||||
)
|
|
||||||
case 3:
|
|
||||||
return !!(
|
|
||||||
wizardData.payment.cardNumber &&
|
|
||||||
wizardData.payment.expiryDate &&
|
|
||||||
wizardData.payment.cvv
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToStep = (step: number) => {
|
|
||||||
// 验证当前步骤
|
|
||||||
if (step > currentStep && !validateStep(currentStep)) {
|
|
||||||
alert('请完成当前步骤的必填信息')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新步骤完成状态
|
|
||||||
if (step > currentStep) {
|
|
||||||
setWizardSteps((prev) =>
|
|
||||||
prev.map((s) => (s.id === currentStep ? { ...s, completed: true } : s))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentStep(step)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInputChange = (section: string, field: string, value: string) => {
|
|
||||||
setWizardData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[section]: {
|
|
||||||
...prev[section as keyof typeof prev],
|
|
||||||
[field]: value,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitOrder = async () => {
|
|
||||||
setIsProcessing(true)
|
|
||||||
|
|
||||||
// 模拟处理时间
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
||||||
|
|
||||||
// 模拟随机失败
|
|
||||||
if (Math.random() < 0.2) {
|
|
||||||
setIsProcessing(false)
|
|
||||||
alert('订单提交失败,请重试')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsProcessing(false)
|
|
||||||
setOrderComplete(true)
|
|
||||||
setShowConfirmDialog(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetWizard = () => {
|
|
||||||
setCurrentStep(1)
|
|
||||||
setWizardData({
|
|
||||||
personalInfo: { name: '', email: '', phone: '' },
|
|
||||||
address: { street: '', city: '', zipCode: '' },
|
|
||||||
payment: { cardNumber: '', expiryDate: '', cvv: '' },
|
|
||||||
})
|
|
||||||
setWizardSteps((prev) => prev.map((s) => ({ ...s, completed: false })))
|
|
||||||
setOrderComplete(false)
|
|
||||||
setShowConfirmDialog(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orderComplete) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
|
||||||
<div className="max-w-md mx-auto text-center">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
|
|
||||||
<div className="text-6xl mb-4">🎉</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
||||||
订单提交成功!
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
|
||||||
您的订单已成功提交,我们将尽快为您处理。
|
|
||||||
</p>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={resetWizard}
|
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
重新开始
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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="mb-8">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
{/* 购物车区域 */}
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 sticky top-8">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
||||||
购物车 ({cartItems.length})
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
|
||||||
{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"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={item.image}
|
|
||||||
alt={item.name}
|
|
||||||
className="w-12 h-12 object-cover rounded"
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
||||||
{item.name}
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
¥{item.price.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<button
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<span className="text-sm font-medium w-8 text-center">{item.quantity}</span>
|
|
||||||
<button
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => removeItem(item.id)}
|
|
||||||
className="w-6 h-6 flex items-center justify-center bg-red-500 text-white rounded text-sm hover:bg-red-600"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={addItem}
|
|
||||||
className="w-full mb-4 py-2 px-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg text-gray-500 dark:text-gray-400 hover:border-blue-500 hover:text-blue-500 transition-colors"
|
|
||||||
>
|
|
||||||
+ 添加商品
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="border-t border-gray-200 dark:border-gray-600 pt-4">
|
|
||||||
<div className="flex justify-between items-center text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
<span>总计:</span>
|
|
||||||
<span>¥{getTotalPrice().toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 向导区域 */}
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
|
|
||||||
{/* 步骤指示器 */}
|
|
||||||
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{wizardSteps.map((step, index) => (
|
|
||||||
<div key={step.id} className="flex items-center">
|
|
||||||
<button
|
|
||||||
onClick={() => goToStep(step.id)}
|
|
||||||
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
|
|
||||||
step.completed
|
|
||||||
? 'bg-green-500 text-white'
|
|
||||||
: step.id === currentStep
|
|
||||||
? 'bg-blue-500 text-white'
|
|
||||||
: 'bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{step.completed ? '✓' : step.id}
|
|
||||||
</button>
|
|
||||||
{index < wizardSteps.length - 1 && (
|
|
||||||
<div
|
|
||||||
className={`w-16 h-1 mx-2 ${
|
|
||||||
step.completed ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-600'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{wizardSteps[currentStep - 1].title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
{wizardSteps[currentStep - 1].description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 步骤内容 */}
|
|
||||||
<div className="p-6">
|
|
||||||
{currentStep === 1 && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
姓名 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={wizardData.personalInfo.name}
|
|
||||||
onChange={(e) => handleInputChange('personalInfo', 'name', 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"
|
|
||||||
placeholder="请输入您的姓名"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
邮箱 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={wizardData.personalInfo.email}
|
|
||||||
onChange={(e) => handleInputChange('personalInfo', '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"
|
|
||||||
placeholder="请输入您的邮箱"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
手机号 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
value={wizardData.personalInfo.phone}
|
|
||||||
onChange={(e) => handleInputChange('personalInfo', 'phone', 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"
|
|
||||||
placeholder="请输入您的手机号"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
详细地址 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={wizardData.address.street}
|
|
||||||
onChange={(e) => handleInputChange('address', 'street', 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"
|
|
||||||
placeholder="请输入详细地址"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
城市 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={wizardData.address.city}
|
|
||||||
onChange={(e) => handleInputChange('address', 'city', 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"
|
|
||||||
placeholder="请输入城市"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
邮政编码 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={wizardData.address.zipCode}
|
|
||||||
onChange={(e) => handleInputChange('address', 'zipCode', 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"
|
|
||||||
placeholder="请输入邮政编码"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep === 3 && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
银行卡号 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={wizardData.payment.cardNumber}
|
|
||||||
onChange={(e) => handleInputChange('payment', 'cardNumber', 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"
|
|
||||||
placeholder="请输入银行卡号"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
有效期 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={wizardData.payment.expiryDate}
|
|
||||||
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"
|
|
||||||
placeholder="MM/YY"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
CVV *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={wizardData.payment.cvv}
|
|
||||||
onChange={(e) => handleInputChange('payment', 'cvv', 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"
|
|
||||||
placeholder="CVV"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep === 4 && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
|
||||||
订单确认
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<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>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
{wizardData.personalInfo.name} | {wizardData.personalInfo.email} |{' '}
|
|
||||||
{wizardData.personalInfo.phone}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
{wizardData.address.street}, {wizardData.address.city}{' '}
|
|
||||||
{wizardData.address.zipCode}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
**** **** **** {wizardData.payment.cardNumber.slice(-4)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 导航按钮 */}
|
|
||||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-600 flex justify-between">
|
|
||||||
<button
|
|
||||||
onClick={() => goToStep(currentStep - 1)}
|
|
||||||
disabled={currentStep === 1}
|
|
||||||
className="px-4 py-2 text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
上一步
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{currentStep < 4 ? (
|
|
||||||
<button
|
|
||||||
onClick={() => goToStep(currentStep + 1)}
|
|
||||||
disabled={!validateStep(currentStep)}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
下一步
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowConfirmDialog(true)}
|
|
||||||
disabled={cartItems.length === 0}
|
|
||||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
提交订单
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 确认对话框 */}
|
|
||||||
{showConfirmDialog && (
|
|
||||||
<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 max-w-md w-full mx-4">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
|
||||||
确认提交订单
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
|
||||||
您确定要提交这个订单吗?订单总金额为 ¥{getTotalPrice().toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-end space-x-3">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowConfirmDialog(false)}
|
|
||||||
disabled={isProcessing}
|
|
||||||
className="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmitOrder}
|
|
||||||
disabled={isProcessing}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors flex items-center"
|
|
||||||
>
|
|
||||||
{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"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
处理中...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'确认提交'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 返回链接 */}
|
|
||||||
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
|
|
||||||
← 返回测试页面列表
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { Link } from 'wouter'
|
|
||||||
|
|
||||||
interface ErrorScenario {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
type: 'network' | 'validation' | 'permission' | 'timeout' | 'server'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ErrorTestPage() {
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [success, setSuccess] = useState<string | null>(null)
|
|
||||||
const [retryCount, setRetryCount] = useState(0)
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
email: '',
|
|
||||||
file: null as File | null,
|
|
||||||
})
|
|
||||||
|
|
||||||
const errorScenarios: ErrorScenario[] = [
|
|
||||||
{
|
|
||||||
id: 'network-error',
|
|
||||||
title: '网络连接错误',
|
|
||||||
description: '模拟网络连接失败,测试重试机制',
|
|
||||||
type: 'network',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'validation-error',
|
|
||||||
title: '表单验证错误',
|
|
||||||
description: '模拟表单验证失败,测试错误提示',
|
|
||||||
type: 'validation',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'permission-error',
|
|
||||||
title: '权限不足错误',
|
|
||||||
description: '模拟权限验证失败,测试权限处理',
|
|
||||||
type: 'permission',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'timeout-error',
|
|
||||||
title: '请求超时错误',
|
|
||||||
description: '模拟请求超时,测试超时处理',
|
|
||||||
type: 'timeout',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'server-error',
|
|
||||||
title: '服务器内部错误',
|
|
||||||
description: '模拟服务器500错误,测试错误恢复',
|
|
||||||
type: 'server',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const simulateError = async (scenario: ErrorScenario): Promise<void> => {
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
setSuccess(null)
|
|
||||||
|
|
||||||
// 模拟网络延迟
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000 + Math.random() * 2000))
|
|
||||||
|
|
||||||
switch (scenario.type) {
|
|
||||||
case 'network':
|
|
||||||
// 70% 概率失败
|
|
||||||
if (Math.random() < 0.7) {
|
|
||||||
throw new Error('网络连接失败:无法连接到服务器,请检查您的网络连接')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'validation':
|
|
||||||
// 检查表单数据
|
|
||||||
if (!formData.username || formData.username.length < 3) {
|
|
||||||
throw new Error('用户名验证失败:用户名至少需要3个字符')
|
|
||||||
}
|
|
||||||
if (!formData.password || formData.password.length < 6) {
|
|
||||||
throw new Error('密码验证失败:密码至少需要6个字符')
|
|
||||||
}
|
|
||||||
if (!formData.email?.includes('@')) {
|
|
||||||
throw new Error('邮箱验证失败:请输入有效的邮箱地址')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'permission':
|
|
||||||
// 模拟权限检查
|
|
||||||
if (formData.username !== 'admin') {
|
|
||||||
throw new Error('权限不足:您没有执行此操作的权限,请联系管理员')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'timeout':
|
|
||||||
// 模拟超时
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 8000))
|
|
||||||
throw new Error('请求超时:服务器响应时间过长,请稍后重试')
|
|
||||||
|
|
||||||
case 'server':
|
|
||||||
// 50% 概率服务器错误
|
|
||||||
if (Math.random() < 0.5) {
|
|
||||||
throw new Error('服务器内部错误:服务器遇到了一个错误,请稍后重试')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error('未知错误:发生了未预期的错误')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 成功情况
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleScenarioTest = async (scenario: ErrorScenario) => {
|
|
||||||
try {
|
|
||||||
await simulateError(scenario)
|
|
||||||
setSuccess(`${scenario.title} 测试成功完成!`)
|
|
||||||
setRetryCount(0)
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : '未知错误'
|
|
||||||
setError(errorMessage)
|
|
||||||
setRetryCount((prev) => prev + 1)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRetry = async (scenario: ErrorScenario) => {
|
|
||||||
if (retryCount >= 3) {
|
|
||||||
setError('重试次数已达上限,请稍后再试或联系技术支持')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await handleScenarioTest(scenario)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileUpload = async () => {
|
|
||||||
if (!formData.file) {
|
|
||||||
setError('请选择要上传的文件')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
setSuccess(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 模拟文件大小检查
|
|
||||||
if (formData.file.size > 5 * 1024 * 1024) {
|
|
||||||
throw new Error('文件上传失败:文件大小不能超过5MB')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟文件类型检查
|
|
||||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
|
|
||||||
if (!allowedTypes.includes(formData.file.type)) {
|
|
||||||
throw new Error('文件上传失败:不支持的文件类型,请上传图片或PDF文件')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟上传过程
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
||||||
|
|
||||||
// 模拟随机失败
|
|
||||||
if (Math.random() < 0.3) {
|
|
||||||
throw new Error('文件上传失败:上传过程中发生错误,请重试')
|
|
||||||
}
|
|
||||||
|
|
||||||
setSuccess('文件上传成功!')
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : '文件上传失败'
|
|
||||||
setError(errorMessage)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearMessages = () => {
|
|
||||||
setError(null)
|
|
||||||
setSuccess(null)
|
|
||||||
setRetryCount(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getErrorIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'network':
|
|
||||||
return '🌐'
|
|
||||||
case 'validation':
|
|
||||||
return '⚠️'
|
|
||||||
case 'permission':
|
|
||||||
return '🔒'
|
|
||||||
case 'timeout':
|
|
||||||
return '⏰'
|
|
||||||
case 'server':
|
|
||||||
return '🔧'
|
|
||||||
default:
|
|
||||||
return '❌'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">错误处理测试</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
测试各种错误场景和重试机制,验证 Agent 的错误处理能力
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 全局消息显示 */}
|
|
||||||
{(error || success) && (
|
|
||||||
<div className="mb-8">
|
|
||||||
{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="flex items-start">
|
|
||||||
<div className="shrink-0">
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 flex-1">
|
|
||||||
<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>
|
|
||||||
{retryCount > 0 && (
|
|
||||||
<p className="mt-2 text-xs text-red-600 dark:text-red-400">
|
|
||||||
已重试 {retryCount} 次 {retryCount >= 3 && '(已达最大重试次数)'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={clearMessages}
|
|
||||||
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">
|
|
||||||
<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>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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="flex items-start">
|
|
||||||
<div className="shrink-0">
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 flex-1">
|
|
||||||
<h3 className="text-sm font-medium text-green-800 dark:text-green-200">
|
|
||||||
操作成功
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-sm text-green-700 dark:text-green-300">{success}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={clearMessages}
|
|
||||||
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">
|
|
||||||
<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>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
||||||
{/* 错误场景测试 */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">错误场景测试</h2>
|
|
||||||
|
|
||||||
{errorScenarios.map((scenario) => (
|
|
||||||
<div key={scenario.id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<div className="text-3xl">{getErrorIcon(scenario.type)}</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
|
||||||
{scenario.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm mb-4">
|
|
||||||
{scenario.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex space-x-3">
|
|
||||||
<button
|
|
||||||
onClick={() => handleScenarioTest(scenario)}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-red-400 text-white rounded-md transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{isLoading ? '测试中...' : '触发错误'}
|
|
||||||
</button>
|
|
||||||
{error && retryCount > 0 && retryCount < 3 && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleRetry(scenario)}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors text-sm"
|
|
||||||
>
|
|
||||||
重试 ({retryCount}/3)
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 表单验证测试 */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">表单验证测试</h2>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
用户名 (至少3个字符)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.username}
|
|
||||||
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"
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
密码 (至少6个字符)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={formData.password}
|
|
||||||
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"
|
|
||||||
placeholder="请输入密码"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
邮箱地址
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
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"
|
|
||||||
placeholder="请输入邮箱地址"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
handleScenarioTest(errorScenarios.find((s) => s.type === 'validation')!)
|
|
||||||
}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{isLoading ? '验证中...' : '提交表单'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 文件上传测试 */}
|
|
||||||
<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>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
选择文件 (最大5MB,支持图片和PDF)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData((prev) => ({ ...prev, file: e.target.files?.[0] || null }))
|
|
||||||
}
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{formData.file && (
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
已选择: {formData.file.name} ({(formData.file.size / 1024 / 1024).toFixed(2)}{' '}
|
|
||||||
MB)
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={handleFileUpload}
|
|
||||||
disabled={isLoading || !formData.file}
|
|
||||||
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
{isLoading ? '上传中...' : '上传文件'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 权限测试说明 */}
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-lg p-4">
|
|
||||||
<h4 className="text-sm font-medium text-yellow-800 dark:text-yellow-200 mb-2">
|
|
||||||
💡 权限测试提示
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
|
||||||
要通过权限测试,请在用户名字段输入 "admin",然后点击"触发错误"按钮测试权限验证。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 加载状态指示器 */}
|
|
||||||
{isLoading && (
|
|
||||||
<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">
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<span className="text-gray-900 dark:text-white">处理中,请稍候...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 返回链接 */}
|
|
||||||
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
|
|
||||||
← 返回测试页面列表
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,488 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { Link } from 'wouter'
|
|
||||||
|
|
||||||
interface FormData {
|
|
||||||
username: string
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
confirmPassword: string
|
|
||||||
age: string
|
|
||||||
birthDate: string
|
|
||||||
phone: string
|
|
||||||
website: string
|
|
||||||
bio: string
|
|
||||||
country: string
|
|
||||||
newsletter: boolean
|
|
||||||
terms: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormErrors = Record<string, string>
|
|
||||||
|
|
||||||
export default function FormTestPage() {
|
|
||||||
const [formData, setFormData] = useState<FormData>({
|
|
||||||
username: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
age: '',
|
|
||||||
birthDate: '',
|
|
||||||
phone: '',
|
|
||||||
website: '',
|
|
||||||
bio: '',
|
|
||||||
country: '',
|
|
||||||
newsletter: false,
|
|
||||||
terms: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState<FormErrors>({})
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
const [submitResult, setSubmitResult] = useState<'success' | 'error' | null>(null)
|
|
||||||
const [submitMessage, setSubmitMessage] = useState('')
|
|
||||||
|
|
||||||
const validateField = (name: string, value: string | boolean): string => {
|
|
||||||
switch (name) {
|
|
||||||
case 'username':
|
|
||||||
if (!value) return '用户名不能为空'
|
|
||||||
if (typeof value === 'string' && value.length < 3) return '用户名至少需要3个字符'
|
|
||||||
if (typeof value === 'string' && !/^[a-zA-Z0-9_]+$/.test(value))
|
|
||||||
return '用户名只能包含字母、数字和下划线'
|
|
||||||
return ''
|
|
||||||
case 'email':
|
|
||||||
if (!value) return '邮箱不能为空'
|
|
||||||
if (typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
|
||||||
return '请输入有效的邮箱地址'
|
|
||||||
return ''
|
|
||||||
case 'password':
|
|
||||||
if (!value) return '密码不能为空'
|
|
||||||
if (typeof value === 'string' && value.length < 6) return '密码至少需要6个字符'
|
|
||||||
if (typeof value === 'string' && !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value))
|
|
||||||
return '密码必须包含大小写字母和数字'
|
|
||||||
return ''
|
|
||||||
case 'confirmPassword':
|
|
||||||
if (!value) return '请确认密码'
|
|
||||||
if (value !== formData.password) return '两次输入的密码不一致'
|
|
||||||
return ''
|
|
||||||
case 'age': {
|
|
||||||
if (!value) return '年龄不能为空'
|
|
||||||
const age = parseInt(value as string)
|
|
||||||
if (isNaN(age) || age < 18 || age > 120) return '年龄必须在18-120之间'
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
case 'phone':
|
|
||||||
if (!value) return '手机号不能为空'
|
|
||||||
if (typeof value === 'string' && !/^1[3-9]\d{9}$/.test(value)) return '请输入有效的手机号'
|
|
||||||
return ''
|
|
||||||
case 'terms':
|
|
||||||
if (!value) return '请同意服务条款'
|
|
||||||
return ''
|
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInputChange = (name: string, value: string | boolean) => {
|
|
||||||
console.log(`Input changed: ${name} = ${value}`)
|
|
||||||
|
|
||||||
setFormData((prev) => ({ ...prev, [name]: value }))
|
|
||||||
|
|
||||||
// 实时验证
|
|
||||||
const error = validateField(name, value)
|
|
||||||
setErrors((prev) => ({ ...prev, [name]: error }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
|
||||||
const newErrors: FormErrors = {}
|
|
||||||
let isValid = true
|
|
||||||
|
|
||||||
Object.keys(formData).forEach((key) => {
|
|
||||||
const error = validateField(key, formData[key as keyof FormData])
|
|
||||||
if (error) {
|
|
||||||
newErrors[key] = error
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setErrors(newErrors)
|
|
||||||
return isValid
|
|
||||||
}
|
|
||||||
|
|
||||||
const simulateSubmit = async (): Promise<{ success: boolean; message: string }> => {
|
|
||||||
// 模拟网络延迟
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000 + Math.random() * 2000))
|
|
||||||
|
|
||||||
// 模拟随机失败
|
|
||||||
if (Math.random() < 0.3) {
|
|
||||||
throw new Error('网络错误:服务器暂时不可用,请稍后重试')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟服务器验证错误
|
|
||||||
if (formData.username.toLowerCase() === 'admin') {
|
|
||||||
throw new Error('用户名 "admin" 已被占用,请选择其他用户名')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: '注册成功!欢迎加入我们的平台。',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (!validateForm()) {
|
|
||||||
setSubmitResult('error')
|
|
||||||
setSubmitMessage('请修正表单中的错误')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true)
|
|
||||||
setSubmitResult(null)
|
|
||||||
setSubmitMessage('')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await simulateSubmit()
|
|
||||||
setSubmitResult('success')
|
|
||||||
setSubmitMessage(result.message)
|
|
||||||
} catch (error) {
|
|
||||||
setSubmitResult('error')
|
|
||||||
setSubmitMessage(error instanceof Error ? error.message : '提交失败,请重试')
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setFormData({
|
|
||||||
username: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
age: '',
|
|
||||||
birthDate: '',
|
|
||||||
phone: '',
|
|
||||||
website: '',
|
|
||||||
bio: '',
|
|
||||||
country: '',
|
|
||||||
newsletter: false,
|
|
||||||
terms: false,
|
|
||||||
})
|
|
||||||
setErrors({})
|
|
||||||
setSubmitResult(null)
|
|
||||||
setSubmitMessage('')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
|
||||||
<div className="max-w-2xl mx-auto px-4">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
|
|
||||||
<div className="mb-8">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
{/* 用户名 */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
用户名 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.username}
|
|
||||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
|
||||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
|
|
||||||
errors.username ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
|
||||||
}`}
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
/>
|
|
||||||
{errors.username && (
|
|
||||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.username}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 邮箱 */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
邮箱地址 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
|
||||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
|
|
||||||
errors.email ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
|
||||||
}`}
|
|
||||||
placeholder="请输入邮箱地址"
|
|
||||||
/>
|
|
||||||
{errors.email && (
|
|
||||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.email}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 密码 */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
密码 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={formData.password}
|
|
||||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
|
||||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
|
|
||||||
errors.password ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
|
||||||
}`}
|
|
||||||
placeholder="请输入密码"
|
|
||||||
/>
|
|
||||||
{errors.password && (
|
|
||||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.password}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
确认密码 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
|
||||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
|
|
||||||
errors.confirmPassword
|
|
||||||
? 'border-red-500'
|
|
||||||
: 'border-gray-300 dark:border-gray-600'
|
|
||||||
}`}
|
|
||||||
placeholder="请再次输入密码"
|
|
||||||
/>
|
|
||||||
{errors.confirmPassword && (
|
|
||||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
|
|
||||||
{errors.confirmPassword}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 年龄和生日 */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
年龄 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={formData.age}
|
|
||||||
onChange={(e) => handleInputChange('age', e.target.value)}
|
|
||||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
|
|
||||||
errors.age ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
|
||||||
}`}
|
|
||||||
placeholder="请输入年龄"
|
|
||||||
min="18"
|
|
||||||
max="120"
|
|
||||||
/>
|
|
||||||
{errors.age && (
|
|
||||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.age}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
出生日期
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={formData.birthDate}
|
|
||||||
onChange={(e) => handleInputChange('birthDate', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 手机和网站 */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
手机号 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
value={formData.phone}
|
|
||||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
|
||||||
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
|
|
||||||
errors.phone ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
|
||||||
}`}
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
/>
|
|
||||||
{errors.phone && (
|
|
||||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.phone}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
个人网站
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
value={formData.website}
|
|
||||||
onChange={(e) => handleInputChange('website', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="https://example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 国家选择 */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
国家/地区
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={formData.country}
|
|
||||||
onChange={(e) => handleInputChange('country', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<option value="">请选择国家/地区</option>
|
|
||||||
<option value="CN">中国</option>
|
|
||||||
<option value="US">美国</option>
|
|
||||||
<option value="JP">日本</option>
|
|
||||||
<option value="KR">韩国</option>
|
|
||||||
<option value="GB">英国</option>
|
|
||||||
<option value="DE">德国</option>
|
|
||||||
<option value="FR">法国</option>
|
|
||||||
<option value="CA">加拿大</option>
|
|
||||||
<option value="AU">澳大利亚</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 个人简介 */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
个人简介
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={formData.bio}
|
|
||||||
onChange={(e) => handleInputChange('bio', e.target.value)}
|
|
||||||
rows={4}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="请简单介绍一下自己..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 复选框 */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="newsletter"
|
|
||||||
checked={formData.newsletter}
|
|
||||||
onChange={(e) => handleInputChange('newsletter', e.target.checked)}
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="newsletter"
|
|
||||||
className="ml-2 block text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
订阅我们的新闻通讯
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="terms"
|
|
||||||
checked={formData.terms}
|
|
||||||
onChange={(e) => handleInputChange('terms', e.target.checked)}
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="terms"
|
|
||||||
className="ml-2 block text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
我同意{' '}
|
|
||||||
<a href="#" className="text-blue-600 hover:text-blue-500">
|
|
||||||
服务条款
|
|
||||||
</a>{' '}
|
|
||||||
和{' '}
|
|
||||||
<a href="#" className="text-blue-600 hover:text-blue-500">
|
|
||||||
隐私政策
|
|
||||||
</a>{' '}
|
|
||||||
*
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{errors.terms && (
|
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">{errors.terms}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 提交结果 */}
|
|
||||||
{submitResult && (
|
|
||||||
<div
|
|
||||||
className={`p-4 rounded-md ${
|
|
||||||
submitResult === 'success'
|
|
||||||
? 'bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700'
|
|
||||||
: 'bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
className={`text-sm ${
|
|
||||||
submitResult === 'success'
|
|
||||||
? 'text-green-800 dark:text-green-200'
|
|
||||||
: 'text-red-800 dark:text-red-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{submitMessage}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 按钮组 */}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<span className="flex items-center justify-center">
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
提交中...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
'注册账户'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={resetForm}
|
|
||||||
className="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
重置表单
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
|
|
||||||
← 返回测试页面列表
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { Link } from 'wouter'
|
|
||||||
|
|
||||||
export default function IndexPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 p-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="text-center mb-12">
|
|
||||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
|
||||||
Page Use Agent 测试页面
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-gray-600 dark:text-gray-300">
|
|
||||||
用于测试 AI Agent 网页操作能力的综合测试套件
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
|
||||||
<TestPageCard
|
|
||||||
title="表单测试"
|
|
||||||
description="测试输入、验证、提交等表单操作"
|
|
||||||
path="/form"
|
|
||||||
icon="📝"
|
|
||||||
difficulty="简单"
|
|
||||||
/>
|
|
||||||
<TestPageCard
|
|
||||||
title="导航测试"
|
|
||||||
description="测试菜单、下拉框、弹窗等交互"
|
|
||||||
path="/navigation"
|
|
||||||
icon="🧭"
|
|
||||||
difficulty="中等"
|
|
||||||
/>
|
|
||||||
<TestPageCard
|
|
||||||
title="列表测试"
|
|
||||||
description="测试滚动、分页、搜索、排序"
|
|
||||||
path="/list"
|
|
||||||
icon="📋"
|
|
||||||
difficulty="中等"
|
|
||||||
/>
|
|
||||||
<TestPageCard
|
|
||||||
title="复杂交互"
|
|
||||||
description="测试多步骤操作和状态管理"
|
|
||||||
path="/complex"
|
|
||||||
icon="⚙️"
|
|
||||||
difficulty="困难"
|
|
||||||
/>
|
|
||||||
<TestPageCard
|
|
||||||
title="错误处理"
|
|
||||||
description="测试错误识别和重试机制"
|
|
||||||
path="/errors"
|
|
||||||
icon="⚠️"
|
|
||||||
difficulty="困难"
|
|
||||||
/>
|
|
||||||
<TestPageCard
|
|
||||||
title="异步操作"
|
|
||||||
description="测试等待、加载状态识别"
|
|
||||||
path="/async"
|
|
||||||
icon="⏳"
|
|
||||||
difficulty="中等"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
← 回到 Page Use 首页
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestPageCardProps {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
path: string
|
|
||||||
icon: string
|
|
||||||
difficulty: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestPageCard({ title, description, path, icon, difficulty }: TestPageCardProps) {
|
|
||||||
const difficultyColors = {
|
|
||||||
简单: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
|
||||||
中等: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
|
||||||
困难: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link href={path}>
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 cursor-pointer border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="text-4xl mb-4">{icon}</div>
|
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{title}</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4 text-sm">{description}</p>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 rounded-full text-xs font-medium ${difficultyColors[difficulty as keyof typeof difficultyColors]}`}
|
|
||||||
>
|
|
||||||
{difficulty}
|
|
||||||
</span>
|
|
||||||
<span className="text-blue-600 dark:text-blue-400 text-sm font-medium">开始测试 →</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
|
||||||
import { Link } from 'wouter'
|
|
||||||
|
|
||||||
interface Product {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
category: string
|
|
||||||
price: number
|
|
||||||
stock: number
|
|
||||||
rating: number
|
|
||||||
image: string
|
|
||||||
description: string
|
|
||||||
tags: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateProducts = (count: number): Product[] => {
|
|
||||||
const categories = ['手机', '电脑', '平板', '耳机', '手表', '相机']
|
|
||||||
const brands = ['Apple', 'Samsung', 'Huawei', 'Xiaomi', 'Sony', 'Dell']
|
|
||||||
const adjectives = ['Pro', 'Max', 'Ultra', 'Plus', 'Air', 'Mini']
|
|
||||||
|
|
||||||
return Array.from({ length: count }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: `${brands[i % brands.length]} ${categories[i % categories.length]} ${adjectives[i % adjectives.length]}`,
|
|
||||||
category: categories[i % categories.length],
|
|
||||||
price: Math.floor(Math.random() * 10000) + 500,
|
|
||||||
stock: Math.floor(Math.random() * 100),
|
|
||||||
rating: Math.round((Math.random() * 2 + 3) * 10) / 10,
|
|
||||||
image: `https://picsum.photos/200/200?random=${i}`,
|
|
||||||
description: `这是一款优秀的${categories[i % categories.length]}产品,具有出色的性能和设计。`,
|
|
||||||
tags: ['热销', '新品', '推荐'].slice(0, Math.floor(Math.random() * 3) + 1),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loading skeleton component
|
|
||||||
const LoadingSkeleton = () => (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
||||||
{Array.from({ length: 12 }, (_, i) => `skeleton-item-${i}`).map((id) => (
|
|
||||||
<div key={id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 animate-pulse">
|
|
||||||
<div className="bg-gray-300 dark:bg-gray-600 h-48 rounded-lg mb-4"></div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-3/4"></div>
|
|
||||||
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/2"></div>
|
|
||||||
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/4"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Product card component
|
|
||||||
const ProductCard = ({ product }: { product: Product }) => (
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-4">
|
|
||||||
<div className="relative mb-4">
|
|
||||||
<img
|
|
||||||
src={product.image}
|
|
||||||
alt={product.name}
|
|
||||||
className="w-full h-48 object-cover rounded-lg"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div className="absolute top-2 right-2 flex flex-wrap gap-1">
|
|
||||||
{product.tags.map((tag) => (
|
|
||||||
<span key={tag} className="bg-red-500 text-white text-xs px-2 py-1 rounded-full">
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 line-clamp-2">
|
|
||||||
{product.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm mb-3 line-clamp-2">
|
|
||||||
{product.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<span className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
|
||||||
¥{product.price.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="text-yellow-400">★</span>
|
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300 ml-1">{product.rating}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">库存: {product.stock}</span>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{product.category}</span>
|
|
||||||
</div>
|
|
||||||
<button className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
|
|
||||||
加入购物车
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Product list item component
|
|
||||||
const ProductListItem = ({ product }: { product: Product }) => (
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex items-center space-x-4">
|
|
||||||
<img
|
|
||||||
src={product.image}
|
|
||||||
alt={product.name}
|
|
||||||
className="w-20 h-20 object-cover rounded-lg shrink-0"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">{product.name}</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm mb-2 line-clamp-1">
|
|
||||||
{product.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
<span>{product.category}</span>
|
|
||||||
<span>库存: {product.stock}</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="text-yellow-400">★</span>
|
|
||||||
<span className="ml-1">{product.rating}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-xl font-bold text-blue-600 dark:text-blue-400">
|
|
||||||
¥{product.price.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
<button className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
|
|
||||||
加入购物车
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Pagination component
|
|
||||||
interface PaginationProps {
|
|
||||||
currentPage: number
|
|
||||||
totalPages: number
|
|
||||||
startIndex: number
|
|
||||||
endIndex: number
|
|
||||||
totalItems: number
|
|
||||||
onPageChange: (page: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const Pagination = ({
|
|
||||||
currentPage,
|
|
||||||
totalPages,
|
|
||||||
startIndex,
|
|
||||||
endIndex,
|
|
||||||
totalItems,
|
|
||||||
onPageChange,
|
|
||||||
}: PaginationProps) => {
|
|
||||||
const getPageNumbers = () => {
|
|
||||||
const pages = []
|
|
||||||
const maxVisible = 5
|
|
||||||
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2))
|
|
||||||
const end = Math.min(totalPages, start + maxVisible - 1)
|
|
||||||
|
|
||||||
if (end - start + 1 < maxVisible) {
|
|
||||||
start = Math.max(1, end - maxVisible + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = start; i <= end; i++) {
|
|
||||||
pages.push(i)
|
|
||||||
}
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between mt-8">
|
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
显示 {startIndex + 1}-{Math.min(endIndex, totalItems)} 条, 共 {totalItems} 条结果
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
上一页
|
|
||||||
</button>
|
|
||||||
{getPageNumbers().map((page) => (
|
|
||||||
<button
|
|
||||||
key={page}
|
|
||||||
onClick={() => onPageChange(page)}
|
|
||||||
className={`px-3 py-2 text-sm font-medium rounded-md ${
|
|
||||||
page === currentPage
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
|
||||||
disabled={currentPage === totalPages}
|
|
||||||
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
下一页
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ListTestPage() {
|
|
||||||
const [products, setProducts] = useState<Product[]>([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState('全部')
|
|
||||||
const [sortBy, setSortBy] = useState('name')
|
|
||||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(12)
|
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
|
||||||
|
|
||||||
const categories = ['全部', '手机', '电脑', '平板', '耳机', '手表', '相机']
|
|
||||||
|
|
||||||
// Helper to set filters and reset page
|
|
||||||
const handleSearchChange = (term: string) => {
|
|
||||||
setSearchTerm(term)
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCategoryChange = (category: string) => {
|
|
||||||
setSelectedCategory(category)
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSortChange = (sort: string) => {
|
|
||||||
setSortBy(sort)
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSortOrderChange = (order: 'asc' | 'desc') => {
|
|
||||||
setSortOrder(order)
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟数据加载
|
|
||||||
useEffect(() => {
|
|
||||||
const loadData = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
// 模拟网络延迟
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
|
||||||
const data = generateProducts(150)
|
|
||||||
setProducts(data)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
loadData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 搜索和过滤 - Use useMemo to compute filtered products
|
|
||||||
const filteredProducts = useMemo(() => {
|
|
||||||
let filtered = [...products]
|
|
||||||
|
|
||||||
// 按类别过滤
|
|
||||||
if (selectedCategory !== '全部') {
|
|
||||||
filtered = filtered.filter((product) => product.category === selectedCategory)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按搜索词过滤
|
|
||||||
if (searchTerm) {
|
|
||||||
filtered = filtered.filter(
|
|
||||||
(product) =>
|
|
||||||
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
product.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序
|
|
||||||
filtered.sort((a, b) => {
|
|
||||||
let aValue: any = a[sortBy as keyof Product]
|
|
||||||
let bValue: any = b[sortBy as keyof Product]
|
|
||||||
|
|
||||||
if (typeof aValue === 'string') {
|
|
||||||
aValue = aValue.toLowerCase()
|
|
||||||
bValue = bValue.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sortOrder === 'asc') {
|
|
||||||
return aValue > bValue ? 1 : -1
|
|
||||||
} else {
|
|
||||||
return aValue < bValue ? 1 : -1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}, [products, searchTerm, selectedCategory, sortBy, sortOrder])
|
|
||||||
|
|
||||||
// 分页计算
|
|
||||||
const totalPages = Math.ceil(filteredProducts.length / itemsPerPage)
|
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage
|
|
||||||
const endIndex = startIndex + itemsPerPage
|
|
||||||
const currentProducts = filteredProducts.slice(startIndex, endIndex)
|
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
setCurrentPage(page)
|
|
||||||
// 滚动到顶部
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
|
||||||
<div className="mb-8">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 搜索和过滤栏 */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
|
||||||
{/* 搜索框 */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
搜索产品
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => handleSearchChange(e.target.value)}
|
|
||||||
placeholder="输入产品名称或描述..."
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 类别过滤 */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
产品类别
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={selectedCategory}
|
|
||||||
onChange={(e) => handleCategoryChange(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<option key={category} value={category}>
|
|
||||||
{category}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 排序方式 */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
排序方式
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={sortBy}
|
|
||||||
onChange={(e) => handleSortChange(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<option value="name">名称</option>
|
|
||||||
<option value="price">价格</option>
|
|
||||||
<option value="rating">评分</option>
|
|
||||||
<option value="stock">库存</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 排序顺序 */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
排序顺序
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={sortOrder}
|
|
||||||
onChange={(e) => handleSortOrderChange(e.target.value as 'asc' | 'desc')}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<option value="asc">升序</option>
|
|
||||||
<option value="desc">降序</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 视图控制 */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
每页显示:
|
|
||||||
</span>
|
|
||||||
<select
|
|
||||||
value={itemsPerPage}
|
|
||||||
onChange={(e) => setItemsPerPage(Number(e.target.value))}
|
|
||||||
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<option value={12}>12</option>
|
|
||||||
<option value={24}>24</option>
|
|
||||||
<option value={48}>48</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">视图:</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setViewMode('grid')}
|
|
||||||
className={`p-2 rounded-md ${
|
|
||||||
viewMode === 'grid'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setViewMode('list')}
|
|
||||||
className={`p-2 rounded-md ${
|
|
||||||
viewMode === 'list'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 8a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 12a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 16a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 产品列表 */}
|
|
||||||
{loading ? (
|
|
||||||
<LoadingSkeleton />
|
|
||||||
) : filteredProducts.length === 0 ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-6xl mb-4">🔍</div>
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
|
||||||
没有找到匹配的产品
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">请尝试调整搜索条件或过滤器</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{viewMode === 'grid' ? (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
||||||
{currentProducts.map((product: Product) => (
|
|
||||||
<ProductCard key={product.id} product={product} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{currentProducts.map((product: Product) => (
|
|
||||||
<ProductListItem key={product.id} product={product} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalPages}
|
|
||||||
startIndex={startIndex}
|
|
||||||
endIndex={endIndex}
|
|
||||||
totalItems={filteredProducts.length}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 返回顶部按钮 */}
|
|
||||||
<button
|
|
||||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
|
||||||
className="fixed bottom-8 right-8 bg-blue-600 hover:bg-blue-700 text-white p-3 rounded-full shadow-lg transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* 返回链接 */}
|
|
||||||
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
|
|
||||||
← 返回测试页面列表
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,566 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { Link } from 'wouter'
|
|
||||||
|
|
||||||
export default function NavigationTestPage() {
|
|
||||||
const [activeTab, setActiveTab] = useState('home')
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
|
||||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false)
|
|
||||||
const [breadcrumbs, setBreadcrumbs] = useState(['首页', '产品', '手机'])
|
|
||||||
const [notifications, setNotifications] = useState([
|
|
||||||
{ id: 1, title: '新消息', content: '您有一条新的私信', time: '2分钟前', unread: true },
|
|
||||||
{ id: 2, title: '系统通知', content: '系统将于今晚维护', time: '1小时前', unread: true },
|
|
||||||
{ id: 3, title: '订单更新', content: '您的订单已发货', time: '3小时前', unread: false },
|
|
||||||
])
|
|
||||||
|
|
||||||
const handleBreadcrumbClick = (index: number) => {
|
|
||||||
const newBreadcrumbs = breadcrumbs.slice(0, index + 1)
|
|
||||||
setBreadcrumbs(newBreadcrumbs)
|
|
||||||
}
|
|
||||||
|
|
||||||
const markNotificationAsRead = (id: number) => {
|
|
||||||
setNotifications((prev) =>
|
|
||||||
prev.map((notif) => (notif.id === id ? { ...notif, unread: false } : notif))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const unreadCount = notifications.filter((n) => n.unread).length
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
||||||
{/* 顶部导航栏 */}
|
|
||||||
<nav className="bg-white dark:bg-gray-800 shadow-lg border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
|
||||||
<div className="flex justify-between items-center h-16">
|
|
||||||
{/* Logo */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">TestNav</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 主导航菜单 */}
|
|
||||||
<div className="hidden md:flex space-x-8">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
|
||||||
>
|
|
||||||
首页
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* 产品下拉菜单 */}
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
||||||
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors flex items-center"
|
|
||||||
>
|
|
||||||
产品
|
|
||||||
<svg
|
|
||||||
className="ml-1 h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isDropdownOpen && (
|
|
||||||
<div className="absolute top-full left-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
|
||||||
<div className="py-1">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
手机
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
电脑
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
平板
|
|
||||||
</a>
|
|
||||||
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
配件
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
|
||||||
>
|
|
||||||
服务
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors"
|
|
||||||
>
|
|
||||||
支持
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧菜单 */}
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
{/* 通知铃铛 */}
|
|
||||||
<div className="relative">
|
|
||||||
<button className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 p-2 rounded-full transition-colors">
|
|
||||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M15 17h5l-5 5v-5zM10.5 3.75a6 6 0 0 1 6 6v2.25l2.25 2.25v2.25H2.25V14.25L4.5 12V9.75a6 6 0 0 1 6-6z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{unreadCount > 0 && (
|
|
||||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
|
|
||||||
{unreadCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 用户菜单 */}
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
|
|
||||||
className="flex items-center text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 p-2 rounded-full transition-colors"
|
|
||||||
>
|
|
||||||
<div className="h-8 w-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-medium">
|
|
||||||
U
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isUserMenuOpen && (
|
|
||||||
<div className="absolute top-full right-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
|
||||||
<div className="py-1">
|
|
||||||
<div className="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-600">
|
|
||||||
user@example.com
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
个人资料
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
设置
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
帮助
|
|
||||||
</a>
|
|
||||||
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
退出登录
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* 面包屑导航 */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 py-3">
|
|
||||||
<nav className="flex" aria-label="Breadcrumb">
|
|
||||||
<ol className="flex items-center space-x-2">
|
|
||||||
{breadcrumbs.map((crumb, crumbIdx) => {
|
|
||||||
const isLast = crumbIdx === breadcrumbs.length - 1
|
|
||||||
const showSeparator = crumbIdx > 0
|
|
||||||
return (
|
|
||||||
<li key={`${crumb}-${crumbIdx + 1}`} className="flex items-center">
|
|
||||||
{showSeparator && (
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 text-gray-400 mx-2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => handleBreadcrumbClick(crumbIdx)}
|
|
||||||
className={`text-sm font-medium transition-colors ${
|
|
||||||
isLast
|
|
||||||
? 'text-gray-500 dark:text-gray-400 cursor-default'
|
|
||||||
: 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{crumb}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 主要内容区域 */}
|
|
||||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
||||||
{/* 标签页导航 */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<nav className="-mb-px flex space-x-8">
|
|
||||||
{[
|
|
||||||
{ id: 'home', label: '概览', icon: '🏠' },
|
|
||||||
{ id: 'products', label: '产品列表', icon: '📱' },
|
|
||||||
{ id: 'orders', label: '订单管理', icon: '📦' },
|
|
||||||
{ id: 'analytics', label: '数据分析', icon: '📊' },
|
|
||||||
{ id: 'settings', label: '设置', icon: '⚙️' },
|
|
||||||
].map((tab) => (
|
|
||||||
<button
|
|
||||||
key={tab.id}
|
|
||||||
onClick={() => setActiveTab(tab.id)}
|
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
|
|
||||||
activeTab === tab.id
|
|
||||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
||||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="mr-2">{tab.icon}</span>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 标签页内容 */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
||||||
{activeTab === 'home' && (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">概览</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
|
||||||
欢迎来到导航测试页面!这里展示了各种常见的导航模式。
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
|
|
||||||
<h3 className="font-semibold text-blue-900 dark:text-blue-100">总销售额</h3>
|
|
||||||
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">¥123,456</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-green-50 dark:bg-green-900 p-4 rounded-lg">
|
|
||||||
<h3 className="font-semibold text-green-900 dark:text-green-100">订单数量</h3>
|
|
||||||
<p className="text-2xl font-bold text-green-600 dark:text-green-400">1,234</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-purple-50 dark:bg-purple-900 p-4 rounded-lg">
|
|
||||||
<h3 className="font-semibold text-purple-900 dark:text-purple-100">用户数量</h3>
|
|
||||||
<p className="text-2xl font-bold text-purple-600 dark:text-purple-400">5,678</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'products' && (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">产品列表</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{['iPhone 15 Pro', 'MacBook Air', 'iPad Pro', 'Apple Watch'].map((product) => (
|
|
||||||
<div
|
|
||||||
key={product}
|
|
||||||
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-600 rounded-lg"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-gray-900 dark:text-white">{product}</h3>
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">产品描述...</p>
|
|
||||||
</div>
|
|
||||||
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
|
|
||||||
查看详情
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'orders' && (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">订单管理</h2>
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
|
||||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
订单号
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
客户
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
状态
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
金额
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-600">
|
|
||||||
{[
|
|
||||||
{ id: '#001', customer: '张三', status: '已发货', amount: '¥1,299' },
|
|
||||||
{ id: '#002', customer: '李四', status: '处理中', amount: '¥2,599' },
|
|
||||||
{ id: '#003', customer: '王五', status: '已完成', amount: '¥899' },
|
|
||||||
].map((order) => (
|
|
||||||
<tr key={order.id}>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
||||||
{order.id}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
||||||
{order.customer}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
|
||||||
order.status === '已完成'
|
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
|
||||||
: order.status === '已发货'
|
|
||||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
|
||||||
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{order.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
||||||
{order.amount}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'analytics' && (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">数据分析</h2>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
|
||||||
销售趋势
|
|
||||||
</h3>
|
|
||||||
<div className="h-32 bg-linear-to-r from-blue-400 to-purple-500 rounded-lg flex items-center justify-center text-white">
|
|
||||||
📈 图表占位符
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
|
||||||
用户分布
|
|
||||||
</h3>
|
|
||||||
<div className="h-32 bg-linear-to-r from-green-400 to-blue-500 rounded-lg flex items-center justify-center text-white">
|
|
||||||
🗺️ 地图占位符
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'settings' && (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">设置</h2>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
|
||||||
通知设置
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
defaultChecked
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-gray-700 dark:text-gray-300">邮件通知</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-gray-700 dark:text-gray-300">短信通知</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
|
||||||
隐私设置
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
defaultChecked
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-gray-700 dark:text-gray-300">公开个人资料</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
defaultChecked
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-gray-700 dark:text-gray-300">允许搜索</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<div className="mt-8 flex flex-wrap gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsModalOpen(true)}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
打开模态框
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setBreadcrumbs([...breadcrumbs, `新页面${breadcrumbs.length}`])}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
添加面包屑
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const newNotif = {
|
|
||||||
id: Date.now(),
|
|
||||||
title: '新通知',
|
|
||||||
content: `这是第 ${notifications.length + 1} 条通知`,
|
|
||||||
time: '刚刚',
|
|
||||||
unread: true,
|
|
||||||
}
|
|
||||||
setNotifications((prev) => [newNotif, ...prev])
|
|
||||||
}}
|
|
||||||
className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
添加通知
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 通知列表 */}
|
|
||||||
{notifications.length > 0 && (
|
|
||||||
<div className="mt-8">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">通知中心</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{notifications.map((notification) => (
|
|
||||||
<div
|
|
||||||
key={notification.id}
|
|
||||||
className={`p-4 rounded-lg border cursor-pointer transition-colors ${
|
|
||||||
notification.unread
|
|
||||||
? 'bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700'
|
|
||||||
: 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600'
|
|
||||||
}`}
|
|
||||||
onClick={() => markNotificationAsRead(notification.id)}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">
|
|
||||||
{notification.title}
|
|
||||||
</h4>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
|
||||||
{notification.content}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{notification.time}
|
|
||||||
</span>
|
|
||||||
{notification.unread && (
|
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 模态框 */}
|
|
||||||
{isModalOpen && (
|
|
||||||
<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 max-w-md w-full mx-4">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">模态框标题</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
||||||
>
|
|
||||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
|
||||||
这是一个模态框示例,用于测试弹窗交互。Agent 需要能够识别并操作这类覆盖层元素。
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-end space-x-3">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
className="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
确认
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 返回链接 */}
|
|
||||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
||||||
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
|
|
||||||
← 返回测试页面列表
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Route, Switch } from 'wouter'
|
|
||||||
|
|
||||||
import AsyncTestPage from './async-test'
|
|
||||||
import ComplexTestPage from './complex-test'
|
|
||||||
import ErrorTestPage from './error-test'
|
|
||||||
import FormTestPage from './form-test'
|
|
||||||
import IndexPage from './index'
|
|
||||||
import ListTestPage from './list-test'
|
|
||||||
import NavigationTestPage from './navigation-test'
|
|
||||||
|
|
||||||
export default function Router() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Switch>
|
|
||||||
<Route path="/form" component={FormTestPage} />
|
|
||||||
<Route path="/navigation" component={NavigationTestPage} />
|
|
||||||
<Route path="/list" component={ListTestPage} />
|
|
||||||
<Route path="/complex" component={ComplexTestPage} />
|
|
||||||
<Route path="/errors" component={ErrorTestPage} />
|
|
||||||
<Route path="/async" component={AsyncTestPage} />
|
|
||||||
<Route path="" component={IndexPage} />
|
|
||||||
</Switch>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user