refactor: monorepo
This commit is contained in:
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -5,9 +5,12 @@ Brief description of changes.
|
|||||||
## Type
|
## Type
|
||||||
|
|
||||||
- [ ] Bug fix
|
- [ ] Bug fix
|
||||||
- [ ] New feature
|
- [ ] Feature / Improvement
|
||||||
- [ ] Breaking change
|
- [ ] Refactor
|
||||||
- [ ] Documentation
|
- [ ] Documentation
|
||||||
|
- [ ] Website
|
||||||
|
- [ ] Demo / Testing
|
||||||
|
- [ ] Breaking change
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@@ -19,10 +22,4 @@ Closes #(issue)
|
|||||||
|
|
||||||
## Requirements / 要求
|
## Requirements / 要求
|
||||||
|
|
||||||
- [ ] I will be polite and respectful. / 我会保持礼貌与尊重。
|
|
||||||
- [ ] My comments and replies are constructive and actionable. / 我的评论与回复具有建设性。
|
|
||||||
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
|
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
|
||||||
|
|
||||||
## Contributing / 贡献
|
|
||||||
|
|
||||||
Constructive suggestions and code contributions are encouraged. If this PR originated from a discussion or issue, please link it above. 欢迎建设性意见与代码贡献;如源自讨论或 Issue,请在上方关联链接。
|
|
||||||
|
|||||||
85
AGENTS.md
85
AGENTS.md
@@ -2,36 +2,56 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a dual-architecture project with **two separate parts**:
|
This is a **monorepo** with npm workspaces containing **two main packages**:
|
||||||
|
|
||||||
1. **Core Library** (`src/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation
|
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
|
||||||
2. **Demo&docs Website** (`pages/`) - React documentation and landing page
|
2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Core Commands
|
### Core Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start # Start React website development server
|
npm start # Start website dev server
|
||||||
npm run build # Build both library AND website
|
npm run dev # Same as start
|
||||||
npm run build:lib # Build pure JS library only (src/ → dist/lib/)
|
npm run build # Build all packages
|
||||||
npm run build:lib:watch # Library development with auto-rebuild
|
npm run build:lib # Build page-agent library only
|
||||||
npm run lint # ESLint with TypeScript strict rules
|
npm run lint # ESLint with TypeScript strict rules
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Package-specific Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Core library
|
||||||
|
npm run build --workspace=page-agent
|
||||||
|
npm run build:watch --workspace=page-agent
|
||||||
|
|
||||||
|
# Website
|
||||||
|
npm run dev --workspace=@page-agent/website
|
||||||
|
npm run build --workspace=@page-agent/website
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture & Critical Patterns
|
## Architecture & Critical Patterns
|
||||||
|
|
||||||
### Dual Build System
|
### Monorepo Structure
|
||||||
|
|
||||||
- **Website build**: `vite.config.js` → React SPA with hash routing → `dist/`
|
```
|
||||||
- **Library build**: `vite.lib.config.js` → UMD/ES modules → `dist/lib/`
|
packages/
|
||||||
- **Entry points**: `src/entry.ts` (library), `pages/main.tsx` (website)
|
├── page-agent/ # npm: "page-agent"
|
||||||
|
│ ├── src/ # Core library source
|
||||||
|
│ ├── vite.config.js # Library build (ES + UMD)
|
||||||
|
│ └── package.json
|
||||||
|
└── website/ # npm: "@page-agent/website" (private)
|
||||||
|
├── src/ # Website source (formerly pages/)
|
||||||
|
├── index.html
|
||||||
|
├── vite.config.js # Website build
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
### Module Boundaries (Critical)
|
### Module Boundaries (Critical)
|
||||||
|
|
||||||
- **Core library** (`src/`): NEVER import from `pages/` - must remain pure JavaScript
|
- **Core library** (`packages/page-agent/`): NEVER import from website - must remain pure JavaScript
|
||||||
- **Website** (`pages/`): CAN import from `src/` via `@/` alias for demos
|
- **Website** (`packages/website/`): CAN import from `page-agent` for demos. Alias `@/` → `website/src/`
|
||||||
- **Import aliases**: `@/` → `src/`, `@pages/` → `pages/`
|
|
||||||
|
|
||||||
### DOM Pipeline
|
### DOM Pipeline
|
||||||
|
|
||||||
@@ -73,7 +93,7 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
|||||||
|
|
||||||
## File Organization
|
## File Organization
|
||||||
|
|
||||||
### Core Library (`src/`)
|
### Core Library (`packages/page-agent/src/`)
|
||||||
|
|
||||||
- `entry.ts` - CDN/UMD entry point with auto-initialization
|
- `entry.ts` - CDN/UMD entry point with auto-initialization
|
||||||
- `PageAgent.ts` - **Main AI agent class** orchestrating DOM operations
|
- `PageAgent.ts` - **Main AI agent class** orchestrating DOM operations
|
||||||
@@ -85,7 +105,7 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
|||||||
- `dom/` - HTML serialization and page analysis utilities
|
- `dom/` - HTML serialization and page analysis utilities
|
||||||
- `config/` - Configuration constants and settings
|
- `config/` - Configuration constants and settings
|
||||||
|
|
||||||
### Website (`pages/`)
|
### Website (`packages/website/src/`)
|
||||||
|
|
||||||
- `main.tsx` - Site entry with hash routing setup
|
- `main.tsx` - Site entry with hash routing setup
|
||||||
- `router.tsx` - **Manual route definitions** (requires explicit registration)
|
- `router.tsx` - **Manual route definitions** (requires explicit registration)
|
||||||
@@ -97,21 +117,20 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
|||||||
|
|
||||||
### New Documentation Page
|
### New Documentation Page
|
||||||
|
|
||||||
1. Create `pages/docs/<section>/<slug>/page.tsx`
|
1. Create `packages/website/src/docs/<section>/<slug>/page.tsx`
|
||||||
2. Add route to `pages/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
2. Add route to `packages/website/src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
||||||
3. Add navigation item to `DocsLayout.tsx`
|
3. Add navigation item to `DocsLayout.tsx`
|
||||||
|
|
||||||
### New Agent Tool
|
### New Agent Tool
|
||||||
|
|
||||||
1. Implement under `src/tools/`
|
1. Implement under `packages/page-agent/src/tools/`
|
||||||
2. Export via `src/tools/index.ts`
|
2. Export via `packages/page-agent/src/tools/index.ts`
|
||||||
3. Wire into `PageAgent.ts` if needed
|
3. Wire into `PageAgent.ts` if needed
|
||||||
|
|
||||||
### New UI Component
|
### New UI Component
|
||||||
|
|
||||||
1. Create in `src/ui/` with colocated CSS modules
|
1. Create in `packages/page-agent/src/ui/` with colocated CSS modules
|
||||||
2. Use event bus for PageAgent communication
|
2. Use event bus for PageAgent communication
|
||||||
3. Test via `pages/test-pages/`
|
|
||||||
|
|
||||||
## Code Standards
|
## Code Standards
|
||||||
|
|
||||||
@@ -136,26 +155,28 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
|||||||
|
|
||||||
## Critical Files to Understand
|
## Critical Files to Understand
|
||||||
|
|
||||||
- `pages/router.tsx` - Central routing definition (manual registration required)
|
- `packages/page-agent/src/PageAgent.ts` - Core AI agent class with DOM manipulation
|
||||||
- `pages/components/DocsLayout.tsx` - Navigation structure
|
- `packages/page-agent/src/dom/dom_tree/index.js` - DOM extraction engine
|
||||||
- `src/PageAgent.ts` - Core AI agent class with DOM manipulation
|
- `packages/page-agent/src/utils/bus.ts` - Type-safe event bus system
|
||||||
- `src/dom/dom_tree/index.js` - DOM extraction engine
|
- `packages/page-agent/src/entry.ts` - Library entry point for CDN usage
|
||||||
- `src/utils/bus.ts` - Type-safe event bus system
|
- `packages/page-agent/vite.config.js` - Library build configuration
|
||||||
- `src/entry.ts` - Library entry point for CDN usage
|
|
||||||
- `vite.config.js` / `vite.lib.config.js` - Dual build configuration
|
- `packages/website/src/router.tsx` - Central routing definition (manual registration required)
|
||||||
|
- `packages/website/src/components/DocsLayout.tsx` - Navigation structure
|
||||||
|
- `packages/website/vite.config.js` - Website build configuration
|
||||||
|
|
||||||
## Debugging Common Issues
|
## Debugging Common Issues
|
||||||
|
|
||||||
### Blank Documentation Pages
|
### Blank Documentation Pages
|
||||||
|
|
||||||
1. Verify route exists in `pages/router.tsx`
|
1. Verify route exists in `packages/website/src/router.tsx`
|
||||||
2. Check component import path
|
2. Check component import path
|
||||||
3. Verify CSS isn't hiding content (check dark mode classes)
|
3. Verify CSS isn't hiding content (check dark mode classes)
|
||||||
4. Test with minimal component first
|
4. Test with minimal component first
|
||||||
|
|
||||||
### Library Integration Issues
|
### Library Integration Issues
|
||||||
|
|
||||||
1. Check `dist/lib/page-agent.umd.js` builds correctly
|
1. Check `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly
|
||||||
2. Test CDN injection with query params
|
2. Test CDN injection with query params
|
||||||
3. Verify event bus communications are properly typed
|
3. Verify event bus communications are properly typed
|
||||||
4. Use `pages/test-pages/` for isolated testing
|
4. Use `packages/website/src/test-pages/` for isolated testing
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
|
|||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
This project has **two separate parts**:
|
This is a **monorepo** with npm workspaces containing **two main packages**:
|
||||||
|
|
||||||
- **Core Library** (`src/`) - Pure JavaScript AI agent library
|
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
|
||||||
- **Documentation Website** (`pages/`) - React web app for landing page and docs
|
2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
||||||
|
|
||||||
|
We use a simplified monorepo solution with native npm-workspace. No fancy tooling. Hoisting is required.
|
||||||
|
|
||||||
## 🤝 How to Contribute
|
## 🤝 How to Contribute
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import globals from 'globals'
|
|||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist', 'test-pages']),
|
globalIgnores(['**/dist', '**/test-pages', '**/node_modules']),
|
||||||
{
|
{
|
||||||
plugins: {
|
plugins: {
|
||||||
'react-hooks': reactHooks,
|
'react-hooks': reactHooks,
|
||||||
@@ -37,7 +37,7 @@ export default defineConfig([
|
|||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ['./tsconfig.json'],
|
project: ['./packages/*/tsconfig.json'],
|
||||||
tsconfigRootDir: import.meta.dirname,
|
tsconfigRootDir: import.meta.dirname,
|
||||||
},
|
},
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
|
|||||||
1465
package-lock.json
generated
1465
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
75
package.json
75
package.json
@@ -1,35 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "page-agent",
|
"name": "root",
|
||||||
"private": false,
|
"private": true,
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent.js",
|
"workspaces": [
|
||||||
"module": "./dist/lib/page-agent.js",
|
"packages/*"
|
||||||
"types": "./dist/lib/PageAgent.d.ts",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./dist/lib/PageAgent.d.ts",
|
|
||||||
"import": "./dist/lib/page-agent.js",
|
|
||||||
"default": "./dist/lib/page-agent.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist/lib/",
|
|
||||||
"README.md",
|
|
||||||
"LICENSE",
|
|
||||||
"NOTICE"
|
|
||||||
],
|
|
||||||
"description": "AI-powered UI agent for web applications - add intelligent automation to any webpage with a single script tag",
|
|
||||||
"keywords": [
|
|
||||||
"ai",
|
|
||||||
"automation",
|
|
||||||
"ui-agent",
|
|
||||||
"browser-automation",
|
|
||||||
"web-agent",
|
|
||||||
"llm",
|
|
||||||
"dom-interaction",
|
|
||||||
"intelligent-ui"
|
|
||||||
],
|
],
|
||||||
|
"description": "AI-powered UI agent for web applications",
|
||||||
"author": "Simon<gaomeng1900>",
|
"author": "Simon<gaomeng1900>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -38,56 +15,36 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://alibaba.github.io/page-agent/",
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0",
|
||||||
|
"npm": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "npm run dev --workspace=@page-agent/website",
|
||||||
"start": "vite",
|
"start": "npm run dev --workspace=@page-agent/website",
|
||||||
"build": "tsc -b && vite build && npm run build:lib && npm run build:umd",
|
"build": "npm run build --workspaces --if-present",
|
||||||
"build:lib": "MODE=lib vite build",
|
"build:lib": "npm run build --workspace=page-agent",
|
||||||
"build:lib:watch": "MODE=lib vite build --watch",
|
|
||||||
"build:umd": "MODE=umd vite build",
|
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"ai-motion": "^0.4.7",
|
|
||||||
"chalk": "^5.6.2",
|
|
||||||
"zod": "^4.1.12"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.1.0",
|
"@commitlint/cli": "^20.1.0",
|
||||||
"@commitlint/config-conventional": "^20.0.0",
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
"@eslint/js": "^9.37.0",
|
"@eslint/js": "^9.37.0",
|
||||||
"@microsoft/api-extractor": "^7.53.1",
|
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
"@types/react": "^19.2.2",
|
|
||||||
"@types/react-dom": "^19.2.1",
|
|
||||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9.37.0",
|
"eslint": "^9.37.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-dom": "^2.0.6",
|
"eslint-plugin-react-dom": "^2.3.9",
|
||||||
"eslint-plugin-react-hooks": "^7.0.0",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.23",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"eslint-plugin-react-x": "^2.0.6",
|
"eslint-plugin-react-x": "^2.3.9",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"i18next": "^25.6.0",
|
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
|
||||||
"lint-staged": "^16.2.4",
|
"lint-staged": "^16.2.4",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"react": "^19.2.0",
|
|
||||||
"react-dom": "^19.2.0",
|
|
||||||
"react-i18next": "^16.1.4",
|
|
||||||
"tailwindcss": "^4.1.14",
|
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.46.0",
|
"typescript-eslint": "^8.46.0",
|
||||||
"unplugin-dts": "^1.0.0-beta.6",
|
"vite": "^7.1.9"
|
||||||
"vite": "^7.1.9",
|
|
||||||
"vite-plugin-css-injected-by-js": "^3.5.2",
|
|
||||||
"wouter": "^3.7.1"
|
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,cjs,cts,mjs,mts}": [
|
"*.{js,ts,cjs,cts,mjs,mts}": [
|
||||||
|
|||||||
5
env.d.ts → packages/page-agent/env.d.ts
vendored
5
env.d.ts → packages/page-agent/env.d.ts
vendored
@@ -11,15 +11,10 @@ declare module '*.md?raw' {
|
|||||||
export default content
|
export default content
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* for local dev and umd demo
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
pageAgent?: PageAgent
|
pageAgent?: PageAgent
|
||||||
PageAgent: typeof PageAgent
|
PageAgent: typeof PageAgent
|
||||||
|
|
||||||
__PAGE_AGENT_IDS__: string[]
|
__PAGE_AGENT_IDS__: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
57
packages/page-agent/package.json
Normal file
57
packages/page-agent/package.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"name": "page-agent",
|
||||||
|
"private": false,
|
||||||
|
"version": "0.0.4",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/lib/page-agent.js",
|
||||||
|
"module": "./dist/lib/page-agent.js",
|
||||||
|
"types": "./dist/lib/PageAgent.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/lib/PageAgent.d.ts",
|
||||||
|
"import": "./dist/lib/page-agent.js",
|
||||||
|
"default": "./dist/lib/page-agent.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE",
|
||||||
|
"NOTICE"
|
||||||
|
],
|
||||||
|
"description": "AI-powered UI agent for web applications - add intelligent automation to any webpage with a single script tag",
|
||||||
|
"keywords": [
|
||||||
|
"ai",
|
||||||
|
"automation",
|
||||||
|
"ui-agent",
|
||||||
|
"browser-automation",
|
||||||
|
"web-agent",
|
||||||
|
"llm",
|
||||||
|
"dom-interaction",
|
||||||
|
"intelligent-ui"
|
||||||
|
],
|
||||||
|
"author": "Simon<gaomeng1900>",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/alibaba/page-agent.git",
|
||||||
|
"directory": "packages/page-agent"
|
||||||
|
},
|
||||||
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
|
"scripts": {
|
||||||
|
"build": "MODE=lib vite build && MODE=umd vite build",
|
||||||
|
"build:lib": "MODE=lib vite build",
|
||||||
|
"build:umd": "MODE=umd vite build",
|
||||||
|
"build:watch": "MODE=lib vite build --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ai-motion": "^0.4.7",
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"zod": "^4.1.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@microsoft/api-extractor": "^7.55.1",
|
||||||
|
"unplugin-dts": "^1.0.0-beta.6",
|
||||||
|
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { AgentHistory, ExecutionResult, PageAgent } from '@/PageAgent'
|
import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent'
|
||||||
import type { DomConfig } from '@/dom'
|
import type { DomConfig } from '../dom'
|
||||||
import type { SupportedLanguage } from '@/i18n'
|
import type { SupportedLanguage } from '../i18n'
|
||||||
import type { PageAgentTool } from '@/tools'
|
import type { PageAgentTool } from '../tools'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_API_KEY,
|
DEFAULT_API_KEY,
|
||||||
DEFAULT_BASE_URL,
|
DEFAULT_BASE_URL,
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { VIEWPORT_EXPANSION } from '@/config/constants'
|
import { VIEWPORT_EXPANSION } from '../config/constants'
|
||||||
import domTree from '@/dom/dom_tree/index'
|
import domTree from './dom_tree/index'
|
||||||
import {
|
import {
|
||||||
ElementDomNode,
|
ElementDomNode,
|
||||||
FlatDomTree,
|
FlatDomTree,
|
||||||
InteractiveElementDomNode,
|
InteractiveElementDomNode,
|
||||||
TextDomNode,
|
TextDomNode,
|
||||||
} from '@/dom/dom_tree/type'
|
} from './dom_tree/type'
|
||||||
|
|
||||||
export interface DomConfig {
|
export interface DomConfig {
|
||||||
interactiveBlacklist?: (Element | (() => Element))[]
|
interactiveBlacklist?: (Element | (() => Element))[]
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* OpenAI Client implementation
|
* OpenAI Client implementation
|
||||||
*/
|
*/
|
||||||
import type { MacroToolInput } from '@/PageAgent'
|
import type { MacroToolInput } from '../PageAgent'
|
||||||
|
|
||||||
import { InvokeError, InvokeErrorType } from './errors'
|
import { InvokeError, InvokeErrorType } from './errors'
|
||||||
import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types'
|
import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types'
|
||||||
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
|
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
|
||||||
@@ -31,10 +31,9 @@
|
|||||||
* - 永远使用 tool call 来返回结构化数据,禁止模型直接返回(视为出错)
|
* - 永远使用 tool call 来返回结构化数据,禁止模型直接返回(视为出错)
|
||||||
* - 不能假设 tool 参数合法,必须有修复机制,而且修复也应该使用 tool call 返回
|
* - 不能假设 tool 参数合法,必须有修复机制,而且修复也应该使用 tool call 返回
|
||||||
*/
|
*/
|
||||||
import type { LLMConfig } from '@/config'
|
import type { LLMConfig } from '../config'
|
||||||
import { parseLLMConfig } from '@/config'
|
import { parseLLMConfig } from '../config'
|
||||||
import { EventBus, getEventBus } from '@/utils/bus'
|
import { EventBus, getEventBus } from '../utils/bus'
|
||||||
|
|
||||||
import { OpenAIClient } from './OpenAILenientClient'
|
import { OpenAIClient } from './OpenAILenientClient'
|
||||||
import { InvokeError } from './errors'
|
import { InvokeError } from './errors'
|
||||||
import type { InvokeResult, LLMClient, Message, Tool } from './types'
|
import type { InvokeResult, LLMClient, Message, Tool } from './types'
|
||||||
@@ -4,8 +4,7 @@
|
|||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
import type { MacroToolInput } from '@/PageAgent'
|
import type { MacroToolInput } from '../PageAgent'
|
||||||
|
|
||||||
import { InvokeError, InvokeErrorType } from './errors'
|
import { InvokeError, InvokeErrorType } from './errors'
|
||||||
import type { Tool } from './types'
|
import type { Tool } from './types'
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PageAgent } from '@/PageAgent'
|
import type { PageAgent } from '../PageAgent'
|
||||||
|
|
||||||
const clearFunctions = [] as (() => void)[]
|
const clearFunctions = [] as (() => void)[]
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PageAgent } from '@/PageAgent'
|
import type { PageAgent } from '../PageAgent'
|
||||||
|
|
||||||
// Find common React root elements and add data-page-agent-not-interactive attribute
|
// Find common React root elements and add data-page-agent-not-interactive attribute
|
||||||
export function patchReact(pageAgent: PageAgent) {
|
export function patchReact(pageAgent: PageAgent) {
|
||||||
@@ -4,8 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import zod, { type z } from 'zod'
|
import zod, { type z } from 'zod'
|
||||||
|
|
||||||
import type { PageAgent } from '@/PageAgent'
|
import type { PageAgent } from '../PageAgent'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clickElement,
|
clickElement,
|
||||||
getElementByIndex,
|
getElementByIndex,
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { PageAgent } from '@/PageAgent'
|
import type { PageAgent } from '../PageAgent'
|
||||||
import type { I18n } from '@/i18n'
|
import type { I18n } from '../i18n'
|
||||||
import { truncate } from '@/utils'
|
import { truncate } from '../utils'
|
||||||
import type { EventBus } from '@/utils/bus'
|
import type { EventBus } from '../utils/bus'
|
||||||
|
|
||||||
import { type Step, UIState } from './UIState'
|
import { type Step, UIState } from './UIState'
|
||||||
|
|
||||||
import styles from './Panel.module.css'
|
import styles from './Panel.module.css'
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Motion } from 'ai-motion'
|
import { Motion } from 'ai-motion'
|
||||||
|
|
||||||
import { isPageDark } from '@/utils/checkDarkMode'
|
import { isPageDark } from '../utils/checkDarkMode'
|
||||||
|
|
||||||
import styles from './SimulatorMask.module.css'
|
import styles from './SimulatorMask.module.css'
|
||||||
import cursorStyles from './cursor.module.css'
|
import cursorStyles from './cursor.module.css'
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Type-safe event bus for decoupling PageAgent and Panel
|
* Type-safe event bus for decoupling PageAgent and Panel
|
||||||
*/
|
*/
|
||||||
import type { Step } from '@/ui/UIState'
|
import type { Step } from '../ui/UIState'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event mapping definitions
|
* Event mapping definitions
|
||||||
@@ -69,10 +69,7 @@ class EventBus extends EventTarget {
|
|||||||
/**
|
/**
|
||||||
* Listen to built-in events
|
* Listen to built-in events
|
||||||
*/
|
*/
|
||||||
on<T extends keyof PageAgentEventMap>(
|
on<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
|
||||||
event: T,
|
|
||||||
handler: EventHandler<T & keyof PageAgentEventMap>
|
|
||||||
): void {
|
|
||||||
const wrappedHandler = (e: Event) => {
|
const wrappedHandler = (e: Event) => {
|
||||||
const customEvent = e as CustomEvent
|
const customEvent = e as CustomEvent
|
||||||
const params = customEvent.detail?.[0]
|
const params = customEvent.detail?.[0]
|
||||||
@@ -84,10 +81,7 @@ class EventBus extends EventTarget {
|
|||||||
/**
|
/**
|
||||||
* Listen to built-in events (one-time)
|
* Listen to built-in events (one-time)
|
||||||
*/
|
*/
|
||||||
once<T extends keyof PageAgentEventMap>(
|
once<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
|
||||||
event: T,
|
|
||||||
handler: EventHandler<T & keyof PageAgentEventMap>
|
|
||||||
): void {
|
|
||||||
const wrappedHandler = (e: Event) => {
|
const wrappedHandler = (e: Event) => {
|
||||||
const customEvent = e as CustomEvent
|
const customEvent = e as CustomEvent
|
||||||
const params = customEvent.detail?.[0]
|
const params = customEvent.detail?.[0]
|
||||||
10
packages/page-agent/tsconfig.json
Normal file
10
packages/page-agent/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src", "env.d.ts"]
|
||||||
|
}
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
// ============================================================================
|
|
||||||
// Export Configuration Based on MODE Environment Variable
|
|
||||||
// ============================================================================
|
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
|
||||||
import react from '@vitejs/plugin-react-swc'
|
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import 'dotenv/config'
|
import 'dotenv/config'
|
||||||
import process from 'node:process'
|
import process from 'node:process'
|
||||||
@@ -13,44 +8,18 @@ import { fileURLToPath } from 'url'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
const __dirname = dirname(__filename)
|
|
||||||
|
|
||||||
// Website Config (React Documentation Site)
|
|
||||||
|
|
||||||
/** @type {import('vite').UserConfig} */
|
|
||||||
const websiteConfig = {
|
|
||||||
// https://vite.dev/config/
|
|
||||||
base: './',
|
|
||||||
plugins: [react(), tailwindcss()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, 'src'),
|
|
||||||
'@pages': resolve(__dirname, 'pages'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
define: {
|
|
||||||
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),
|
|
||||||
'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
|
|
||||||
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Library Config (ES Module for NPM Package)
|
// Library Config (ES Module for NPM Package)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
/** @type {import('vite').UserConfig} */
|
/** @type {import('vite').UserConfig} */
|
||||||
const libConfig = {
|
const libConfig = {
|
||||||
// Library build configuration
|
clearScreen: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
dts({ tsconfigPath: './tsconfig.json', bundleTypes: true }),
|
dts({ tsconfigPath: './tsconfig.json', bundleTypes: true }),
|
||||||
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||||
],
|
],
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, 'src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
esbuild: {
|
||||||
keepNames: true,
|
keepNames: true,
|
||||||
@@ -66,7 +35,6 @@ const libConfig = {
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['ai', 'ai-motion', 'chalk', 'zod'],
|
external: ['ai', 'ai-motion', 'chalk', 'zod'],
|
||||||
},
|
},
|
||||||
// minify: 'terser',
|
|
||||||
minify: false,
|
minify: false,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
cssCodeSplit: true,
|
cssCodeSplit: true,
|
||||||
@@ -81,13 +49,7 @@ const libConfig = {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
/** @type {import('vite').UserConfig} */
|
/** @type {import('vite').UserConfig} */
|
||||||
const umdConfig = {
|
const umdConfig = {
|
||||||
// Library build configuration
|
|
||||||
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, 'src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
esbuild: {
|
||||||
keepNames: true,
|
keepNames: true,
|
||||||
@@ -109,19 +71,15 @@ const umdConfig = {
|
|||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const MODE = process.env.MODE
|
const MODE = process.env.MODE
|
||||||
|
|
||||||
console.log(chalk.cyan(`📦 Build mode: ${chalk.bold(MODE || 'website')}`))
|
console.log(chalk.cyan(`📦 Build mode: ${chalk.bold(MODE || 'lib')}`))
|
||||||
|
|
||||||
let config
|
let config
|
||||||
if (MODE === 'lib') {
|
if (MODE === 'umd') {
|
||||||
config = libConfig
|
|
||||||
} else if (MODE === 'umd') {
|
|
||||||
config = umdConfig
|
config = umdConfig
|
||||||
} else {
|
} else {
|
||||||
config = websiteConfig
|
config = libConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig(config)
|
export default defineConfig(config)
|
||||||
1
packages/website/README.md
Normal file
1
packages/website/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Landing Page & Docs
|
||||||
6
packages/website/env.d.ts
vendored
Normal file
6
packages/website/env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: Record<string, string>
|
||||||
|
export default classes
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="./pages/main.tsx"></script>
|
<script type="module" src="./src/main.tsx"></script>
|
||||||
<script>
|
<script>
|
||||||
// Dynamically update html lang attribute based on i18n detection
|
// Dynamically update html lang attribute based on i18n detection
|
||||||
const updateHtmlLang = () => {
|
const updateHtmlLang = () => {
|
||||||
28
packages/website/package.json
Normal file
28
packages/website/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@page-agent/website",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.4",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"page-agent": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.1",
|
||||||
|
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||||
|
"i18next": "^25.6.0",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-i18next": "^16.1.4",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"wouter": "^3.7.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,6 @@
|
|||||||
color: #dcdcaa;
|
color: #dcdcaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 箭头函数 (=>) */
|
/* 箭头函数 (=>) */
|
||||||
.arrow {
|
.arrow {
|
||||||
color: #d73a49;
|
color: #d73a49;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import BetaNotice from '@pages/components/BetaNotice'
|
|
||||||
import CodeEditor from '@pages/components/CodeEditor'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
export default function CustomTools() {
|
export default function CustomTools() {
|
||||||
const { t } = useTranslation('docs')
|
const { t } = useTranslation('docs')
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import BetaNotice from '@pages/components/BetaNotice'
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
import CodeEditor from '@pages/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
export default function DataMasking() {
|
export default function DataMasking() {
|
||||||
return (
|
return (
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import BetaNotice from '@pages/components/BetaNotice'
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
import CodeEditor from '@pages/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
export default function KnowledgeInjection() {
|
export default function KnowledgeInjection() {
|
||||||
return (
|
return (
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import BetaNotice from '@pages/components/BetaNotice'
|
|
||||||
import CodeEditor from '@pages/components/CodeEditor'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
export default function ModelIntegration() {
|
export default function ModelIntegration() {
|
||||||
const { t } = useTranslation('docs')
|
const { t } = useTranslation('docs')
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import BetaNotice from '@pages/components/BetaNotice'
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
|
|
||||||
export default function SecurityPermissions() {
|
export default function SecurityPermissions() {
|
||||||
return (
|
return (
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import BetaNotice from '@pages/components/BetaNotice'
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
import CodeEditor from '@pages/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
export default function BestPractices() {
|
export default function BestPractices() {
|
||||||
return (
|
return (
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import BetaNotice from '@pages/components/BetaNotice'
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
import CodeEditor from '@pages/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
export default function CdnSetup() {
|
export default function CdnSetup() {
|
||||||
return (
|
return (
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import CodeEditor from '@pages/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
export default function Configuration() {
|
export default function Configuration() {
|
||||||
return (
|
return (
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import CodeEditor from '@pages/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
export default function ThirdPartyAgentPage() {
|
export default function ThirdPartyAgentPage() {
|
||||||
return (
|
return (
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import BetaNotice from '@pages/components/BetaNotice'
|
|
||||||
import CodeEditor from '@pages/components/CodeEditor'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
|
||||||
export default function QuickStart() {
|
export default function QuickStart() {
|
||||||
const { t } = useTranslation('docs')
|
const { t } = useTranslation('docs')
|
||||||
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
|
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
|
||||||
|
import { PageAgent } from 'page-agent'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Link, useSearchParams } from 'wouter'
|
import { Link, useSearchParams } from 'wouter'
|
||||||
|
|
||||||
import { PageAgent } from '@/PageAgent.js'
|
|
||||||
|
|
||||||
import Footer from './components/Footer'
|
import Footer from './components/Footer'
|
||||||
import Header from './components/Header'
|
import Header from './components/Header'
|
||||||
|
|
||||||
@@ -44,9 +43,10 @@ export default function HomePage() {
|
|||||||
if (!task.trim()) return
|
if (!task.trim()) return
|
||||||
|
|
||||||
let pageAgent: PageAgent
|
let pageAgent: PageAgent
|
||||||
|
const win = window as any
|
||||||
|
|
||||||
if (window.pageAgent && !window.pageAgent.disposed) {
|
if (win.pageAgent && !win.pageAgent.disposed) {
|
||||||
pageAgent = window.pageAgent
|
pageAgent = win.pageAgent
|
||||||
} else {
|
} else {
|
||||||
pageAgent = new PageAgent({
|
pageAgent = new PageAgent({
|
||||||
// 把 react 根元素排除掉,挂了很多冒泡时间导致假阳
|
// 把 react 根元素排除掉,挂了很多冒泡时间导致假阳
|
||||||
@@ -61,7 +61,7 @@ export default function HomePage() {
|
|||||||
// baseURL: DEMO_BASE_URL,
|
// baseURL: DEMO_BASE_URL,
|
||||||
// apiKey: DEMO_API_KEY,
|
// apiKey: DEMO_API_KEY,
|
||||||
})
|
})
|
||||||
window.pageAgent = pageAgent
|
win.pageAgent = pageAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pageAgent.execute(task)
|
const result = await pageAgent.execute(task)
|
||||||
@@ -19,19 +19,31 @@ interface WizardStep {
|
|||||||
export default function ComplexTestPage() {
|
export default function ComplexTestPage() {
|
||||||
const [currentStep, setCurrentStep] = useState(1)
|
const [currentStep, setCurrentStep] = useState(1)
|
||||||
const [cartItems, setCartItems] = useState<CartItem[]>([
|
const [cartItems, setCartItems] = useState<CartItem[]>([
|
||||||
{ id: 1, name: 'iPhone 15 Pro', price: 7999, quantity: 1, image: 'https://picsum.photos/100/100?random=1' },
|
{
|
||||||
{ id: 2, name: 'MacBook Air', price: 8999, quantity: 1, image: 'https://picsum.photos/100/100?random=2' }
|
id: 1,
|
||||||
|
name: 'iPhone 15 Pro',
|
||||||
|
price: 7999,
|
||||||
|
quantity: 1,
|
||||||
|
image: 'https://picsum.photos/100/100?random=1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'MacBook Air',
|
||||||
|
price: 8999,
|
||||||
|
quantity: 1,
|
||||||
|
image: 'https://picsum.photos/100/100?random=2',
|
||||||
|
},
|
||||||
])
|
])
|
||||||
const [wizardData, setWizardData] = useState({
|
const [wizardData, setWizardData] = useState({
|
||||||
personalInfo: { name: '', email: '', phone: '' },
|
personalInfo: { name: '', email: '', phone: '' },
|
||||||
address: { street: '', city: '', zipCode: '' },
|
address: { street: '', city: '', zipCode: '' },
|
||||||
payment: { cardNumber: '', expiryDate: '', cvv: '' }
|
payment: { cardNumber: '', expiryDate: '', cvv: '' },
|
||||||
})
|
})
|
||||||
const [wizardSteps, setWizardSteps] = useState<WizardStep[]>([
|
const [wizardSteps, setWizardSteps] = useState<WizardStep[]>([
|
||||||
{ id: 1, title: '个人信息', description: '填写基本信息', completed: false },
|
{ id: 1, title: '个人信息', description: '填写基本信息', completed: false },
|
||||||
{ id: 2, title: '收货地址', description: '填写收货地址', completed: false },
|
{ id: 2, title: '收货地址', description: '填写收货地址', completed: false },
|
||||||
{ id: 3, title: '支付方式', description: '选择支付方式', completed: false },
|
{ id: 3, title: '支付方式', description: '选择支付方式', completed: false },
|
||||||
{ id: 4, title: '确认订单', description: '确认订单信息', completed: false }
|
{ id: 4, title: '确认订单', description: '确认订单信息', completed: false },
|
||||||
])
|
])
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
@@ -43,15 +55,13 @@ export default function ComplexTestPage() {
|
|||||||
removeItem(id)
|
removeItem(id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setCartItems(prev =>
|
setCartItems((prev) =>
|
||||||
prev.map(item =>
|
prev.map((item) => (item.id === id ? { ...item, quantity: newQuantity } : item))
|
||||||
item.id === id ? { ...item, quantity: newQuantity } : item
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeItem = (id: number) => {
|
const removeItem = (id: number) => {
|
||||||
setCartItems(prev => prev.filter(item => item.id !== id))
|
setCartItems((prev) => prev.filter((item) => item.id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const addItem = () => {
|
const addItem = () => {
|
||||||
@@ -60,9 +70,9 @@ export default function ComplexTestPage() {
|
|||||||
name: `新产品 ${cartItems.length + 1}`,
|
name: `新产品 ${cartItems.length + 1}`,
|
||||||
price: Math.floor(Math.random() * 5000) + 1000,
|
price: Math.floor(Math.random() * 5000) + 1000,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
image: `https://picsum.photos/100/100?random=${Date.now()}`
|
image: `https://picsum.photos/100/100?random=${Date.now()}`,
|
||||||
}
|
}
|
||||||
setCartItems(prev => [...prev, newItem])
|
setCartItems((prev) => [...prev, newItem])
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTotalPrice = () => {
|
const getTotalPrice = () => {
|
||||||
@@ -73,11 +83,23 @@ export default function ComplexTestPage() {
|
|||||||
const validateStep = (step: number): boolean => {
|
const validateStep = (step: number): boolean => {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 1:
|
case 1:
|
||||||
return !!(wizardData.personalInfo.name && wizardData.personalInfo.email && wizardData.personalInfo.phone)
|
return !!(
|
||||||
|
wizardData.personalInfo.name &&
|
||||||
|
wizardData.personalInfo.email &&
|
||||||
|
wizardData.personalInfo.phone
|
||||||
|
)
|
||||||
case 2:
|
case 2:
|
||||||
return !!(wizardData.address.street && wizardData.address.city && wizardData.address.zipCode)
|
return !!(
|
||||||
|
wizardData.address.street &&
|
||||||
|
wizardData.address.city &&
|
||||||
|
wizardData.address.zipCode
|
||||||
|
)
|
||||||
case 3:
|
case 3:
|
||||||
return !!(wizardData.payment.cardNumber && wizardData.payment.expiryDate && wizardData.payment.cvv)
|
return !!(
|
||||||
|
wizardData.payment.cardNumber &&
|
||||||
|
wizardData.payment.expiryDate &&
|
||||||
|
wizardData.payment.cvv
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -92,10 +114,8 @@ export default function ComplexTestPage() {
|
|||||||
|
|
||||||
// 更新步骤完成状态
|
// 更新步骤完成状态
|
||||||
if (step > currentStep) {
|
if (step > currentStep) {
|
||||||
setWizardSteps(prev =>
|
setWizardSteps((prev) =>
|
||||||
prev.map(s =>
|
prev.map((s) => (s.id === currentStep ? { ...s, completed: true } : s))
|
||||||
s.id === currentStep ? { ...s, completed: true } : s
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,21 +123,21 @@ export default function ComplexTestPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleInputChange = (section: string, field: string, value: string) => {
|
const handleInputChange = (section: string, field: string, value: string) => {
|
||||||
setWizardData(prev => ({
|
setWizardData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[section]: {
|
[section]: {
|
||||||
...prev[section as keyof typeof prev],
|
...prev[section as keyof typeof prev],
|
||||||
[field]: value
|
[field]: value,
|
||||||
}
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmitOrder = async () => {
|
const handleSubmitOrder = async () => {
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
|
|
||||||
// 模拟处理时间
|
// 模拟处理时间
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||||
|
|
||||||
// 模拟随机失败
|
// 模拟随机失败
|
||||||
if (Math.random() < 0.2) {
|
if (Math.random() < 0.2) {
|
||||||
setIsProcessing(false)
|
setIsProcessing(false)
|
||||||
@@ -135,9 +155,9 @@ export default function ComplexTestPage() {
|
|||||||
setWizardData({
|
setWizardData({
|
||||||
personalInfo: { name: '', email: '', phone: '' },
|
personalInfo: { name: '', email: '', phone: '' },
|
||||||
address: { street: '', city: '', zipCode: '' },
|
address: { street: '', city: '', zipCode: '' },
|
||||||
payment: { cardNumber: '', expiryDate: '', cvv: '' }
|
payment: { cardNumber: '', expiryDate: '', cvv: '' },
|
||||||
})
|
})
|
||||||
setWizardSteps(prev => prev.map(s => ({ ...s, completed: false })))
|
setWizardSteps((prev) => prev.map((s) => ({ ...s, completed: false })))
|
||||||
setOrderComplete(false)
|
setOrderComplete(false)
|
||||||
setShowConfirmDialog(false)
|
setShowConfirmDialog(false)
|
||||||
}
|
}
|
||||||
@@ -162,7 +182,10 @@ export default function ComplexTestPage() {
|
|||||||
>
|
>
|
||||||
重新开始
|
重新开始
|
||||||
</button>
|
</button>
|
||||||
<Link href="/test-pages" className="block w-full bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-md transition-colors text-center">
|
<Link
|
||||||
|
href="/test-pages"
|
||||||
|
className="block w-full bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-md transition-colors text-center"
|
||||||
|
>
|
||||||
返回测试页面
|
返回测试页面
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,12 +199,8 @@ export default function ComplexTestPage() {
|
|||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
<div className="max-w-6xl mx-auto px-4">
|
<div className="max-w-6xl mx-auto px-4">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">复杂交互测试</h1>
|
||||||
复杂交互测试
|
<p className="text-gray-600 dark:text-gray-300">测试多步骤操作、状态管理和复杂用户交互</p>
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
测试多步骤操作、状态管理和复杂用户交互
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
@@ -191,10 +210,13 @@ export default function ComplexTestPage() {
|
|||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
购物车 ({cartItems.length})
|
购物车 ({cartItems.length})
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
<div className="space-y-4 mb-6">
|
||||||
{cartItems.map(item => (
|
{cartItems.map((item) => (
|
||||||
<div key={item.id} className="flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-600 rounded-lg">
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-600 rounded-lg"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={item.image}
|
src={item.image}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
@@ -215,9 +237,7 @@ export default function ComplexTestPage() {
|
|||||||
>
|
>
|
||||||
-
|
-
|
||||||
</button>
|
</button>
|
||||||
<span className="text-sm font-medium w-8 text-center">
|
<span className="text-sm font-medium w-8 text-center">{item.quantity}</span>
|
||||||
{item.quantity}
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => updateQuantity(item.id, item.quantity + 1)}
|
onClick={() => updateQuantity(item.id, item.quantity + 1)}
|
||||||
className="w-6 h-6 flex items-center justify-center bg-gray-200 dark:bg-gray-600 rounded text-sm hover:bg-gray-300 dark:hover:bg-gray-500"
|
className="w-6 h-6 flex items-center justify-center bg-gray-200 dark:bg-gray-600 rounded text-sm hover:bg-gray-300 dark:hover:bg-gray-500"
|
||||||
@@ -265,16 +285,18 @@ export default function ComplexTestPage() {
|
|||||||
step.completed
|
step.completed
|
||||||
? 'bg-green-500 text-white'
|
? 'bg-green-500 text-white'
|
||||||
: step.id === currentStep
|
: step.id === currentStep
|
||||||
? 'bg-blue-500 text-white'
|
? 'bg-blue-500 text-white'
|
||||||
: 'bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400'
|
: 'bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{step.completed ? '✓' : step.id}
|
{step.completed ? '✓' : step.id}
|
||||||
</button>
|
</button>
|
||||||
{index < wizardSteps.length - 1 && (
|
{index < wizardSteps.length - 1 && (
|
||||||
<div className={`w-16 h-1 mx-2 ${
|
<div
|
||||||
step.completed ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-600'
|
className={`w-16 h-1 mx-2 ${
|
||||||
}`} />
|
step.completed ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -395,7 +417,9 @@ export default function ComplexTestPage() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={wizardData.payment.expiryDate}
|
value={wizardData.payment.expiryDate}
|
||||||
onChange={(e) => handleInputChange('payment', 'expiryDate', e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleInputChange('payment', 'expiryDate', e.target.value)
|
||||||
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||||
placeholder="MM/YY"
|
placeholder="MM/YY"
|
||||||
/>
|
/>
|
||||||
@@ -419,25 +443,35 @@ export default function ComplexTestPage() {
|
|||||||
{currentStep === 4 && (
|
{currentStep === 4 && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-4">订单确认</h4>
|
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
订单确认
|
||||||
|
</h4>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
|
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
|
||||||
<h5 className="font-medium text-gray-900 dark:text-white mb-2">个人信息</h5>
|
<h5 className="font-medium text-gray-900 dark:text-white mb-2">
|
||||||
|
个人信息
|
||||||
|
</h5>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
{wizardData.personalInfo.name} | {wizardData.personalInfo.email} | {wizardData.personalInfo.phone}
|
{wizardData.personalInfo.name} | {wizardData.personalInfo.email} |{' '}
|
||||||
|
{wizardData.personalInfo.phone}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
|
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
|
||||||
<h5 className="font-medium text-gray-900 dark:text-white mb-2">收货地址</h5>
|
<h5 className="font-medium text-gray-900 dark:text-white mb-2">
|
||||||
|
收货地址
|
||||||
|
</h5>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
{wizardData.address.street}, {wizardData.address.city} {wizardData.address.zipCode}
|
{wizardData.address.street}, {wizardData.address.city}{' '}
|
||||||
|
{wizardData.address.zipCode}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
|
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
|
||||||
<h5 className="font-medium text-gray-900 dark:text-white mb-2">支付方式</h5>
|
<h5 className="font-medium text-gray-900 dark:text-white mb-2">
|
||||||
|
支付方式
|
||||||
|
</h5>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
**** **** **** {wizardData.payment.cardNumber.slice(-4)}
|
**** **** **** {wizardData.payment.cardNumber.slice(-4)}
|
||||||
</p>
|
</p>
|
||||||
@@ -457,7 +491,7 @@ export default function ComplexTestPage() {
|
|||||||
>
|
>
|
||||||
上一步
|
上一步
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{currentStep < 4 ? (
|
{currentStep < 4 ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => goToStep(currentStep + 1)}
|
onClick={() => goToStep(currentStep + 1)}
|
||||||
@@ -505,9 +539,25 @@ export default function ComplexTestPage() {
|
|||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
处理中...
|
处理中...
|
||||||
</>
|
</>
|
||||||
@@ -16,7 +16,7 @@ interface FormData {
|
|||||||
terms: boolean
|
terms: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormErrors = Record<string, string>;
|
type FormErrors = Record<string, string>
|
||||||
|
|
||||||
export default function FormTestPage() {
|
export default function FormTestPage() {
|
||||||
const [formData, setFormData] = useState<FormData>({
|
const [formData, setFormData] = useState<FormData>({
|
||||||
@@ -31,7 +31,7 @@ export default function FormTestPage() {
|
|||||||
bio: '',
|
bio: '',
|
||||||
country: '',
|
country: '',
|
||||||
newsletter: false,
|
newsletter: false,
|
||||||
terms: false
|
terms: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [errors, setErrors] = useState<FormErrors>({})
|
const [errors, setErrors] = useState<FormErrors>({})
|
||||||
@@ -44,16 +44,19 @@ export default function FormTestPage() {
|
|||||||
case 'username':
|
case 'username':
|
||||||
if (!value) return '用户名不能为空'
|
if (!value) return '用户名不能为空'
|
||||||
if (typeof value === 'string' && value.length < 3) return '用户名至少需要3个字符'
|
if (typeof value === 'string' && value.length < 3) return '用户名至少需要3个字符'
|
||||||
if (typeof value === 'string' && !/^[a-zA-Z0-9_]+$/.test(value)) return '用户名只能包含字母、数字和下划线'
|
if (typeof value === 'string' && !/^[a-zA-Z0-9_]+$/.test(value))
|
||||||
|
return '用户名只能包含字母、数字和下划线'
|
||||||
return ''
|
return ''
|
||||||
case 'email':
|
case 'email':
|
||||||
if (!value) return '邮箱不能为空'
|
if (!value) return '邮箱不能为空'
|
||||||
if (typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return '请输入有效的邮箱地址'
|
if (typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
||||||
|
return '请输入有效的邮箱地址'
|
||||||
return ''
|
return ''
|
||||||
case 'password':
|
case 'password':
|
||||||
if (!value) return '密码不能为空'
|
if (!value) return '密码不能为空'
|
||||||
if (typeof value === 'string' && value.length < 6) return '密码至少需要6个字符'
|
if (typeof value === 'string' && value.length < 6) return '密码至少需要6个字符'
|
||||||
if (typeof value === 'string' && !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) return '密码必须包含大小写字母和数字'
|
if (typeof value === 'string' && !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value))
|
||||||
|
return '密码必须包含大小写字母和数字'
|
||||||
return ''
|
return ''
|
||||||
case 'confirmPassword':
|
case 'confirmPassword':
|
||||||
if (!value) return '请确认密码'
|
if (!value) return '请确认密码'
|
||||||
@@ -79,19 +82,19 @@ export default function FormTestPage() {
|
|||||||
|
|
||||||
const handleInputChange = (name: string, value: string | boolean) => {
|
const handleInputChange = (name: string, value: string | boolean) => {
|
||||||
console.log(`Input changed: ${name} = ${value}`)
|
console.log(`Input changed: ${name} = ${value}`)
|
||||||
|
|
||||||
setFormData(prev => ({ ...prev, [name]: value }))
|
setFormData((prev) => ({ ...prev, [name]: value }))
|
||||||
|
|
||||||
// 实时验证
|
// 实时验证
|
||||||
const error = validateField(name, value)
|
const error = validateField(name, value)
|
||||||
setErrors(prev => ({ ...prev, [name]: error }))
|
setErrors((prev) => ({ ...prev, [name]: error }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: FormErrors = {}
|
const newErrors: FormErrors = {}
|
||||||
let isValid = true
|
let isValid = true
|
||||||
|
|
||||||
Object.keys(formData).forEach(key => {
|
Object.keys(formData).forEach((key) => {
|
||||||
const error = validateField(key, formData[key as keyof FormData])
|
const error = validateField(key, formData[key as keyof FormData])
|
||||||
if (error) {
|
if (error) {
|
||||||
newErrors[key] = error
|
newErrors[key] = error
|
||||||
@@ -105,27 +108,27 @@ export default function FormTestPage() {
|
|||||||
|
|
||||||
const simulateSubmit = async (): Promise<{ success: boolean; message: string }> => {
|
const simulateSubmit = async (): Promise<{ success: boolean; message: string }> => {
|
||||||
// 模拟网络延迟
|
// 模拟网络延迟
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 2000))
|
await new Promise((resolve) => setTimeout(resolve, 2000 + Math.random() * 2000))
|
||||||
|
|
||||||
// 模拟随机失败
|
// 模拟随机失败
|
||||||
if (Math.random() < 0.3) {
|
if (Math.random() < 0.3) {
|
||||||
throw new Error('网络错误:服务器暂时不可用,请稍后重试')
|
throw new Error('网络错误:服务器暂时不可用,请稍后重试')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟服务器验证错误
|
// 模拟服务器验证错误
|
||||||
if (formData.username.toLowerCase() === 'admin') {
|
if (formData.username.toLowerCase() === 'admin') {
|
||||||
throw new Error('用户名 "admin" 已被占用,请选择其他用户名')
|
throw new Error('用户名 "admin" 已被占用,请选择其他用户名')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: '注册成功!欢迎加入我们的平台。'
|
message: '注册成功!欢迎加入我们的平台。',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
setSubmitResult('error')
|
setSubmitResult('error')
|
||||||
setSubmitMessage('请修正表单中的错误')
|
setSubmitMessage('请修正表单中的错误')
|
||||||
@@ -161,7 +164,7 @@ export default function FormTestPage() {
|
|||||||
bio: '',
|
bio: '',
|
||||||
country: '',
|
country: '',
|
||||||
newsletter: false,
|
newsletter: false,
|
||||||
terms: false
|
terms: false,
|
||||||
})
|
})
|
||||||
setErrors({})
|
setErrors({})
|
||||||
setSubmitResult(null)
|
setSubmitResult(null)
|
||||||
@@ -173,12 +176,8 @@ export default function FormTestPage() {
|
|||||||
<div className="max-w-2xl mx-auto px-4">
|
<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="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">用户注册表单</h1>
|
||||||
用户注册表单
|
<p className="text-gray-600 dark:text-gray-300">测试各种表单输入、验证和提交功能</p>
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
测试各种表单输入、验证和提交功能
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
@@ -248,12 +247,16 @@ export default function FormTestPage() {
|
|||||||
value={formData.confirmPassword}
|
value={formData.confirmPassword}
|
||||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
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 ${
|
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'
|
errors.confirmPassword
|
||||||
|
? 'border-red-500'
|
||||||
|
: 'border-gray-300 dark:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
placeholder="请再次输入密码"
|
placeholder="请再次输入密码"
|
||||||
/>
|
/>
|
||||||
{errors.confirmPassword && (
|
{errors.confirmPassword && (
|
||||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.confirmPassword}</p>
|
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||||
|
{errors.confirmPassword}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -372,7 +375,10 @@ export default function FormTestPage() {
|
|||||||
onChange={(e) => handleInputChange('newsletter', e.target.checked)}
|
onChange={(e) => handleInputChange('newsletter', e.target.checked)}
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
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
|
||||||
|
htmlFor="newsletter"
|
||||||
|
className="ml-2 block text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
订阅我们的新闻通讯
|
订阅我们的新闻通讯
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -384,8 +390,19 @@ export default function FormTestPage() {
|
|||||||
onChange={(e) => handleInputChange('terms', e.target.checked)}
|
onChange={(e) => handleInputChange('terms', e.target.checked)}
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
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">
|
<label
|
||||||
我同意 <a href="#" className="text-blue-600 hover:text-blue-500">服务条款</a> 和 <a href="#" className="text-blue-600 hover:text-blue-500">隐私政策</a> *
|
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{errors.terms && (
|
{errors.terms && (
|
||||||
@@ -395,16 +412,20 @@ export default function FormTestPage() {
|
|||||||
|
|
||||||
{/* 提交结果 */}
|
{/* 提交结果 */}
|
||||||
{submitResult && (
|
{submitResult && (
|
||||||
<div className={`p-4 rounded-md ${
|
<div
|
||||||
submitResult === 'success'
|
className={`p-4 rounded-md ${
|
||||||
? 'bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700'
|
submitResult === 'success'
|
||||||
: 'bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700'
|
? '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'
|
<p
|
||||||
: 'text-red-800 dark:text-red-200'
|
className={`text-sm ${
|
||||||
}`}>
|
submitResult === 'success'
|
||||||
|
? 'text-green-800 dark:text-green-200'
|
||||||
|
: 'text-red-800 dark:text-red-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{submitMessage}
|
{submitMessage}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -419,9 +440,25 @@ export default function FormTestPage() {
|
|||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<span className="flex items-center justify-center">
|
<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">
|
<svg
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
提交中...
|
提交中...
|
||||||
</span>
|
</span>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Route, Switch } from 'wouter'
|
import { Route, Switch } from 'wouter'
|
||||||
import FormTestPage from './form-test'
|
|
||||||
import NavigationTestPage from './navigation-test'
|
import AsyncTestPage from './async-test'
|
||||||
import ListTestPage from './list-test'
|
|
||||||
import ComplexTestPage from './complex-test'
|
import ComplexTestPage from './complex-test'
|
||||||
import ErrorTestPage from './error-test'
|
import ErrorTestPage from './error-test'
|
||||||
import AsyncTestPage from './async-test'
|
import FormTestPage from './form-test'
|
||||||
import IndexPage from './index'
|
import IndexPage from './index'
|
||||||
|
import ListTestPage from './list-test'
|
||||||
|
import NavigationTestPage from './navigation-test'
|
||||||
|
|
||||||
export default function Router() {
|
export default function Router() {
|
||||||
return (
|
return (
|
||||||
17
packages/website/tsconfig.json
Normal file
17
packages/website/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
// Self root
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
|
||||||
|
// Simplified monorepo solution (raw npm workspace with hoisting)
|
||||||
|
"page-agent": ["../page-agent/src/PageAgent.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src", "env.d.ts"],
|
||||||
|
"references": [{ "path": "../page-agent" }]
|
||||||
|
}
|
||||||
30
packages/website/vite.config.js
Normal file
30
packages/website/vite.config.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
import 'dotenv/config'
|
||||||
|
import process from 'node:process'
|
||||||
|
import { dirname, resolve } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
// Website Config (React Documentation Site)
|
||||||
|
export default defineConfig({
|
||||||
|
base: './',
|
||||||
|
clearScreen: false,
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// Self root
|
||||||
|
'@': resolve(__dirname, 'src'),
|
||||||
|
|
||||||
|
// Simplified monorepo solution (raw npm workspace with hoisting)
|
||||||
|
'page-agent': resolve(__dirname, '../page-agent/src/PageAgent.ts'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),
|
||||||
|
'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
|
||||||
|
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Landing Page & Docs
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
|
||||||
"target": "ES2024",
|
"target": "ES2024",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
||||||
@@ -10,7 +9,7 @@
|
|||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
// "allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": false,
|
"verbatimModuleSyntax": false,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
@@ -22,13 +21,8 @@
|
|||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true
|
||||||
|
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"],
|
|
||||||
"@pages/*": ["pages/*"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["src", "pages", "env.d.ts"]
|
"references": [{ "path": "./packages/page-agent" }, { "path": "./packages/website" }],
|
||||||
|
"files": []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user