Compare commits

..

7 Commits

Author SHA1 Message Date
Simon 6abe75f199 docs(ext): clarify data handling and privacy in PRIVACY.md 2026-02-14 17:03:41 +08:00
Simon e27ebda960 chore(ext): docs and locales 2026-02-14 16:51:37 +08:00
Simon 9b603d90ba docs: update limitations page 2026-02-14 16:23:34 +08:00
Simon b89228693d feat(ext): add lang setting 2026-02-14 16:10:46 +08:00
Simon 66cc9e0a3c style: pretty up; fix extension shadcn 2026-02-14 15:12:45 +08:00
Simon 53144a8021 chore(ext): rm zh doc 2026-02-14 15:12:03 +08:00
Simon 43b7c1b136 style(ext): make over 2026-02-13 19:09:46 +08:00
20 changed files with 589 additions and 436 deletions
+2 -1
View File
@@ -11074,13 +11074,14 @@
},
"packages/extension": {
"name": "@page-agent/ext",
"version": "0.1.5",
"version": "0.1.7",
"hasInstallScript": true,
"dependencies": {
"@page-agent/core": "1.3.0",
"@page-agent/llms": "1.3.0",
"@page-agent/page-controller": "1.3.0",
"@page-agent/ui": "1.3.0",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2",
"zod": "^4.3.5"
},
+2 -1
View File
@@ -7,7 +7,7 @@ import type { BrowserState, PageController } from '@page-agent/page-controller'
import chalk from 'chalk'
import * as zod from 'zod'
import { type PageAgentConfig } from './config'
import { type PageAgentConfig, type SupportedLanguage } from './config'
import { DEFAULT_MAX_STEPS } from './config/constants'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools'
@@ -24,6 +24,7 @@ import type {
import { assert, normalizeResponse, uid, waitFor } from './utils'
export { type PageAgentConfig }
export type { SupportedLanguage }
export { tool, type PageAgentTool } from './tools'
export type * from './types'
+7 -4
View File
@@ -1,6 +1,6 @@
# Privacy Policy for Page Agent Extension
**Last updated:** January 2026
**Last updated:** February 2026
## Overview
@@ -10,24 +10,27 @@ Page Agent Extension is a browser automation tool that uses AI to help you inter
### Local Processing
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension itself.
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension developer.
### Data Transmission
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
- Your task instructions (natural language commands)
- Sanitized page structure (simplified DOM, excluding sensitive form values)
- Simplified page structure (cleaned DOM) of all pages under the extension's control
are sent to the LLM API endpoint configured in **your settings**.
> **Note:** The DOM cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
**If you use our testing endpoint**, your requests are proxied to [DeepSeek](https://deepseek.com) for AI processing. Regarding this test endpoint:
- This endpoint is provided for evaluation purposes only and is not recommended for production or daily use
- The free model and their service providers may change at any time without prior notice
- We do **not** store your task content, page content, or visited URLs
- Minimal logging (timestamps, request metadata, IP addresses) may occur for abuse prevention and service stability
- Minimal logging (timestamps, request metadata, IP addresses) may be collected for abuse prevention and service stability
- DeepSeek's [Privacy Policy](https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html) applies to their processing of your requests
## Data Storage
+2 -2
View File
@@ -5,7 +5,7 @@
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"css": "src/assets/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
@@ -16,7 +16,7 @@
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
"hooks": "@/lib/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
+30
View File
@@ -0,0 +1,30 @@
AI-powered browser automation. Control web pages with natural language.
Page Agent Ext — AI-Powered Browser Automation
🌟 What is Page Agent Ext?
Page Agent Ext brings AI-powered automation to your browser. Built on the open-source Page Agent framework, it lets you control web pages across multiple tabs using natural language — no scripting required.
🌟 Key Features:
- Natural Language Control — Command your browser in plain language, no code needed
- Cross-Tab Automation — Seamlessly operate across multiple tabs and pages
- Smart HTML Cleaning — Intelligently extracts and simplifies page structure for accurate AI understanding
- Bring Your Own LLM — Use OpenAI, Anthropic, or any compatible API with full data control
- Privacy-First — Zero data collection; all data flows directly to your chosen LLM provider
- Open Source — MIT licensed, built on the Page Agent framework with full transparency
🌟 How It Works & Privacy:
Page Agent Ext performs DOM analysis locally in your browser. When you initiate a task, sanitized page structure is sent to the LLM API you configure. Your data is never collected or stored by us.
- Your API Key — Configure your own LLM API (OpenAI, Anthropic, etc.). Data goes directly to your provider
- Test API — A free test endpoint is available for evaluation; we recommend your own key for regular use
Privacy policy: https://github.com/alibaba/page-agent/blob/main/packages/extension/PRIVACY.md
🌟 Open Source:
This project is MIT licensed. Review the code, verify privacy claims, or extend it for your needs:
https://github.com/alibaba/page-agent
@@ -0,0 +1,30 @@
AI 驱动的浏览器自动化助手,用自然语言控制网页。
Page Agent Ext — AI 驱动的浏览器自动化助手
🌟 什么是 Page Agent Ext
Page Agent Ext 为浏览器带来 AI 自动化能力。基于开源的 Page Agent 框架,你可以用自然语言跨标签页控制网页,无需编写任何脚本。
🌟 核心特性:
- 自然语言控制 — 用日常语言指挥浏览器,无需编程
- 跨标签页操作 — 在多个标签页之间无缝切换和操控
- HTML 智能清洗 — 智能提取和精简页面结构,让 AI 准确理解网页内容
- 使用你自己的模型 — 支持 OpenAI、Anthropic 或任何兼容 API,数据完全自主可控
- 隐私优先 — 零数据收集,所有数据直接发送到你配置的 LLM 服务商
- 开源透明 — MIT 协议,基于 Page Agent 开源框架,代码完全公开
🌟 工作原理与隐私:
Page Agent Ext 在浏览器本地进行 DOM 分析。当你发起任务时,经清洗的页面结构会发送到你配置的 LLM API。我们不会收集或存储你的任何数据。
- 你的 API Key — 配置你自己的 LLM APIOpenAI、Anthropic 等),数据直接发送到你的服务商
- 测试 API — 提供免费测试端点供体验,日常使用建议配置自己的 Key
隐私政策:https://github.com/alibaba/page-agent/blob/main/packages/extension/PRIVACY.md
🌟 开源项目:
MIT 协议开源。查看源码、验证隐私承诺,或按需扩展:
https://github.com/alibaba/page-agent
-226
View File
@@ -1,226 +0,0 @@
# Page Agent 浏览器插件 API
在你的网页应用中接入 Page Agent 插件,并通过页面 JavaScript 发起多页面浏览器任务。
## 安装
### 1. 安装浏览器插件
首选渠道:
- Chrome 应用商店:https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj
通常更快提供最新构建的渠道:
- GitHub Releaseshttps://github.com/alibaba/page-agent/releases
### 2. 安装类型定义(推荐)
```bash
npm install @page-agent/core --save-dev
```
### 3. 授权(Token
token 用于让页面 JS 调用扩展 API(`window.PAGE_AGENT_EXT`)并执行多页面任务。
为什么必须使用 token
- 插件具备较广的浏览器权限(页面访问、导航、多标签控制)。
- 若被滥用,可能危害用户隐私与安全。
- 用户必须主动将 token 提供给其信任的应用。
配置步骤:
1. 在扩展侧边栏中复制 auth token。
2. 在页面中设置 token
```typescript
localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
```
## 快速开始
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
// 等待插件注入(最多 1 秒)
async function waitForExtension(timeout = 1000): Promise<boolean> {
const start = Date.now()
while (Date.now() - start < timeout) {
if (window.PAGE_AGENT_EXT) return true
await new Promise((r) => setTimeout(r, 100))
}
return false
}
// 使用
if (await waitForExtension()) {
const result = await window.PAGE_AGENT_EXT!.execute('点击登录按钮', {
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5.2',
onStatusChange: (status) => console.log('状态:', status),
onActivity: (activity) => console.log('活动:', activity),
})
console.log('结果:', result)
}
```
## 全局 API
token 匹配后,插件会在 `window` 上注入 API。
### `window.PAGE_AGENT_EXT_VERSION`
插件版本号字符串,可用于在访问主 API 前做能力检查。
### `window.PAGE_AGENT_EXT`
主命名空间对象。
#### `PAGE_AGENT_EXT.execute(task, config)`
执行 Agent 任务。
参数:
| 名称 | 类型 | 必填 | 说明 |
| ---- | ---- | ---- | ---- |
| `task` | `string` | 是 | 任务描述 |
| `config` | `ExecuteConfig` | 是 | LLM 设置、执行选项和回调 |
返回:`Promise<ExecutionResult>`
#### `PAGE_AGENT_EXT.stop()`
停止当前任务。
## 类型定义
安装 `@page-agent/core` 获取完整类型:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
export interface ExecuteConfig {
baseURL: string
apiKey: string
model: string
// 是否包含启动脚本所在标签页。默认 true。
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
```
`AgentStatus`
```typescript
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
```
`AgentActivity`
```typescript
type AgentActivity =
| { type: 'thinking' }
| { type: 'executing'; tool: string; input: unknown }
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
| { type: 'retrying'; attempt: number; maxAttempts: number }
| { type: 'error'; message: string }
```
`HistoricalEvent`
```typescript
type HistoricalEvent =
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
| { type: 'observation'; content: string }
| { type: 'user_takeover' }
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
| { type: 'error'; message: string; rawResponse?: unknown }
```
`ExecutionResult`
```typescript
interface ExecutionResult {
success: boolean
data: string
history: HistoricalEvent[]
}
```
## 使用示例
### 基础执行
```typescript
const result = await window.PAGE_AGENT_EXT!.execute(
'在邮箱输入框填入 test@example.com 然后点击提交',
{
baseURL: 'https://api.openai.com/v1',
apiKey: process.env.OPENAI_API_KEY!,
model: 'gpt-5.2',
includeInitialTab: false, // 可选:排除当前标签页
onStatusChange: (status) => console.log(status),
onActivity: (activity) => console.log(activity),
}
)
```
### 停止当前任务
```typescript
window.PAGE_AGENT_EXT!.stop()
```
## Window 类型声明
如果你不直接引入 `@page-agent/core`,可添加以下声明:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
interface ExecuteConfig {
baseURL: string
apiKey: string
model: string
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
declare global {
interface Window {
PAGE_AGENT_EXT_VERSION?: string
PAGE_AGENT_EXT?: {
version: string
execute: Execute
stop: () => void
}
}
}
```
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/ext",
"private": true,
"version": "0.1.5",
"version": "0.1.7",
"type": "module",
"scripts": {
"dev": "wxt",
@@ -42,6 +42,7 @@
"@page-agent/llms": "1.3.0",
"@page-agent/page-controller": "1.3.0",
"@page-agent/ui": "1.3.0",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2",
"zod": "^4.3.5"
}
@@ -0,0 +1,11 @@
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI-powered browser automation assistant. Control web pages with natural language."
},
"extActionTitle": {
"message": "Open Page Agent"
}
}
@@ -0,0 +1,11 @@
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
},
"extActionTitle": {
"message": "打开 Page Agent"
}
}
+30 -12
View File
@@ -1,22 +1,34 @@
/**
* React hook for using AgentController
*/
import type { AgentActivity, AgentStatus, HistoricalEvent } from '@page-agent/core'
import type {
AgentActivity,
AgentStatus,
HistoricalEvent,
SupportedLanguage,
} from '@page-agent/core'
import type { LLMConfig } from '@page-agent/llms'
import { useCallback, useEffect, useRef, useState } from 'react'
import { MultiPageAgent } from './MultiPageAgent'
import { DEMO_CONFIG } from './constants'
/** Language preference: undefined means follow system */
export type LanguagePreference = SupportedLanguage | undefined
export interface ExtConfig extends LLMConfig {
language?: LanguagePreference
}
export interface UseAgentResult {
status: AgentStatus
history: HistoricalEvent[]
activity: AgentActivity | null
currentTask: string
config: LLMConfig | null
config: ExtConfig | null
execute: (task: string) => Promise<void>
stop: () => void
configure: (config: LLMConfig) => Promise<void>
configure: (config: ExtConfig) => Promise<void>
}
export function useAgent(): UseAgentResult {
@@ -25,16 +37,17 @@ export function useAgent(): UseAgentResult {
const [history, setHistory] = useState<HistoricalEvent[]>([])
const [activity, setActivity] = useState<AgentActivity | null>(null)
const [currentTask, setCurrentTask] = useState('')
const [config, setConfig] = useState<LLMConfig | null>(null)
const [config, setConfig] = useState<ExtConfig | null>(null)
useEffect(() => {
chrome.storage.local.get('llmConfig').then((result) => {
if (result.llmConfig) {
setConfig(result.llmConfig as LLMConfig)
} else {
chrome.storage.local.get(['llmConfig', 'language']).then((result) => {
const llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG
const language = (result.language as SupportedLanguage) || undefined
const full = { ...llmConfig, language }
if (!result.llmConfig) {
chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
setConfig(DEMO_CONFIG)
}
setConfig(full)
})
}, [])
@@ -87,9 +100,14 @@ export function useAgent(): UseAgentResult {
agentRef.current?.stop()
}, [])
const configure = useCallback(async (newConfig: LLMConfig) => {
await chrome.storage.local.set({ llmConfig: newConfig })
setConfig(newConfig)
const configure = useCallback(async ({ language, ...llmConfig }: ExtConfig) => {
await chrome.storage.local.set({ llmConfig })
if (language) {
await chrome.storage.local.set({ language })
} else {
await chrome.storage.local.remove('language')
}
setConfig({ ...llmConfig, language })
}, [])
return {
+35
View File
@@ -111,6 +111,41 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
@keyframes blink-cursor {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
}
@keyframes glow-a {
0%,
100% {
opacity: 0.45;
transform: scale(1);
}
50% {
opacity: 0;
transform: scale(1.1);
}
}
@keyframes glow-b {
0%,
100% {
opacity: 0;
transform: scale(1.1);
}
50% {
opacity: 0.45;
transform: scale(1);
}
}
@layer base {
@@ -21,8 +21,9 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
// Focus state.
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
// Focus state — soft multi-color glow matching ai-motion palette
'has-[[data-slot=input-group-control]:focus-visible]:border-blue-400/60',
'has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_1px_rgba(57,182,255,0.2),0_0_8px_rgba(57,182,255,0.15),0_0_16px_rgba(189,69,251,0.1)]',
// Error state.
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
@@ -0,0 +1,164 @@
import { MotionProps, motion, useInView } from 'motion/react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
interface TypingAnimationProps extends MotionProps {
children?: string
words?: string[]
className?: string
duration?: number
typeSpeed?: number
deleteSpeed?: number
delay?: number
pauseDelay?: number
loop?: boolean
as?: React.ElementType
startOnView?: boolean
showCursor?: boolean
blinkCursor?: boolean
cursorStyle?: 'line' | 'block' | 'underscore'
}
export function TypingAnimation({
children,
words,
className,
duration = 100,
typeSpeed,
deleteSpeed,
delay = 0,
pauseDelay = 1000,
loop = false,
as: Component = 'span',
startOnView = true,
showCursor = true,
blinkCursor = true,
cursorStyle = 'line',
...props
}: TypingAnimationProps) {
const MotionComponent = motion.create(Component, {
forwardMotionProps: true,
})
const [displayedText, setDisplayedText] = useState<string>('')
const [currentWordIndex, setCurrentWordIndex] = useState(0)
const [currentCharIndex, setCurrentCharIndex] = useState(0)
const [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')
const elementRef = useRef<HTMLElement | null>(null)
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
const wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])
const hasMultipleWords = wordsToAnimate.length > 1
const typingSpeed = typeSpeed || duration
const deletingSpeed = deleteSpeed || typingSpeed / 2
const shouldStart = startOnView ? isInView : true
useEffect(() => {
if (!shouldStart || wordsToAnimate.length === 0) return
const timeoutDelay =
delay > 0 && displayedText === ''
? delay
: phase === 'typing'
? typingSpeed
: phase === 'deleting'
? deletingSpeed
: pauseDelay
const timeout = setTimeout(() => {
const currentWord = wordsToAnimate[currentWordIndex] || ''
const graphemes = Array.from(currentWord)
switch (phase) {
case 'typing':
if (currentCharIndex < graphemes.length) {
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))
setCurrentCharIndex(currentCharIndex + 1)
} else {
if (hasMultipleWords || loop) {
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
if (!isLastWord || loop) {
setPhase('pause')
}
}
}
break
case 'pause':
setPhase('deleting')
break
case 'deleting':
if (currentCharIndex > 0) {
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))
setCurrentCharIndex(currentCharIndex - 1)
} else {
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
setCurrentWordIndex(nextIndex)
setPhase('typing')
}
break
}
}, timeoutDelay)
return () => clearTimeout(timeout)
}, [
shouldStart,
phase,
currentCharIndex,
currentWordIndex,
displayedText,
wordsToAnimate,
hasMultipleWords,
loop,
typingSpeed,
deletingSpeed,
pauseDelay,
delay,
])
const currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')
const isComplete =
!loop &&
currentWordIndex === wordsToAnimate.length - 1 &&
currentCharIndex >= currentWordGraphemes.length &&
phase !== 'deleting'
const shouldShowCursor =
showCursor &&
!isComplete &&
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
const getCursorChar = () => {
switch (cursorStyle) {
case 'block':
return '▌'
case 'underscore':
return '_'
case 'line':
default:
return '|'
}
}
return (
<MotionComponent
ref={elementRef}
className={cn('leading-[5rem] tracking-[-0.02em]', className)}
{...props}
>
{displayedText}
{shouldShowCursor && (
<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>
{getCursorChar()}
</span>
)}
</MotionComponent>
)
}
@@ -15,7 +15,7 @@ import { ConfigPanel } from './components/ConfigPanel'
import { HistoryDetail } from './components/HistoryDetail'
import { HistoryList } from './components/HistoryList'
import { ActivityCard, EventCard } from './components/cards'
import { EmptyState, Logo, StatusDot } from './components/misc'
import { EmptyState, Logo, MotionOverlay, StatusDot } from './components/misc'
type View =
| { name: 'chat' }
@@ -117,14 +117,15 @@ export default function App() {
const showEmptyState = !currentTask && history.length === 0 && !isRunning
return (
<div className="flex flex-col h-screen bg-background">
<div className="relative flex flex-col h-screen bg-background">
<MotionOverlay active={isRunning} />
{/* Header */}
<header className="flex items-center justify-between border-b px-3 py-2">
<div className="flex items-center gap-2">
<Logo className="size-5" />
<span className="text-sm font-medium">Page Agent Ext</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<StatusDot status={status} />
<Button
variant="ghost"
@@ -1,15 +1,15 @@
import type { LLMConfig } from '@page-agent/llms'
import { Copy, CornerUpLeft, Eye, EyeOff, HatGlasses, Home, Loader2, Scale } from 'lucide-react'
import { useEffect, useState } from 'react'
import { siGithub } from 'simple-icons'
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/agent/constants'
import type { ExtConfig, LanguagePreference } from '@/agent/useAgent'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
interface ConfigPanelProps {
config: LLMConfig | null
onSave: (config: LLMConfig) => Promise<void>
config: ExtConfig | null
onSave: (config: ExtConfig) => Promise<void>
onClose: () => void
}
@@ -17,6 +17,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [apiKey, setApiKey] = useState(config?.apiKey || DEMO_API_KEY)
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
const [model, setModel] = useState(config?.model || DEMO_MODEL)
const [language, setLanguage] = useState<LanguagePreference>(config?.language)
const [saving, setSaving] = useState(false)
const [userAuthToken, setUserAuthToken] = useState<string>('')
const [copied, setCopied] = useState(false)
@@ -28,6 +29,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
setApiKey(config?.apiKey || DEMO_API_KEY)
setBaseURL(config?.baseURL || DEMO_BASE_URL)
setModel(config?.model || DEMO_MODEL)
setLanguage(config?.language)
}, [config])
// Poll for user auth token every second until found
@@ -65,7 +67,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const handleSave = async () => {
setSaving(true)
try {
await onSave({ apiKey, baseURL, model })
await onSave({ apiKey, baseURL, model, language })
} finally {
setSaving(false)
}
@@ -165,6 +167,19 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Language</label>
<select
value={language ?? ''}
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
className="h-8 text-xs rounded-md border border-input bg-background px-2 cursor-pointer"
>
<option value="">System</option>
<option value="en-US">English</option>
<option value="zh-CN">中文</option>
</select>
</div>
<div className="flex gap-2 mt-2">
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
Cancel
@@ -1,5 +1,10 @@
import type { AgentStatus } from '@page-agent/core'
import { Motion } from 'ai-motion'
import { BookOpen, Globe } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { siGithub } from 'simple-icons'
import { TypingAnimation } from '@/components/ui/typing-animation'
import { cn } from '@/lib/utils'
// Status dot indicator
@@ -19,7 +24,7 @@ export function StatusDot({ status }: { status: AgentStatus }) {
}[status]
return (
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1.5 mr-2">
<span
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
/>
@@ -32,14 +37,111 @@ export function Logo({ className }: { className?: string }) {
return <img src="/assets/page-agent-256.webp" alt="Page Agent" className={cn('', className)} />
}
// Empty state with logo
// Full-screen ai-motion glow overlay, shown only while running
export function MotionOverlay({ active }: { active: boolean }) {
const containerRef = useRef<HTMLDivElement>(null)
const motionRef = useRef<Motion | null>(null)
useEffect(() => {
const mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
const motion = new Motion({
mode,
borderWidth: 4,
borderRadius: 14,
glowWidth: mode === 'dark' ? 120 : 60,
styles: { position: 'absolute', inset: '0' },
})
motionRef.current = motion
containerRef.current!.appendChild(motion.element)
motion.autoResize(containerRef.current!)
return () => {
motion.dispose()
motionRef.current = null
}
}, [])
useEffect(() => {
const motion = motionRef.current
if (!motion) return
let disposed = false
if (active) {
motion.start()
motion.fadeIn()
} else {
motion.fadeOut().then(() => !disposed && motion.pause())
}
return () => {
disposed = true
}
}, [active])
return (
<div
ref={containerRef}
className="pointer-events-none absolute inset-0 z-10 opacity-60"
style={{ display: active ? undefined : 'none' }}
/>
)
}
// Empty state with logo and breathing glow
export function EmptyState() {
return (
<div className="flex flex-col items-center justify-center h-full gap-3 text-center px-6">
<Logo className="size-20 opacity-80" />
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6">
<div className="relative select-none pointer-events-none">
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_180deg,oklch(0.55_0.2_280),oklch(0.5_0.15_230),oklch(0.6_0.18_310),oklch(0.55_0.2_280))] blur-2xl animate-[glow-a_5s_ease-in-out_infinite]" />
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_0deg,oklch(0.55_0.18_160),oklch(0.5_0.2_200),oklch(0.6_0.15_120),oklch(0.55_0.18_160))] blur-2xl animate-[glow-b_5s_ease-in-out_infinite]" />
<Logo className="relative size-20 opacity-80" />
</div>
<div>
<h2 className="text-sm font-medium text-foreground">Page Agent Ext</h2>
<p className="text-xs text-muted-foreground mt-1">Enter a task to automate this page</p>
<h2 className="text-base font-medium text-foreground mb-1">Page Agent Ext</h2>
<TypingAnimation
className="text-sm text-muted-foreground"
words={[
'Enter a task to automate this page',
'Execute multi-page tasks',
'Call this extension from your web page',
'Use this extension in your own agents',
]}
cursorStyle="underscore"
loop
typeSpeed={20}
deleteSpeed={10}
pauseDelay={3000}
/>
</div>
<div className="flex items-center gap-3 mt-1 text-muted-foreground">
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
title="GitHub"
>
<svg role="img" viewBox="0 0 24 24" className="size-4 fill-current">
<path d={siGithub.path} />
</svg>
</a>
<a
href="https://alibaba.github.io/page-agent/#/docs/features/chrome-extension"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
title="Documentation"
>
<BookOpen className="size-4" />
</a>
<a
href="https://alibaba.github.io/page-agent"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
title="Website"
>
<Globe className="size-4" />
</a>
</div>
</div>
)
+4 -4
View File
@@ -38,9 +38,9 @@ export default defineConfig({
},
}),
manifest: {
name: 'Page Agent Ext',
description:
'AI-powered browser automation assistant. Control web pages with natural language.',
default_locale: 'en',
name: '__MSG_extName__',
description: '__MSG_extDescription__',
homepage_url: 'https://alibaba.github.io/page-agent/',
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],
host_permissions: ['<all_urls>'],
@@ -48,7 +48,7 @@ export default defineConfig({
64: 'assets/page-agent-64.png',
},
action: {
default_title: 'Open Page Agent',
default_title: '__MSG_extActionTitle__',
},
web_accessible_resources: [
{
@@ -63,7 +63,7 @@ export default function ChromeExtension() {
href={chromeWebStoreUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white! font-medium rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={siChromewebstore.path} />
@@ -74,7 +74,7 @@ export default function ChromeExtension() {
href={githubReleasesUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 text-white font-medium rounded-lg transition-colors"
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 text-white! font-medium rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={siGithub.path} />
@@ -164,15 +164,7 @@ localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')
rel="noopener noreferrer"
className="block text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
>
📄 {isZh ? '英文版 API 文档' : 'API Documentation (English)'}
</a>
<a
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/docs/extension_api_zh.md"
target="_blank"
rel="noopener noreferrer"
className="block text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
>
📄 {isZh ? '中文版 API 文档' : 'API Documentation (Chinese)'}
📄 {isZh ? 'API 文档' : 'API Documentation'}
</a>
</div>
</section>
@@ -317,6 +309,7 @@ window.PAGE_AGENT_EXT.stop()`
? '将 MultiPageAgent 集成你自己的插件'
: 'Integrate MultiPageAgent into Your Extension'}
</h2>
<p>@TODO</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '建议先阅读扩展 API 文档,再参考 background entry implementation。'
@@ -1,3 +1,5 @@
import { Link } from 'wouter'
import { useLanguage } from '@/i18n/context'
export default function LimitationsPage() {
@@ -11,184 +13,144 @@ export default function LimitationsPage() {
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300">
{isZh
? '了解 page-agent 当前的功能边界和技术限制'
: "Understand page-agent's current capabilities and technical constraints"}
? 'Page Agent 基于 DOM 理解网页并执行操作。这决定了它的能力边界。'
: 'Page Agent understands web pages via DOM and performs actions accordingly. This defines its capability boundary.'}
</p>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none">
{/* PageAgent.js vs PageAgentExt */}
<h2 className="text-2xl font-bold mb-3">
{isZh ? '页面支持限制' : 'Page Support Limitations'}
{isZh ? 'PageAgent.js vs PageAgentExt' : 'PageAgent.js vs PageAgentExt'}
</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">
{isZh ? '单页应用限制' : 'Single Page Application Limits'}
</h3>
<ul className="text-blue-700 dark:text-blue-300 space-y-2">
<li>
{isZh
? '• 仅支持单页应用(SPA):目前只能在单个页面内进行操作'
: '• SPA only: Currently operates within a single page'}
</li>
<li>
{isZh
? '• 多页接力功能正在设计中:暂时无法跨页面执行连续任务'
: '• Multi-page relay in design: Cannot execute continuous tasks across pages yet'}
</li>
<li>
{isZh
? '• 无法操作未接入该能力的网站:需要目标网站主动集成 page-agent'
: '• Requires integration: Cannot operate on sites without page-agent'}
</li>
</ul>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? 'PageAgent.js 是核心库,运行在页面内。PageAgentExt 是可选的浏览器扩展,提供额外的浏览器级控制能力。'
: 'PageAgent.js is the core library running inside a page. PageAgentExt is an optional browser extension that adds browser-level control.'}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 pr-4"></th>
<th className="text-left py-3 px-4 font-semibold">PageAgent.js</th>
<th className="text-left py-3 pl-4 font-semibold">
PageAgentExt{' '}
<Link
href="/docs/features/chrome-extension"
className="text-xs font-normal text-blue-600 dark:text-blue-400 hover:underline"
>
{isZh ? '了解更多' : 'learn more'}
</Link>
</th>
</tr>
</thead>
<tbody className="text-gray-600 dark:text-gray-300">
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
{isZh ? '接入方式' : 'Integration'}
</td>
<td className="py-3 px-4">
{isZh ? '网站开发者主动集成' : 'Site developer integrates the library'}
</td>
<td className="py-3 pl-4">
{isZh ? '用户安装浏览器扩展' : 'User installs a browser extension'}
</td>
</tr>
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
{isZh ? '可操作范围' : 'Scope'}
</td>
<td className="py-3 px-4">
{isZh ? '当前页面(为 SPA 设计)' : 'Current page (designed for SPAs)'}
</td>
<td className="py-3 pl-4">
{isZh ? '任意网页、多标签页' : 'Any web page, multi-tab'}
</td>
</tr>
<tr>
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
{isZh ? '额外能力' : 'Extra capabilities'}
</td>
<td className="py-3 px-4"></td>
<td className="py-3 pl-4">
{isZh ? '新建/切换/关闭标签页' : 'Open / switch / close tabs'}
</td>
</tr>
</tbody>
</table>
</div>
{/* Interaction Limitations */}
<h2 className="text-2xl font-bold mb-3">
{isZh ? '交互行为限制' : 'Interaction Limitations'}
{isZh ? '交互能力' : 'Interaction Capabilities'}
</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<h3 className="font-semibold mb-4">{isZh ? '支持的操作' : 'Supported Operations'}</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>{isZh ? '点击操作' : 'Click'}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{isZh ? '文本输入' : 'Text input'}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{isZh ? '页面滚动' : 'Scroll'}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{isZh ? '表单提交' : 'Form 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>{isZh ? '选择操作' : 'Select'}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{isZh ? '焦点切换' : 'Focus'}</span>
</div>
</div>
</div>
<h3 className="font-semibold mb-4">{isZh ? '不支持的操作' : 'Unsupported Operations'}</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>{isZh ? '鼠标悬停(hover' : 'Mouse hover'}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{isZh ? '拖拽操作' : 'Drag & drop'}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{isZh ? '右键菜单' : 'Right-click menu'}</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>{isZh ? '图形绘制' : 'Drawing'}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{isZh ? '键盘快捷键' : 'Keyboard shortcuts'}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{isZh ? '基于点击区域或鼠标位置的控制' : 'Position-based control'}</span>
</div>
</div>
</div>
</div>
<h2 className="text-2xl font-bold mb-3">
{isZh ? '网页理解限制' : 'Understanding Limitations'}
</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">
{isZh ? '无视觉能力' : 'No Visual Recognition'}
</h3>
<p className="text-red-700 dark:text-red-300 mb-3">
{isZh
? 'page-agent 基于 DOM 结构进行理解和操作,没有视觉识别能力,无法理解以下内容:'
: 'page-agent operates based on DOM structure with no visual recognition. Cannot understand:'}
</p>
<ul className="text-red-700 dark:text-red-300 space-y-1">
<li>
{isZh
? '• 图片内容:无法识别图片中的文字、图标或视觉元素'
: '• Image content: Cannot recognize text, icons, or visual elements in images'}
</li>
<li>
{isZh
? '• Canvas 画布:无法理解 Canvas 中绘制的图形和内容'
: '• Canvas: Cannot understand graphics drawn on Canvas'}
</li>
<li>
{isZh
? '• WebGL 3D 内容:无法操作 3D 场景中的元素'
: '• WebGL 3D: Cannot operate elements in 3D scenes'}
</li>
<li>
{isZh
? '• SVG 图形:无法理解 SVG 中的视觉内容和路径'
: '• SVG graphics: Cannot understand visual content and paths in SVG'}
</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-3">
{isZh ? '被操作网站要求' : 'Website Requirements'}
</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<div className="space-y-4">
<div className="grid md:grid-cols-2 gap-6">
<div>
<h3 className="font-semibold mb-2">
{isZh ? '语义化和易用性' : 'Semantic & Usability'}
<h3 className="font-semibold mb-3 text-green-700 dark:text-green-400">
{isZh ? '支持' : 'Supported'}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{isZh
? '所有操作都基于 DOM 元素的语义化标签和属性。如果页面结构不够语义化,或者没有任何 accessibility 特性,可能影响 AI 的理解准确性。'
: 'All operations rely on semantic tags and attributes. Poor semantic structure or lack of accessibility features may affect AI understanding accuracy.'}
</p>
<ul className="space-y-1.5 text-sm">
{[
isZh ? '点击、文本输入、选择' : 'Click, text input, select',
isZh ? '页面滚动(垂直 / 水平)' : 'Scroll (vertical / horizontal)',
isZh ? '表单提交、焦点切换' : 'Form submit, focus',
isZh ? '执行 JavaScript(可选)' : 'Execute JavaScript (opt-in)',
].map((text) => (
<li key={text} className="flex items-center text-gray-700 dark:text-gray-300">
<span className="mr-2 text-green-600 dark:text-green-400"></span>
{text}
</li>
))}
</ul>
</div>
<div>
<h3 className="font-semibold mb-2">{isZh ? 'UI/UX' : 'UI/UX'}</h3>
<p className="text-gray-600 dark:text-gray-300">
{isZh
? '反常识的交互规则、基于视觉的操作提示、复杂的鼠标交互、快速出现快速消失的元素等,都会影响 AI 的理解和操作。'
: 'Counter-intuitive interaction rules, visual-only operation hints, complex mouse interactions, or rapidly appearing/disappearing elements can affect AI understanding and operation.'}
</p>
</div>
<div>
<h3 className="font-semibold mb-2">{isZh ? '环境要求' : 'Environment'}</h3>
<p className="text-gray-600 dark:text-gray-300">modern browser</p>
<h3 className="font-semibold mb-3 text-red-700 dark:text-red-400">
{isZh ? '不支持' : 'Not supported'}
</h3>
<ul className="space-y-1.5 text-sm">
{[
isZh ? '悬停、拖拽、右键菜单' : 'Hover, drag & drop, right-click',
isZh ? '键盘快捷键' : 'Keyboard shortcuts',
isZh ? '坐标定位操作' : 'Position-based control',
isZh ? '绘图操作' : 'Drawing',
].map((text) => (
<li key={text} className="flex items-center text-gray-700 dark:text-gray-300">
<span className="mr-2 text-red-600 dark:text-red-400"></span>
{text}
</li>
))}
</ul>
</div>
</div>
</div>
<h2>{isZh ? '未来规划' : 'Future Plans'}</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">
{isZh ? '即将支持' : 'Coming Soon'}
</h3>
<ul className="text-green-700 dark:text-green-300 space-y-1">
<li>{isZh ? '• 多页面接力操作能力' : '• Multi-page relay capabilities'}</li>
<li>{isZh ? '• 更丰富的鼠标交互支持' : '• Richer mouse interaction support'}</li>
<li>{isZh ? '• 基础的视觉理解能力' : '• Basic visual understanding'}</li>
<li>{isZh ? '• 更智能的错误恢复机制' : '• Smarter error recovery'}</li>
</ul>
{/* Understanding Limitations */}
<h2 className="text-2xl font-bold mb-3">{isZh ? '理解能力' : 'Understanding'}</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 border-l-4 border-amber-400 p-4 mb-6">
<p className="text-amber-800 dark:text-amber-200 mb-2 font-medium">
{isZh
? 'Page Agent 不使用多模态模型,不截图,没有视觉能力。仅通过 DOM 结构理解页面。'
: 'Page Agent does not use multimodal models, does not take screenshots, and has no visual capability. It reads pages through DOM structure only.'}
</p>
<p className="text-amber-700 dark:text-amber-300 text-sm">
{isZh
? '图片、Canvas、WebGL、SVG 等视觉内容无法被识别。页面的语义化程度和可访问性直接影响 AI 的理解准确性。'
: 'Images, Canvas, WebGL, SVG and other visual content cannot be recognized. Page semantic quality and accessibility directly affect AI accuracy.'}
</p>
</div>
{/* Website Quality */}
<h2 className="text-2xl font-bold mb-3">
{isZh ? '网页质量影响' : 'Page Quality Matters'}
</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
<p className="text-gray-600 dark:text-gray-300 text-sm">
{isZh
? '反常识的交互逻辑、纯视觉的操作提示、快速出现消失的元素等都会降低自动化成功率。语义化的 HTML 和良好的可访问性会显著提升效果。'
: 'Counter-intuitive interactions, visual-only cues, and rapidly appearing/disappearing elements reduce automation success. Semantic HTML and good accessibility significantly improve results.'}
</p>
</div>
</div>
</div>