refactor: monorepo

This commit is contained in:
Simon
2025-12-01 20:11:12 +08:00
committed by GitHub
parent 1b9970da14
commit adec9d8197
98 changed files with 1144 additions and 1129 deletions

View File

@@ -5,9 +5,12 @@ Brief description of changes.
## Type ## Type
- [ ] Bug fix - [ ] Bug fix
- [ ] New feature - [ ] Feature / Improvement
- [ ] Breaking change - [ ] Refactor
- [ ] Documentation - [ ] Documentation
- [ ] Website
- [ ] Demo / Testing
- [ ] Breaking change
## Testing ## Testing
@@ -19,10 +22,4 @@ Closes #(issue)
## Requirements / 要求 ## Requirements / 要求
- [ ] I will be polite and respectful. / 我会保持礼貌与尊重。
- [ ] My comments and replies are constructive and actionable. / 我的评论与回复具有建设性。
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。 - [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
## Contributing / 贡献
Constructive suggestions and code contributions are encouraged. If this PR originated from a discussion or issue, please link it above. 欢迎建设性意见与代码贡献;如源自讨论或 Issue请在上方关联链接。

View File

@@ -2,36 +2,56 @@
## Project Overview ## Project Overview
This is a dual-architecture project with **two separate parts**: This is a **monorepo** with npm workspaces containing **two main packages**:
1. **Core Library** (`src/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation 1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
2. **Demo&docs Website** (`pages/`) - React documentation and landing page 2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
## Development Commands ## Development Commands
### Core Commands ### Core Commands
```bash ```bash
npm start # Start React website development server npm start # Start website dev server
npm run build # Build both library AND website npm run dev # Same as start
npm run build:lib # Build pure JS library only (src/ → dist/lib/) npm run build # Build all packages
npm run build:lib:watch # Library development with auto-rebuild npm run build:lib # Build page-agent library only
npm run lint # ESLint with TypeScript strict rules npm run lint # ESLint with TypeScript strict rules
``` ```
### Package-specific Commands
```bash
# Core library
npm run build --workspace=page-agent
npm run build:watch --workspace=page-agent
# Website
npm run dev --workspace=@page-agent/website
npm run build --workspace=@page-agent/website
```
## Architecture & Critical Patterns ## Architecture & Critical Patterns
### Dual Build System ### Monorepo Structure
- **Website build**: `vite.config.js` → React SPA with hash routing → `dist/` ```
- **Library build**: `vite.lib.config.js` → UMD/ES modules → `dist/lib/` packages/
- **Entry points**: `src/entry.ts` (library), `pages/main.tsx` (website) ├── page-agent/ # npm: "page-agent"
│ ├── src/ # Core library source
│ ├── vite.config.js # Library build (ES + UMD)
│ └── package.json
└── website/ # npm: "@page-agent/website" (private)
├── src/ # Website source (formerly pages/)
├── index.html
├── vite.config.js # Website build
└── package.json
```
### Module Boundaries (Critical) ### Module Boundaries (Critical)
- **Core library** (`src/`): NEVER import from `pages/` - must remain pure JavaScript - **Core library** (`packages/page-agent/`): NEVER import from website - must remain pure JavaScript
- **Website** (`pages/`): CAN import from `src/` via `@/` alias for demos - **Website** (`packages/website/`): CAN import from `page-agent` for demos. Alias `@/` `website/src/`
- **Import aliases**: `@/``src/`, `@pages/``pages/`
### DOM Pipeline ### DOM Pipeline
@@ -73,7 +93,7 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
## File Organization ## File Organization
### Core Library (`src/`) ### Core Library (`packages/page-agent/src/`)
- `entry.ts` - CDN/UMD entry point with auto-initialization - `entry.ts` - CDN/UMD entry point with auto-initialization
- `PageAgent.ts` - **Main AI agent class** orchestrating DOM operations - `PageAgent.ts` - **Main AI agent class** orchestrating DOM operations
@@ -85,7 +105,7 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
- `dom/` - HTML serialization and page analysis utilities - `dom/` - HTML serialization and page analysis utilities
- `config/` - Configuration constants and settings - `config/` - Configuration constants and settings
### Website (`pages/`) ### Website (`packages/website/src/`)
- `main.tsx` - Site entry with hash routing setup - `main.tsx` - Site entry with hash routing setup
- `router.tsx` - **Manual route definitions** (requires explicit registration) - `router.tsx` - **Manual route definitions** (requires explicit registration)
@@ -97,21 +117,20 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
### New Documentation Page ### New Documentation Page
1. Create `pages/docs/<section>/<slug>/page.tsx` 1. Create `packages/website/src/docs/<section>/<slug>/page.tsx`
2. Add route to `pages/router.tsx` with `<Header /> + <DocsLayout>` wrapper 2. Add route to `packages/website/src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
3. Add navigation item to `DocsLayout.tsx` 3. Add navigation item to `DocsLayout.tsx`
### New Agent Tool ### New Agent Tool
1. Implement under `src/tools/` 1. Implement under `packages/page-agent/src/tools/`
2. Export via `src/tools/index.ts` 2. Export via `packages/page-agent/src/tools/index.ts`
3. Wire into `PageAgent.ts` if needed 3. Wire into `PageAgent.ts` if needed
### New UI Component ### New UI Component
1. Create in `src/ui/` with colocated CSS modules 1. Create in `packages/page-agent/src/ui/` with colocated CSS modules
2. Use event bus for PageAgent communication 2. Use event bus for PageAgent communication
3. Test via `pages/test-pages/`
## Code Standards ## Code Standards
@@ -136,26 +155,28 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
## Critical Files to Understand ## Critical Files to Understand
- `pages/router.tsx` - Central routing definition (manual registration required) - `packages/page-agent/src/PageAgent.ts` - Core AI agent class with DOM manipulation
- `pages/components/DocsLayout.tsx` - Navigation structure - `packages/page-agent/src/dom/dom_tree/index.js` - DOM extraction engine
- `src/PageAgent.ts` - Core AI agent class with DOM manipulation - `packages/page-agent/src/utils/bus.ts` - Type-safe event bus system
- `src/dom/dom_tree/index.js` - DOM extraction engine - `packages/page-agent/src/entry.ts` - Library entry point for CDN usage
- `src/utils/bus.ts` - Type-safe event bus system - `packages/page-agent/vite.config.js` - Library build configuration
- `src/entry.ts` - Library entry point for CDN usage
- `vite.config.js` / `vite.lib.config.js` - Dual build configuration - `packages/website/src/router.tsx` - Central routing definition (manual registration required)
- `packages/website/src/components/DocsLayout.tsx` - Navigation structure
- `packages/website/vite.config.js` - Website build configuration
## Debugging Common Issues ## Debugging Common Issues
### Blank Documentation Pages ### Blank Documentation Pages
1. Verify route exists in `pages/router.tsx` 1. Verify route exists in `packages/website/src/router.tsx`
2. Check component import path 2. Check component import path
3. Verify CSS isn't hiding content (check dark mode classes) 3. Verify CSS isn't hiding content (check dark mode classes)
4. Test with minimal component first 4. Test with minimal component first
### Library Integration Issues ### Library Integration Issues
1. Check `dist/lib/page-agent.umd.js` builds correctly 1. Check `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly
2. Test CDN injection with query params 2. Test CDN injection with query params
3. Verify event bus communications are properly typed 3. Verify event bus communications are properly typed
4. Use `pages/test-pages/` for isolated testing 4. Use `packages/website/src/test-pages/` for isolated testing

View File

@@ -22,10 +22,12 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
### Project Structure ### Project Structure
This project has **two separate parts**: This is a **monorepo** with npm workspaces containing **two main packages**:
- **Core Library** (`src/`) - Pure JavaScript AI agent library 1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
- **Documentation Website** (`pages/`) - React web app for landing page and docs 2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
We use a simplified monorepo solution with native npm-workspace. No fancy tooling. Hoisting is required.
## 🤝 How to Contribute ## 🤝 How to Contribute

View File

@@ -8,7 +8,7 @@ import globals from 'globals'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
export default defineConfig([ export default defineConfig([
globalIgnores(['dist', 'test-pages']), globalIgnores(['**/dist', '**/test-pages', '**/node_modules']),
{ {
plugins: { plugins: {
'react-hooks': reactHooks, 'react-hooks': reactHooks,
@@ -37,7 +37,7 @@ export default defineConfig([
], ],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: ['./tsconfig.json'], project: ['./packages/*/tsconfig.json'],
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
ecmaVersion: 2020, ecmaVersion: 2020,

1465
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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}": [

View File

@@ -11,15 +11,10 @@ declare module '*.md?raw' {
export default content export default content
} }
/**
* for local dev and umd demo
*/
declare global { declare global {
interface Window { interface Window {
pageAgent?: PageAgent pageAgent?: PageAgent
PageAgent: typeof PageAgent PageAgent: typeof PageAgent
__PAGE_AGENT_IDS__: string[] __PAGE_AGENT_IDS__: string[]
} }
} }

View 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"
}
}

View File

@@ -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,

View File

@@ -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))[]

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -1,4 +1,4 @@
import type { PageAgent } from '@/PageAgent' import type { PageAgent } from '../PageAgent'
const clearFunctions = [] as (() => void)[] const clearFunctions = [] as (() => void)[]

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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'

View File

@@ -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'

View File

@@ -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]

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"noEmit": false,
"outDir": "./dist",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
},
"include": ["src", "env.d.ts"]
}

View File

@@ -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)

View File

@@ -0,0 +1 @@
# Landing Page & Docs

6
packages/website/env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}

View File

@@ -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 = () => {

View 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"
}
}

View File

@@ -88,7 +88,6 @@
color: #dcdcaa; color: #dcdcaa;
} }
/* 箭头函数 (=>) */ /* 箭头函数 (=>) */
.arrow { .arrow {
color: #d73a49; color: #d73a49;

View File

@@ -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')

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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')

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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')

View File

@@ -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)

View File

@@ -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>
... ...
</> </>

View File

@@ -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>

View File

@@ -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 (

View 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" }]
}

View 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),
},
})

View File

@@ -1 +0,0 @@
# Landing Page & Docs

View File

@@ -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": []
} }