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

@@ -0,0 +1,21 @@
import { useTranslation } from 'react-i18next'
export default function BetaNotice() {
const { t } = useTranslation('common')
return (
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-8">
<div className="flex items-start">
<div className="shrink-0">
<span className="text-xl">🚧</span>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-orange-800 dark:text-orange-200 mb-1">
{t('beta_notice.title')}
</h3>
<p className="text-sm text-orange-700 dark:text-orange-300">{t('beta_notice.content')}</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,129 @@
/**
* 代码编辑器组件,模拟现代代码编辑器的外观
*/
import React from 'react'
import HighlightSyntax from './HighlightSyntax'
interface CodeEditorProps {
code: string
language?: string
title?: string
showLineNumbers?: boolean
showHeader?: boolean
showFooter?: boolean
className?: string
}
const CodeEditor: React.FC<CodeEditorProps> = ({
code,
language = 'javascript',
title,
showLineNumbers = false,
showHeader = false,
showFooter = false,
className = '',
}) => {
const lines = code.split('\n')
// 使用 Tailwind 的 dark: 前缀实现自动主题切换
const containerClasses =
'bg-linear-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-gray-300 dark:border-gray-700'
const headerClasses = 'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700'
const headerTextClasses = 'text-gray-700 dark:text-gray-300'
const languageTextClasses = 'text-gray-600 dark:text-gray-400'
const lineNumbersClasses =
'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-500'
const codeAreaClasses = 'bg-white dark:bg-gray-900'
const footerClasses =
'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-400'
const copyButtonClasses =
'bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-white'
return (
<div
className={`group relative ${containerClasses} rounded-xl border shadow-2xl overflow-hidden ${className}`}
>
{/* 编辑器顶部栏 */}
{showHeader && (
<div className={`flex items-center justify-between px-4 py-3 ${headerClasses} border-b`}>
<div className="flex items-center space-x-3">
{/* 窗口控制按钮 */}
<div className="flex space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
</div>
{title && (
<span className={`text-sm ${headerTextClasses} font-medium ml-2`}>{title}</span>
)}
</div>
<div className="flex items-center space-x-2">
<span className={`text-xs ${languageTextClasses} uppercase tracking-wide`}>
{language}
</span>
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
</div>
</div>
)}
{/* 代码内容区域 */}
<div className="relative">
<div className="flex">
{/* 行号 */}
{showLineNumbers && (
<div className={`shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
<div className="text-xs font-mono leading-6">
{lines.map((line, lineIdx) => {
const lineNum = lineIdx + 1
return (
<div key={`${lineNum}-${line.substring(0, 20)}`} className="text-right">
{lineNum}
</div>
)
})}
</div>
</div>
)}
{/* 代码内容 */}
<div className={`flex-1 px-4 py-4 ${codeAreaClasses} overflow-x-auto`}>
<div className="text-sm font-mono leading-6">
<HighlightSyntax code={code} />
</div>
</div>
</div>
{/* 复制按钮 */}
<button
onClick={() => {
navigator.clipboard.writeText(code).catch(console.error)
}}
className={`absolute top-3 right-3 p-2 ${copyButtonClasses} rounded-lg transition-all duration-200 opacity-0 group-hover:opacity-100`}
title="复制代码"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
{/* 底部状态栏 */}
{showFooter && (
<div className={`px-4 py-2 ${footerClasses} border-t`}>
<div className="flex items-center justify-between text-xs">
<span>{lines.length} lines</span>
<span>UTF-8</span>
</div>
</div>
)}
</div>
)
}
export default CodeEditor

View File

@@ -0,0 +1,98 @@
import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, useLocation } from 'wouter'
interface DocsLayoutProps {
children: ReactNode
}
interface NavItem {
title: string
path: string
}
interface NavSection {
title: string
items: NavItem[]
}
export default function DocsLayout({ children }: DocsLayoutProps) {
const { t } = useTranslation('common')
const [location] = useLocation()
const navigationSections: NavSection[] = [
{
title: t('nav.introduction'),
items: [
{ title: t('nav.overview'), path: '/docs/introduction/overview' },
{ title: t('nav.quick_start'), path: '/docs/introduction/quick-start' },
{ title: t('nav.limitations'), path: '/docs/introduction/limitations' },
],
},
{
title: t('nav.features'),
items: [
{ title: t('nav.model_integration'), path: '/docs/features/model-integration' },
{ title: t('nav.custom_tools'), path: '/docs/features/custom-tools' },
{ title: t('nav.knowledge_injection'), path: '/docs/features/knowledge-injection' },
{ title: t('nav.security_permissions'), path: '/docs/features/security-permissions' },
{ title: t('nav.data_masking'), path: '/docs/features/data-masking' },
],
},
{
title: t('nav.integration'),
items: [
{ title: t('nav.cdn_setup'), path: '/docs/integration/cdn-setup' },
{ title: t('nav.configuration'), path: '/docs/integration/configuration' },
{ title: t('nav.best_practices'), path: '/docs/integration/best-practices' },
{ title: t('nav.third_party_agent'), path: '/docs/integration/third-party-agent' },
],
},
]
return (
<div className="max-w-7xl mx-auto px-6 py-8 overflow-x-auto">
<div className="flex gap-8 min-w-[900px]">
{/* Sidebar */}
<aside className="w-64 shrink-0" role="complementary" aria-label="文档导航">
<div className="sticky top-8">
<nav className="space-y-8" role="navigation" aria-label="文档章节">
{navigationSections.map((section) => (
<section key={section.title}>
<h3 className="font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-3">
{section.title}
</h3>
<ul className="space-y-2" role="list">
{section.items.map((item) => {
const isActive = location === item.path
return (
<li key={item.path}>
<Link
href={item.path}
className={`block px-3 py-2 rounded-lg transition-colors duration-200 ${
isActive
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-medium'
: 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
aria-current={isActive ? 'page' : undefined}
>
{item.title}
</Link>
</li>
)
})}
</ul>
</section>
))}
</nav>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 min-w-0" id="main-content" role="main">
<div className="prose prose-lg dark:prose-invert max-w-none">{children}</div>
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import { useTranslation } from 'react-i18next'
export default function Footer() {
const { t } = useTranslation('common')
return (
<footer
className="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700"
role="contentinfo"
>
<div className="max-w-7xl mx-auto px-6 py-6">
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<p className="text-gray-600 dark:text-gray-300 text-sm">{t('footer.copyright')}</p>
<div className="flex items-center space-x-6">
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200"
aria-label={t('footer.github_label')}
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</a>
</div>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,142 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'wouter'
import LanguageSwitcher from './LanguageSwitcher'
import ThemeSwitcher from './ThemeSwitcher'
import { BookIcon, CloseIcon, GithubIcon, MenuIcon } from './icons'
export default function Header() {
const { t } = useTranslation('common')
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
<>
<header
className="relative z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
role="banner"
>
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between gap-2">
{/* Logo */}
<Link
href="/"
className="flex items-center gap-2 sm:gap-3 group shrink-0"
aria-label={t('header.logo_alt')}
onClick={() => setMobileMenuOpen(false)}
>
<img
src="https://img.alicdn.com/imgextra/i2/O1CN01HB8ylu1uozANEMZw2_!!6000000006085-49-tps-128-128.webp"
alt="PageAgent Logo"
className="w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200"
/>
<div>
<span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white block leading-tight">
page-agent
</span>
<p className="hidden sm:block text-xs text-gray-600 dark:text-gray-300">
{t('header.slogan')}
</p>
</div>
</Link>
{/* Mobile Icon Navigation (横向滚动) */}
<nav
className="md:hidden flex items-center gap-1 overflow-x-auto scrollbar-hide flex-1"
role="navigation"
aria-label="Mobile navigation"
>
<Link
href="/docs/introduction/overview"
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
aria-label={t('header.nav_docs')}
>
<BookIcon className="w-5 h-5" />
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
aria-label={t('header.nav_source')}
>
<GithubIcon className="w-5 h-5" />
</a>
</nav>
{/* Desktop Navigation */}
<nav
className="hidden md:flex items-center space-x-6"
role="navigation"
aria-label={t('header.nav_docs')}
>
<Link
href="/docs/introduction/overview"
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
>
<BookIcon />
{t('header.nav_docs')}
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
aria-label={t('header.nav_source')}
>
<GithubIcon />
{t('header.nav_source')}
</a>
<ThemeSwitcher />
<LanguageSwitcher />
</nav>
{/* Mobile menu button */}
<button
type="button"
className="md:hidden p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200 shrink-0"
aria-label={t('header.mobile_menu')}
aria-expanded={mobileMenuOpen}
aria-controls="mobile-menu"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
</button>
</div>
{/* Mobile Navigation */}
{mobileMenuOpen && (
<nav
id="mobile-menu"
className="md:hidden pt-4 pb-2 space-y-3 border-t border-gray-200 dark:border-gray-700 mt-4"
role="navigation"
>
<Link
href="/docs/introduction/overview"
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
onClick={() => setMobileMenuOpen(false)}
>
<BookIcon className="w-5 h-5" />
{t('header.nav_docs')}
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
aria-label={t('header.nav_source')}
>
<GithubIcon className="w-5 h-5" />
{t('header.nav_source')}
</a>
<div className="flex items-center gap-3 px-3 py-2">
<ThemeSwitcher />
<LanguageSwitcher />
</div>
</nav>
)}
</div>
</header>
</>
)
}

View File

@@ -0,0 +1,126 @@
.syntax {
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
font-family: monospace;
color: #171717;
}
:global(.dark) .syntax {
color: #e0e0e0;
}
/* JavaScript/TypeScript 关键字 */
.keyword {
color: #d73a49;
font-weight: 600;
}
:global(.dark) .keyword {
color: #ff6b6b;
}
/* TypeScript 特定关键字 (interface, type, enum, etc.) */
.tsKeyword {
color: #af00db;
font-weight: 600;
}
:global(.dark) .tsKeyword {
color: #c792ea;
}
/* TypeScript 内置类型 */
.type {
color: #267f99;
font-weight: 500;
}
:global(.dark) .type {
color: #4ec9b0;
}
/* 字符串 */
.string {
color: #1d6eca;
}
:global(.dark) .string {
color: #4fc3f7;
}
/* 数字 */
.number {
color: #00c583;
}
:global(.dark) .number {
color: #66bb6a;
}
/* 布尔值和字面量 (true, false, null, undefined) */
.literal {
color: #0000ff;
font-weight: 500;
}
:global(.dark) .literal {
color: #569cd6;
}
/* 注释 */
.comment {
color: #6a737d;
font-style: italic;
}
:global(.dark) .comment {
color: #9e9e9e;
}
/* 装饰器 (@decorator) */
.decorator {
color: #e0aa00;
font-weight: 500;
}
:global(.dark) .decorator {
color: #dcdcaa;
}
/* 箭头函数 (=>) */
.arrow {
color: #d73a49;
font-weight: bold;
}
:global(.dark) .arrow {
color: #ff6b6b;
}
/* 标识符(变量名、函数名等) */
.identifier {
color: #171717;
}
:global(.dark) .identifier {
color: #e0e0e0;
}
/* 属性访问 (.property) */
.property {
color: #0550ae;
}
:global(.dark) .property {
color: #9cdcfe;
}
/* 运算符 */
.operator {
color: #5a5a5a;
}
:global(.dark) .operator {
color: #d4d4d4;
}

View File

@@ -0,0 +1,176 @@
/**
* js 语法高亮组件,适合在文章中演示代码片段
*/
import React from 'react'
import styles from './HighlightSyntax.module.css'
interface HighlightSyntaxProps {
code: string
}
// JavaScript/TypeScript 关键字
const keywords =
'async|await|function|const|let|var|if|else|for|while|return|try|catch|finally|class|extends|from|import|export|default|undefined|throw|break|continue|switch|case|do|with|yield|delete|typeof|void|static|get|set|super|debugger'
// TypeScript 特定关键字
const tsKeywords =
'interface|type|enum|namespace|module|declare|abstract|implements|public|private|protected|readonly|as|satisfies|infer|keyof|is'
// 布尔值和空值
const literals = 'true|false|null|undefined|NaN|Infinity'
// TypeScript 内置类型
const tsTypes =
'string|number|boolean|any|unknown|never|void|object|symbol|bigint|Array|Promise|Record|Partial|Required|Readonly|Pick|Omit|Exclude|Extract|NonNullable|ReturnType|Parameters|ConstructorParameters|InstanceType|ThisType|Uppercase|Lowercase|Capitalize|Uncapitalize'
// 辅助函数:转义 HTML 特殊字符
function escapeHtml(text: string): string {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
// 语法高亮函数,先提取 token 再转义和高亮
function highlightSyntax(code: string): string {
// 构建正则模式,包含更多 token 类型(在原始文本上匹配)
const pattern = new RegExp(
'(' +
// 1. 字符串(双引号、单引号、模板字符串)
'"([^"\\\\]|\\\\.)*"|' +
"'([^'\\\\]|\\\\.)*'|" +
'`([^`\\\\]|\\\\.)*`|' +
// 2. 注释(单行和多行)
'//[^\\n]*|' +
'/\\*[\\s\\S]*?\\*/|' +
// 3. 装饰器
'@[a-zA-Z_$][\\w$]*|' +
// 4. 数字(包括小数、十六进制、科学计数法)
'\\b0[xX][0-9a-fA-F]+\\b|' +
'\\b\\d+\\.?\\d*(?:[eE][+-]?\\d+)?\\b|' +
// 5. TypeScript/JavaScript 关键字
'\\b(?:' +
keywords +
'|' +
tsKeywords +
'|' +
literals +
')\\b|' +
// 6. TypeScript 内置类型
'\\b(?:' +
tsTypes +
')\\b|' +
// 7. 箭头函数
'=>|' +
// 8. 函数调用(函数名后跟括号)
'\\b[a-zA-Z_$][\\w$]*(?=\\()|' +
// 9. 属性访问
'\\.[a-zA-Z_$][\\w$]*|' +
// 10. 运算符和特殊符号
'[+\\-*/%&|^!~<>=?:]+|' +
'[{}\\[\\]();,.]' +
')',
'g'
)
const tokens: string[] = []
let lastIndex = 0
let match: RegExpExecArray | null
while ((match = pattern.exec(code)) !== null) {
if (match.index > lastIndex) {
const gap = code.slice(lastIndex, match.index)
// 将间隙按空白符分割,保留空白符
tokens.push(...gap.split(/(\s+)/))
}
tokens.push(match[0])
lastIndex = pattern.lastIndex
}
if (lastIndex < code.length) {
tokens.push(...code.slice(lastIndex).split(/(\s+)/))
}
const highlighted = tokens
.map((token) => {
// 空白符直接返回
if (/^\s+$/.test(token)) {
return token
}
// 1. 注释(单行和多行)
if (/^\/\/.*$/.test(token) || /^\/\*[\s\S]*?\*\/$/.test(token)) {
return `<span class="${styles.comment}">${escapeHtml(token)}</span>`
}
// 2. 字符串
if (
/^"([^"\\]|\\.)*"$/.test(token) ||
/^'([^'\\]|\\.)*'$/.test(token) ||
/^`([^`\\]|\\.)*`$/.test(token)
) {
return `<span class="${styles.string}">${escapeHtml(token)}</span>`
}
// 3. 数字
if (/^(0[xX][0-9a-fA-F]+|\d+\.?\d*(?:[eE][+-]?\d+)?)$/.test(token)) {
return `<span class="${styles.number}">${escapeHtml(token)}</span>`
}
// 4. 布尔值和特殊字面量
if (new RegExp(`^(?:${literals})$`).test(token)) {
return `<span class="${styles.literal}">${escapeHtml(token)}</span>`
}
// 5. JavaScript/TypeScript 关键字
if (new RegExp(`^(?:${keywords})$`).test(token)) {
return `<span class="${styles.keyword}">${escapeHtml(token)}</span>`
}
// 6. TypeScript 特定关键字
if (new RegExp(`^(?:${tsKeywords})$`).test(token)) {
return `<span class="${styles.tsKeyword}">${escapeHtml(token)}</span>`
}
// 7. TypeScript 内置类型
if (new RegExp(`^(?:${tsTypes})$`).test(token)) {
return `<span class="${styles.type}">${escapeHtml(token)}</span>`
}
// 8. 装饰器
if (/^@[a-zA-Z_$][\w$]*$/.test(token)) {
return `<span class="${styles.decorator}">${escapeHtml(token)}</span>`
}
// 9. 箭头函数
if (token === '=>') {
return `<span class="${styles.arrow}">${escapeHtml(token)}</span>`
}
// 10. 函数调用和标识符
if (/^[a-zA-Z_$][\w$]*$/.test(token)) {
return `<span class="${styles.identifier}">${escapeHtml(token)}</span>`
}
// 11. 属性访问
if (/^\.[a-zA-Z_$][\w$]*$/.test(token)) {
return `<span class="${styles.property}">${escapeHtml(token)}</span>`
}
// 12. 运算符
if (/^[+\-*/%&|^!~<>=?:]+$/.test(token)) {
return `<span class="${styles.operator}">${escapeHtml(token)}</span>`
}
// 13. 其他符号,需要转义
return escapeHtml(token)
})
.join('')
return highlighted
}
const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {
const htmlContent = highlightSyntax(code)
// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml
return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />
}
export default HighlightSyntaxClient

View File

@@ -0,0 +1,164 @@
.console {
display: flex;
flex-direction: column;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
line-height: 1;
overflow: hidden;
scroll-behavior: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.historyArea {
flex: 1;
overflow-y: auto;
padding: 12px;
background-color: #fafafa;
min-height: 200px;
display: flex;
flex-direction: column;
scroll-behavior: contain;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: #d0d0d0;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #b0b0b0;
}
.historyItem {
display: flex;
align-items: center;
font-size: 12px;
line-height: 1;
padding-bottom: 6px;
border-bottom: #ccdeeebd 1px solid;
margin-bottom: 6px;
flex: 0 0 auto;
&:last-child {
margin-bottom: 0;
border-bottom: none;
}
&.input {
}
&.output {
}
.content {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
flex: 1;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: #2563eb;
}
/* 错误样式 */
&.error .content {
color: #dc2626;
background-color: #fef2f2;
padding: 4px 8px;
border-radius: 4px;
border-left: 3px solid #dc2626;
}
}
}
.prompt {
display: flex;
height: 100%;
align-items: flex-start;
width: 12px;
color: #666;
margin-right: 8px;
font-weight: 500;
flex-shrink: 0;
user-select: none;
}
.executing {
color: #f59e0b;
font-style: italic;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.inputArea {
display: flex;
align-items: center;
padding: 12px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
.prompt {
margin-top: 8px;
}
.input {
flex: auto;
border: none;
outline: none;
background: transparent;
color: #333;
resize: none;
line-height: 20px;
}
.input::placeholder {
color: #999;
font-style: italic;
}
.input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.console {
font-size: 12px;
border-radius: 6px;
}
.historyArea,
.inputLine {
padding: 8px;
}
.prompt {
margin-right: 6px;
}
}

View File

@@ -0,0 +1,369 @@
/**
* JS 调试台,适合在文档中直接让用户运行代码,并且实时查看运行结果
*/
/* eslint-disable @typescript-eslint/no-base-to-string */
import { KeyboardEvent, useEffect, useImperativeHandle, useRef, useState } from 'react'
import HighlightSyntax from './HighlightSyntax'
import styles from './JSConsole.module.css'
// 全局console拦截管理器
class ConsoleInterceptor {
private static instance: ConsoleInterceptor
private subscribers = new Set<(type: string, args: unknown[]) => void>()
private originalConsole: {
log: typeof console.log
warn: typeof console.warn
error: typeof console.error
}
private isIntercepting = false
private constructor() {
this.originalConsole = {
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
}
}
static getInstance() {
if (!ConsoleInterceptor.instance) {
ConsoleInterceptor.instance = new ConsoleInterceptor()
}
return ConsoleInterceptor.instance
}
subscribe(callback: (type: string, args: unknown[]) => void) {
this.subscribers.add(callback)
this.startIntercepting()
}
unsubscribe(callback: (type: string, args: unknown[]) => void) {
this.subscribers.delete(callback)
if (this.subscribers.size === 0) {
this.stopIntercepting()
}
}
private startIntercepting() {
if (this.isIntercepting) return
this.isIntercepting = true
console.log = (...args: unknown[]) => {
this.originalConsole.log(...args)
this.notifySubscribers('log', args)
}
console.warn = (...args: unknown[]) => {
this.originalConsole.warn(...args)
this.notifySubscribers('warn', args)
}
console.error = (...args: unknown[]) => {
this.originalConsole.error(...args)
this.notifySubscribers('error', args)
}
}
private stopIntercepting() {
if (!this.isIntercepting) return
this.isIntercepting = false
console.log = this.originalConsole.log
console.warn = this.originalConsole.warn
console.error = this.originalConsole.error
}
private notifySubscribers(type: string, args: unknown[]) {
this.subscribers.forEach((callback) => {
callback(type, args)
})
}
}
interface JSConsoleProps {
context?: Record<string, unknown>
height?: string
onExecute?: (code: string, result: unknown) => void
placeholder?: string
ref?: React.Ref<JSConsoleRef>
}
export interface JSConsoleRef {
executeCode: (code: string) => Promise<unknown>
clear: () => void
appendOutput: (content: string) => void
}
interface OutputItem {
type: 'input' | 'output' | 'error' | 'log'
content: string
timestamp: number
}
const DEFAULT_CONTEXT = {}
function JSConsole({
context = DEFAULT_CONTEXT,
height = '400px',
onExecute,
placeholder = 'Enter JavaScript code...',
ref,
}: JSConsoleProps) {
const [input, setInput] = useState('')
const [outputs, setOutputs] = useState<OutputItem[]>([])
const [isExecuting, setIsExecuting] = useState(false)
const inputRef = useRef<HTMLTextAreaElement>(null)
const outputRef = useRef<HTMLDivElement>(null)
// 持久的执行上下文,用于多轮对话共享作用域
const executionContextRef = useRef<Record<string, unknown>>({})
// 格式化结果
const formatResult = (value: unknown): string => {
if (value === null) return 'null'
if (value === undefined) return 'undefined'
if (typeof value === 'string') return `"${value}"`
if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`
if (typeof value === 'object') {
try {
return JSON.stringify(value, null, 2)
} catch {
return value.toString()
}
}
return String(value)
}
// 全局console拦截处理
useEffect(() => {
const interceptor = ConsoleInterceptor.getInstance()
const handleGlobalConsole = (type: string, args: unknown[]) => {
const content = args.map((arg) => formatResult(arg)).join(' ')
const outputItem: OutputItem = {
type: type as any,
content: content,
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, outputItem])
// 自动滚动到底部
setTimeout(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight
}
}, 0)
}
interceptor.subscribe(handleGlobalConsole)
return () => {
interceptor.unsubscribe(handleGlobalConsole)
}
}, [])
// 执行代码
const executeCode = async (code: string): Promise<unknown> => {
if (!code.trim()) return
setIsExecuting(true)
// 添加输入到输出
const inputItem: OutputItem = {
type: 'input',
content: code,
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, inputItem])
try {
// 创建异步函数以支持 await
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
// 合并外部上下文和持久执行上下文
const allContext = { ...context, ...executionContextRef.current }
const contextKeys = Object.keys(allContext)
const contextValues = Object.values(allContext)
// 注入 console.log 重定向
const logs: string[] = []
const mockConsole = {
log: (...args: unknown[]) => {
logs.push(args.map((arg) => formatResult(arg)).join(' '))
},
error: (...args: unknown[]) => {
logs.push('ERROR: ' + args.map((arg) => formatResult(arg)).join(' '))
},
warn: (...args: unknown[]) => {
logs.push('WARN: ' + args.map((arg) => formatResult(arg)).join(' '))
},
}
// 检测代码是否是表达式还是语句
const trimmedCode = code.trim()
const isExpression =
!trimmedCode.includes(';') &&
!trimmedCode.startsWith('const ') &&
!trimmedCode.startsWith('let ') &&
!trimmedCode.startsWith('var ') &&
!trimmedCode.startsWith('function ') &&
!trimmedCode.startsWith('class ') &&
!trimmedCode.startsWith('if ') &&
!trimmedCode.startsWith('for ') &&
!trimmedCode.startsWith('while ') &&
!trimmedCode.startsWith('try ') &&
!trimmedCode.startsWith('{') &&
!trimmedCode.includes('\n')
// 如果是表达式,自动添加 return
const codeToExecute = isExpression ? `return ${code}` : code
const wrappedCode = `
return (async function() {
${codeToExecute}
})();
`
// 执行代码
const func = new AsyncFunction('console', ...contextKeys, wrappedCode)
const result = await func(mockConsole, ...contextValues)
// 添加 console.log 输出
if (logs.length > 0) {
const logItem: OutputItem = {
type: 'log',
content: logs.join('\n'),
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, logItem])
}
// 总是添加执行结果输出(包括 undefined
const outputItem: OutputItem = {
type: 'output',
content: formatResult(result),
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, outputItem])
onExecute?.(code, result)
return result
} catch (error) {
const errorItem: OutputItem = {
type: 'error',
content: error instanceof Error ? error.message : String(error),
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, errorItem])
throw error
} finally {
setIsExecuting(false)
// 滚动到底部
setTimeout(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight
}
}, 0)
}
}
// 清空控制台
const clear = () => {
setOutputs([])
// 同时清空执行上下文
executionContextRef.current = {}
}
// 添加输出
const appendOutput = (content: string) => {
const outputItem: OutputItem = {
type: 'output',
content,
timestamp: Date.now(),
}
setOutputs((prev) => [...prev, outputItem])
}
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
executeCode,
clear,
appendOutput,
}))
// 处理键盘事件
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
if (e.shiftKey) {
// Shift+Enter 换行
return
} else {
// Enter 执行
e.preventDefault()
if (!isExecuting && input.trim()) {
executeCode(input)
setInput('')
setTimeout(() => inputRef.current?.focus(), 0)
}
}
}
}
function getPrompt(type: string) {
let prompt = ' '
if (type === 'input') {
prompt = '>'
} else if (type === 'output') {
prompt = '<'
}
return prompt
}
return (
<div className={styles.console} style={{ height }}>
{/* 历史记录和输入区域 */}
<div className={styles.historyArea} ref={outputRef}>
{outputs.map((item) => (
<div key={item.timestamp} className={`${styles.historyItem} ${styles[item.type]}`}>
<span className={styles.prompt}>{getPrompt(item.type)}</span>
<pre className={styles.content}>
<HighlightSyntax code={item.content} />
</pre>
</div>
))}
{isExecuting && (
<div className={styles.historyItem}>
<span className={styles.prompt}>{'> '}</span>
<span className={styles.executing}>Executing...</span>
</div>
)}
</div>
{/* 当前输入行 */}
<div className={styles.inputArea}>
<span className={styles.prompt}>{'> '}</span>
<textarea
ref={inputRef}
className={styles.input}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={isExecuting}
rows={1}
style={{
height: Math.min(Math.max(20, input.split('\n').length * 20), 120),
}}
/>
</div>
</div>
)
}
export default JSConsole

View File

@@ -0,0 +1,115 @@
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function LanguageSwitcher() {
const { i18n, t } = useTranslation('common')
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const currentLang = i18n.language
const languages = [
{ code: 'zh-CN', label: '中文' },
{ code: 'en-US', label: 'English' },
]
const currentLanguage = languages.find((lang) => lang.code === currentLang) || languages[0]
const handleLanguageChange = (langCode: string) => {
i18n.changeLanguage(langCode)
setIsOpen(false)
}
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [isOpen])
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300"
aria-label={t('language.switch_label')}
aria-expanded={isOpen}
aria-haspopup="true"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
/>
</svg>
<span>{currentLanguage.label}</span>
<svg
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div
className="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50"
role="menu"
aria-orientation="vertical"
>
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => handleLanguageChange(lang.code)}
className={`w-full flex items-center gap-2 px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${
currentLang === lang.code
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300'
}`}
role="menuitem"
>
<span>{lang.label}</span>
{currentLang === lang.code && (
<svg
className="w-4 h-4 ml-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
))}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,133 @@
import { useEffect, useState } from 'react'
type Theme = 'light' | 'dark'
export default function ThemeSwitcher() {
const [theme, setTheme] = useState<Theme>(() => {
// 初始化时读取保存的主题
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme') as Theme | null
if (savedTheme === 'light' || savedTheme === 'dark') {
return savedTheme
}
// 默认跟随系统
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
return 'light'
})
useEffect(() => {
// 应用主题
if (theme === 'dark') {
document.documentElement.classList.add('dark')
document.documentElement.style.colorScheme = 'dark'
} else {
document.documentElement.classList.remove('dark')
document.documentElement.style.colorScheme = 'light'
}
// 保存到 localStorage
localStorage.setItem('theme', theme)
}, [theme])
// 监听系统主题变化
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
// 只有在用户未手动设置时才自动跟随系统
const savedTheme = localStorage.getItem('theme')
if (!savedTheme) {
setTheme(e.matches ? 'dark' : 'light')
}
}
mediaQuery.addEventListener('change', handleSystemThemeChange)
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange)
}, [])
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
}
return (
<button
onClick={toggleTheme}
className="relative inline-flex items-center h-8 w-16 rounded-full transition-colors duration-300 ease-in-out focus:outline-none"
style={{
backgroundColor: theme === 'dark' ? '#1e293b' : '#e0f2fe',
}}
aria-label={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
role="switch"
aria-checked={theme === 'dark'}
>
{/* 滑块 */}
<span
className="inline-block h-6 w-6 transform rounded-full transition-all duration-300 ease-in-out shadow-md"
style={{
backgroundColor: theme === 'dark' ? '#475569' : '#fbbf24',
transform: theme === 'dark' ? 'translateX(2.25rem)' : 'translateX(0.25rem)',
}}
>
{/* 图标 */}
<span className="flex items-center justify-center h-full w-full">
{theme === 'light' ? (
// 太阳图标
<svg
className="w-4 h-4 text-white"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>
) : (
// 月亮图标
<svg
className="w-4 h-4 text-slate-200"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
)}
</span>
</span>
{/* 背景装饰 */}
<span
className="absolute inset-0 flex items-center justify-between px-2 pointer-events-none"
aria-hidden="true"
>
{/* 左侧太阳(浅色模式时显示) */}
<span
className={`transition-opacity duration-300 ${
theme === 'light' ? 'opacity-0' : 'opacity-40'
}`}
>
<svg className="w-4 h-4 text-sky-400" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
</svg>
</span>
{/* 右侧月亮(深色模式时显示) */}
<span
className={`transition-opacity duration-300 ${
theme === 'dark' ? 'opacity-0' : 'opacity-40'
}`}
>
<svg className="w-4 h-4 text-slate-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
</span>
</span>
</button>
)
}

View File

@@ -0,0 +1,75 @@
// SVG图标组件集合用于Header等地方复用
interface IconProps {
className?: string
'aria-hidden'?: boolean
}
export function BookIcon({ className = 'w-4 h-4', ...props }: IconProps) {
return (
<svg
className={className}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
)
}
export function GithubIcon({ className = 'w-4 h-4', ...props }: IconProps) {
return (
<svg
className={className}
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
{...props}
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
)
}
export function MenuIcon({ className = 'w-6 h-6', ...props }: IconProps) {
return (
<svg
className={className}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
)
}
export function CloseIcon({ className = 'w-6 h-6', ...props }: IconProps) {
return (
<svg
className={className}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
{...props}
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)
}

View File

@@ -0,0 +1,104 @@
import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function CustomTools() {
const { t } = useTranslation('docs')
return (
<div>
<h1 className="text-4xl font-bold mb-6">{t('custom_tools.title')}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{t('custom_tools.subtitle')}
</p>
<div className="space-y-8">
<section>
<h2 className="text-2xl font-bold mb-4">{t('custom_tools.registration')}</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t('custom_tools.registration_desc')}
</p>
<CodeEditor
code={`import zod from 'zod'
import { PageAgent, tool } from 'page-agent'
// override internal tool
const customTools = {
ask_user: tool({
description:
'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
inputSchema: zod.object({
question: zod.string(),
}),
execute: async function (this: PageAgent, input) {
const answer = await do_some_thing(input.question)
return "✅ Received user answer: " + answer
},
})
}
// remove internal tool
const customTools = {
ask_user: null // never ask user questions
}
const pageAgent = new PageAgent({customTools})
`}
language="javascript"
/>
</section>
<section>
<h2 className="text-2xl font-bold mb-4">{t('custom_tools.page_filter')}</h2>
<BetaNotice />
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t('custom_tools.page_filter_desc')}
</p>
<CodeEditor
code={`pageAgent.registerTool({
name: 'approveOrder',
description: '审批订单',
input: z.object({
orderId: z.string(),
approved: z.boolean()
}),
execute: async (params) => {
// 审批逻辑
},
// 可选:页面过滤器
pageFilter: {
// 只在订单管理页面显示
include: ['/admin/orders', '/admin/orders/*'],
// 排除特定页面
exclude: ['/admin/orders/archived']
}
})`}
language="javascript"
/>
</section>
<section>
<h2 className="text-2xl font-bold mb-4">{t('custom_tools.best_practices')}</h2>
<div className="space-y-4">
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-300 mb-2">
{t('custom_tools.bp_performance')}
</h3>
<ul className="text-gray-600 dark:text-gray-300 space-y-1 text-sm">
<li>{t('custom_tools.bp_1')}</li>
<li>{t('custom_tools.bp_2')}</li>
<li>{t('custom_tools.bp_3')}</li>
</ul>
</div>
</div>
</section>
</div>
</div>
)
}

View File

@@ -0,0 +1,48 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function DataMasking() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
AI
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🔒
</h3>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
</h3>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
</div>
<CodeEditor
code={`// 数据脱敏配置
// @todo
const rules = [
{ pattern: /\\d{11}/, replacement: '***-****-****' },
{ pattern: /\\d{4}-\\d{4}-\\d{4}-\\d{4}/, replacement: '****-****-****-****' }
]
pageAgent.maskData(rules)`}
/>
</div>
)
}

View File

@@ -0,0 +1,162 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function KnowledgeInjection() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
AI
</p>
{/* Custom Instruction */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">Instruction - </h2>
<div className="p-6 bg-purple-50 dark:bg-purple-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-purple-900 dark:text-purple-300">
🎯
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
AI
</p>
<ul className="list-disc list-inside space-y-2 text-gray-500 dark:text-gray-400">
<li> AI </li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<CodeEditor
className="mb-6"
code={`// 构造函数中设置系统指令
const pageAgent = new PageAgent({
instruction: \`
# 角色定义
你是专业的电商运营助手。
# 工作风格
- 谨慎:操作前确认
- 准确:确保正确性
- 高效:优化流程
# 错误处理
遇到错误时暂停并报告。
\`
});`}
/>
</section>
{/* App Knowledge */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">App Knowledge - </h2>
<div className="p-6 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-blue-900 dark:text-blue-300">
<EFBFBD>
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
AI
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-blue-800 dark:text-blue-200"></h4>
<ul className="list-disc list-inside text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</div>
<CodeEditor
className="mb-6"
code={`// 应用知识
pageAgent.knowledge.setAppKnowledge(\`
# 产品介绍
电商管理系统:面向中小企业的一站式解决方案。
# 操作流程
## 商品上架
1. 进入商品管理页面 2. 点击新增商品 3. 填写基本信息 4. 设置库存 5. 提交审核
# 术语解释
- SKU库存量单位
- SPU标准产品单位
- 运费模板:物流费用计算规则
# 业务规则
- 库存为0时自动下架
- VIP会员享9.5折
\`);`}
/>
</section>
{/* Page Knowledge */}
<section className="mb-12">
<h2 className="text-3xl font-bold mb-6">Page Knowledge - </h2>
<div className="p-6 bg-green-50 dark:bg-green-900/20 rounded-lg mb-6">
<h3 className="text-xl font-semibold mb-3 text-green-900 dark:text-green-300">
📄
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
AI
</p>
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-gray-500 dark:text-gray-400"></p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-green-800 dark:text-green-200"></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
</div>
</div>
<CodeEditor
className="mb-6"
code={`// 页面知识库
// 添加页面知识
pageAgent.knowledge.addPageKnowledge("/products", \`
商品列表页面,包含搜索、筛选、批量操作功能。
#add-product-btn新增商品按钮
.product-item商品列表项
#search-input搜索框最少2个字符
\`);
pageAgent.knowledge.addPageKnowledge("/orders/*", \`
订单详情页面。
.order-status订单状态标签
#update-status-btn状态更新按钮
\`);
// 移除页面知识
pageAgent.knowledge.removePageKnowledge("/products");`}
/>
</section>
</div>
)
}

View File

@@ -0,0 +1,164 @@
import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function ModelIntegration() {
const { t } = useTranslation('docs')
return (
<div>
<h1 className="text-4xl font-bold mb-6">{t('model_integration.title')}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
{t('model_integration.subtitle')}
</p>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.recommended')}</h2>
<div className="grid md:grid-cols-3 gap-4 mb-6">
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
{t('model_integration.model_gpt4_title')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{t('model_integration.model_gpt4_badge')}
</p>
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li>{t('model_integration.model_gpt4_1')}</li>
<li>{t('model_integration.model_gpt4_2')}</li>
<li>{t('model_integration.model_gpt4_3')}</li>
</ul>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
{t('model_integration.model_deepseek_title')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{t('model_integration.model_deepseek_badge')}
</p>
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li>{t('model_integration.model_deepseek_1')}</li>
<li>{t('model_integration.model_deepseek_2')}</li>
<li>{t('model_integration.model_deepseek_3')}</li>
</ul>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
{t('model_integration.model_qwen_title')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{t('model_integration.model_qwen_badge')}
</p>
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
<li>{t('model_integration.model_qwen_1')}</li>
<li>{t('model_integration.model_qwen_2')}</li>
<li>{t('model_integration.model_qwen_3')}</li>
</ul>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
{t('model_integration.model_gemini_title')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{t('model_integration.model_gemini_badge')}
</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.available')}</h2>
<div className="p-4 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg mb-6">
<h3 className="text-lg font-semibold mb-3 text-emerald-900 dark:text-emerald-300">
{t('model_integration.available_verified')}
</h3>
<div className="flex flex-wrap gap-2">
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
gpt-4.1-mini/4.1/5
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
grok-4/grok-code-fast
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
qwen3
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
deepseek-v3.1/3.2
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
claude-sonnet-4/4.5/haiku-4.5
</span>
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
gemini-2.5
</span>
</div>
</div>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.tips')}</h2>
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg mb-6">
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-disc pl-5">
<li>{t('model_integration.tip_1')}</li>
<li>{t('model_integration.tip_2')}</li>
<li>{t('model_integration.tip_3')}</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.security')}</h2>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-500 mb-4">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
{t('model_integration.security_warning')}
</p>
</div>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t('model_integration.security_desc')}
</p>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
<h3 className="text-lg font-semibold mb-3 text-blue-900 dark:text-blue-300">
{t('model_integration.security_backend_proxy')}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
{t('model_integration.security_backend_desc')}
</p>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-none pl-0">
<li>{t('model_integration.security_method_1')}</li>
<li>{t('model_integration.security_method_2')}</li>
<li>{t('model_integration.security_method_3')}</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3">{t('model_integration.configuration')}</h2>
<CodeEditor
code={`
// 百炼等其他兼容服务
const pageAgent = new PageAgent({
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: 'your-api-key',
model: 'qwen-plus'
});
// 私有部署模型
const pageAgent = new PageAgent({
baseURL: 'http://localhost:11434/v1',
apiKey: 'N/A', // Ollama 通常使用任意值
model: 'qwen3:latest'
});
// 测试接口
// @note: 限流,限制 prompt 内容,限制来源,随时变更,请替换成你自己的
// @note: 使用 DeepSeek-chat(3.2) 官方版本,使用协议和隐私策略见 DeepSeek 网站
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
`}
/>
</div>
)
}

View File

@@ -0,0 +1,81 @@
import BetaNotice from '@/components/BetaNotice'
export default function SecurityPermissions() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
page-agent AI
</p>
<div className="space-y-6">
<section>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-3">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫
</h3>
<p className="text-gray-600 dark:text-gray-300">
AI
</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
</h3>
<p className="text-gray-600 dark:text-gray-300"> AI </p>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">URL </h2>
<div className="space-y-3">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
🚫 URL
</h3>
<p className="text-gray-600 dark:text-gray-300"> AI 访</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300">
URL
</h3>
<p className="text-gray-600 dark:text-gray-300"> AI 访</p>
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">Instruction </h2>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-3">
AI
</p>
<div className="space-y-2">
<div className="pl-3 border-l-2 border-red-400">
<p className="font-medium text-red-700 dark:text-red-300"></p>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
<div className="pl-3 border-l-2 border-orange-400">
<p className="font-medium text-orange-700 dark:text-orange-300"></p>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
</div>
</div>
</section>
</div>
</div>
)
}

View File

@@ -0,0 +1,66 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function BestPractices() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
使 page-agent
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
API
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-3">
AI
</p>
<CodeEditor
code={`// 推荐:合并操作
await pageAgent.execute('填写表单姓名张三邮箱test@example.com然后提交');
// 不推荐:分别操作
await pageAgent.execute('填写姓名张三');
await pageAgent.execute('填写邮箱test@example.com');
await pageAgent.execute('点击提交按钮');`}
/>
</div>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
🎯
</h3>
<p className="text-gray-600 dark:text-gray-300">
使
</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<div className="space-y-3 mb-6">
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border-l-4 border-red-500">
<h3 className="font-semibold mb-1 text-red-900 dark:text-red-300"></h3>
<p className="text-gray-600 dark:text-gray-300"></p>
</div>
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border-l-4 border-yellow-500">
<h3 className="font-semibold mb-1 text-yellow-900 dark:text-yellow-300"></h3>
<p className="text-gray-600 dark:text-gray-300"></p>
</div>
</div>
<h2 className="text-2xl font-bold mb-3"></h2>
<CodeEditor code={`// TODO`} />
</div>
)
}

View File

@@ -0,0 +1,38 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function CdnSetup() {
return (
<div>
<h1 className="text-4xl font-bold mb-6">CDN </h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
CDN page-agent
</p>
<h2 className="text-2xl font-bold mb-3"></h2>
<CodeEditor
className="mb-8"
code={`
// 仅供测试使用,稳定 CDN 待定
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>
<script>
window.pageAgent.panel.show()
</script>`}
/>
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
</h3>
<ul className="text-gray-600 dark:text-gray-300 space-y-1">
<li> 使</li>
<li> HTTPS 使</li>
<li> CSP </li>
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,108 @@
import CodeEditor from '@/components/CodeEditor'
export default function Configuration() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<CodeEditor
className="mb-8"
language="typescript"
code={`// config
type PageAgentConfig = LLMConfig & AgentConfig & DomConfig
interface LLMConfig {
baseURL?: string
apiKey?: string
model?: string
temperature?: number
maxTokens?: number
maxRetries?: number
}
interface AgentConfig {
language?: "en-US" | "zh-CN"
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see [tools](../tools/index.ts)
*
* @example
* // override internal tool
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: zod.object({
* question: zod.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
// lifecycle hooks
// @todo: use event instead of hooks
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void
onBeforeTask?: (this: PageAgent) => Promise<void> | void
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
/**
* @note this hook can block the disposal process
* @note when dispose caused by page unload, "reason" will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut.
*/
onDispose?: (this: PageAgent, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* TODO: @unimplemented
* hook when action causes a new page to be opened
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
*/
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
/**
* TODO: @unimplemented
* try to navigate to a new page instead of opening a new tab/window.
* @note will unload the current page when a action tries to open a new page. so that things keep in the same tab/window.
*/
experimentalPreventNewPage?: boolean
}
interface DomConfig {
interactiveBlacklist?: (Element | (() => Element))[]
interactiveWhitelist?: (Element | (() => Element))[]
include_attributes?: string[]
highlightOpacity?: number
highlightLabelOpacity?: number
}
`}
/>
</div>
)
}

View File

@@ -0,0 +1,115 @@
import CodeEditor from '@/components/CodeEditor'
export default function ThirdPartyAgentPage() {
return (
<div>
<h1 className="text-4xl font-bold mb-6"> Agent</h1>
<p className="mb-6 leading-relaxed text-gray-600 dark:text-gray-300">
pageAgent Agent Agent
</p>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">💡 </h3>
<p className="text-blue-800 dark:text-blue-200">
"嘴巴""眼睛""手"
</p>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
1. Function Calling
</h3>
<CodeEditor
code={`// 定义工具
const pageAgentTool = {
name: "page_agent",
description: "执行网页操作",
parameters: {
type: "object",
properties: {
instruction: { type: "string", description: "操作指令" }
},
required: ["instruction"]
},
execute: async (params) => {
const result = await pageAgent.execute(params.instruction)
return { success: result.success, message: result.message }
}
}
// 注册到你的 agent 中`}
language="javascript"
/>
</div>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🤖 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"帮我提交工单"
</p>
</div>
<div className="bg-linear-to-br from-green-50 to-blue-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">📋 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"完成客户入职"
</p>
</div>
<div className="bg-linear-to-br from-purple-50 to-pink-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🎯 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"预订会议室"
</p>
</div>
<div className="bg-linear-to-br from-orange-50 to-red-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🔧 </h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"重启服务器"
</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white"></h3>
<CodeEditor code={`// @TODO`} language="javascript" />
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white"></h3>
<CodeEditor code={`// @TODO`} language="javascript" />
</div>
</div>
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-100 mb-2">
</h3>
<ul className="text-yellow-800 dark:text-yellow-200 space-y-1 text-sm">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="bg-linear-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">🎉 </h3>
<p className="mb-3 text-gray-700 dark:text-gray-300">
Agent
</p>
<a
href="/docs/integration/configuration"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200"
>
</a>
</div>
</div>
)
}

View File

@@ -0,0 +1,144 @@
import { useTranslation } from 'react-i18next'
export default function LimitationsPage() {
const { t } = useTranslation('docs')
return (
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">
{t('limitations.title')}
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300">{t('limitations.subtitle')}</p>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none">
<h2 className="text-2xl font-bold mb-3">{t('limitations.page_support')}</h2>
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-400 p-4 mb-6">
<h3 className="font-semibold text-blue-800 dark:text-blue-200 mb-2">
{t('limitations.spa_limit_title')}
</h3>
<ul className="text-blue-700 dark:text-blue-300 space-y-2">
<li>{t('limitations.spa_limit_1')}</li>
<li>{t('limitations.spa_limit_2')}</li>
<li>{t('limitations.spa_limit_3')}</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3">{t('limitations.interaction_limits')}</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<h3 className="font-semibold mb-4">{t('limitations.supported_ops')}</h3>
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="space-y-2">
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_click')}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_input')}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_scroll')}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_submit')}</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_select')}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_focus')}</span>
</div>
</div>
</div>
<h3 className="font-semibold mb-4">{t('limitations.unsupported_ops')}</h3>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_hover')}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_drag')}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_context')}</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_draw')}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_keyboard')}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_position')}</span>
</div>
</div>
</div>
</div>
<h2 className="text-2xl font-bold mb-3">{t('limitations.understanding_limits')}</h2>
<div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-400 p-4 mb-6">
<h3 className="font-semibold text-red-800 dark:text-red-200 mb-2">
{t('limitations.no_vision_title')}
</h3>
<p className="text-red-700 dark:text-red-300 mb-3">{t('limitations.no_vision_desc')}</p>
<ul className="text-red-700 dark:text-red-300 space-y-1">
<li>{t('limitations.no_vision_1')}</li>
<li>{t('limitations.no_vision_2')}</li>
<li>{t('limitations.no_vision_3')}</li>
<li>{t('limitations.no_vision_4')}</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3">{t('limitations.website_requirements')}</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<div className="space-y-4">
<div>
<h3 className="font-semibold mb-2">{t('limitations.req_semantic_title')}</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('limitations.req_semantic_desc')}
</p>
</div>
<div>
<h3 className="font-semibold mb-2">{t('limitations.req_ux_title')}</h3>
<p className="text-gray-600 dark:text-gray-300">{t('limitations.req_ux_desc')}</p>
</div>
<div>
<h3 className="font-semibold mb-2">{t('limitations.req_env_title')}</h3>
<p className="text-gray-600 dark:text-gray-300">{t('limitations.req_env_desc')}</p>
</div>
</div>
</div>
<h2>{t('limitations.future')}</h2>
<div className="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400 p-4">
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2">
{t('limitations.future_title')}
</h3>
<ul className="text-green-700 dark:text-green-300 space-y-1">
<li>{t('limitations.future_1')}</li>
<li>{t('limitations.future_2')}</li>
<li>{t('limitations.future_3')}</li>
<li>{t('limitations.future_4')}</li>
</ul>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,212 @@
import { useTranslation } from 'react-i18next'
export default function Overview() {
const { t } = useTranslation('docs')
return (
<article>
{/* 头图 */}
<figure className="mb-8 rounded-xl overflow-hidden">
<img
src="https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg"
alt="page-agent"
className="w-full h-64 object-cover"
/>
</figure>
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4">{t('overview.title')}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-4 leading-relaxed">
{t('overview.subtitle')}
</p>
{/* Status Badges */}
<div className="flex flex-wrap gap-2 items-center">
<a
href="https://www.npmjs.com/package/page-agent"
target="_blank"
rel="noopener noreferrer"
>
<img src="https://badge.fury.io/js/page-agent.svg" alt="npm version" />
</a>
<a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener noreferrer">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="MIT License" />
</a>
<a href="http://www.typescriptlang.org/" target="_blank" rel="noopener noreferrer">
<img
src="https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg"
alt="TypeScript"
/>
</a>
<a
href="https://www.npmjs.com/package/page-agent"
target="_blank"
rel="noopener noreferrer"
>
<img src="https://img.shields.io/npm/dt/page-agent.svg" alt="Downloads" />
</a>
<a
href="https://bundlephobia.com/package/page-agent"
target="_blank"
rel="noopener noreferrer"
>
<img src="https://img.shields.io/bundlephobia/minzip/page-agent" alt="Bundle Size" />
</a>
<a href="https://github.com/alibaba/page-agent" target="_blank" rel="noopener noreferrer">
<img
src="https://img.shields.io/github/stars/alibaba/page-agent.svg"
alt="GitHub stars"
/>
</a>
</div>
</div>
<section>
<h2 className="text-2xl font-bold mb-4">{t('overview.what_is')}</h2>
<p className="text-gray-600 dark:text-gray-300 mb-8 leading-relaxed ">
{t('overview.what_is_desc')}
</p>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">{t('overview.features_title')}</h2>
<div className="grid md:grid-cols-2 gap-4 mb-8" role="list">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
{t('overview.feature_dom.title')}
</h3>
<p className="text-gray-700 dark:text-gray-300">{t('overview.feature_dom.desc')}</p>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
{t('overview.feature_secure.title')}
</h3>
<p className="text-gray-700 dark:text-gray-300">{t('overview.feature_secure.desc')}</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
{t('overview.feature_backend.title')}
</h3>
<p className="text-gray-700 dark:text-gray-300">{t('overview.feature_backend.desc')}</p>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
{t('overview.feature_accessible.title')}
</h3>
<p className="text-gray-700 dark:text-gray-300">
{t('overview.feature_accessible.desc')}
</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-4">{t('overview.vs_browser_use')}</h2>
<div className="overflow-x-auto mb-8">
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
<thead>
<tr className="bg-gray-50 dark:bg-gray-800">
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
{t('overview.table_feature')}
</th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
page-agent
</th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
browser-use
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_deployment')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_deployment_pa')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_deployment_bu')}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_scope')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scope_pa')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scope_bu')}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_user')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_user_pa')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_user_bu')}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_scenario')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scenario_pa')}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scenario_bu')}
</td>
</tr>
</tbody>
</table>
</div>
<h2 className="text-2xl font-bold mb-4">{t('overview.use_cases_title')}</h2>
<ul className="space-y-4 mb-8">
<li className="flex items-start space-x-3">
<span className="w-6 h-6 min-w-6 bg-blue-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
1
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case1_title')}</strong> {t('overview.use_case1_desc')}
</div>
</li>
<li className="flex items-start space-x-3">
<span className="w-6 h-6 min-w-6 bg-green-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
2
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case2_title')}</strong> {t('overview.use_case2_desc')}
</div>
</li>
<li className="flex items-start space-x-3">
<span className="w-6 h-6 min-w-6 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
3
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case3_title')}</strong> {t('overview.use_case3_desc')}
</div>
</li>
<li className="flex items-start space-x-3">
<span className="w-6 h-6 min-w-6 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
4
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case4_title')}</strong> {t('overview.use_case4_desc')}
</div>
</li>
</ul>
</section>
</article>
)
}

View File

@@ -0,0 +1,82 @@
import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
export default function QuickStart() {
const { t } = useTranslation('docs')
return (
<div>
<h1 className="text-4xl font-bold mb-6">{t('quick_start.title')}</h1>
<p className=" mb-6 leading-relaxed">{t('quick_start.subtitle')}</p>
<h2 className="text-2xl font-bold mb-3">{t('quick_start.installation')}</h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
{t('quick_start.step1_title')}
</h3>
<div className="space-y-3">
<div>
<p className="text-sm font-medium mb-2">{t('quick_start.step1_cdn')}</p>
<CodeEditor
code={`// 仅供测试使用
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>`}
language="html"
/>
</div>
<div>
<p className="text-sm font-medium mb-2">{t('quick_start.step1_npm')}</p>
<CodeEditor
code={`// npm install page-agent
import PageAgent from 'page-agent'`}
language="bash"
/>
</div>
</div>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
{t('quick_start.step2_title')}
</h3>
<CodeEditor
code={`// 仅供测试使用,生产环境需要配置 LLM 接入点,本工具不提供 LLM 服务
// 测试接口
// @note: 限流,限制 prompt 内容,限制来源,随时变更,请替换成你自己的
// @note: 使用 DeepSeek-chat(3.2) 官方版本,使用协议和隐私策略见 DeepSeek 网站
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({
modelName: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'zh-CN'
})`}
language="javascript"
/>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
{t('quick_start.step3_title')}
</h3>
<CodeEditor
code={`// 程序化执行自然语言指令
await pageAgent.execute('点击提交按钮,然后填写用户名为张三');
// 或者显示对话框让用户输入指令
pageAgent.panel.show()
`}
language="javascript"
/>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,74 @@
# 国际化配置说明
本项目使用 `react-i18next` 实现国际化支持。
## 目录结构
```
pages/i18n/
├── config.ts # i18next 配置和初始化
├── types.ts # TypeScript 类型声明
├── locales/
│ ├── zh-CN/ # 中文翻译
│ │ ├── common.json # 通用组件Header, Footer等
│ │ ├── home.json # 首页
│ │ └── docs.json # 文档页(待完善)
│ └── en-US/ # 英文翻译
│ ├── common.json
│ ├── home.json
│ └── docs.json
└── README.md # 本文件
```
## 使用方法
### 在组件中使用
```tsx
import { useTranslation } from 'react-i18next'
function MyComponent() {
const { t } = useTranslation('common') // 指定命名空间
return <h1>{t('header.nav_docs')}</h1>
}
```
### 使用多个命名空间
```tsx
const { t } = useTranslation(['home', 'common'])
// 使用时指定命名空间
t('home:hero.title')
t('common:header.nav_docs')
```
## 语言切换
用户可以通过以下方式切换语言:
1. **自动检测**:首次访问根据浏览器语言自动选择
2. **手动切换**:点击页面右上角的语言切换按钮
3. **持久化**:语言选择保存在 `localStorage` 中,刷新后保持
## 添加新翻译
1. 在对应的 JSON 文件中添加翻译条目(如 `zh-CN/home.json`
2. 在对应的英文文件中添加翻译(如 `en-US/home.json`
3. 在组件中使用 `t('namespace:key')` 获取翻译
## TypeScript 支持
`types.ts` 文件提供了类型声明,使得翻译 key 具有:
- 自动补全
- 编译期类型检查
- 防止拼写错误
## 待完成
- [ ] 文档页翻译(`docs.json`
- [ ] DocsLayout 导航结构国际化
- [ ] 404 页面国际化

View File

@@ -0,0 +1,45 @@
import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
import commonEn from './locales/en-US/common'
import docsEn from './locales/en-US/docs'
import homeEn from './locales/en-US/home'
import commonZh from './locales/zh-CN/common'
import docsZh from './locales/zh-CN/docs'
import homeZh from './locales/zh-CN/home'
const resources = {
'zh-CN': {
common: commonZh,
home: homeZh,
docs: docsZh,
},
'en-US': {
common: commonEn,
home: homeEn,
docs: docsEn,
},
}
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en-US',
defaultNS: 'common',
// 语言检测配置
detection: {
// localStorage 优先(用户手动选择),其次检测浏览器语言
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
interpolation: {
escapeValue: false, // React 已经做了 XSS 防护
},
})
export default i18n

View File

@@ -0,0 +1,40 @@
export default {
header: {
logo_alt: 'page-agent home',
slogan: 'GUI Agent in your webpage',
nav_docs: 'Docs',
nav_source: 'GitHub',
mobile_menu: 'Open navigation',
},
footer: {
copyright: '© 2025 page-agent. All rights reserved.',
github_label: 'Visit GitHub repository',
},
beta_notice: {
title: 'Beta Stage',
content:
'Current features are incomplete and the API may change at any time. Please do not use in production environments before the official release.',
},
language: {
zh: '中文',
en: 'English',
switch_label: 'Switch language',
},
nav: {
introduction: 'Introduction',
features: 'Features',
integration: 'Integration',
overview: 'Overview',
quick_start: 'Quick Start',
limitations: 'Limitations',
model_integration: 'Model Integration',
custom_tools: 'Custom Tools',
knowledge_injection: 'Knowledge Injection',
security_permissions: 'Security & Permissions',
data_masking: 'Data Masking',
cdn_setup: 'CDN Setup',
configuration: 'Configuration',
best_practices: 'Best Practices',
third_party_agent: 'Third-party Agent',
},
}

View File

@@ -0,0 +1,172 @@
export default {
overview: {
title: 'Overview',
subtitle:
'page-agent is a purely web-based GUI Agent. Gives your website an AI operator in simple steps.',
what_is: 'What is page-agent?',
what_is_desc:
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent targets web developers, not scrapers or agent clients builders. Integrate it into your site to let users interact with pages through natural language.',
features_title: 'Core Features',
feature_dom: {
title: '🧠 Smart DOM Analysis',
desc: 'DOM-based analysis with high-intensity dehydration. No visual recognition needed. Pure text for fast and precise operations.',
},
feature_secure: {
title: '🔒 Secure & Controllable',
desc: 'Supports operation allowlists, data masking protection. Inject custom knowledge to make AI work by your rules.',
},
feature_backend: {
title: '⚡ Zero Backend',
desc: 'CDN or NPM import with custom LLM endpoints.',
},
feature_accessible: {
title: '♿ Accessible Intelligence',
desc: 'Provides natural language interface for complex B2B systems and admin panels. Makes software easy for everyone.',
},
vs_browser_use: 'vs. browser-use',
table_feature: 'Feature',
table_deployment: 'Deployment',
table_deployment_pa: 'Embedded component',
table_deployment_bu: 'External tool',
table_scope: 'Scope',
table_scope_pa: 'Current page',
table_scope_bu: 'Entire browser',
table_user: 'Target Users',
table_user_pa: 'Web developers',
table_user_bu: 'Scraper/Agent developers',
table_scenario: 'Use Case',
table_scenario_pa: 'UX enhancement',
table_scenario_bu: 'Automation tasks',
use_cases_title: 'Use Cases',
use_case1_title: 'Connect Support Bots:',
use_case1_desc:
"Turn your support assistant into a full agent. Customer service bots no longer just say 'Please click the settings button then click...'—they operate for users directly.",
use_case2_title: 'Modernize Legacy Apps:',
use_case2_desc:
'One line of code transforms old apps into agents. Product experts help users navigate complex B2B software. Reduce support costs and improve satisfaction.',
use_case3_title: 'Interactive Training:',
use_case3_desc:
"Demonstrate workflows in real-time. Let AI show the complete process of 'how to submit an expense report.'",
use_case4_title: 'Accessibility:',
use_case4_desc:
'Provide natural language interaction for visually impaired and elderly users. Connect screen readers or voice assistants to make software accessible to everyone.',
get_started_title: '🚀 Get Started',
get_started_desc:
'Ready to add an AI operator to your website? Check our quick start guide for integration in minutes.',
get_started_button: 'Quick Start →',
},
quick_start: {
title: 'Quick Start',
subtitle: 'Integrate page-agent in minutes.',
installation: 'Installation Steps',
step1_title: '1. Import Options',
step1_cdn: 'CDN Import',
step1_npm: 'NPM Install',
step2_title: '2. Initialize Configuration',
step3_title: '3. Start Using',
},
limitations: {
title: 'Limitations',
subtitle: "Understand page-agent's current capabilities and technical constraints",
page_support: 'Page Support Limitations',
spa_limit_title: 'Single Page Application Limits',
spa_limit_1: '• SPA only: Currently operates within a single page',
spa_limit_2: '• Multi-page relay in design: Cannot execute continuous tasks across pages yet',
spa_limit_3: '• Requires integration: Cannot operate on sites without page-agent',
interaction_limits: 'Interaction Limitations',
supported_ops: 'Supported Operations',
op_click: 'Click',
op_input: 'Text input',
op_scroll: 'Scroll',
op_submit: 'Form submit',
op_select: 'Select',
op_focus: 'Focus',
unsupported_ops: 'Unsupported Operations',
op_hover: 'Mouse hover',
op_drag: 'Drag & drop',
op_context: 'Right-click menu',
op_draw: 'Drawing',
op_keyboard: 'Keyboard shortcuts',
op_position: 'Position-based control',
understanding_limits: 'Understanding Limitations',
no_vision_title: 'No Visual Recognition',
no_vision_desc:
'page-agent operates based on DOM structure with no visual recognition. Cannot understand:',
no_vision_1: '• Image content: Cannot recognize text, icons, or visual elements in images',
no_vision_2: '• Canvas: Cannot understand graphics drawn on Canvas',
no_vision_3: '• WebGL 3D: Cannot operate elements in 3D scenes',
no_vision_4: '• SVG graphics: Cannot understand visual content and paths in SVG',
website_requirements: 'Website Requirements',
req_semantic_title: 'Semantic & Usability',
req_semantic_desc:
'All operations rely on semantic tags and attributes. Poor semantic structure or lack of accessibility features may affect AI understanding accuracy.',
req_ux_title: 'UI/UX',
req_ux_desc:
'Counter-intuitive interaction rules, visual-only operation hints, complex mouse interactions, or rapidly appearing/disappearing elements can affect AI understanding and operation.',
req_env_title: 'Environment',
req_env_desc: 'modern browser',
future: 'Future Plans',
future_title: 'Coming Soon',
future_1: '• Multi-page relay capabilities',
future_2: '• Richer mouse interaction support',
future_3: '• Basic visual understanding',
future_4: '• Smarter error recovery',
},
model_integration: {
title: 'Model Integration',
subtitle:
'Supports OpenAI-compatible models with tool call support, including public cloud services and private deployments.',
recommended: 'Recommended Models',
model_gpt4_title: '⚡ gpt-4.1-mini',
model_gpt4_badge: 'Evaluation Baseline ✅',
model_gpt4_1: '• Cost-effective',
model_gpt4_2: '• Fast',
model_gpt4_3: '• High success rate',
model_deepseek_title: '💰 DeepSeek-3.2',
model_deepseek_badge: 'Economical',
model_deepseek_1: '• Much cheaper than similar models',
model_deepseek_2: '• ToolCall may error but usually auto-recovers',
model_deepseek_3: "• This site's free demo uses DeepSeek",
model_qwen_title: '🛡️ qwen3',
model_qwen_badge: 'Secure & Compliant',
model_qwen_1: '• Controllable, decent results, reasonable price',
model_qwen_2: '• ToolCall may error but usually auto-recovers',
model_qwen_3: '• Best for scenarios with detailed steps',
model_gemini_title: '⚡ gemini-2.5-flash',
model_gemini_badge: 'Highly efficient, high success rate, reasonable price',
available: 'Available Models',
available_verified: '✅ Verified Working',
tips: 'Tips',
tip_1: 'Reasoning models (like GPT-5) are slower with no advantage',
tip_2:
"Non-OpenAI models don't guarantee JSON schema compliance—tool call may error but usually recovers. Higher temperature recommended",
tip_3: 'Small/nano models perform poorly',
security: '🔐 Production Authentication',
security_warning: '⚠️ Never commit real LLM API Keys to your frontend codebase',
security_desc:
'In production environments, to hide the real LLM API Keys, we recommend the following architecture:',
security_backend_proxy: 'Backend Proxy Pattern',
security_backend_desc:
'Set up a backend LLM proxy endpoint that uses the same authentication method as other APIs in your website, such as:',
security_method_1: '• Session/Cookie-based authentication',
security_method_2: '• OIDC (OpenID Connect) single sign-on',
security_method_3: '• Temporary Access Key or JWT Token',
configuration: 'Configuration',
},
custom_tools: {
title: 'Custom Tools',
subtitle:
'Extend AI Agent capabilities by registering custom tools. Use Zod to define strict input schemas for safe business logic calls.',
registration: 'Tool Registration',
registration_desc:
'Each custom tool requires four core properties: name, description, input schema, and execute function.',
page_filter: 'Page Filter',
page_filter_desc:
'Control tool visibility on specific pages via the pageFilter property to enhance security and UX.',
best_practices: 'Best Practices',
bp_performance: '⚡ Performance Optimization',
bp_1: '• Use pageFilter to reduce unnecessary tool loading',
bp_2: '• Implement appropriate caching in execute functions',
bp_3: '• Avoid long-running sync operations in tools',
},
}

View File

@@ -0,0 +1,79 @@
export default {
hero: {
badge: 'GUI Agent in your webpage',
title_line1: 'The AI Operator',
title_line2: 'Living in Your Web App',
subtitle_emoji: '🪄 One line of CDN',
subtitle_main: ' adds intelligent GUI Agents to your website.',
subtitle_detail: 'Users give natural language commands, AI handles the rest.',
tab_try: '🚀 Try It Now',
tab_other: '🌐 Try on Other Sites',
input_placeholder: 'Describe what you want AI to do...',
execute_button: 'Run',
default_task:
'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown',
},
try_other: {
step1_title: 'Step 1:',
step1_content: 'Show your bookmarks bar',
step2_title: 'Step 2:',
step2_content: 'Drag this button to your bookmarks',
step3_title: 'Step 3:',
step3_content: 'Click the bookmark on any site to activate',
notice_title: '⚠️ Heads Up',
notice_items: {
item1: 'Demo only—link may expire without notice',
item2: 'This free demo uses DeepSeek API (see their terms and privacy policy)',
item3: 'Some sites block script injection (CSP policies)',
item4: 'Works on single-page apps only—reload required after navigation',
item5: 'Text-only understanding—no image recognition or drag-and-drop',
item6_prefix: 'Full limitations in',
item6_link: 'Docs',
},
},
benefits: {
no_backend: 'Pure Front-end Solution',
private_model: 'Your Own Models',
data_masking: 'Built-in Privacy',
open_source: 'MIT Open Source',
},
features: {
section_title: 'Why PageAgent',
in_page: {
title: 'In-page Solution',
desc: 'Runs entirely within your page. No browser extensions, no headless browsers, and no backend required.',
},
secure_integration: {
title: 'Secure by Design',
desc: 'Control what AI can access with allowlists, data masking, and custom knowledge injection. Your rules, your data.',
},
zero_backend: {
title: 'Zero Backend Setup',
desc: 'Just drop in a script. Works with any LLM provider—OpenAI, Anthropic, or your own models.',
},
accessible: {
title: 'Natural Language UI',
desc: 'Transform complex admin panels into chat interfaces. Make powerful tools accessible to everyone, not just experts.',
},
},
use_cases: {
section_title: 'Where It Shines',
section_subtitle: 'From simple forms to complex workflows, AI understands and executes',
case1: {
title: 'Supercharge Support Bots',
desc: 'Stop telling users where to click—let AI do it for them. Turn your chatbot from a guide into an operator that actually completes tasks.',
},
case2: {
title: 'Modernize Legacy Apps',
desc: 'Add AI superpowers to old software without rebuilding. One script tag transforms complex enterprise tools into chat-driven interfaces.',
},
case3: {
title: 'Interactive Walkthroughs',
desc: "Show, don't tell. Let AI demonstrate workflows in real-time—perfect for onboarding or training new users on complex systems.",
},
case4: {
title: 'Accessibility First',
desc: 'Make web apps accessible through natural language. Perfect for screen readers, voice control, or users who find traditional interfaces challenging.',
},
},
}

View File

@@ -0,0 +1,39 @@
export default {
header: {
logo_alt: 'page-agent 首页',
slogan: 'GUI Agent in your webpage',
nav_docs: '文档',
nav_source: 'GitHub',
mobile_menu: '打开导航栏',
},
footer: {
copyright: '© 2025 page-agent. All rights reserved.',
github_label: '访问 GitHub 仓库',
},
beta_notice: {
title: 'Beta 阶段',
content: '当前功能未完成,接口可能随时变更。正式版本发布前请勿用于生产环境。',
},
language: {
zh: '中文',
en: 'English',
switch_label: '切换语言',
},
nav: {
introduction: '介绍',
features: '功能特性',
integration: '集成指南',
overview: '概览',
quick_start: '快速开始',
limitations: '使用限制',
model_integration: '模型接入',
custom_tools: '自定义工具',
knowledge_injection: '知识库注入',
security_permissions: '安全与权限',
data_masking: '数据脱敏',
cdn_setup: 'CDN 引入',
configuration: '配置选项',
best_practices: '最佳实践',
third_party_agent: '接入第三方 Agent',
},
}

View File

@@ -0,0 +1,168 @@
export default {
overview: {
title: 'Overview',
subtitle:
'page-agent 是一个完全基于 Web 技术的 GUI Agent简单几步让你的网站拥有 AI 操作员。',
what_is: '什么是 page-agent',
what_is_desc:
'page-agent 是一个页面内嵌式 GUI Agent。与传统的浏览器自动化工具不同page-agent 面向网站开发者而非爬虫或Agent客户端开发者将 Agent 集成到你的网站中,让用户可以通过自然语言与页面进行交互。',
features_title: '核心特性',
feature_dom: {
title: '🧠 智能 DOM 理解',
desc: '基于 DOM 分析,高强度脱水。无需视觉识别,纯文本实现精准操作。',
},
feature_secure: {
title: '🔒 安全可控',
desc: '支持操作黑白名单、数据脱敏保护。注入自定义知识库,让 AI 按你的规则工作。',
},
feature_backend: {
title: '⚡ 零后端部署',
desc: 'CDN 或 NPM 引入,自定义 LLM 接入点。',
},
feature_accessible: {
title: '♿ 普惠智能',
desc: '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。',
},
vs_browser_use: '与 browser-use 的区别',
table_feature: '特性',
table_deployment: '部署方式',
table_deployment_pa: '页面内嵌组件',
table_deployment_bu: '外部工具',
table_scope: '操作范围',
table_scope_pa: '当前页面',
table_scope_bu: '整个浏览器',
table_user: '目标用户',
table_user_pa: '网站开发者',
table_user_bu: '爬虫/Agent 开发者',
table_scenario: '使用场景',
table_scenario_pa: '用户体验增强',
table_scenario_bu: '自动化任务',
use_cases_title: '应用场景',
use_case1_title: '对接答疑机器人:',
use_case1_desc:
'把你的答疑助手变成全能Agent。客服机器人不再只说「请先点击设置按钮然后点击...」,而是直接帮用户现场操作。',
use_case2_title: '交互升级/智能化改造:',
use_case2_desc:
'一行代码老应用变身Agent产品专家帮用户操作复杂 B 端软件。降低人工支持成本,提高用户满意度。',
use_case3_title: '产品教学:',
use_case3_desc:
'向用户演示交互过程边做边教。例如让AI演示「如何提交报销申请」的完整操作流程。',
use_case4_title: '无障碍支持:',
use_case4_desc:
'为视障用户、老年用户提供自然语言交互,对接屏幕阅读器或语音助理,让软件人人可用。',
get_started_title: '🚀 开始使用',
get_started_desc:
'准备好为你的网站添加 AI 操作员了吗?查看我们的快速开始指南,几分钟内完成集成。',
get_started_button: '快速开始 →',
},
quick_start: {
title: 'Quick Start',
subtitle: '几分钟内完成 page-agent 的集成。',
installation: '安装步骤',
step1_title: '1. 引入方式',
step1_cdn: 'CDN 引入',
step1_npm: 'NPM 安装',
step2_title: '2. 初始化配置',
step3_title: '3. 开始使用',
},
limitations: {
title: '使用限制',
subtitle: '了解 page-agent 当前的功能边界和技术限制',
page_support: '页面支持限制',
spa_limit_title: '单页应用限制',
spa_limit_1: '• 仅支持单页应用SPA目前只能在单个页面内进行操作',
spa_limit_2: '• 多页接力功能正在设计中:暂时无法跨页面执行连续任务',
spa_limit_3: '• 无法操作未接入该能力的网站:需要目标网站主动集成 page-agent',
interaction_limits: '交互行为限制',
supported_ops: '支持的操作',
op_click: '点击操作',
op_input: '文本输入',
op_scroll: '页面滚动',
op_submit: '表单提交',
op_select: '选择操作',
op_focus: '焦点切换',
unsupported_ops: '不支持的操作',
op_hover: '鼠标悬停hover',
op_drag: '拖拽操作',
op_context: '右键菜单',
op_draw: '图形绘制',
op_keyboard: '键盘快捷键',
op_position: '基于点击区域或鼠标位置的控制',
understanding_limits: '网页理解限制',
no_vision_title: '无视觉能力',
no_vision_desc: 'page-agent 基于 DOM 结构进行理解和操作,没有视觉识别能力,无法理解以下内容:',
no_vision_1: '• 图片内容:无法识别图片中的文字、图标或视觉元素',
no_vision_2: '• Canvas 画布:无法理解 Canvas 中绘制的图形和内容',
no_vision_3: '• WebGL 3D 内容:无法操作 3D 场景中的元素',
no_vision_4: '• SVG 图形:无法理解 SVG 中的视觉内容和路径',
website_requirements: '被操作网站要求',
req_semantic_title: '语义化和易用性',
req_semantic_desc:
'所有操作都基于 DOM 元素的语义化标签和属性。如果页面结构不够语义化,或者没有任何 accessibility 特性,可能影响 AI 的理解准确性。',
req_ux_title: 'UI/UX',
req_ux_desc:
'反常识的交互规则、基于视觉的操作提示、复杂的鼠标交互、快速出现快速消失的元素等,都会影响 AI 的理解和操作。',
req_env_title: '环境要求',
req_env_desc: 'modern browser',
future: '未来规划',
future_title: '即将支持',
future_1: '• 多页面接力操作能力',
future_2: '• 更丰富的鼠标交互支持',
future_3: '• 基础的视觉理解能力',
future_4: '• 更智能的错误恢复机制',
},
model_integration: {
title: '模型接入',
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
recommended: '推荐模型',
model_gpt4_title: '⚡ gpt-4.1-mini',
model_gpt4_badge: '评估基准 ✅',
model_gpt4_1: '• 性价比高',
model_gpt4_2: '• 速度快',
model_gpt4_3: '• 成功率高',
model_deepseek_title: '💰 DeepSeek-3.2',
model_deepseek_badge: '经济实惠',
model_deepseek_1: '• 价格远低于同等级其他模型',
model_deepseek_2: '• ToolCall 有出错率,通常能够自动修复',
model_deepseek_3: '• 本网站提供的免费试用为 DeepSeek',
model_qwen_title: '🛡️ qwen3',
model_qwen_badge: '安全合规',
model_qwen_1: '• 可控、效果尚可,价格合理',
model_qwen_2: '• ToolCall 有出错率,通常能够自动修复',
model_qwen_3: '• 适合能给出详细步骤的场景',
model_gemini_title: '⚡ gemini-2.5-flash',
model_gemini_badge: '极其高效,成功率高,价格合理',
available: '可用模型',
available_verified: '✅ 已验证可用',
tips: '提示',
tip_1: 'reasoning 模型(如 GPT-5),速度偏慢,没有必要',
tip_2:
'不保证 json schema 的模型(openAI 以外的几乎所有模型),tool call 有概率出错,通常能自动修复,建议 temperature 设置高一些',
tip_3: '小模型、nano 模型,效果不佳',
security: '🔐 生产环境鉴权建议',
security_warning: '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码库',
security_desc: '在实际应用中,为了隐藏真实的 LLM API Key建议采用以下架构:',
security_backend_proxy: '后端代理转发',
security_backend_desc:
'在后端搭建一个 LLM 流量转发接口,该接口使用与你网站上其他接口相同的鉴权方式,例如:',
security_method_1: '• Session/Cookie 会话认证',
security_method_2: '• OIDC (OpenID Connect) 单点登录',
security_method_3: '• 临时 Access Key 或 JWT Token',
configuration: '配置方式',
},
custom_tools: {
title: '自定义工具',
subtitle:
'通过注册自定义工具,扩展 AI Agent 的能力边界。使用 Zod 定义严格的输入接口,让 AI 安全调用你的业务逻辑。',
registration: '工具注册',
registration_desc:
'每个自定义工具需要定义四个核心属性name、description、input schema 和 execute 函数。',
page_filter: '页面过滤器',
page_filter_desc: '通过 pageFilter 属性控制工具在哪些页面可见,提升安全性和用户体验。',
best_practices: '最佳实践',
bp_performance: '⚡ 性能优化',
bp_1: '• 使用 pageFilter 减少不必要的工具加载',
bp_2: '• 在 execute 函数中实现适当的缓存机制',
bp_3: '• 避免在工具中执行耗时的同步操作',
},
}

View File

@@ -0,0 +1,78 @@
export default {
hero: {
badge: 'GUI Agent in your webpage',
title_line1: '让你的 Web 应用',
title_line2: '拥有 AI 操作员',
subtitle_emoji: '🪄 一行 CDN 引入',
subtitle_main: ',为你的网站添加 GUI Agent。',
subtitle_detail: '用户/答疑机器人给出文字指示AI 帮你操作页面。',
tab_try: '🚀 立即尝试',
tab_other: '🌐 其他网页尝试',
input_placeholder: '输入您想要 AI 执行的任务...',
execute_button: '执行',
default_task: '从导航栏中进入文档页,打开“快速开始”相关的文档,帮我总结成 markdown',
},
try_other: {
step1_title: '步骤 1:',
step1_content: '显示收藏夹栏',
step2_title: '步骤 2:',
step2_content: '拖拽下面按钮到收藏夹栏',
step3_title: '步骤 3:',
step3_content: '在其他网站点击收藏夹中的按钮即可使用',
notice_title: '⚠️ 注意',
notice_items: {
item1: '仅做技术评估,链接定期失效',
item2: '使用 DeepSeek 模型,参考 DeepSeek 用户协议和隐私政策',
item3: '部分网站屏蔽了链接嵌入,将无反应',
item4: '仅支持单页应用,页面跳转后需要重新注入',
item5: '仅识别文本,不识别图像,不支持拖拽等复杂交互',
item6_prefix: '详细使用限制参照',
item6_link: '《文档》',
},
},
benefits: {
no_backend: '纯前端方案',
private_model: '支持私有模型',
data_masking: '无痛脱敏',
open_source: 'MIT 开源',
},
features: {
section_title: '核心特性',
in_page: {
title: '纯页面内方案',
desc: '完全运行在你的页面内。不需要浏览器插件、不需要无头浏览器,不需要后端。',
},
secure_integration: {
title: '安全可控集成',
desc: '支持操作黑白名单、数据脱敏保护。注入自定义知识库,让 AI 按你的规则工作。',
},
zero_backend: {
title: '零后端部署',
desc: '前端脚本引入,自定义 LLM 接入点。从 OpenAI 到 qwen3完全由你掌控。',
},
accessible: {
title: '普惠智能交互',
desc: '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。',
},
},
use_cases: {
section_title: '应用场景',
section_subtitle: '从简单的表单填写到复杂的业务流程AI 都能理解并执行',
case1: {
title: '对接答疑机器人',
desc: '把你的答疑助手变成全能Agent。客服机器人不再只说「请先点击设置按钮然后点击...」,而是直接帮用户现场操作。',
},
case2: {
title: '交互升级/智能化改造',
desc: '一行代码老应用变身Agent产品专家帮用户操作复杂 B 端软件。降低人工支持成本,提高用户满意度。',
},
case3: {
title: '产品教学',
desc: '向用户演示交互过程边做边教。例如让AI演示「如何提交报销申请」的完整操作流程。',
},
case4: {
title: '无障碍支持',
desc: '为视障用户、老年用户提供自然语言交互,对接屏幕阅读器或语音助理,让软件人人可用。',
},
},
}

View File

@@ -0,0 +1,16 @@
import 'react-i18next'
import type commonZh from './locales/zh-CN/common'
import type docsZh from './locales/zh-CN/docs'
import type homeZh from './locales/zh-CN/home'
declare module 'react-i18next' {
interface CustomTypeOptions {
defaultNS: 'common'
resources: {
common: typeof commonZh
home: typeof homeZh
docs: typeof docsZh
}
}
}

View File

@@ -0,0 +1,204 @@
@import 'tailwindcss';
/* 启用 class-based dark mode for Tailwind v4 */
@variant dark (.dark &);
:root {
--background: #ffffff;
--foreground: #171717;
/* 主题色渐变 */
--theme-color-1: rgb(88, 192, 252);
--theme-color-2: rgb(189, 69, 251);
}
/* class-based dark mode - 应用到 html.dark */
html.dark,
:root.dark {
--background: #0a0a0a;
--foreground: #ededed;
}
/* 同时支持系统偏好 */
/* @media (prefers-color-scheme: dark) {
html:not(.light),
:root:not(.light) {
--background: #0a0a0a;
--foreground: #ededed;
}
} */
/* 添加 Tailwind 自定义颜色 */
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
/* 确保文档页面标题在暗色模式下可见 - 只针对 prose 内的标题 */
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
color: rgba(23, 23, 23, 0.85);
}
.dark .prose h1,
.dark .prose h2,
.dark .prose h3,
.dark .prose h4,
.dark .prose h5,
.dark .prose h6 {
color: rgba(255, 255, 255, 0.9);
}
table,
th,
td {
color: #171717;
}
.dark table,
.dark th,
.dark td {
color: #ededed;
}
/* 文档页深色模式优化 */
.dark .prose {
color: rgba(255, 255, 255, 0.7);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose {
color: rgba(255, 255, 255, 0.7);
}
} */
.dark .dark\:prose-invert {
--tw-prose-body: rgba(255, 255, 255, 0.7);
--tw-prose-headings: rgba(255, 255, 255, 0.95);
--tw-prose-lead: rgba(255, 255, 255, 0.7);
--tw-prose-links: rgba(147, 197, 253, 0.9);
--tw-prose-bold: rgba(255, 255, 255, 0.9);
--tw-prose-counters: rgba(255, 255, 255, 0.6);
--tw-prose-bullets: rgba(255, 255, 255, 0.5);
--tw-prose-hr: rgba(255, 255, 255, 0.2);
--tw-prose-quotes: rgba(255, 255, 255, 0.8);
--tw-prose-quote-borders: rgba(255, 255, 255, 0.3);
--tw-prose-captions: rgba(255, 255, 255, 0.6);
--tw-prose-code: rgba(255, 255, 255, 0.9);
--tw-prose-pre-code: rgba(255, 255, 255, 0.95);
--tw-prose-pre-bg: rgba(0, 0, 0, 0.5);
--tw-prose-th-borders: rgba(255, 255, 255, 0.3);
--tw-prose-td-borders: rgba(255, 255, 255, 0.2);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .dark\:prose-invert {
--tw-prose-body: rgba(255, 255, 255, 0.7);
--tw-prose-headings: rgba(255, 255, 255, 0.95);
--tw-prose-lead: rgba(255, 255, 255, 0.7);
--tw-prose-links: rgba(147, 197, 253, 0.9);
--tw-prose-bold: rgba(255, 255, 255, 0.9);
--tw-prose-counters: rgba(255, 255, 255, 0.6);
--tw-prose-bullets: rgba(255, 255, 255, 0.5);
--tw-prose-hr: rgba(255, 255, 255, 0.2);
--tw-prose-quotes: rgba(255, 255, 255, 0.8);
--tw-prose-quote-borders: rgba(255, 255, 255, 0.3);
--tw-prose-captions: rgba(255, 255, 255, 0.6);
--tw-prose-code: rgba(255, 255, 255, 0.9);
--tw-prose-pre-code: rgba(255, 255, 255, 0.95);
--tw-prose-pre-bg: rgba(0, 0, 0, 0.5);
--tw-prose-th-borders: rgba(255, 255, 255, 0.3);
--tw-prose-td-borders: rgba(255, 255, 255, 0.2);
}
} */
/* 标题更清晰 */
.dark .prose h1,
.dark .prose h2,
.dark .prose h3,
.dark .prose h4 {
color: rgba(255, 255, 255, 0.95);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose h1,
:root:not(.light) .prose h2,
:root:not(.light) .prose h3,
:root:not(.light) .prose h4 {
color: rgba(255, 255, 255, 0.95);
}
} */
/* 链接更清晰 */
.dark .prose a {
color: rgba(147, 197, 253, 0.9);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose a {
color: rgba(147, 197, 253, 0.9);
}
} */
/* 代码块背景更黑 */
.dark .prose pre {
background-color: rgba(0, 0, 0, 0.6);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose pre {
background-color: rgba(0, 0, 0, 0.6);
}
} */
/* 表格样式 */
.dark .prose table {
color: rgba(255, 255, 255, 0.7);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose table {
color: rgba(255, 255, 255, 0.7);
}
} */
.dark .prose thead {
color: rgba(255, 255, 255, 0.9);
border-bottom-color: rgba(255, 255, 255, 0.3);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose thead {
color: rgba(255, 255, 255, 0.9);
border-bottom-color: rgba(255, 255, 255, 0.3);
}
} */
.dark .prose tbody tr {
border-bottom-color: rgba(255, 255, 255, 0.2);
}
/* @media (prefers-color-scheme: dark) {
:root:not(.light) .prose tbody tr {
border-bottom-color: rgba(255, 255, 255, 0.2);
}
} */
/* 隐藏滚动条,但保持滚动功能 */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}

View File

@@ -0,0 +1,19 @@
import { createRoot } from 'react-dom/client'
import { Route, Router, Switch } from 'wouter'
import { useHashLocation } from 'wouter/use-hash-location'
import './i18n/config'
import './i18n/types'
import { default as PagesRouter } from './router.tsx'
import { default as TestPagesRouter } from './test-pages/router.tsx'
import './index.css'
createRoot(document.getElementById('root')!).render(
<Router hook={useHashLocation}>
<Switch>
<Route path="/test-pages" component={TestPagesRouter} nest />
<Route path="/" component={PagesRouter} nest />
</Switch>
</Router>
)

View File

@@ -0,0 +1,474 @@
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
import { PageAgent } from 'page-agent'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, useSearchParams } from 'wouter'
import Footer from './components/Footer'
import Header from './components/Header'
const injection = encodeURI(
"javascript:(function(){var s=document.createElement('script');s.src=`https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js?t=${Math.random()}`;s.setAttribute('crossorigin', true);s.type=`text/javascript`;s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();"
)
const injectionA = `
<a
href=${injection}
class="inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
draggable="true"
onclick="return false;"
title="Drag me to your bookmarks bar!"
>
✨PageAgent
</a>
`
export default function HomePage() {
const { t, i18n } = useTranslation(['home', 'common'])
const [task, setTask] = useState(() => t('home:hero.default_task'))
// Update task when language changes
const defaultTask = t('home:hero.default_task')
useEffect(() => {
setTask(defaultTask)
}, [defaultTask])
const [params, setParams] = useSearchParams()
const isOther = params.has('try_other')
const [activeTab, setActiveTab] = useState<'try' | 'other'>(isOther ? 'other' : 'try')
const handleExecute = async () => {
if (!task.trim()) return
let pageAgent: PageAgent
const win = window as any
if (win.pageAgent && !win.pageAgent.disposed) {
pageAgent = win.pageAgent
} else {
pageAgent = new PageAgent({
// 把 react 根元素排除掉,挂了很多冒泡时间导致假阳
interactiveBlacklist: [document.getElementById('root')!],
language: i18n.language as any,
// experimentalScriptExecutionTool: true,
// testing server
// @note: rate limit. prompt limit.
// model: DEMO_MODEL,
// baseURL: DEMO_BASE_URL,
// apiKey: DEMO_API_KEY,
})
win.pageAgent = pageAgent
}
const result = await pageAgent.execute(task)
console.log(result)
}
return (
<div className="min-h-screen bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800">
<Header />
{/* Hero Section */}
<main id="main-content">
<section className="relative px-6 py-22 lg:py-28" aria-labelledby="hero-heading">
<div className="max-w-7xl mx-auto text-center">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-30" aria-hidden="true">
<div className="absolute inset-0 bg-linear-to-r from-blue-400/20 to-purple-400/20 rounded-3xl transform rotate-1"></div>
<div className="absolute inset-0 bg-linear-to-l from-purple-400/20 to-blue-400/20 rounded-3xl transform -rotate-1"></div>
</div>
<div className="relative z-10">
<div className="inline-flex items-center px-4 py-2 mb-8 text-sm font-medium text-blue-700 bg-blue-100 rounded-full dark:text-blue-300 dark:bg-blue-900/30">
<span
className="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse"
aria-hidden="true"
></span>
{t('home:hero.badge')}
</div>
<h1
id="hero-heading"
className="text-5xl lg:text-7xl font-bold mb-8 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent pb-1"
>
{t('home:hero.title_line1')}
<br />
{t('home:hero.title_line2')}
</h1>
<p className="text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-4xl mx-auto leading-relaxed">
<span className="bg-linear-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold">
{t('home:hero.subtitle_emoji')}
</span>
{t('home:hero.subtitle_main')}
<br />
{t('home:hero.subtitle_detail')}
</p>
{/* Try It Now Section - Tab Card */}
<div className="mt-8 mb-6">
<div className="max-w-3xl mx-auto">
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Tab Headers */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setActiveTab('try')}
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 ${
activeTab === 'try'
? 'bg-linear-to-r from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
{t('home:hero.tab_try')}
</button>
<button
onClick={() => setActiveTab('other')}
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 ${
activeTab === 'other'
? 'bg-linear-to-r from-green-50 to-blue-50 dark:from-green-900/30 dark:to-blue-900/30 text-green-700 dark:text-green-300 border-b-2 border-green-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
{t('home:hero.tab_other')}
</button>
</div>
{/* Tab Content */}
<div className="p-4">
{activeTab === 'try' && (
<div className="space-y-4">
<div className="relative">
<input
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder={t('home:hero.input_placeholder')}
className="w-full px-4 py-3 pr-20 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm mb-0"
data-page-agent-not-interactive
/>
<button
onClick={handleExecute}
// disabled
// disabled={!task.trim()}
className="absolute right-2 top-2 px-5 py-1.5 bg-linear-to-r from-blue-600 to-purple-600 text-white font-medium rounded-md hover:shadow-md transform hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none text-sm"
data-page-agent-not-interactive
>
{t('home:hero.execute_button')}
</button>
</div>
</div>
)}
{activeTab === 'other' && (
<div className="grid md:grid-cols-2 gap-6">
{/* 左侧:操作步骤 */}
<div className="space-y-4">
{/* Keyboard Shortcut Hint */}
<div className="bg-blue-50 dark:bg-gray-700 p-4 rounded-lg">
<p className="text-gray-700 dark:text-gray-300 text-sm mb-3">
<span className="font-semibold">
{t('home:try_other.step1_title')}
</span>{' '}
{t('home:try_other.step1_content')}
</p>
<div className="flex items-center justify-center gap-2">
<kbd className="px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono">
Ctrl + Shift + B
</kbd>
<span className="text-gray-500 dark:text-gray-400"></span>
<kbd className="px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono">
+ Shift + B
</kbd>
</div>
</div>
{/* Draggable Bookmarklet */}
<div className="bg-green-50 dark:bg-gray-700 p-4 rounded-lg">
<p className="text-gray-700 dark:text-gray-300 text-sm mb-3">
<span className="font-semibold">
{t('home:try_other.step2_title')}
</span>{' '}
{t('home:try_other.step2_content')}
</p>
<div
className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400"
dangerouslySetInnerHTML={{ __html: injectionA }}
></div>
</div>
{/* Usage Instructions */}
<div className="bg-purple-50 dark:bg-gray-700 p-4 rounded-lg">
<p className="text-gray-700 dark:text-gray-300 text-sm">
<span className="font-semibold">
{t('home:try_other.step3_title')}
</span>{' '}
{t('home:try_other.step3_content')}
</p>
</div>
</div>
{/* 右侧:注意事项 */}
<div className="bg-yellow-50 dark:bg-gray-700 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 dark:text-white mb-3 text-sm">
{t('home:try_other.notice_title')}
</h4>
<ul className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item1')}
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item2')}
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item3')}
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item4')}
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item5')}
</li>
<li className="flex items-start text-left">
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
{t('home:try_other.notice_items.item6_prefix')}{' '}
<Link
href="/docs/introduction/limitations"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
{t('home:try_other.notice_items.item6_link')}
</Link>
</li>
</ul>
</div>
</div>
)}
</div>
</div>
</div>
</div>
<ul
className="flex flex-wrap justify-center gap-6 text-sm text-gray-500 dark:text-gray-400"
role="list"
>
<li className="flex items-center">
<span
className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true"
></span>
{t('home:benefits.no_backend')}
</li>
<li className="flex items-center">
<span
className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true"
></span>
{t('home:benefits.private_model')}
</li>
<li className="flex items-center">
<span
className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true"
></span>
{t('home:benefits.data_masking')}
</li>
<li className="flex items-center">
<span
className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true"
></span>
{t('home:benefits.open_source')}
</li>
</ul>
</div>
</div>
</section>
{/* Features Section */}
<section
className="px-6 py-20 bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm"
aria-labelledby="features-heading"
>
<div className="max-w-7xl mx-auto">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8" role="list">
{/* Feature 1 */}
<article
className="group p-8 bg-linear-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem"
>
<div
className="w-12 h-12 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
aria-hidden="true"
>
<span className="text-white text-xl">📦</span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.in_page.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.in_page.desc')}
</p>
</article>
{/* Feature 2 */}
<article
className="group p-8 bg-linear-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem"
>
<div
className="w-12 h-12 bg-linear-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
aria-hidden="true"
>
<span className="text-white text-xl"></span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.zero_backend.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.zero_backend.desc')}
</p>
</article>
{/* Feature 3 */}
<article
className="group p-8 bg-linear-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem"
>
<div
className="w-12 h-12 bg-linear-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
aria-hidden="true"
>
<span className="text-white text-xl">🌈</span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.accessible.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.accessible.desc')}
</p>
</article>
{/* Feature 4 */}
<article
className="group p-8 bg-linear-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
role="listitem"
>
<div
className="w-12 h-12 bg-linear-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
aria-hidden="true"
>
<span className="text-white text-xl">🔒</span>
</div>
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
{t('home:features.secure_integration.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.secure_integration.desc')}
</p>
</article>
</div>
</div>
</section>
{/* Use Cases Section */}
<section className="px-6 py-20" aria-labelledby="use-cases-heading">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-16">
<h2
id="use-cases-heading"
className="text-4xl lg:text-5xl font-bold mb-6 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
>
{t('home:use_cases.section_title')}
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
{t('home:use_cases.section_subtitle')}
</p>
</div>
<div className="grid lg:grid-cols-2 gap-12" role="list">
{/* Use Case 1 */}
<div className="bg-linear-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold">1</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case1.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('home:use_cases.case1.desc')}
</p>
</div>
</div>
</div>
{/* Use Case 2 */}
<div className="bg-linear-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold">2</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case2.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('home:use_cases.case2.desc')}
</p>
</div>
</div>
</div>
{/* Use Case 3 */}
<div className="bg-linear-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-purple-500 rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold">3</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case3.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('home:use_cases.case3.desc')}
</p>
</div>
</div>
</div>
{/* Use Case 4 */}
<div className="bg-linear-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold">4</span>
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case4.title')}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('home:use_cases.case4.desc')}
</p>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<Footer />
</div>
)
}

View File

@@ -0,0 +1,148 @@
import { Route, Switch } from 'wouter'
import DocsLayout from './components/DocsLayout'
import Header from './components/Header'
// Features pages
import CustomTools from './docs/features/custom-tools/page'
import DataMasking from './docs/features/data-masking/page'
import KnowledgeInjection from './docs/features/knowledge-injection/page'
import ModelIntegration from './docs/features/model-integration/page'
import SecurityPermissions from './docs/features/security-permissions/page'
import BestPractices from './docs/integration/best-practices/page'
// Integration pages
import CdnSetup from './docs/integration/cdn-setup/page'
import Configuration from './docs/integration/configuration/page'
import ThirdPartyAgent from './docs/integration/third-party-agent/page'
import Limitations from './docs/introduction/limitations/page'
// Introduction pages
import Overview from './docs/introduction/overview/page'
import QuickStart from './docs/introduction/quick-start/page'
import HomePage from './page'
export default function Router() {
return (
<Switch>
{/* Home page */}
<Route path="/" component={HomePage} />
{/* Documentation pages with layout */}
<Route path="/docs/introduction/overview">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<Overview />
</DocsLayout>
</div>
</Route>
<Route path="/docs/introduction/quick-start">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<QuickStart />
</DocsLayout>
</div>
</Route>
<Route path="/docs/introduction/limitations">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<Limitations />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/security-permissions">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<SecurityPermissions />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/custom-tools">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<CustomTools />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/data-masking">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<DataMasking />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/knowledge-injection">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<KnowledgeInjection />
</DocsLayout>
</div>
</Route>
<Route path="/docs/features/model-integration">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<ModelIntegration />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/cdn-setup">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<CdnSetup />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/configuration">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<Configuration />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/best-practices">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<BestPractices />
</DocsLayout>
</div>
</Route>
<Route path="/docs/integration/third-party-agent">
<div className="min-h-screen bg-white dark:bg-gray-900">
<Header />
<DocsLayout>
<ThirdPartyAgent />
</DocsLayout>
</div>
</Route>
{/* 404 page */}
<Route>
<div className="min-h-screen bg-white dark:bg-gray-900 flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">404</h1>
<p className="text-xl text-gray-600 dark:text-gray-300"></p>
</div>
</div>
</Route>
</Switch>
)
}

View File

@@ -0,0 +1,209 @@
# Page Use Agent 测试页面
这个目录包含了一系列专门设计的测试页面,用于验证 Page Use Agent 的各种能力。每个页面都模拟了真实 Web 应用中的常见交互模式和边界情况。
## 测试页面列表
### 1. 表单测试页面 (`/test-pages/form`)
**测试目标:** 表单填写、验证和提交能力
**包含功能:**
- 各种输入类型文本、邮箱、密码、数字、日期、电话、URL
- 下拉选择框和复选框
- 实时表单验证
- 异步提交和错误处理
- 重置表单功能
**测试任务示例:**
- 填写完整的用户注册表单并提交
- 故意输入错误信息触发验证错误
- 测试密码确认功能
- 尝试提交空表单查看错误提示
### 2. 导航测试页面 (`/test-pages/navigation`)
**测试目标:** 复杂导航和交互元素处理
**包含功能:**
- 顶部导航栏和下拉菜单
- 面包屑导航
- 标签页切换
- 模态框弹窗
- 通知系统
- 用户菜单
**测试任务示例:**
- 点击产品下拉菜单选择不同选项
- 切换不同的标签页查看内容
- 打开和关闭模态框
- 点击面包屑导航
- 添加新通知并标记为已读
### 3. 列表测试页面 (`/test-pages/list`)
**测试目标:** 列表操作、搜索、过滤和分页
**包含功能:**
- 产品列表展示(网格和列表视图)
- 搜索功能
- 类别过滤
- 排序功能
- 分页导航
- 加载状态和骨架屏
**测试任务示例:**
- 搜索特定产品名称
- 按价格排序产品列表
- 切换网格和列表视图
- 使用分页浏览不同页面
- 按类别过滤产品
### 4. 复杂交互测试页面 (`/test-pages/complex`)
**测试目标:** 多步骤流程和状态管理
**包含功能:**
- 购物车管理(添加、删除、修改数量)
- 多步骤向导流程
- 步骤验证和导航
- 订单确认流程
- 异步提交处理
**测试任务示例:**
- 完成完整的购买流程
- 在向导中前进和后退
- 修改购物车中的商品数量
- 添加新商品到购物车
- 提交订单并处理可能的错误
### 5. 错误处理测试页面 (`/test-pages/errors`)
**测试目标:** 错误识别和重试机制
**包含功能:**
- 网络连接错误模拟
- 表单验证错误
- 权限不足错误
- 请求超时错误
- 服务器内部错误
- 文件上传错误处理
**测试任务示例:**
- 触发网络错误并重试
- 提交不完整表单查看验证错误
- 测试权限验证(用户名需为"admin"
- 上传超大文件触发错误
- 处理各种错误场景的重试逻辑
### 6. 异步操作测试页面 (`/test-pages/async`)
**测试目标:** 等待和异步操作处理
**包含功能:**
- 文件上传进度条
- 实时数据更新
- 数据加载骨架屏
- 长时间运行任务
- 进度跟踪和日志显示
**测试任务示例:**
- 启动文件上传并等待完成
- 开启实时数据更新功能
- 加载数据并等待所有项目完成
- 执行长时间任务并监控进度
- 处理上传失败的重试
## 测试任务集合
### 基础操作测试
1. **导航测试**
- 前往表单测试页面
- 返回测试页面首页
- 前往导航测试页面
2. **表单填写测试**
- 填写用户注册表单的所有必填字段
- 提交表单并等待结果
- 重置表单并重新填写
3. **搜索和过滤测试**
- 在列表页面搜索"Apple"
- 按价格降序排列产品
- 过滤显示"手机"类别的产品
### 中级交互测试
4. **购物流程测试**
- 前往复杂交互页面
- 添加商品到购物车
- 完成多步骤购买流程
- 填写个人信息、地址和支付信息
- 提交订单
5. **导航和菜单测试**
- 点击产品下拉菜单选择"手机"
- 切换到"订单管理"标签页
- 打开模态框并关闭
- 添加新的面包屑导航
6. **异步操作测试**
- 启动文件上传
- 开启实时数据更新
- 执行长时间任务并等待完成
### 高级错误处理测试
7. **错误恢复测试**
- 触发网络连接错误
- 重试失败的操作
- 处理表单验证错误
- 测试权限验证(用户名输入"admin"
8. **边界情况测试**
- 提交空表单查看错误
- 上传不支持的文件类型
- 在向导中跳过必填步骤
- 处理超时错误
### 综合场景测试
9. **完整用户流程**
- 浏览产品列表
- 搜索并过滤产品
- 添加产品到购物车
- 完成购买流程
- 处理可能出现的错误
10. **压力和边界测试**
- 快速连续点击按钮
- 在加载过程中尝试其他操作
- 测试各种错误恢复场景
- 验证所有异步操作的完成
## 使用说明
### 对于 Agent 开发者
- 每个页面都包含了详细的状态指示器和反馈信息
- 错误信息清晰明确,便于 Agent 理解和处理
- 异步操作都有明确的完成标志
- 所有交互元素都有适当的可访问性标记
### 对于测试人员
- 可以按照测试任务逐一验证 Agent 的能力
- 每个页面都是独立的,可以单独测试
- 包含了各种真实场景的模拟
- 错误场景是随机的,确保测试的真实性
### 技术特性
- 使用 React + TypeScript 构建
- 响应式设计,支持不同屏幕尺寸
- 深色模式支持
- 无需外部依赖,完全自包含
- 模拟真实的网络延迟和错误
## 扩展建议
如需添加新的测试场景,建议考虑以下方面:
- 特定行业的业务流程
- 更复杂的数据可视化交互
- 多媒体内容处理
- 实时协作功能
- 移动端特有的交互模式
每个新页面都应该:
- 有明确的测试目标
- 包含多种难度级别的任务
- 提供清晰的状态反馈
- 模拟真实的用户场景

View File

@@ -0,0 +1,543 @@
import { useEffect, useState } from 'react'
import { Link } from 'wouter'
interface UploadProgress {
id: string
name: string
progress: number
status: 'uploading' | 'completed' | 'error'
speed: string
timeRemaining: string
}
interface DataItem {
id: number
title: string
content: string
timestamp: string
status: 'loading' | 'loaded' | 'error'
}
export default function AsyncTestPage() {
const [uploads, setUploads] = useState<UploadProgress[]>([])
const [dataItems, setDataItems] = useState<DataItem[]>([])
const [isLoadingData, setIsLoadingData] = useState(false)
const [realTimeData, setRealTimeData] = useState<string[]>([])
const [isRealTimeActive, setIsRealTimeActive] = useState(false)
const [longRunningTask, setLongRunningTask] = useState<{
isRunning: boolean
progress: number
currentStep: string
logs: string[]
}>({
isRunning: false,
progress: 0,
currentStep: '',
logs: [],
})
// 模拟实时数据更新
useEffect(() => {
let interval: NodeJS.Timeout
if (isRealTimeActive) {
interval = setInterval(() => {
const newData = `数据更新 ${new Date().toLocaleTimeString()}: ${Math.floor(Math.random() * 1000)}`
setRealTimeData((prev) => [newData, ...prev.slice(0, 9)]) // 保持最新10条
}, 2000)
}
return () => {
if (interval) clearInterval(interval)
}
}, [isRealTimeActive])
// 模拟文件上传
const simulateFileUpload = (fileName: string) => {
const uploadId = Date.now().toString()
const newUpload: UploadProgress = {
id: uploadId,
name: fileName,
progress: 0,
status: 'uploading',
speed: '0 KB/s',
timeRemaining: '计算中...',
}
setUploads((prev) => [...prev, newUpload])
// 模拟上传进度
const interval = setInterval(() => {
setUploads((prev) =>
prev.map((upload) => {
if (upload.id === uploadId) {
const newProgress = Math.min(upload.progress + Math.random() * 15, 100)
const speed = `${(Math.random() * 500 + 100).toFixed(0)} KB/s`
const timeRemaining =
newProgress >= 100 ? '完成' : `${Math.ceil((100 - newProgress) / 10)}`
// 模拟随机失败
if (newProgress > 50 && Math.random() < 0.1) {
clearInterval(interval)
return {
...upload,
status: 'error' as const,
speed: '0 KB/s',
timeRemaining: '失败',
}
}
if (newProgress >= 100) {
clearInterval(interval)
return {
...upload,
progress: 100,
status: 'completed' as const,
speed,
timeRemaining,
}
}
return {
...upload,
progress: newProgress,
speed,
timeRemaining,
}
}
return upload
})
)
}, 500)
}
// 模拟数据加载
const loadData = async () => {
setIsLoadingData(true)
setDataItems([])
// 创建骨架屏数据
const skeletonItems: DataItem[] = Array.from({ length: 6 }, (_, i) => ({
id: i,
title: '',
content: '',
timestamp: '',
status: 'loading',
}))
setDataItems(skeletonItems)
// 逐个加载数据项
for (let i = 0; i < 6; i++) {
await new Promise((resolve) => setTimeout(resolve, 800 + Math.random() * 1000))
setDataItems((prev) =>
prev.map((item) => {
if (item.id === i) {
// 模拟随机加载失败
if (Math.random() < 0.15) {
return {
...item,
status: 'error',
title: '加载失败',
content: '数据加载失败,请重试',
}
}
return {
...item,
status: 'loaded',
title: `数据项 ${i + 1}`,
content: `这是第 ${i + 1} 个数据项的内容,包含了一些示例文本用于展示加载效果。`,
timestamp: new Date().toLocaleString(),
}
}
return item
})
)
}
setIsLoadingData(false)
}
// 模拟长时间运行的任务
const startLongRunningTask = async () => {
setLongRunningTask({
isRunning: true,
progress: 0,
currentStep: '初始化任务...',
logs: ['任务开始'],
})
const steps = [
{ name: '初始化任务...', duration: 2000 },
{ name: '连接服务器...', duration: 1500 },
{ name: '验证权限...', duration: 1000 },
{ name: '下载数据...', duration: 3000 },
{ name: '处理数据...', duration: 2500 },
{ name: '生成报告...', duration: 2000 },
{ name: '保存结果...', duration: 1000 },
{ name: '清理资源...', duration: 500 },
]
for (let i = 0; i < steps.length; i++) {
const step = steps[i]
setLongRunningTask((prev) => ({
...prev,
currentStep: step.name,
logs: [...prev.logs, `开始: ${step.name}`],
}))
// 模拟步骤执行时间
const startTime = Date.now()
while (Date.now() - startTime < step.duration) {
await new Promise((resolve) => setTimeout(resolve, 100))
const elapsed = Date.now() - startTime
const stepProgress = Math.min((elapsed / step.duration) * 100, 100)
const totalProgress = ((i + stepProgress / 100) / steps.length) * 100
setLongRunningTask((prev) => ({
...prev,
progress: totalProgress,
}))
}
setLongRunningTask((prev) => ({
...prev,
logs: [...prev.logs, `完成: ${step.name}`],
}))
// 模拟随机失败
if (i === 3 && Math.random() < 0.2) {
setLongRunningTask((prev) => ({
...prev,
isRunning: false,
currentStep: '任务失败',
logs: [...prev.logs, '错误: 数据下载失败,请重试'],
}))
return
}
}
setLongRunningTask((prev) => ({
...prev,
isRunning: false,
progress: 100,
currentStep: '任务完成',
logs: [...prev.logs, '任务成功完成!'],
}))
}
const clearUploads = () => {
setUploads([])
}
const retryFailedUpload = (uploadId: string) => {
const failedUpload = uploads.find((u) => u.id === uploadId)
if (failedUpload) {
setUploads((prev) => prev.filter((u) => u.id !== uploadId))
simulateFileUpload(failedUpload.name)
}
}
const retryDataLoad = (itemId: number) => {
setDataItems((prev) =>
prev.map((item) => {
if (item.id === itemId) {
return { ...item, status: 'loading', title: '', content: '', timestamp: '' }
}
return item
})
)
setTimeout(() => {
setDataItems((prev) =>
prev.map((item) => {
if (item.id === itemId) {
return {
...item,
status: 'loaded',
title: `数据项 ${itemId + 1}`,
content: `这是重新加载的第 ${itemId + 1} 个数据项的内容。`,
timestamp: new Date().toLocaleString(),
}
}
return item
})
)
}, 1000)
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-6xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"></h1>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 文件上传进度 */}
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
</h2>
<div className="space-x-2">
<button
type="button"
onClick={() => simulateFileUpload(`文件_${Date.now()}.pdf`)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm"
>
</button>
<button
type="button"
onClick={clearUploads}
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors text-sm"
>
</button>
</div>
</div>
<div className="space-y-4">
{uploads.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
"开始上传"
</div>
) : (
uploads.map((upload) => (
<div
key={upload.id}
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4"
>
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-gray-900 dark:text-white">
{upload.name}
</span>
<span
className={`text-sm ${
upload.status === 'completed'
? 'text-green-600 dark:text-green-400'
: upload.status === 'error'
? 'text-red-600 dark:text-red-400'
: 'text-blue-600 dark:text-blue-400'
}`}
>
{upload.status === 'completed'
? '✓ 完成'
: upload.status === 'error'
? '✗ 失败'
: '上传中...'}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
upload.status === 'completed'
? 'bg-green-500'
: upload.status === 'error'
? 'bg-red-500'
: 'bg-blue-500'
}`}
style={{ width: `${upload.progress}%` }}
/>
</div>
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-300">
<span>{upload.progress.toFixed(1)}%</span>
<span>{upload.speed}</span>
<span>{upload.timeRemaining}</span>
</div>
{upload.status === 'error' && (
<button
type="button"
onClick={() => retryFailedUpload(upload.id)}
className="mt-2 px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-sm transition-colors"
>
</button>
)}
</div>
))
)}
</div>
</div>
{/* 实时数据更新 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
</h2>
<button
type="button"
onClick={() => setIsRealTimeActive(!isRealTimeActive)}
className={`px-4 py-2 rounded-md transition-colors text-sm ${
isRealTimeActive
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
>
{isRealTimeActive ? '停止更新' : '开始更新'}
</button>
</div>
<div className="space-y-2 max-h-64 overflow-y-auto">
{realTimeData.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
"开始更新"
</div>
) : (
realTimeData.map((data) => (
<div
key={data}
className={`p-3 rounded-lg border transition-all duration-300 ${
data === realTimeData[0]
? 'bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700'
: 'bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-600'
}`}
>
<span className="text-sm text-gray-900 dark:text-white">{data}</span>
</div>
))
)}
</div>
</div>
</div>
{/* 数据加载和长时间任务 */}
<div className="space-y-6">
{/* 数据加载骨架屏 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
</h2>
<button
type="button"
onClick={loadData}
disabled={isLoadingData}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors text-sm"
>
{isLoadingData ? '加载中...' : '加载数据'}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{dataItems.map((item) => (
<div
key={item.id}
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4"
>
{item.status === 'loading' ? (
<div className="animate-pulse">
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-3/4 mb-2"></div>
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-full mb-1"></div>
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-2/3"></div>
</div>
) : item.status === 'error' ? (
<div>
<h3 className="font-medium text-red-600 dark:text-red-400 mb-2">
{item.title}
</h3>
<p className="text-sm text-red-500 dark:text-red-400 mb-2">
{item.content}
</p>
<button
type="button"
onClick={() => retryDataLoad(item.id)}
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-sm transition-colors"
>
</button>
</div>
) : (
<div>
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
{item.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
{item.content}
</p>
<span className="text-xs text-gray-500 dark:text-gray-400">
{item.timestamp}
</span>
</div>
)}
</div>
))}
</div>
</div>
{/* 长时间运行任务 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white"></h2>
<button
type="button"
onClick={startLongRunningTask}
disabled={longRunningTask.isRunning}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-400 text-white rounded-md transition-colors text-sm"
>
{longRunningTask.isRunning ? '执行中...' : '开始任务'}
</button>
</div>
{longRunningTask.progress > 0 && (
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-900 dark:text-white">
{longRunningTask.currentStep}
</span>
<span className="text-sm text-gray-600 dark:text-gray-300">
{longRunningTask.progress.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-purple-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${longRunningTask.progress}%` }}
/>
</div>
</div>
)}
{longRunningTask.logs.length > 0 && (
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 max-h-48 overflow-y-auto">
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-2">
:
</h4>
<div className="space-y-1">
{longRunningTask.logs.map((log, logIdx) => {
const logKey = `${logIdx + 1}-${log.substring(0, 30)}`
return (
<div
key={logKey}
className="text-sm text-gray-600 dark:text-gray-300 font-mono"
>
{log}
</div>
)
})}
</div>
</div>
)}
</div>
</div>
</div>
{/* 返回链接 */}
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,582 @@
import { useState } from 'react'
import { Link } from 'wouter'
interface CartItem {
id: number
name: string
price: number
quantity: number
image: string
}
interface WizardStep {
id: number
title: string
description: string
completed: boolean
}
export default function ComplexTestPage() {
const [currentStep, setCurrentStep] = useState(1)
const [cartItems, setCartItems] = useState<CartItem[]>([
{
id: 1,
name: 'iPhone 15 Pro',
price: 7999,
quantity: 1,
image: 'https://picsum.photos/100/100?random=1',
},
{
id: 2,
name: 'MacBook Air',
price: 8999,
quantity: 1,
image: 'https://picsum.photos/100/100?random=2',
},
])
const [wizardData, setWizardData] = useState({
personalInfo: { name: '', email: '', phone: '' },
address: { street: '', city: '', zipCode: '' },
payment: { cardNumber: '', expiryDate: '', cvv: '' },
})
const [wizardSteps, setWizardSteps] = useState<WizardStep[]>([
{ id: 1, title: '个人信息', description: '填写基本信息', completed: false },
{ id: 2, title: '收货地址', description: '填写收货地址', completed: false },
{ id: 3, title: '支付方式', description: '选择支付方式', completed: false },
{ id: 4, title: '确认订单', description: '确认订单信息', completed: false },
])
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [orderComplete, setOrderComplete] = useState(false)
// 购物车操作
const updateQuantity = (id: number, newQuantity: number) => {
if (newQuantity <= 0) {
removeItem(id)
return
}
setCartItems((prev) =>
prev.map((item) => (item.id === id ? { ...item, quantity: newQuantity } : item))
)
}
const removeItem = (id: number) => {
setCartItems((prev) => prev.filter((item) => item.id !== id))
}
const addItem = () => {
const newItem: CartItem = {
id: Date.now(),
name: `新产品 ${cartItems.length + 1}`,
price: Math.floor(Math.random() * 5000) + 1000,
quantity: 1,
image: `https://picsum.photos/100/100?random=${Date.now()}`,
}
setCartItems((prev) => [...prev, newItem])
}
const getTotalPrice = () => {
return cartItems.reduce((total, item) => total + item.price * item.quantity, 0)
}
// 向导步骤验证
const validateStep = (step: number): boolean => {
switch (step) {
case 1:
return !!(
wizardData.personalInfo.name &&
wizardData.personalInfo.email &&
wizardData.personalInfo.phone
)
case 2:
return !!(
wizardData.address.street &&
wizardData.address.city &&
wizardData.address.zipCode
)
case 3:
return !!(
wizardData.payment.cardNumber &&
wizardData.payment.expiryDate &&
wizardData.payment.cvv
)
default:
return true
}
}
const goToStep = (step: number) => {
// 验证当前步骤
if (step > currentStep && !validateStep(currentStep)) {
alert('请完成当前步骤的必填信息')
return
}
// 更新步骤完成状态
if (step > currentStep) {
setWizardSteps((prev) =>
prev.map((s) => (s.id === currentStep ? { ...s, completed: true } : s))
)
}
setCurrentStep(step)
}
const handleInputChange = (section: string, field: string, value: string) => {
setWizardData((prev) => ({
...prev,
[section]: {
...prev[section as keyof typeof prev],
[field]: value,
},
}))
}
const handleSubmitOrder = async () => {
setIsProcessing(true)
// 模拟处理时间
await new Promise((resolve) => setTimeout(resolve, 3000))
// 模拟随机失败
if (Math.random() < 0.2) {
setIsProcessing(false)
alert('订单提交失败,请重试')
return
}
setIsProcessing(false)
setOrderComplete(true)
setShowConfirmDialog(false)
}
const resetWizard = () => {
setCurrentStep(1)
setWizardData({
personalInfo: { name: '', email: '', phone: '' },
address: { street: '', city: '', zipCode: '' },
payment: { cardNumber: '', expiryDate: '', cvv: '' },
})
setWizardSteps((prev) => prev.map((s) => ({ ...s, completed: false })))
setOrderComplete(false)
setShowConfirmDialog(false)
}
if (orderComplete) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
<div className="max-w-md mx-auto text-center">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
<div className="text-6xl mb-4">🎉</div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
</p>
<div className="space-y-3">
<button
type="button"
onClick={resetWizard}
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors"
>
</button>
<Link
href="/test-pages"
className="block w-full bg-gray-600 hover:bg-gray-700 text-white py-2 px-4 rounded-md transition-colors text-center"
>
</Link>
</div>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-6xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"></h1>
<p className="text-gray-600 dark:text-gray-300"></p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* 购物车区域 */}
<div className="lg:col-span-1">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 sticky top-8">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
({cartItems.length})
</h3>
<div className="space-y-4 mb-6">
{cartItems.map((item) => (
<div
key={item.id}
className="flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-600 rounded-lg"
>
<img
src={item.image}
alt={item.name}
className="w-12 h-12 object-cover rounded"
/>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium text-gray-900 dark:text-white truncate">
{item.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
¥{item.price.toLocaleString()}
</p>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="w-6 h-6 flex items-center justify-center bg-gray-200 dark:bg-gray-600 rounded text-sm hover:bg-gray-300 dark:hover:bg-gray-500"
>
-
</button>
<span className="text-sm font-medium w-8 text-center">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="w-6 h-6 flex items-center justify-center bg-gray-200 dark:bg-gray-600 rounded text-sm hover:bg-gray-300 dark:hover:bg-gray-500"
>
+
</button>
<button
onClick={() => removeItem(item.id)}
className="w-6 h-6 flex items-center justify-center bg-red-500 text-white rounded text-sm hover:bg-red-600"
>
×
</button>
</div>
</div>
))}
</div>
<button
onClick={addItem}
className="w-full mb-4 py-2 px-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg text-gray-500 dark:text-gray-400 hover:border-blue-500 hover:text-blue-500 transition-colors"
>
+
</button>
<div className="border-t border-gray-200 dark:border-gray-600 pt-4">
<div className="flex justify-between items-center text-lg font-semibold text-gray-900 dark:text-white">
<span>:</span>
<span>¥{getTotalPrice().toLocaleString()}</span>
</div>
</div>
</div>
</div>
{/* 向导区域 */}
<div className="lg:col-span-2">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
{/* 步骤指示器 */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-600">
<div className="flex items-center justify-between">
{wizardSteps.map((step, index) => (
<div key={step.id} className="flex items-center">
<button
onClick={() => goToStep(step.id)}
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
step.completed
? 'bg-green-500 text-white'
: step.id === currentStep
? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400'
}`}
>
{step.completed ? '✓' : step.id}
</button>
{index < wizardSteps.length - 1 && (
<div
className={`w-16 h-1 mx-2 ${
step.completed ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-600'
}`}
/>
)}
</div>
))}
</div>
<div className="mt-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{wizardSteps[currentStep - 1].title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{wizardSteps[currentStep - 1].description}
</p>
</div>
</div>
{/* 步骤内容 */}
<div className="p-6">
{currentStep === 1 && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.personalInfo.name}
onChange={(e) => handleInputChange('personalInfo', 'name', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入您的姓名"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="email"
value={wizardData.personalInfo.email}
onChange={(e) => handleInputChange('personalInfo', 'email', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入您的邮箱"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="tel"
value={wizardData.personalInfo.phone}
onChange={(e) => handleInputChange('personalInfo', 'phone', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入您的手机号"
/>
</div>
</div>
)}
{currentStep === 2 && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.address.street}
onChange={(e) => handleInputChange('address', 'street', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入详细地址"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.address.city}
onChange={(e) => handleInputChange('address', 'city', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入城市"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.address.zipCode}
onChange={(e) => handleInputChange('address', 'zipCode', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入邮政编码"
/>
</div>
</div>
)}
{currentStep === 3 && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.payment.cardNumber}
onChange={(e) => handleInputChange('payment', 'cardNumber', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入银行卡号"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={wizardData.payment.expiryDate}
onChange={(e) =>
handleInputChange('payment', 'expiryDate', e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="MM/YY"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
CVV *
</label>
<input
type="text"
value={wizardData.payment.cvv}
onChange={(e) => handleInputChange('payment', 'cvv', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="CVV"
/>
</div>
</div>
</div>
)}
{currentStep === 4 && (
<div className="space-y-6">
<div>
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h4>
<div className="space-y-4">
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 dark:text-white mb-2">
</h5>
<p className="text-sm text-gray-600 dark:text-gray-300">
{wizardData.personalInfo.name} | {wizardData.personalInfo.email} |{' '}
{wizardData.personalInfo.phone}
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 dark:text-white mb-2">
</h5>
<p className="text-sm text-gray-600 dark:text-gray-300">
{wizardData.address.street}, {wizardData.address.city}{' '}
{wizardData.address.zipCode}
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 dark:text-white mb-2">
</h5>
<p className="text-sm text-gray-600 dark:text-gray-300">
**** **** **** {wizardData.payment.cardNumber.slice(-4)}
</p>
</div>
</div>
</div>
</div>
)}
</div>
{/* 导航按钮 */}
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-600 flex justify-between">
<button
onClick={() => goToStep(currentStep - 1)}
disabled={currentStep === 1}
className="px-4 py-2 text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
</button>
{currentStep < 4 ? (
<button
onClick={() => goToStep(currentStep + 1)}
disabled={!validateStep(currentStep)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors"
>
</button>
) : (
<button
onClick={() => setShowConfirmDialog(true)}
disabled={cartItems.length === 0}
className="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white rounded-md transition-colors"
>
</button>
)}
</div>
</div>
</div>
</div>
{/* 确认对话框 */}
{showConfirmDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-6">
¥{getTotalPrice().toLocaleString()}
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowConfirmDialog(false)}
disabled={isProcessing}
className="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 transition-colors"
>
</button>
<button
onClick={handleSubmitOrder}
disabled={isProcessing}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors flex items-center"
>
{isProcessing ? (
<>
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
...
</>
) : (
'确认提交'
)}
</button>
</div>
</div>
</div>
)}
{/* 返回链接 */}
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,464 @@
import { useState } from 'react'
import { Link } from 'wouter'
interface ErrorScenario {
id: string
title: string
description: string
type: 'network' | 'validation' | 'permission' | 'timeout' | 'server'
}
export default function ErrorTestPage() {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [retryCount, setRetryCount] = useState(0)
const [formData, setFormData] = useState({
username: '',
password: '',
email: '',
file: null as File | null,
})
const errorScenarios: ErrorScenario[] = [
{
id: 'network-error',
title: '网络连接错误',
description: '模拟网络连接失败,测试重试机制',
type: 'network',
},
{
id: 'validation-error',
title: '表单验证错误',
description: '模拟表单验证失败,测试错误提示',
type: 'validation',
},
{
id: 'permission-error',
title: '权限不足错误',
description: '模拟权限验证失败,测试权限处理',
type: 'permission',
},
{
id: 'timeout-error',
title: '请求超时错误',
description: '模拟请求超时,测试超时处理',
type: 'timeout',
},
{
id: 'server-error',
title: '服务器内部错误',
description: '模拟服务器500错误测试错误恢复',
type: 'server',
},
]
const simulateError = async (scenario: ErrorScenario): Promise<void> => {
setIsLoading(true)
setError(null)
setSuccess(null)
// 模拟网络延迟
await new Promise((resolve) => setTimeout(resolve, 1000 + Math.random() * 2000))
switch (scenario.type) {
case 'network':
// 70% 概率失败
if (Math.random() < 0.7) {
throw new Error('网络连接失败:无法连接到服务器,请检查您的网络连接')
}
break
case 'validation':
// 检查表单数据
if (!formData.username || formData.username.length < 3) {
throw new Error('用户名验证失败用户名至少需要3个字符')
}
if (!formData.password || formData.password.length < 6) {
throw new Error('密码验证失败密码至少需要6个字符')
}
if (!formData.email?.includes('@')) {
throw new Error('邮箱验证失败:请输入有效的邮箱地址')
}
break
case 'permission':
// 模拟权限检查
if (formData.username !== 'admin') {
throw new Error('权限不足:您没有执行此操作的权限,请联系管理员')
}
break
case 'timeout':
// 模拟超时
await new Promise((resolve) => setTimeout(resolve, 8000))
throw new Error('请求超时:服务器响应时间过长,请稍后重试')
case 'server':
// 50% 概率服务器错误
if (Math.random() < 0.5) {
throw new Error('服务器内部错误:服务器遇到了一个错误,请稍后重试')
}
break
default:
throw new Error('未知错误:发生了未预期的错误')
}
// 成功情况
return Promise.resolve()
}
const handleScenarioTest = async (scenario: ErrorScenario) => {
try {
await simulateError(scenario)
setSuccess(`${scenario.title} 测试成功完成!`)
setRetryCount(0)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '未知错误'
setError(errorMessage)
setRetryCount((prev) => prev + 1)
} finally {
setIsLoading(false)
}
}
const handleRetry = async (scenario: ErrorScenario) => {
if (retryCount >= 3) {
setError('重试次数已达上限,请稍后再试或联系技术支持')
return
}
await handleScenarioTest(scenario)
}
const handleFileUpload = async () => {
if (!formData.file) {
setError('请选择要上传的文件')
return
}
setIsLoading(true)
setError(null)
setSuccess(null)
try {
// 模拟文件大小检查
if (formData.file.size > 5 * 1024 * 1024) {
throw new Error('文件上传失败文件大小不能超过5MB')
}
// 模拟文件类型检查
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
if (!allowedTypes.includes(formData.file.type)) {
throw new Error('文件上传失败不支持的文件类型请上传图片或PDF文件')
}
// 模拟上传过程
await new Promise((resolve) => setTimeout(resolve, 2000))
// 模拟随机失败
if (Math.random() < 0.3) {
throw new Error('文件上传失败:上传过程中发生错误,请重试')
}
setSuccess('文件上传成功!')
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '文件上传失败'
setError(errorMessage)
} finally {
setIsLoading(false)
}
}
const clearMessages = () => {
setError(null)
setSuccess(null)
setRetryCount(0)
}
const getErrorIcon = (type: string) => {
switch (type) {
case 'network':
return '🌐'
case 'validation':
return '⚠️'
case 'permission':
return '🔒'
case 'timeout':
return '⏰'
case 'server':
return '🔧'
default:
return '❌'
}
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-4xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"></h1>
<p className="text-gray-600 dark:text-gray-300">
Agent
</p>
</div>
{/* 全局消息显示 */}
{(error || success) && (
<div className="mb-8">
{error && (
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg p-4 mb-4">
<div className="flex items-start">
<div className="shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200"></h3>
<p className="mt-1 text-sm text-red-700 dark:text-red-300">{error}</p>
{retryCount > 0 && (
<p className="mt-2 text-xs text-red-600 dark:text-red-400">
{retryCount} {retryCount >= 3 && '(已达最大重试次数)'}
</p>
)}
</div>
<button
onClick={clearMessages}
className="ml-3 text-red-400 hover:text-red-600 dark:hover:text-red-300"
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
)}
{success && (
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-4 mb-4">
<div className="flex items-start">
<div className="shrink-0">
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-green-800 dark:text-green-200">
</h3>
<p className="mt-1 text-sm text-green-700 dark:text-green-300">{success}</p>
</div>
<button
onClick={clearMessages}
className="ml-3 text-green-400 hover:text-green-600 dark:hover:text-green-300"
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
)}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 错误场景测试 */}
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white"></h2>
{errorScenarios.map((scenario) => (
<div key={scenario.id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-start space-x-4">
<div className="text-3xl">{getErrorIcon(scenario.type)}</div>
<div className="flex-1">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
{scenario.title}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-4">
{scenario.description}
</p>
<div className="flex space-x-3">
<button
onClick={() => handleScenarioTest(scenario)}
disabled={isLoading}
className="px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-red-400 text-white rounded-md transition-colors text-sm"
>
{isLoading ? '测试中...' : '触发错误'}
</button>
{error && retryCount > 0 && retryCount < 3 && (
<button
onClick={() => handleRetry(scenario)}
disabled={isLoading}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors text-sm"
>
({retryCount}/3)
</button>
)}
</div>
</div>
</div>
</div>
))}
</div>
{/* 表单验证测试 */}
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white"></h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
(3)
</label>
<input
type="text"
value={formData.username}
onChange={(e) => setFormData((prev) => ({ ...prev, username: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入用户名"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
(6)
</label>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData((prev) => ({ ...prev, password: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入密码"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData((prev) => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入邮箱地址"
/>
</div>
<button
onClick={() =>
handleScenarioTest(errorScenarios.find((s) => s.type === 'validation')!)
}
disabled={isLoading}
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md transition-colors"
>
{isLoading ? '验证中...' : '提交表单'}
</button>
</div>
</div>
{/* 文件上传测试 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
(5MBPDF)
</label>
<input
type="file"
onChange={(e) =>
setFormData((prev) => ({ ...prev, file: e.target.files?.[0] || null }))
}
accept="image/*,.pdf"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
{formData.file && (
<div className="text-sm text-gray-600 dark:text-gray-300">
: {formData.file.name} ({(formData.file.size / 1024 / 1024).toFixed(2)}{' '}
MB)
</div>
)}
<button
onClick={handleFileUpload}
disabled={isLoading || !formData.file}
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white rounded-md transition-colors"
>
{isLoading ? '上传中...' : '上传文件'}
</button>
</div>
</div>
{/* 权限测试说明 */}
<div className="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-lg p-4">
<h4 className="text-sm font-medium text-yellow-800 dark:text-yellow-200 mb-2">
💡
</h4>
<p className="text-sm text-yellow-700 dark:text-yellow-300">
"admin""触发错误"
</p>
</div>
</div>
</div>
{/* 加载状态指示器 */}
{isLoading && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 flex items-center space-x-4">
<svg
className="animate-spin h-8 w-8 text-blue-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span className="text-gray-900 dark:text-white">...</span>
</div>
</div>
)}
{/* 返回链接 */}
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,488 @@
import { useState } from 'react'
import { Link } from 'wouter'
interface FormData {
username: string
email: string
password: string
confirmPassword: string
age: string
birthDate: string
phone: string
website: string
bio: string
country: string
newsletter: boolean
terms: boolean
}
type FormErrors = Record<string, string>
export default function FormTestPage() {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
password: '',
confirmPassword: '',
age: '',
birthDate: '',
phone: '',
website: '',
bio: '',
country: '',
newsletter: false,
terms: false,
})
const [errors, setErrors] = useState<FormErrors>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitResult, setSubmitResult] = useState<'success' | 'error' | null>(null)
const [submitMessage, setSubmitMessage] = useState('')
const validateField = (name: string, value: string | boolean): string => {
switch (name) {
case 'username':
if (!value) return '用户名不能为空'
if (typeof value === 'string' && value.length < 3) return '用户名至少需要3个字符'
if (typeof value === 'string' && !/^[a-zA-Z0-9_]+$/.test(value))
return '用户名只能包含字母、数字和下划线'
return ''
case 'email':
if (!value) return '邮箱不能为空'
if (typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
return '请输入有效的邮箱地址'
return ''
case 'password':
if (!value) return '密码不能为空'
if (typeof value === 'string' && value.length < 6) return '密码至少需要6个字符'
if (typeof value === 'string' && !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value))
return '密码必须包含大小写字母和数字'
return ''
case 'confirmPassword':
if (!value) return '请确认密码'
if (value !== formData.password) return '两次输入的密码不一致'
return ''
case 'age': {
if (!value) return '年龄不能为空'
const age = parseInt(value as string)
if (isNaN(age) || age < 18 || age > 120) return '年龄必须在18-120之间'
return ''
}
case 'phone':
if (!value) return '手机号不能为空'
if (typeof value === 'string' && !/^1[3-9]\d{9}$/.test(value)) return '请输入有效的手机号'
return ''
case 'terms':
if (!value) return '请同意服务条款'
return ''
default:
return ''
}
}
const handleInputChange = (name: string, value: string | boolean) => {
console.log(`Input changed: ${name} = ${value}`)
setFormData((prev) => ({ ...prev, [name]: value }))
// 实时验证
const error = validateField(name, value)
setErrors((prev) => ({ ...prev, [name]: error }))
}
const validateForm = (): boolean => {
const newErrors: FormErrors = {}
let isValid = true
Object.keys(formData).forEach((key) => {
const error = validateField(key, formData[key as keyof FormData])
if (error) {
newErrors[key] = error
isValid = false
}
})
setErrors(newErrors)
return isValid
}
const simulateSubmit = async (): Promise<{ success: boolean; message: string }> => {
// 模拟网络延迟
await new Promise((resolve) => setTimeout(resolve, 2000 + Math.random() * 2000))
// 模拟随机失败
if (Math.random() < 0.3) {
throw new Error('网络错误:服务器暂时不可用,请稍后重试')
}
// 模拟服务器验证错误
if (formData.username.toLowerCase() === 'admin') {
throw new Error('用户名 "admin" 已被占用,请选择其他用户名')
}
return {
success: true,
message: '注册成功!欢迎加入我们的平台。',
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validateForm()) {
setSubmitResult('error')
setSubmitMessage('请修正表单中的错误')
return
}
setIsSubmitting(true)
setSubmitResult(null)
setSubmitMessage('')
try {
const result = await simulateSubmit()
setSubmitResult('success')
setSubmitMessage(result.message)
} catch (error) {
setSubmitResult('error')
setSubmitMessage(error instanceof Error ? error.message : '提交失败,请重试')
} finally {
setIsSubmitting(false)
}
}
const resetForm = () => {
setFormData({
username: '',
email: '',
password: '',
confirmPassword: '',
age: '',
birthDate: '',
phone: '',
website: '',
bio: '',
country: '',
newsletter: false,
terms: false,
})
setErrors({})
setSubmitResult(null)
setSubmitMessage('')
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-2xl mx-auto px-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"></h1>
<p className="text-gray-600 dark:text-gray-300"></p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* 用户名 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.username ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入用户名"
/>
{errors.username && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.username}</p>
)}
</div>
{/* 邮箱 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.email ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入邮箱地址"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.email}</p>
)}
</div>
{/* 密码 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="password"
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.password ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入密码"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.password}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="password"
value={formData.confirmPassword}
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.confirmPassword
? 'border-red-500'
: 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请再次输入密码"
/>
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
{errors.confirmPassword}
</p>
)}
</div>
</div>
{/* 年龄和生日 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="number"
value={formData.age}
onChange={(e) => handleInputChange('age', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.age ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入年龄"
min="18"
max="120"
/>
{errors.age && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.age}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="date"
value={formData.birthDate}
onChange={(e) => handleInputChange('birthDate', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
{/* 手机和网站 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white ${
errors.phone ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="请输入手机号"
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.phone}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="url"
value={formData.website}
onChange={(e) => handleInputChange('website', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="https://example.com"
/>
</div>
</div>
{/* 国家选择 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
/
</label>
<select
value={formData.country}
onChange={(e) => handleInputChange('country', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="">/</option>
<option value="CN"></option>
<option value="US"></option>
<option value="JP"></option>
<option value="KR"></option>
<option value="GB"></option>
<option value="DE"></option>
<option value="FR"></option>
<option value="CA"></option>
<option value="AU"></option>
</select>
</div>
{/* 个人简介 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<textarea
value={formData.bio}
onChange={(e) => handleInputChange('bio', e.target.value)}
rows={4}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="请简单介绍一下自己..."
/>
</div>
{/* 复选框 */}
<div className="space-y-3">
<div className="flex items-center">
<input
type="checkbox"
id="newsletter"
checked={formData.newsletter}
onChange={(e) => handleInputChange('newsletter', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label
htmlFor="newsletter"
className="ml-2 block text-sm text-gray-700 dark:text-gray-300"
>
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="terms"
checked={formData.terms}
onChange={(e) => handleInputChange('terms', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label
htmlFor="terms"
className="ml-2 block text-sm text-gray-700 dark:text-gray-300"
>
{' '}
<a href="#" className="text-blue-600 hover:text-blue-500">
</a>{' '}
{' '}
<a href="#" className="text-blue-600 hover:text-blue-500">
</a>{' '}
*
</label>
</div>
{errors.terms && (
<p className="text-sm text-red-600 dark:text-red-400">{errors.terms}</p>
)}
</div>
{/* 提交结果 */}
{submitResult && (
<div
className={`p-4 rounded-md ${
submitResult === 'success'
? 'bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700'
: 'bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700'
}`}
>
<p
className={`text-sm ${
submitResult === 'success'
? 'text-green-800 dark:text-green-200'
: 'text-red-800 dark:text-red-200'
}`}
>
{submitMessage}
</p>
</div>
)}
{/* 按钮组 */}
<div className="flex flex-col sm:flex-row gap-4">
<button
type="submit"
disabled={isSubmitting}
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
{isSubmitting ? (
<span className="flex items-center justify-center">
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
...
</span>
) : (
'注册账户'
)}
</button>
<button
type="button"
onClick={resetForm}
className="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
>
</button>
</div>
</form>
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,106 @@
import { Link } from 'wouter'
export default function IndexPage() {
return (
<div className="min-h-screen bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 p-8">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
Page Use Agent
</h1>
<p className="text-lg text-gray-600 dark:text-gray-300">
AI Agent
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<TestPageCard
title="表单测试"
description="测试输入、验证、提交等表单操作"
path="/form"
icon="📝"
difficulty="简单"
/>
<TestPageCard
title="导航测试"
description="测试菜单、下拉框、弹窗等交互"
path="/navigation"
icon="🧭"
difficulty="中等"
/>
<TestPageCard
title="列表测试"
description="测试滚动、分页、搜索、排序"
path="/list"
icon="📋"
difficulty="中等"
/>
<TestPageCard
title="复杂交互"
description="测试多步骤操作和状态管理"
path="/complex"
icon="⚙️"
difficulty="困难"
/>
<TestPageCard
title="错误处理"
description="测试错误识别和重试机制"
path="/errors"
icon="⚠️"
difficulty="困难"
/>
<TestPageCard
title="异步操作"
description="测试等待、加载状态识别"
path="/async"
icon="⏳"
difficulty="中等"
/>
</div>
<div className="text-center">
<Link
href="/"
className="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
>
Page Use
</Link>
</div>
</div>
</div>
)
}
interface TestPageCardProps {
title: string
description: string
path: string
icon: string
difficulty: string
}
function TestPageCard({ title, description, path, icon, difficulty }: TestPageCardProps) {
const difficultyColors = {
: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
}
return (
<Link href={path}>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 cursor-pointer border border-gray-200 dark:border-gray-700">
<div className="text-4xl mb-4">{icon}</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{title}</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4 text-sm">{description}</p>
<div className="flex justify-between items-center">
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${difficultyColors[difficulty as keyof typeof difficultyColors]}`}
>
{difficulty}
</span>
<span className="text-blue-600 dark:text-blue-400 text-sm font-medium"> </span>
</div>
</div>
</Link>
)
}

View File

@@ -0,0 +1,481 @@
import { useEffect, useMemo, useState } from 'react'
import { Link } from 'wouter'
interface Product {
id: number
name: string
category: string
price: number
stock: number
rating: number
image: string
description: string
tags: string[]
}
const generateProducts = (count: number): Product[] => {
const categories = ['手机', '电脑', '平板', '耳机', '手表', '相机']
const brands = ['Apple', 'Samsung', 'Huawei', 'Xiaomi', 'Sony', 'Dell']
const adjectives = ['Pro', 'Max', 'Ultra', 'Plus', 'Air', 'Mini']
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `${brands[i % brands.length]} ${categories[i % categories.length]} ${adjectives[i % adjectives.length]}`,
category: categories[i % categories.length],
price: Math.floor(Math.random() * 10000) + 500,
stock: Math.floor(Math.random() * 100),
rating: Math.round((Math.random() * 2 + 3) * 10) / 10,
image: `https://picsum.photos/200/200?random=${i}`,
description: `这是一款优秀的${categories[i % categories.length]}产品,具有出色的性能和设计。`,
tags: ['热销', '新品', '推荐'].slice(0, Math.floor(Math.random() * 3) + 1),
}))
}
// Loading skeleton component
const LoadingSkeleton = () => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: 12 }, (_, i) => `skeleton-item-${i}`).map((id) => (
<div key={id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 animate-pulse">
<div className="bg-gray-300 dark:bg-gray-600 h-48 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-3/4"></div>
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/2"></div>
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/4"></div>
</div>
</div>
))}
</div>
)
// Product card component
const ProductCard = ({ product }: { product: Product }) => (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-4">
<div className="relative mb-4">
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover rounded-lg"
loading="lazy"
/>
<div className="absolute top-2 right-2 flex flex-wrap gap-1">
{product.tags.map((tag) => (
<span key={tag} className="bg-red-500 text-white text-xs px-2 py-1 rounded-full">
{tag}
</span>
))}
</div>
</div>
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 line-clamp-2">
{product.name}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-3 line-clamp-2">
{product.description}
</p>
<div className="flex items-center justify-between mb-3">
<span className="text-2xl font-bold text-blue-600 dark:text-blue-400">
¥{product.price.toLocaleString()}
</span>
<div className="flex items-center">
<span className="text-yellow-400"></span>
<span className="text-sm text-gray-600 dark:text-gray-300 ml-1">{product.rating}</span>
</div>
</div>
<div className="flex items-center justify-between mb-4">
<span className="text-sm text-gray-500 dark:text-gray-400">: {product.stock}</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{product.category}</span>
</div>
<button className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
</button>
</div>
)
// Product list item component
const ProductListItem = ({ product }: { product: Product }) => (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex items-center space-x-4">
<img
src={product.image}
alt={product.name}
className="w-20 h-20 object-cover rounded-lg shrink-0"
loading="lazy"
/>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">{product.name}</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-2 line-clamp-1">
{product.description}
</p>
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
<span>{product.category}</span>
<span>: {product.stock}</span>
<div className="flex items-center">
<span className="text-yellow-400"></span>
<span className="ml-1">{product.rating}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<span className="text-xl font-bold text-blue-600 dark:text-blue-400">
¥{product.price.toLocaleString()}
</span>
<button className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
</button>
</div>
</div>
)
// Pagination component
interface PaginationProps {
currentPage: number
totalPages: number
startIndex: number
endIndex: number
totalItems: number
onPageChange: (page: number) => void
}
const Pagination = ({
currentPage,
totalPages,
startIndex,
endIndex,
totalItems,
onPageChange,
}: PaginationProps) => {
const getPageNumbers = () => {
const pages = []
const maxVisible = 5
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2))
const end = Math.min(totalPages, start + maxVisible - 1)
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1)
}
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
}
return (
<div className="flex items-center justify-between mt-8">
<div className="text-sm text-gray-700 dark:text-gray-300">
{startIndex + 1}-{Math.min(endIndex, totalItems)} {totalItems}
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>
</button>
{getPageNumbers().map((page) => (
<button
key={page}
onClick={() => onPageChange(page)}
className={`px-3 py-2 text-sm font-medium rounded-md ${
page === currentPage
? 'bg-blue-600 text-white'
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
}`}
>
{page}
</button>
))}
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>
</button>
</div>
</div>
)
}
export default function ListTestPage() {
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
const [selectedCategory, setSelectedCategory] = useState('全部')
const [sortBy, setSortBy] = useState('name')
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
const [currentPage, setCurrentPage] = useState(1)
const [itemsPerPage, setItemsPerPage] = useState(12)
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const categories = ['全部', '手机', '电脑', '平板', '耳机', '手表', '相机']
// Helper to set filters and reset page
const handleSearchChange = (term: string) => {
setSearchTerm(term)
setCurrentPage(1)
}
const handleCategoryChange = (category: string) => {
setSelectedCategory(category)
setCurrentPage(1)
}
const handleSortChange = (sort: string) => {
setSortBy(sort)
setCurrentPage(1)
}
const handleSortOrderChange = (order: 'asc' | 'desc') => {
setSortOrder(order)
setCurrentPage(1)
}
// 模拟数据加载
useEffect(() => {
const loadData = async () => {
setLoading(true)
// 模拟网络延迟
await new Promise((resolve) => setTimeout(resolve, 1500))
const data = generateProducts(150)
setProducts(data)
setLoading(false)
}
loadData()
}, [])
// 搜索和过滤 - Use useMemo to compute filtered products
const filteredProducts = useMemo(() => {
let filtered = [...products]
// 按类别过滤
if (selectedCategory !== '全部') {
filtered = filtered.filter((product) => product.category === selectedCategory)
}
// 按搜索词过滤
if (searchTerm) {
filtered = filtered.filter(
(product) =>
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.toLowerCase())
)
}
// 排序
filtered.sort((a, b) => {
let aValue: any = a[sortBy as keyof Product]
let bValue: any = b[sortBy as keyof Product]
if (typeof aValue === 'string') {
aValue = aValue.toLowerCase()
bValue = bValue.toLowerCase()
}
if (sortOrder === 'asc') {
return aValue > bValue ? 1 : -1
} else {
return aValue < bValue ? 1 : -1
}
})
return filtered
}, [products, searchTerm, selectedCategory, sortBy, sortOrder])
// 分页计算
const totalPages = Math.ceil(filteredProducts.length / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const currentProducts = filteredProducts.slice(startIndex, endIndex)
const handlePageChange = (page: number) => {
setCurrentPage(page)
// 滚动到顶部
window.scrollTo({ top: 0, behavior: 'smooth' })
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-7xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"></h1>
<p className="text-gray-600 dark:text-gray-300"></p>
</div>
{/* 搜索和过滤栏 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
{/* 搜索框 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="text"
value={searchTerm}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="输入产品名称或描述..."
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
{/* 类别过滤 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<select
value={selectedCategory}
onChange={(e) => handleCategoryChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
{categories.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
</div>
{/* 排序方式 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<select
value={sortBy}
onChange={(e) => handleSortChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="name"></option>
<option value="price"></option>
<option value="rating"></option>
<option value="stock"></option>
</select>
</div>
{/* 排序顺序 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<select
value={sortOrder}
onChange={(e) => handleSortOrderChange(e.target.value as 'asc' | 'desc')}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="asc"></option>
<option value="desc"></option>
</select>
</div>
</div>
{/* 视图控制 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
:
</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value={12}>12</option>
<option value={24}>24</option>
<option value={48}>48</option>
</select>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">:</span>
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded-md ${
viewMode === 'grid'
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300'
}`}
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded-md ${
viewMode === 'list'
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300'
}`}
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 8a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 12a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 16a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" />
</svg>
</button>
</div>
</div>
</div>
{/* 产品列表 */}
{loading ? (
<LoadingSkeleton />
) : filteredProducts.length === 0 ? (
<div className="text-center py-12">
<div className="text-6xl mb-4">🔍</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
</h3>
<p className="text-gray-600 dark:text-gray-300"></p>
</div>
) : (
<>
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{currentProducts.map((product: Product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="space-y-4">
{currentProducts.map((product: Product) => (
<ProductListItem key={product.id} product={product} />
))}
</div>
)}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
startIndex={startIndex}
endIndex={endIndex}
totalItems={filteredProducts.length}
onPageChange={handlePageChange}
/>
</>
)}
{/* 返回顶部按钮 */}
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className="fixed bottom-8 right-8 bg-blue-600 hover:bg-blue-700 text-white p-3 rounded-full shadow-lg transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 10l7-7m0 0l7 7m-7-7v18"
/>
</svg>
</button>
{/* 返回链接 */}
<div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,566 @@
import { useState } from 'react'
import { Link } from 'wouter'
export default function NavigationTestPage() {
const [activeTab, setActiveTab] = useState('home')
const [isModalOpen, setIsModalOpen] = useState(false)
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false)
const [breadcrumbs, setBreadcrumbs] = useState(['首页', '产品', '手机'])
const [notifications, setNotifications] = useState([
{ id: 1, title: '新消息', content: '您有一条新的私信', time: '2分钟前', unread: true },
{ id: 2, title: '系统通知', content: '系统将于今晚维护', time: '1小时前', unread: true },
{ id: 3, title: '订单更新', content: '您的订单已发货', time: '3小时前', unread: false },
])
const handleBreadcrumbClick = (index: number) => {
const newBreadcrumbs = breadcrumbs.slice(0, index + 1)
setBreadcrumbs(newBreadcrumbs)
}
const markNotificationAsRead = (id: number) => {
setNotifications((prev) =>
prev.map((notif) => (notif.id === id ? { ...notif, unread: false } : notif))
)
}
const unreadCount = notifications.filter((n) => n.unread).length
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* 顶部导航栏 */}
<nav className="bg-white dark:bg-gray-800 shadow-lg border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex items-center">
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">TestNav</div>
</div>
{/* 主导航菜单 */}
<div className="hidden md:flex space-x-8">
<a
href="#"
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
</a>
{/* 产品下拉菜单 */}
<div className="relative">
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors flex items-center"
>
<svg
className="ml-1 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{isDropdownOpen && (
<div className="absolute top-full left-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
<div className="py-1">
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
</a>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
</a>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
</a>
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
</a>
</div>
</div>
)}
</div>
<a
href="#"
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
</a>
<a
href="#"
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
</a>
</div>
{/* 右侧菜单 */}
<div className="flex items-center space-x-4">
{/* 通知铃铛 */}
<div className="relative">
<button className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 p-2 rounded-full transition-colors">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-5 5v-5zM10.5 3.75a6 6 0 0 1 6 6v2.25l2.25 2.25v2.25H2.25V14.25L4.5 12V9.75a6 6 0 0 1 6-6z"
/>
</svg>
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
{unreadCount}
</span>
)}
</button>
</div>
{/* 用户菜单 */}
<div className="relative">
<button
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
className="flex items-center text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 p-2 rounded-full transition-colors"
>
<div className="h-8 w-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-medium">
U
</div>
</button>
{isUserMenuOpen && (
<div className="absolute top-full right-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
<div className="py-1">
<div className="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-600">
user@example.com
</div>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
</a>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
</a>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
</a>
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
退
</a>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</nav>
{/* 面包屑导航 */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4 py-3">
<nav className="flex" aria-label="Breadcrumb">
<ol className="flex items-center space-x-2">
{breadcrumbs.map((crumb, crumbIdx) => {
const isLast = crumbIdx === breadcrumbs.length - 1
const showSeparator = crumbIdx > 0
return (
<li key={`${crumb}-${crumbIdx + 1}`} className="flex items-center">
{showSeparator && (
<svg
className="h-4 w-4 text-gray-400 mx-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
)}
<button
onClick={() => handleBreadcrumbClick(crumbIdx)}
className={`text-sm font-medium transition-colors ${
isLast
? 'text-gray-500 dark:text-gray-400 cursor-default'
: 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300'
}`}
>
{crumb}
</button>
</li>
)
})}
</ol>
</nav>
</div>
</div>
{/* 主要内容区域 */}
<div className="max-w-7xl mx-auto px-4 py-8">
{/* 标签页导航 */}
<div className="mb-8">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
{[
{ id: 'home', label: '概览', icon: '🏠' },
{ id: 'products', label: '产品列表', icon: '📱' },
{ id: 'orders', label: '订单管理', icon: '📦' },
{ id: 'analytics', label: '数据分析', icon: '📊' },
{ id: 'settings', label: '设置', icon: '⚙️' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<span className="mr-2">{tab.icon}</span>
{tab.label}
</button>
))}
</nav>
</div>
</div>
{/* 标签页内容 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
{activeTab === 'home' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
<h3 className="font-semibold text-blue-900 dark:text-blue-100"></h3>
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">¥123,456</p>
</div>
<div className="bg-green-50 dark:bg-green-900 p-4 rounded-lg">
<h3 className="font-semibold text-green-900 dark:text-green-100"></h3>
<p className="text-2xl font-bold text-green-600 dark:text-green-400">1,234</p>
</div>
<div className="bg-purple-50 dark:bg-purple-900 p-4 rounded-lg">
<h3 className="font-semibold text-purple-900 dark:text-purple-100"></h3>
<p className="text-2xl font-bold text-purple-600 dark:text-purple-400">5,678</p>
</div>
</div>
</div>
)}
{activeTab === 'products' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<div className="space-y-4">
{['iPhone 15 Pro', 'MacBook Air', 'iPad Pro', 'Apple Watch'].map((product) => (
<div
key={product}
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-600 rounded-lg"
>
<div>
<h3 className="font-medium text-gray-900 dark:text-white">{product}</h3>
<p className="text-gray-500 dark:text-gray-400">...</p>
</div>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
</button>
</div>
))}
</div>
</div>
)}
{activeTab === 'orders' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-600">
{[
{ id: '#001', customer: '张三', status: '已发货', amount: '¥1,299' },
{ id: '#002', customer: '李四', status: '处理中', amount: '¥2,599' },
{ id: '#003', customer: '王五', status: '已完成', amount: '¥899' },
].map((order) => (
<tr key={order.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{order.id}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{order.customer}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
order.status === '已完成'
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: order.status === '已发货'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
}`}
>
{order.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{order.amount}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'analytics' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h3>
<div className="h-32 bg-linear-to-r from-blue-400 to-purple-500 rounded-lg flex items-center justify-center text-white">
📈
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h3>
<div className="h-32 bg-linear-to-r from-green-400 to-blue-500 rounded-lg flex items-center justify-center text-white">
🗺
</div>
</div>
</div>
</div>
)}
{activeTab === 'settings' && (
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"></h2>
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
</h3>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
defaultChecked
/>
<span className="ml-2 text-gray-700 dark:text-gray-300"></span>
</label>
<label className="flex items-center">
<input
type="checkbox"
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-gray-700 dark:text-gray-300"></span>
</label>
</div>
</div>
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
</h3>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
defaultChecked
/>
<span className="ml-2 text-gray-700 dark:text-gray-300"></span>
</label>
<label className="flex items-center">
<input
type="checkbox"
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
defaultChecked
/>
<span className="ml-2 text-gray-700 dark:text-gray-300"></span>
</label>
</div>
</div>
</div>
</div>
)}
</div>
{/* 操作按钮 */}
<div className="mt-8 flex flex-wrap gap-4">
<button
onClick={() => setIsModalOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md transition-colors"
>
</button>
<button
onClick={() => setBreadcrumbs([...breadcrumbs, `新页面${breadcrumbs.length}`])}
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-md transition-colors"
>
</button>
<button
onClick={() => {
const newNotif = {
id: Date.now(),
title: '新通知',
content: `这是第 ${notifications.length + 1} 条通知`,
time: '刚刚',
unread: true,
}
setNotifications((prev) => [newNotif, ...prev])
}}
className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-md transition-colors"
>
</button>
</div>
{/* 通知列表 */}
{notifications.length > 0 && (
<div className="mt-8">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4"></h3>
<div className="space-y-2">
{notifications.map((notification) => (
<div
key={notification.id}
className={`p-4 rounded-lg border cursor-pointer transition-colors ${
notification.unread
? 'bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700'
: 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600'
}`}
onClick={() => markNotificationAsRead(notification.id)}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-white">
{notification.title}
</h4>
<p className="text-gray-600 dark:text-gray-300 text-sm">
{notification.content}
</p>
</div>
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500 dark:text-gray-400">
{notification.time}
</span>
{notification.unread && (
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
{/* 模态框 */}
{isModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white"></h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Agent
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
</button>
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
>
</button>
</div>
</div>
</div>
)}
{/* 返回链接 */}
<div className="max-w-7xl mx-auto px-4 py-8">
<Link href="/" className="text-blue-600 hover:text-blue-500 dark:text-blue-400">
</Link>
</div>
</div>
)
}

View File

@@ -0,0 +1,25 @@
import { Route, Switch } from 'wouter'
import AsyncTestPage from './async-test'
import ComplexTestPage from './complex-test'
import ErrorTestPage from './error-test'
import FormTestPage from './form-test'
import IndexPage from './index'
import ListTestPage from './list-test'
import NavigationTestPage from './navigation-test'
export default function Router() {
return (
<>
<Switch>
<Route path="/form" component={FormTestPage} />
<Route path="/navigation" component={NavigationTestPage} />
<Route path="/list" component={ListTestPage} />
<Route path="/complex" component={ComplexTestPage} />
<Route path="/errors" component={ErrorTestPage} />
<Route path="/async" component={AsyncTestPage} />
<Route path="" component={IndexPage} />
</Switch>
</>
)
}