Compare commits
60 Commits
v0.2.0
...
v0.3.0-beta.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b69a7ee00 | |||
| 25e4d07fc5 | |||
| 6a381dede1 | |||
| b3bfbb96bb | |||
| 3d10953324 | |||
| 8dd4c5d78e | |||
| 8bde218f28 | |||
| 0643cc4ff9 | |||
| 4c5dd23fcd | |||
| 3ce4d8e3fe | |||
| b217e6a2ca | |||
| c9f049a733 | |||
| 09c3084629 | |||
| 38c8020e4a | |||
| 5057f7472b | |||
| 6820eec676 | |||
| ef2777b521 | |||
| 8bba3a63f5 | |||
| 722eb10286 | |||
| 15ffc27059 | |||
| 5f162b1a1c | |||
| 083c002ce0 | |||
| 1ede1d9911 | |||
| 8abe3afed9 | |||
| af36f43104 | |||
| b5f260207c | |||
| 9075204c29 | |||
| ce3a80bc53 | |||
| 64c36f67bb | |||
| 78d4919289 | |||
| 1af2607cca | |||
| ab713f3d3a | |||
| 05cc4f36c8 | |||
| 54e9bdad7d | |||
| 0fc88c0080 | |||
| 02f97ef2fa | |||
| 0853ffbbc6 | |||
| 60ac6da09f | |||
| 11a9fb1181 | |||
| 3762d0f1e0 | |||
| ebbe92f7ca | |||
| 9f239b2de3 | |||
| 2410c27b55 | |||
| 6d6c82648c | |||
| f41119c1ad | |||
| 9e6e1b996c | |||
| 1ab23e181f | |||
| a1b485691f | |||
| 694e05b800 | |||
| b761795c1a | |||
| 8cc54983aa | |||
| 59a599285e | |||
| 2f8c2b395c | |||
| 28f836674c | |||
| 6f01632231 | |||
| 41123ac561 | |||
| c8bf0dd20f | |||
| ec2b744bab | |||
| eab9cf64e8 | |||
| 42130d6f1d |
@@ -58,4 +58,4 @@ body:
|
|||||||
- label: I have read the Code of Conduct. / 我已阅读行为准则。
|
- label: I have read the Code of Conduct. / 我已阅读行为准则。
|
||||||
required: true
|
required: true
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -44,4 +44,4 @@ body:
|
|||||||
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
|
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
|
||||||
required: true
|
required: true
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write # Required for OIDC
|
id-token: write # Required for OIDC
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
# Ensure npm 11.5.1 or later is installed
|
# Ensure npm 11.5.1 or later is installed
|
||||||
- name: Update npm
|
- name: Update npm
|
||||||
run: npm install -g npm@latest
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -32,4 +32,4 @@ jobs:
|
|||||||
run: npm run build:libs
|
run: npm run build:libs
|
||||||
|
|
||||||
- name: Publish all public packages
|
- name: Publish all public packages
|
||||||
run: npm publish --workspaces --access public
|
run: npm publish --workspaces --access public
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["dbaeumer.vscode-eslint", "prettier.prettier-vscode"]
|
"recommendations": ["dbaeumer.vscode-eslint", "prettier.prettier-vscode"]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+29
-26
@@ -1,28 +1,31 @@
|
|||||||
{
|
{
|
||||||
"editor.fontLigatures": true,
|
"editor.fontLigatures": true,
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"deepseek",
|
"deepseek",
|
||||||
"HITL",
|
"historychange",
|
||||||
"innerhtml",
|
"HITL",
|
||||||
"llms",
|
"innerhtml",
|
||||||
"opensource",
|
"llms",
|
||||||
"qwen",
|
"onwarn",
|
||||||
"retryable",
|
"opensource",
|
||||||
"shadcn",
|
"qwen",
|
||||||
"wouter"
|
"retryable",
|
||||||
],
|
"shadcn",
|
||||||
"markdownlint.config": {
|
"statuschange",
|
||||||
// "comment": "Relaxed rules",
|
"wouter"
|
||||||
"default": true,
|
],
|
||||||
"whitespace": false,
|
"markdownlint.config": {
|
||||||
"line_length": false,
|
// "comment": "Relaxed rules",
|
||||||
"ul-indent": false,
|
"default": true,
|
||||||
"no-inline-html": false,
|
"whitespace": false,
|
||||||
"no-bare-urls": false,
|
"line_length": false,
|
||||||
"fenced-code-language": false,
|
"ul-indent": false,
|
||||||
"first-line-h1": false,
|
"no-inline-html": false,
|
||||||
"block-spacing": false,
|
"no-bare-urls": false,
|
||||||
"blanks-around-lists": false,
|
"fenced-code-language": false,
|
||||||
"ol-prefix": false
|
"first-line-h1": false,
|
||||||
}
|
"block-spacing": false,
|
||||||
|
"blanks-around-lists": false,
|
||||||
|
"ol-prefix": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
This is a **monorepo** with npm workspaces:
|
This is a **monorepo** with npm workspaces:
|
||||||
|
|
||||||
- **Core Library** (`packages/page-agent/`) - AI agent for browser DOM automation, published as `page-agent` on npm
|
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
|
||||||
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
|
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
|
||||||
|
|
||||||
Internal packages:
|
Internal packages:
|
||||||
|
|
||||||
- **CDN** (`packages/cdn/`) - IIFE builds for script tag usage (npm: `@page-agent/cdn`)
|
- **Core** (`packages/core/`) - PageAgentCore without UI (npm: `@page-agent/core`)
|
||||||
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
|
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
|
||||||
- **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback (SimulatorMask), independent of LLM
|
- **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback (SimulatorMask), independent of LLM
|
||||||
- **UI** (`packages/ui/`) - Panel and i18n. Decoupled from PageAgent
|
- **UI** (`packages/ui/`) - Panel and i18n. Decoupled from PageAgent
|
||||||
@@ -31,8 +31,8 @@ Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig
|
|||||||
|
|
||||||
```
|
```
|
||||||
packages/
|
packages/
|
||||||
├── page-agent/ # npm: "page-agent" ⭐ MAIN
|
├── page-agent/ # npm: "page-agent" ⭐ MAIN (with Panel UI + IIFE builds)
|
||||||
├── cdn/ # npm: "@page-agent/cdn" (IIFE builds)
|
├── core/ # npm: "@page-agent/core" (headless, no UI)
|
||||||
├── website/ # @page-agent/website (private)
|
├── website/ # @page-agent/website (private)
|
||||||
├── llms/ # @page-agent/llms
|
├── llms/ # @page-agent/llms
|
||||||
├── page-controller/ # @page-agent/page-controller
|
├── page-controller/ # @page-agent/page-controller
|
||||||
@@ -43,9 +43,10 @@ packages/
|
|||||||
|
|
||||||
### Module Boundaries
|
### Module Boundaries
|
||||||
|
|
||||||
- **Page Agent**: Core lib. Imports from `@page-agent/llms`, `@page-agent/page-controller`, `@page-agent/ui`
|
- **Page Agent**: Main entry with UI. Extends PageAgentCore and adds Panel. Imports from `@page-agent/core`, `@page-agent/ui`
|
||||||
|
- **Core**: PageAgentCore without UI. Imports from `@page-agent/llms`, `@page-agent/page-controller`
|
||||||
- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent
|
- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent
|
||||||
- **UI**: Panel and i18n. No dependency on page-agent
|
- **UI**: Panel and i18n. Decoupled from PageAgent via PanelAgentAdapter interface
|
||||||
- **Page Controller**: DOM operations with optional visual feedback (SimulatorMask). No LLM dependency. Enable mask via `enableMask: true` config
|
- **Page Controller**: DOM operations with optional visual feedback (SimulatorMask). No LLM dependency. Enable mask via `enableMask: true` config
|
||||||
|
|
||||||
### PageController ↔ PageAgent Communication
|
### PageController ↔ PageAgent Communication
|
||||||
@@ -71,7 +72,7 @@ const pageInfo = await this.pageController.getPageInfo()
|
|||||||
3. **LLM Processing**: AI returns action plans (page-agent)
|
3. **LLM Processing**: AI returns action plans (page-agent)
|
||||||
4. **Indexed Operations**: PageAgent calls PageController by element index
|
4. **Indexed Operations**: PageAgent calls PageController by element index
|
||||||
|
|
||||||
### CDN Builds (`packages/cdn/`)
|
### IIFE Builds (`packages/page-agent/dist/iife/`)
|
||||||
|
|
||||||
Two IIFE builds for script tag usage:
|
Two IIFE builds for script tag usage:
|
||||||
|
|
||||||
@@ -82,15 +83,26 @@ Two IIFE builds for script tag usage:
|
|||||||
|
|
||||||
Demo build supports query params (e.g., `?model=gpt-4&lang=en-US`).
|
Demo build supports query params (e.g., `?model=gpt-4&lang=en-US`).
|
||||||
|
|
||||||
|
Build with `npm run build:iife --workspace=page-agent`.
|
||||||
|
|
||||||
## Key Files Reference
|
## Key Files Reference
|
||||||
|
|
||||||
### Page Agent (`packages/page-agent/`)
|
### Page Agent (`packages/page-agent/`)
|
||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
| ------------------ | --------------------------------------- |
|
| ----------------------- | -------------------------------------------- |
|
||||||
| `src/PageAgent.ts` | ⭐ Main AI agent class |
|
| `src/PageAgent.ts` | ⭐ Main class with UI, extends PageAgentCore |
|
||||||
| `src/umd.ts` | CDN/UMD entry with auto-init |
|
| `src/entry-iife.ts` | IIFE entry (exposes PageAgent class) |
|
||||||
| `src/tools/` | Tool definitions calling PageController |
|
| `src/entry-iife-demo.ts`| IIFE demo entry (auto-init with demo API) |
|
||||||
|
|
||||||
|
### Core (`packages/core/`)
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
| ----------------------- | ------------------------------------------- |
|
||||||
|
| `src/PageAgentCore.ts` | ⭐ Core agent class without UI |
|
||||||
|
| `src/tools/` | Tool definitions calling PageController |
|
||||||
|
| `src/config/` | Configuration types and constants |
|
||||||
|
| `src/prompts/` | System prompt templates |
|
||||||
|
|
||||||
### LLMs (`packages/llms/`)
|
### LLMs (`packages/llms/`)
|
||||||
|
|
||||||
@@ -113,7 +125,7 @@ Demo build supports query params (e.g., `?model=gpt-4&lang=en-US`).
|
|||||||
|
|
||||||
### New Agent Tool
|
### New Agent Tool
|
||||||
|
|
||||||
1. Implement in `packages/page-agent/src/tools/index.ts`
|
1. Implement in `packages/core/src/tools/index.ts`
|
||||||
2. If tool needs DOM ops, add method to PageController first
|
2. If tool needs DOM ops, add method to PageController first
|
||||||
3. Tool calls `this.pageController.methodName()` for DOM interactions
|
3. Tool calls `this.pageController.methodName()` for DOM interactions
|
||||||
|
|
||||||
|
|||||||
+65
-54
@@ -7,29 +7,30 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
|
|||||||
### Development Setup
|
### Development Setup
|
||||||
|
|
||||||
1. **Prerequisites**
|
1. **Prerequisites**
|
||||||
- `node.js >= 20` with `npm >= 10`
|
- `node.js >= 20` with `npm >= 10`
|
||||||
- An editor that supports `ts/eslint/prettier`
|
- An editor that supports `ts/eslint/prettier`
|
||||||
- Make sure `eslint`, `prettier` and `commitlint` work well
|
- Make sure `eslint`, `prettier` and `commitlint` work well
|
||||||
|
|
||||||
2. **Setup**
|
2. **Setup**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm ci
|
||||||
npm start # Start demo and documentation site
|
npm start # Start demo and documentation site
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
This is a **monorepo** with npm workspaces containing **two main packages**:
|
This is a **monorepo** with npm workspaces containing **3 main packages**:
|
||||||
|
|
||||||
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
|
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
|
||||||
2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
|
||||||
|
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
||||||
|
|
||||||
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
|
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
|
||||||
|
|
||||||
- When developing. Use alias so that we don't have to pre-build.
|
- When developing. Use alias so that we don't have to pre-build.
|
||||||
- When bundling. Use external and disable ts `paths` alias to leave deps out.
|
- When bundling. Use external and disable ts `paths` alias to leave deps out.
|
||||||
- When bundling `UMD` and `Website`. Bundle everything including local packages.
|
- When bundling `IIFE` and `Website`. Bundle everything including local packages.
|
||||||
|
|
||||||
## 🤝 How to Contribute
|
## 🤝 How to Contribute
|
||||||
|
|
||||||
@@ -44,40 +45,39 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
|
|||||||
|
|
||||||
1. **Fork and Clone**
|
1. **Fork and Clone**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/your-username/page-agent.git
|
git clone https://github.com/your-username/page-agent.git
|
||||||
cd page-agent
|
cd page-agent
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Create Feature Branch**
|
2. **Create Feature Branch**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout -b feat/your-feature-name
|
git checkout -b feat/your-feature-name
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Make Changes**
|
3. **Make Changes**
|
||||||
- Follow existing code style and patterns
|
- Follow existing code style and patterns
|
||||||
- Add tests for new functionality
|
- Add tests for new functionality
|
||||||
- Update documentation as needed
|
- Update documentation as needed
|
||||||
|
|
||||||
4. **Test Your Changes**
|
4. **Test Your Changes**
|
||||||
|
- Test in our demo website
|
||||||
```bash
|
- Test it on other websites if applicable
|
||||||
# TODO
|
- `@TODO: test suite`
|
||||||
```
|
|
||||||
|
|
||||||
5. **Commit and Push**
|
5. **Commit and Push**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add .
|
git add .
|
||||||
git commit -m "feat: add awesome feature"
|
git commit -m "feat: add awesome feature"
|
||||||
git push origin feat/your-feature-name
|
git push origin feat/your-feature-name
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Create Pull Request**
|
6. **Create Pull Request**
|
||||||
- Provide clear description of changes
|
- Provide clear description of changes
|
||||||
- Link related issues
|
- Link related issues
|
||||||
- Include screenshots for UI changes
|
- Include screenshots for UI changes
|
||||||
|
|
||||||
## 📝 Code Style
|
## 📝 Code Style
|
||||||
|
|
||||||
@@ -94,41 +94,52 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
|
|||||||
- It's **recommended** to heavily rely on AI (aka "vibe coding") when maintaining **demo pages and tests**.
|
- It's **recommended** to heavily rely on AI (aka "vibe coding") when maintaining **demo pages and tests**.
|
||||||
- BUT **NOT the core lib!!!** Be very careful if AI ever touched the core lib!!!
|
- BUT **NOT the core lib!!!** Be very careful if AI ever touched the core lib!!!
|
||||||
- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.
|
- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.
|
||||||
- Update the AI instructions when structure changed.
|
- Update the [AI instructions](AGENTS.md) when structure changed.
|
||||||
- Cursor and Cline: `./.cursor/rules`
|
|
||||||
- Github Copilot: `./.github/copilot-instructions.md`
|
|
||||||
- Claude Code: `./CLAUDE.md`
|
|
||||||
|
|
||||||
## 🔧 Development Workflows
|
## 🔧 Development Workflows
|
||||||
|
|
||||||
|
### Test With Your Own LLM API
|
||||||
|
|
||||||
|
- Create a `.env` file in the repo root with your LLM API config
|
||||||
|
|
||||||
|
```env
|
||||||
|
LLM_MODEL_NAME=gpt-5.2
|
||||||
|
LLM_API_KEY=your-api-key
|
||||||
|
LLM_BASE_URL=https://api.your-llm-provider.com/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
- Restart the dev server to load new env vars
|
||||||
|
- If not provided, the demo will the free testing proxy by default
|
||||||
|
|
||||||
### Website Development
|
### Website Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start # React development server
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Core Lib Development and Testing
|
### Testing on Other Websites
|
||||||
|
|
||||||
> @TODO this part is outdated. Update this.
|
- Start and serve a local `iife` script
|
||||||
|
|
||||||
- Config your LLM API
|
```bash
|
||||||
- Start and serve a local umd script
|
npm run dev:iife # Serving IIFE with auto rebuild at http://localhost:5174/page-agent.js
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
- Add a new bookmark
|
||||||
npm run dev:umd # Serving UMD with auto rebuild at http://localhost:5173/page-agent.umd.cjs
|
|
||||||
```
|
|
||||||
|
|
||||||
- Add a new bookmark enable it on other website
|
```javascript
|
||||||
|
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5174/page-agent.js?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
|
||||||
|
```
|
||||||
|
|
||||||
```
|
- Click the bookmark on any page to load Page-Agent
|
||||||
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
|
|
||||||
```
|
> Warning: AK in your local `.env` will be inlined in the iife script.
|
||||||
|
|
||||||
### Adding Documentation
|
### Adding Documentation
|
||||||
|
|
||||||
1. Create `website/src/docs/section/page-name/page.tsx`
|
Ask an AI to help you add documentation to the `website/` package. Follow the existing style.
|
||||||
2. Add route to `website/src/router.tsx`
|
|
||||||
3. Add navigation link to `website/src/components/DocsLayout.tsx`
|
> Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated.
|
||||||
|
|
||||||
## 🎯 Contribution Areas
|
## 🎯 Contribution Areas
|
||||||
|
|
||||||
@@ -143,7 +154,7 @@ We especially welcome contributions in:
|
|||||||
|
|
||||||
## 🚫 What We Don't Accept
|
## 🚫 What We Don't Accept
|
||||||
|
|
||||||
- Changes that break existing API compatibility
|
- Changes that break existing API compatibility (Discuss first)
|
||||||
- Heavy dependencies to core library
|
- Heavy dependencies to core library
|
||||||
- Contributions without proper testing
|
- Contributions without proper testing
|
||||||
- Code that doesn't follow project conventions
|
- Code that doesn't follow project conventions
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Alibaba
|
Copyright (c) 2026 Alibaba
|
||||||
|
Copyright (c) 2026 Simon
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
+34
-51
@@ -1,8 +1,11 @@
|
|||||||
# PageAgent 🤖🪄
|
# PageAgent 🤖🪄
|
||||||
|
|
||||||

|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
|
||||||
|
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
|
||||||
|
</picture>
|
||||||
|
|
||||||
[](https://badge.fury.io/js/page-agent) [](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://www.npmjs.com/package/page-agent) [](https://bundlephobia.com/package/page-agent) [](https://github.com/alibaba/page-agent)
|
[](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://www.npmjs.com/package/page-agent) [](https://bundlephobia.com/package/page-agent) [](https://github.com/alibaba/page-agent)
|
||||||
|
|
||||||
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
|
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
|
||||||
|
|
||||||
@@ -10,7 +13,7 @@
|
|||||||
|
|
||||||
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
||||||
|
|
||||||
<video id="demo-video" src="https://github.com/user-attachments/assets/141bbb01-8022-4d1f-919d-9efc9a1dc1cf" width="640" crossorigin muted autoplay loop></video>
|
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,22 +31,25 @@
|
|||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
### 快速体验 (Demo CDN)
|
### 快速体验 (With Testing LLM)
|
||||||
|
|
||||||
最快的体验方式:
|
最快的体验方式:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js" crossorigin="true"></script>
|
<script
|
||||||
|
src="https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js"
|
||||||
|
crossorigin="true"
|
||||||
|
></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ **仅用于技术评估。** Demo 模型有速率限制和使用限制,生产环境请使用 NPM 方式。
|
> ⚠️ **仅用于技术评估。** Demo 模型有速率限制和使用限制,生产环境请使用 NPM 方式。
|
||||||
|
|
||||||
| 位置 | URL |
|
| Mirrors | URL |
|
||||||
| ------ | --------------------------------------------------------------------------------------- |
|
| ------- | ----------------------------------------------------------------------------------- |
|
||||||
| 全球 | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js |
|
||||||
| 中国 | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js |
|
||||||
|
|
||||||
### NPM 安装(推荐)
|
### NPM 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install page-agent
|
npm install page-agent
|
||||||
@@ -53,35 +59,16 @@ npm install page-agent
|
|||||||
import { PageAgent } from 'page-agent'
|
import { PageAgent } from 'page-agent'
|
||||||
|
|
||||||
const agent = new PageAgent({
|
const agent = new PageAgent({
|
||||||
model: 'deepseek-chat',
|
model: 'deepseek-chat',
|
||||||
baseURL: 'https://api.deepseek.com',
|
baseURL: 'https://api.deepseek.com',
|
||||||
apiKey: 'YOUR_API_KEY',
|
apiKey: 'YOUR_API_KEY',
|
||||||
language: 'zh-CN',
|
language: 'zh-CN',
|
||||||
})
|
})
|
||||||
|
|
||||||
await agent.execute('点击登录按钮')
|
await agent.execute('点击登录按钮')
|
||||||
```
|
```
|
||||||
|
|
||||||
### CDN 引入
|
适用于无法使用 NPM 的环境,我们也提供了 IIFE 构建的 CDN 方式。[@see CDN Usage](https://alibaba.github.io/page-agent/#/docs/integration/cdn-setup)
|
||||||
|
|
||||||
适用于无法使用 NPM 的环境,用法与 NPM 一致:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js" crossorigin="true"></script>
|
|
||||||
<script>
|
|
||||||
const agent = new PageAgent({
|
|
||||||
model: 'deepseek-chat',
|
|
||||||
baseURL: 'https://api.deepseek.com',
|
|
||||||
apiKey: 'YOUR_API_KEY',
|
|
||||||
})
|
|
||||||
await agent.execute('点击登录按钮')
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
| 位置 | URL |
|
|
||||||
| ------ | ----------------------------------------------------------------------------------- |
|
|
||||||
| 全球 | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js |
|
|
||||||
| 中国 | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js |
|
|
||||||
|
|
||||||
## 🏗️ 架构设计
|
## 🏗️ 架构设计
|
||||||
|
|
||||||
@@ -89,7 +76,8 @@ PageAgent adopts a simplified monorepo structure:
|
|||||||
|
|
||||||
```
|
```
|
||||||
packages/
|
packages/
|
||||||
├── page-agent/ # AI agent (npm: page-agent)
|
├── page-agent/ # AI agent with UI Panel(npm: page-agent)
|
||||||
|
├── core/ # Agent core logic without UI(npm: @page-agent/core)
|
||||||
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
|
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
|
||||||
├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
|
├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
|
||||||
├── ui/ # 面板 & i18n (npm: @page-agent/ui)
|
├── ui/ # 面板 & i18n (npm: @page-agent/ui)
|
||||||
@@ -101,28 +89,19 @@ packages/
|
|||||||
|
|
||||||
欢迎社区贡献!以下是参与方式:
|
欢迎社区贡献!以下是参与方式:
|
||||||
|
|
||||||
### 开发环境
|
1. Fork 并 clone。`git clone https://github.com/alibaba/page-agent.git && cd page-agent`
|
||||||
|
2. 安装依赖: `npm install`
|
||||||
|
3. 启动开发: `npm start`
|
||||||
|
|
||||||
1. Fork 项目仓库
|
更多详情请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
||||||
2. Clone or fork: `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
|
|
||||||
3. 安装依赖: `npm install`
|
|
||||||
4. 启动开发: `npm start`
|
|
||||||
|
|
||||||
### 贡献指南
|
请在贡献前阅读我们的[行为准则](CODE_OF_CONDUCT.md)。
|
||||||
|
|
||||||
请在贡献前阅读我们的[行为准则](CODE_OF_CONDUCT.md)和[贡献指南](CONTRIBUTING.md)。
|
|
||||||
|
|
||||||
## 👏 致谢
|
## 👏 致谢
|
||||||
|
|
||||||
本项目基于以下优秀项目构建:
|
本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建。
|
||||||
|
|
||||||
- **[browser-use](https://github.com/browser-use/browser-use)**
|
`PageAgent` 专为**客户端网页增强**设计,不是服务端自动化工具。
|
||||||
|
|
||||||
PageAgent 专为**客户端网页增强**设计,不是服务端自动化工具。
|
|
||||||
|
|
||||||
## 📄 许可证
|
|
||||||
|
|
||||||
MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。
|
|
||||||
|
|
||||||
```
|
```
|
||||||
DOM processing components and prompt are derived from browser-use:
|
DOM processing components and prompt are derived from browser-use:
|
||||||
@@ -141,6 +120,10 @@ Third-party dependencies and their licenses can be found in the package.json
|
|||||||
file and in the node_modules directory after installation.
|
file and in the node_modules directory after installation.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
[MIT License](LICENSE)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
|
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
# PageAgent 🤖🪄
|
# PageAgent 🤖🪄
|
||||||
|
|
||||||

|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
|
||||||
|
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
|
||||||
|
</picture>
|
||||||
|
|
||||||
[](https://badge.fury.io/js/page-agent) [](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://www.npmjs.com/package/page-agent) [](https://bundlephobia.com/package/page-agent) [](https://github.com/alibaba/page-agent)
|
[](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://www.npmjs.com/package/page-agent) [](https://bundlephobia.com/package/page-agent) [](https://github.com/alibaba/page-agent)
|
||||||
|
|
||||||
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
|
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
|
||||||
|
|
||||||
@@ -10,7 +13,7 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
|
|||||||
|
|
||||||
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
||||||
|
|
||||||
<video id="demo-video" src="https://github.com/user-attachments/assets/de8d1964-8bde-494f-a52f-2975469557a5" width="640" crossorigin muted autoplay loop></video>
|
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,22 +31,25 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
|
|||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Quick Try (Demo CDN)
|
### Quick Try (With Testing LLM)
|
||||||
|
|
||||||
Fastest way to try PageAgent:
|
Fastest way to try PageAgent:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js" crossorigin="true"></script>
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/page-agent/dist/iife/page-agent.demo.js"
|
||||||
|
crossorigin="true"
|
||||||
|
></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ **For technical evaluation only.** Demo model has rate limits and usage restrictions. Use NPM for production.
|
> ⚠️ **For technical evaluation only.** Demo model has rate limits and usage restrictions. Use NPM for production.
|
||||||
|
|
||||||
| Location | URL |
|
| Mirrors | URL |
|
||||||
| -------- | --------------------------------------------------------------------------------------- |
|
| ------- | ----------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/latest/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
### NPM Installation (Recommended)
|
### NPM Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install page-agent
|
npm install page-agent
|
||||||
@@ -53,43 +59,23 @@ npm install page-agent
|
|||||||
import { PageAgent } from 'page-agent'
|
import { PageAgent } from 'page-agent'
|
||||||
|
|
||||||
const agent = new PageAgent({
|
const agent = new PageAgent({
|
||||||
model: 'deepseek-chat',
|
model: 'deepseek-chat',
|
||||||
baseURL: 'https://api.deepseek.com',
|
baseURL: 'https://api.deepseek.com',
|
||||||
apiKey: 'YOUR_API_KEY',
|
apiKey: 'YOUR_API_KEY',
|
||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
})
|
})
|
||||||
|
|
||||||
await agent.execute('Click the login button')
|
await agent.execute('Click the login button')
|
||||||
```
|
```
|
||||||
|
|
||||||
### CDN Build
|
|
||||||
|
|
||||||
For environments where NPM is not available. Usage is identical to NPM:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js" crossorigin="true"></script>
|
|
||||||
<script>
|
|
||||||
const agent = new PageAgent({
|
|
||||||
model: 'deepseek-chat',
|
|
||||||
baseURL: 'https://api.deepseek.com',
|
|
||||||
apiKey: 'YOUR_API_KEY',
|
|
||||||
})
|
|
||||||
await agent.execute('Click the login button')
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
| Location | URL |
|
|
||||||
| -------- | ----------------------------------------------------------------------------------- |
|
|
||||||
| Global | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js |
|
|
||||||
| China | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js |
|
|
||||||
|
|
||||||
## 🏗️ Structure
|
## 🏗️ Structure
|
||||||
|
|
||||||
PageAgent adopts a simplified monorepo structure:
|
PageAgent adopts a simplified monorepo structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
packages/
|
packages/
|
||||||
├── page-agent/ # AI agent (npm: page-agent)
|
├── page-agent/ # AI agent with UI Panel(npm: page-agent)
|
||||||
|
├── core/ # Agent core logic without UI(npm: @page-agent/core)
|
||||||
├── llms/ # LLM client (npm: @page-agent/llms)
|
├── llms/ # LLM client (npm: @page-agent/llms)
|
||||||
├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
|
├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
|
||||||
├── ui/ # Panel & i18n (npm: @page-agent/ui)
|
├── ui/ # Panel & i18n (npm: @page-agent/ui)
|
||||||
@@ -101,28 +87,19 @@ packages/
|
|||||||
|
|
||||||
We welcome contributions from the community! Here's how to get started:
|
We welcome contributions from the community! Here's how to get started:
|
||||||
|
|
||||||
### Setup
|
1. Fork and clone. `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
|
||||||
|
2. Install dependencies: `npm install`
|
||||||
|
3. Start development: `npm start`
|
||||||
|
|
||||||
1. Fork the repository
|
More details in [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
2. Clone your fork: `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
|
|
||||||
3. Install dependencies: `npm install`
|
|
||||||
4. Start development: `npm start`
|
|
||||||
|
|
||||||
### Contributing Guidelines
|
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing.
|
||||||
|
|
||||||
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) before contributing.
|
|
||||||
|
|
||||||
## 👏 Acknowledgments
|
## 👏 Acknowledgments
|
||||||
|
|
||||||
This project builds upon the excellent work of:
|
This project builds upon the excellent work of **[`browser-use`](https://github.com/browser-use/browser-use)**.
|
||||||
|
|
||||||
- **[browser-use](https://github.com/browser-use/browser-use)**
|
`PageAgent` is designed for **client-side web enhancement**, not server-side automation.
|
||||||
|
|
||||||
PageAgent is designed for **client-side web enhancement**, not server-side automation.
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
MIT License - see the [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
DOM processing components and prompt are derived from browser-use:
|
DOM processing components and prompt are derived from browser-use:
|
||||||
@@ -141,6 +118,10 @@ Third-party dependencies and their licenses can be found in the package.json
|
|||||||
file and in the node_modules directory after installation.
|
file and in the node_modules directory after installation.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
[MIT License](LICENSE)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**⭐ Star this repo if you find PageAgent helpful!**
|
**⭐ Star this repo if you find PageAgent helpful!**
|
||||||
|
|||||||
Generated
+31
-32
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
"packages/ui",
|
"packages/ui",
|
||||||
"packages/llms",
|
"packages/llms",
|
||||||
|
"packages/core",
|
||||||
"packages/page-agent",
|
"packages/page-agent",
|
||||||
"packages/cdn",
|
|
||||||
"packages/website"
|
"packages/website"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1540,20 +1540,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@microsoft/api-extractor/node_modules/typescript": {
|
|
||||||
"version": "5.8.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
|
||||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@microsoft/api-extractor/node_modules/yallist": {
|
"node_modules/@microsoft/api-extractor/node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
@@ -1598,8 +1584,8 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@page-agent/cdn": {
|
"node_modules/@page-agent/core": {
|
||||||
"resolved": "packages/cdn",
|
"resolved": "packages/core",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@page-agent/llms": {
|
"node_modules/@page-agent/llms": {
|
||||||
@@ -4627,9 +4613,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
|
||||||
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
|
"integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -8147,15 +8133,27 @@
|
|||||||
},
|
},
|
||||||
"packages/cdn": {
|
"packages/cdn": {
|
||||||
"name": "@page-agent/cdn",
|
"name": "@page-agent/cdn",
|
||||||
"version": "0.2.0",
|
"version": "0.2.5",
|
||||||
|
"extraneous": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"page-agent": "0.2.0"
|
"page-agent": "0.2.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/core": {
|
||||||
|
"name": "@page-agent/core",
|
||||||
|
"version": "0.3.0-beta.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@page-agent/llms": "0.3.0-beta.1",
|
||||||
|
"@page-agent/page-controller": "0.3.0-beta.1",
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/llms": {
|
"packages/llms": {
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
@@ -8163,19 +8161,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/page-agent": {
|
"packages/page-agent": {
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/llms": "0.2.0",
|
"@page-agent/core": "0.3.0-beta.1",
|
||||||
"@page-agent/page-controller": "0.2.0",
|
"@page-agent/llms": "0.3.0-beta.1",
|
||||||
"@page-agent/ui": "0.2.0",
|
"@page-agent/page-controller": "0.3.0-beta.1",
|
||||||
|
"@page-agent/ui": "0.3.0-beta.1",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/page-controller": {
|
"packages/page-controller": {
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ai-motion": "^0.4.8"
|
"ai-motion": "^0.4.8"
|
||||||
@@ -8183,12 +8182,12 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"packages/website": {
|
"packages/website": {
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
|||||||
+23
-4
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
"packages/ui",
|
"packages/ui",
|
||||||
"packages/llms",
|
"packages/llms",
|
||||||
|
"packages/core",
|
||||||
"packages/page-agent",
|
"packages/page-agent",
|
||||||
"packages/cdn",
|
|
||||||
"packages/website"
|
"packages/website"
|
||||||
],
|
],
|
||||||
"description": "AI-powered UI agent for web applications",
|
"description": "AI-powered UI agent for web applications",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"build:website": "npm run build:website --workspace=@page-agent/website",
|
"build:website": "npm run build:website --workspace=@page-agent/website",
|
||||||
"build:libs": "npm run build --workspaces --if-present",
|
"build:libs": "npm run build --workspaces --if-present",
|
||||||
"build": "npm run build:libs && npm run build:website",
|
"build": "npm run build:libs && npm run build:website",
|
||||||
"dev:umd": "npm run dev:umd --workspace=page-agent",
|
"dev:iife": "npm run dev:iife --workspace=page-agent",
|
||||||
"version": "node scripts/sync-version.js",
|
"version": "node scripts/sync-version.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
@@ -59,6 +59,9 @@
|
|||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-css-injected-by-js": "^3.5.2"
|
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,cjs,cts,mjs,mts}": [
|
"*.{js,ts,cjs,cts,mjs,mts}": [
|
||||||
"npx prettier --write --ignore-unknown",
|
"npx prettier --write --ignore-unknown",
|
||||||
@@ -99,6 +102,22 @@
|
|||||||
".css$"
|
".css$"
|
||||||
],
|
],
|
||||||
"importOrderSeparation": true,
|
"importOrderSeparation": true,
|
||||||
"importOrderSortSpecifiers": true
|
"importOrderSortSpecifiers": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.md",
|
||||||
|
"options": {
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "*.json",
|
||||||
|
"options": {
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@page-agent/cdn",
|
|
||||||
"private": false,
|
|
||||||
"version": "0.2.0",
|
|
||||||
"type": "module",
|
|
||||||
"files": [
|
|
||||||
"dist/"
|
|
||||||
],
|
|
||||||
"description": "CDN builds for page-agent - IIFE bundles for script tag usage",
|
|
||||||
"author": "Simon<gaomeng1900>",
|
|
||||||
"license": "MIT",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/alibaba/page-agent.git"
|
|
||||||
},
|
|
||||||
"homepage": "https://alibaba.github.io/page-agent/",
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build && vite build --mode demo"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"page-agent": "0.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
/**
|
|
||||||
* Demo CDN build for page-agent
|
|
||||||
* Auto-initializes with built-in demo API for testing
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* <script src="https://unpkg.com/@page-agent/cdn/dist/page-agent.demo.js"></script>
|
|
||||||
*/
|
|
||||||
import { PageAgent } from 'page-agent'
|
|
||||||
|
|
||||||
// Clean up existing instances to prevent multiple injections from bookmarklet
|
|
||||||
if (window.pageAgent) {
|
|
||||||
window.pageAgent.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mount to global window object
|
|
||||||
window.PageAgent = PageAgent
|
|
||||||
|
|
||||||
// Export for IIFE
|
|
||||||
export { PageAgent }
|
|
||||||
|
|
||||||
console.log('🚀 page-agent.js loaded!')
|
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
// Demo warning
|
|
||||||
console.log(
|
|
||||||
'%c⚠️ DEMO MODE %c\n' +
|
|
||||||
'This build uses a shared testing LLM with rate limits.\n' +
|
|
||||||
'For production, use page-agent.js with your own API key:\n' +
|
|
||||||
'https://alibaba.github.io/page-agent/#/docs/features/models',
|
|
||||||
'background: #f59e0b; color: #000; font-size: 14px; font-weight: bold; padding: 4px 8px; border-radius: 4px;',
|
|
||||||
'color: #f59e0b; font-size: 12px;'
|
|
||||||
)
|
|
||||||
|
|
||||||
// in case document.x is not ready yet
|
|
||||||
setTimeout(() => {
|
|
||||||
const currentScript = document.currentScript
|
|
||||||
|
|
||||||
if (currentScript) {
|
|
||||||
console.log('🚀 page-agent.js detected current script:', currentScript.src)
|
|
||||||
const url = new URL(currentScript.src)
|
|
||||||
const model = url.searchParams.get('model') || DEMO_MODEL
|
|
||||||
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
|
|
||||||
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
|
|
||||||
const language = url.searchParams.get('lang') || 'zh-CN'
|
|
||||||
const config = { model, baseURL, apiKey, language }
|
|
||||||
window.pageAgent = new PageAgent(config)
|
|
||||||
} else {
|
|
||||||
console.log('🚀 page-agent.js no current script detected, using default demo config')
|
|
||||||
const config = {
|
|
||||||
model: DEMO_MODEL,
|
|
||||||
baseURL: DEMO_BASE_URL,
|
|
||||||
apiKey: DEMO_API_KEY,
|
|
||||||
}
|
|
||||||
window.pageAgent = new PageAgent(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
|
||||||
|
|
||||||
window.pageAgent.panel.show()
|
|
||||||
})
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
/**
|
|
||||||
* Full CDN build for page-agent
|
|
||||||
* Exposes PageAgent class for manual instantiation and configuration
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* <script src="https://unpkg.com/@page-agent/cdn/dist/page-agent.js"></script>
|
|
||||||
* <script>
|
|
||||||
* const agent = new PageAgent({ model: 'gpt-4o', apiKey: 'your-key' })
|
|
||||||
* agent.panel.show()
|
|
||||||
* </script>
|
|
||||||
*/
|
|
||||||
import { PageAgent } from 'page-agent'
|
|
||||||
|
|
||||||
export { PageAgent }
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import { dirname, resolve } from 'path'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CDN IIFE builds for page-agent
|
|
||||||
*
|
|
||||||
* Build targets (via --mode):
|
|
||||||
* - demo: page-agent.demo.js with auto-init and built-in API
|
|
||||||
* - full (default): page-agent.js exposes PageAgent class only
|
|
||||||
*/
|
|
||||||
export default defineConfig(({ mode }) => {
|
|
||||||
const isDemo = mode === 'demo'
|
|
||||||
const entry = isDemo ? resolve(__dirname, 'src/demo.js') : resolve(__dirname, 'src/full.js')
|
|
||||||
const fileName = isDemo ? 'page-agent.demo' : 'page-agent'
|
|
||||||
|
|
||||||
return {
|
|
||||||
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
|
||||||
publicDir: false,
|
|
||||||
esbuild: {
|
|
||||||
keepNames: true,
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry,
|
|
||||||
name: 'PageAgent',
|
|
||||||
fileName: () => `${fileName}.js`,
|
|
||||||
formats: ['iife'],
|
|
||||||
},
|
|
||||||
outDir: resolve(__dirname, 'dist'),
|
|
||||||
emptyOutDir: !isDemo, // only empty on first build (full)
|
|
||||||
minify: false,
|
|
||||||
},
|
|
||||||
define: {
|
|
||||||
'process.env.NODE_ENV': '"production"',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "@page-agent/core",
|
||||||
|
"private": false,
|
||||||
|
"version": "0.3.0-beta.1",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/esm/page-agent-core.js",
|
||||||
|
"module": "./dist/esm/page-agent-core.js",
|
||||||
|
"types": "./dist/esm/PageAgentCore.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/esm/PageAgentCore.d.ts",
|
||||||
|
"import": "./dist/esm/page-agent-core.js",
|
||||||
|
"default": "./dist/esm/page-agent-core.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/"
|
||||||
|
],
|
||||||
|
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
|
||||||
|
"keywords": [
|
||||||
|
"ai",
|
||||||
|
"automation",
|
||||||
|
"ui-agent",
|
||||||
|
"GUI-agent",
|
||||||
|
"browser-automation",
|
||||||
|
"web-agent",
|
||||||
|
"llm",
|
||||||
|
"dom-interaction",
|
||||||
|
"web-automation",
|
||||||
|
"GUI-simulation"
|
||||||
|
],
|
||||||
|
"author": "Simon<gaomeng1900>",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/alibaba/page-agent.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
|
||||||
|
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
||||||
|
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"zod": "^4.3.5",
|
||||||
|
"@page-agent/llms": "0.3.0-beta.1",
|
||||||
|
"@page-agent/page-controller": "0.3.0-beta.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,588 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025 Alibaba Group Holding Limited
|
||||||
|
* All rights reserved.
|
||||||
|
*/
|
||||||
|
import { LLM, type Tool } from '@page-agent/llms'
|
||||||
|
import type { PageController } from '@page-agent/page-controller'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import zod from 'zod'
|
||||||
|
|
||||||
|
import { type PageAgentConfig } from './config'
|
||||||
|
import { MAX_STEPS } from './config/constants'
|
||||||
|
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
||||||
|
import { tools } from './tools'
|
||||||
|
import {
|
||||||
|
AgentActivity,
|
||||||
|
AgentReflection,
|
||||||
|
AgentStatus,
|
||||||
|
AgentStep,
|
||||||
|
ExecutionResult,
|
||||||
|
HistoricalEvent,
|
||||||
|
MacroToolInput,
|
||||||
|
MacroToolResult,
|
||||||
|
} from './types'
|
||||||
|
import { normalizeResponse, trimLines, uid } from './utils'
|
||||||
|
import { assert } from './utils/assert'
|
||||||
|
|
||||||
|
export { type PageAgentConfig }
|
||||||
|
export { tool, type PageAgentTool } from './tools'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI agent for browser DOM automation.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* ## Event System
|
||||||
|
* - `statuschange` - Agent status transitions (idle → running → completed/error)
|
||||||
|
* - `historychange` - History events updated (persistent, part of agent memory)
|
||||||
|
* - `activity` - Real-time activity feedback (transient, for UI only)
|
||||||
|
* - `dispose` - Agent cleanup triggered
|
||||||
|
*
|
||||||
|
* ## Information Streams
|
||||||
|
* 1. **History Events** (`history` array)
|
||||||
|
* - Persistent event stream that forms agent's memory
|
||||||
|
* - Included in LLM context across steps
|
||||||
|
* - Types: steps, observations, user takeovers, llm errors
|
||||||
|
*
|
||||||
|
* 2. **Activity Events** (via `activity` event)
|
||||||
|
* - Transient UI feedback during task execution
|
||||||
|
* - NOT included in LLM context
|
||||||
|
* - Types: thinking, executing, executed, retrying, error
|
||||||
|
*/
|
||||||
|
export class PageAgentCore extends EventTarget {
|
||||||
|
config: PageAgentConfig
|
||||||
|
id = uid()
|
||||||
|
tools: typeof tools
|
||||||
|
disposed = false
|
||||||
|
task = ''
|
||||||
|
taskId = ''
|
||||||
|
|
||||||
|
/** Agent execution status */
|
||||||
|
#status: AgentStatus = 'idle'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when agent needs user input (ask_user tool)
|
||||||
|
* If not set, ask_user tool will be disabled
|
||||||
|
* @example onAskUser: (q) => window.prompt(q) || ''
|
||||||
|
*/
|
||||||
|
onAskUser?: (question: string) => Promise<string>
|
||||||
|
|
||||||
|
#llm: LLM
|
||||||
|
#abortController = new AbortController()
|
||||||
|
|
||||||
|
/** PageController for DOM operations */
|
||||||
|
pageController: PageController
|
||||||
|
|
||||||
|
/** Runtime states for tracking across steps */
|
||||||
|
states = {
|
||||||
|
/** Accumulated wait time in seconds, used by wait tool */
|
||||||
|
totalWaitTime: 0,
|
||||||
|
/** Last known URL for detecting navigation */
|
||||||
|
lastURL: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** History events */
|
||||||
|
history: HistoricalEvent[] = []
|
||||||
|
|
||||||
|
constructor(config: PageAgentConfig & { pageController: PageController }) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.config = config
|
||||||
|
this.#llm = new LLM(this.config)
|
||||||
|
this.tools = new Map(tools)
|
||||||
|
this.pageController = config.pageController
|
||||||
|
|
||||||
|
// Listen to LLM retry events
|
||||||
|
this.#llm.addEventListener('retry', (e) => {
|
||||||
|
const { attempt, maxAttempts } = (e as CustomEvent).detail
|
||||||
|
this.emitActivity({ type: 'retrying', attempt, maxAttempts })
|
||||||
|
// Also push to history for panel rendering
|
||||||
|
this.history.push({
|
||||||
|
type: 'error',
|
||||||
|
errorType: 'retry',
|
||||||
|
message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
|
||||||
|
attempt,
|
||||||
|
maxAttempts,
|
||||||
|
})
|
||||||
|
this.#emitHistoryChange()
|
||||||
|
})
|
||||||
|
this.#llm.addEventListener('error', (e) => {
|
||||||
|
const { error } = (e as CustomEvent).detail
|
||||||
|
const message = String(error)
|
||||||
|
this.emitActivity({ type: 'error', message })
|
||||||
|
// Also push to history for panel rendering
|
||||||
|
this.history.push({
|
||||||
|
type: 'error',
|
||||||
|
errorType: 'error',
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
this.#emitHistoryChange()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.config.customTools) {
|
||||||
|
for (const [name, tool] of Object.entries(this.config.customTools)) {
|
||||||
|
if (tool === null) {
|
||||||
|
this.tools.delete(name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.tools.set(name, tool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.experimentalScriptExecutionTool) {
|
||||||
|
this.tools.delete('execute_javascript')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get current agent status */
|
||||||
|
get status(): AgentStatus {
|
||||||
|
return this.#status
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emit statuschange event */
|
||||||
|
#emitStatusChange(): void {
|
||||||
|
this.dispatchEvent(new Event('statuschange'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emit historychange event */
|
||||||
|
#emitHistoryChange(): void {
|
||||||
|
this.dispatchEvent(new Event('historychange'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit activity event - for transient UI feedback
|
||||||
|
* @param activity - Current agent activity
|
||||||
|
*/
|
||||||
|
emitActivity(activity: AgentActivity): void {
|
||||||
|
this.dispatchEvent(new CustomEvent('activity', { detail: activity }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update status and emit event */
|
||||||
|
#setStatus(status: AgentStatus): void {
|
||||||
|
if (this.#status !== status) {
|
||||||
|
this.#status = status
|
||||||
|
this.#emitStatusChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a persistent observation to the history event stream.
|
||||||
|
* This will be visible in <agent_history> and remain in memory across steps.
|
||||||
|
*/
|
||||||
|
pushObservation(content: string): void {
|
||||||
|
this.history.push({ type: 'observation', content })
|
||||||
|
this.#emitHistoryChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(task: string): Promise<ExecutionResult> {
|
||||||
|
if (!task) throw new Error('Task is required')
|
||||||
|
this.task = task
|
||||||
|
this.taskId = uid()
|
||||||
|
|
||||||
|
// Disable ask_user tool if onAskUser is not set
|
||||||
|
if (!this.onAskUser) {
|
||||||
|
this.tools.delete('ask_user')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBeforeStep = this.config.onBeforeStep || (() => void 0)
|
||||||
|
const onAfterStep = this.config.onAfterStep || (() => void 0)
|
||||||
|
const onBeforeTask = this.config.onBeforeTask || (() => void 0)
|
||||||
|
const onAfterTask = this.config.onAfterTask || (() => void 0)
|
||||||
|
|
||||||
|
await onBeforeTask.call(this)
|
||||||
|
|
||||||
|
// Show mask
|
||||||
|
await this.pageController.showMask()
|
||||||
|
|
||||||
|
if (this.#abortController) {
|
||||||
|
this.#abortController.abort()
|
||||||
|
this.#abortController = new AbortController()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.history = []
|
||||||
|
this.#setStatus('running')
|
||||||
|
this.#emitHistoryChange()
|
||||||
|
|
||||||
|
// Reset states
|
||||||
|
this.states = {
|
||||||
|
totalWaitTime: 0,
|
||||||
|
lastURL: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let step = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
await this.#generateObservations(step)
|
||||||
|
|
||||||
|
await onBeforeStep.call(this, step)
|
||||||
|
|
||||||
|
console.group(`step: ${step}`)
|
||||||
|
|
||||||
|
// abort
|
||||||
|
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
||||||
|
|
||||||
|
// Thinking
|
||||||
|
console.log(chalk.blue('Thinking...'))
|
||||||
|
this.emitActivity({ type: 'thinking' })
|
||||||
|
|
||||||
|
const result = await this.#llm.invoke(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: this.#getSystemPrompt(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: await this.#assembleUserPrompt(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ AgentOutput: this.#packMacroTool() },
|
||||||
|
this.#abortController.signal,
|
||||||
|
{
|
||||||
|
toolChoiceName: 'AgentOutput',
|
||||||
|
normalizeResponse,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const macroResult = result.toolResult as MacroToolResult
|
||||||
|
const input = macroResult.input
|
||||||
|
const output = macroResult.output
|
||||||
|
const reflection: Partial<AgentReflection> = {
|
||||||
|
evaluation_previous_goal: input.evaluation_previous_goal,
|
||||||
|
memory: input.memory,
|
||||||
|
next_goal: input.next_goal,
|
||||||
|
}
|
||||||
|
const actionName = Object.keys(input.action)[0]
|
||||||
|
const action: AgentStep['action'] = {
|
||||||
|
name: actionName,
|
||||||
|
input: input.action[actionName],
|
||||||
|
output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.history.push({
|
||||||
|
type: 'step',
|
||||||
|
reflection,
|
||||||
|
action,
|
||||||
|
usage: result.usage,
|
||||||
|
} as AgentStep)
|
||||||
|
this.#emitHistoryChange()
|
||||||
|
|
||||||
|
console.log(chalk.green('Step finished:'), actionName)
|
||||||
|
console.groupEnd()
|
||||||
|
|
||||||
|
await onAfterStep.call(this, this.history)
|
||||||
|
|
||||||
|
step++
|
||||||
|
if (step > MAX_STEPS) {
|
||||||
|
this.#onDone('Step count exceeded maximum limit', false)
|
||||||
|
const result: ExecutionResult = {
|
||||||
|
success: false,
|
||||||
|
data: 'Step count exceeded maximum limit',
|
||||||
|
history: this.history,
|
||||||
|
}
|
||||||
|
await onAfterTask.call(this, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if (actionName === 'done') {
|
||||||
|
const success = action.input?.success ?? false
|
||||||
|
const text = action.input?.text || 'no text provided'
|
||||||
|
console.log(chalk.green.bold('Task completed'), success, text)
|
||||||
|
this.#onDone(text, success)
|
||||||
|
const result: ExecutionResult = {
|
||||||
|
success,
|
||||||
|
data: text,
|
||||||
|
history: this.history,
|
||||||
|
}
|
||||||
|
await onAfterTask.call(this, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Task failed', error)
|
||||||
|
const errorMessage = String(error)
|
||||||
|
this.emitActivity({ type: 'error', message: errorMessage })
|
||||||
|
this.#onDone(errorMessage, false)
|
||||||
|
const result: ExecutionResult = {
|
||||||
|
success: false,
|
||||||
|
data: errorMessage,
|
||||||
|
history: this.history,
|
||||||
|
}
|
||||||
|
await onAfterTask.call(this, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge all tools into a single MacroTool with the following input:
|
||||||
|
* - thinking: string
|
||||||
|
* - evaluation_previous_goal: string
|
||||||
|
* - memory: string
|
||||||
|
* - next_goal: string
|
||||||
|
* - action: { toolName: toolInput }
|
||||||
|
* where action must be selected from tools defined in this.tools
|
||||||
|
*/
|
||||||
|
#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
|
||||||
|
const tools = this.tools
|
||||||
|
|
||||||
|
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
||||||
|
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
|
||||||
|
})
|
||||||
|
|
||||||
|
const actionSchema = zod.union(
|
||||||
|
actionSchemas as unknown as [zod.ZodType, zod.ZodType, ...zod.ZodType[]]
|
||||||
|
)
|
||||||
|
|
||||||
|
const macroToolSchema = zod.object({
|
||||||
|
// thinking: zod.string().optional(),
|
||||||
|
evaluation_previous_goal: zod.string().optional(),
|
||||||
|
memory: zod.string().optional(),
|
||||||
|
next_goal: zod.string().optional(),
|
||||||
|
action: actionSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: 'You MUST call this tool every step. Outputs your reflections and next action.',
|
||||||
|
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
|
||||||
|
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
|
||||||
|
// abort
|
||||||
|
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
||||||
|
|
||||||
|
console.log(chalk.blue.bold('MacroTool execute'), input)
|
||||||
|
const action = input.action
|
||||||
|
|
||||||
|
const toolName = Object.keys(action)[0]
|
||||||
|
const toolInput = action[toolName]
|
||||||
|
|
||||||
|
// Build reflection text, only include non-empty fields
|
||||||
|
const reflectionLines: string[] = []
|
||||||
|
if (input.evaluation_previous_goal)
|
||||||
|
reflectionLines.push(`✅: ${input.evaluation_previous_goal}`)
|
||||||
|
if (input.memory) reflectionLines.push(`💾: ${input.memory}`)
|
||||||
|
if (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`)
|
||||||
|
|
||||||
|
const reflectionText = reflectionLines.length > 0 ? reflectionLines.join('\n') : ''
|
||||||
|
|
||||||
|
if (reflectionText) {
|
||||||
|
console.log(reflectionText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the corresponding tool
|
||||||
|
const tool = tools.get(toolName)
|
||||||
|
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
|
||||||
|
|
||||||
|
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
|
||||||
|
|
||||||
|
// Emit executing activity
|
||||||
|
this.emitActivity({ type: 'executing', tool: toolName, input: toolInput })
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
// Execute tool, bind `this` to PageAgent
|
||||||
|
const result = await tool.execute.bind(this)(toolInput)
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime
|
||||||
|
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
|
||||||
|
|
||||||
|
// Emit executed activity
|
||||||
|
this.emitActivity({
|
||||||
|
type: 'executed',
|
||||||
|
tool: toolName,
|
||||||
|
input: toolInput,
|
||||||
|
output: result,
|
||||||
|
duration,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset wait time for non-wait tools
|
||||||
|
if (toolName !== 'wait') {
|
||||||
|
this.states.totalWaitTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return structured result
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
output: result,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system prompt, dynamically replace language settings based on configured language
|
||||||
|
*/
|
||||||
|
#getSystemPrompt(): string {
|
||||||
|
let systemPrompt = SYSTEM_PROMPT
|
||||||
|
|
||||||
|
const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
|
||||||
|
systemPrompt = systemPrompt.replace(
|
||||||
|
/Default working language: \*\*.*?\*\*/,
|
||||||
|
`Default working language: **${targetLanguage}**`
|
||||||
|
)
|
||||||
|
|
||||||
|
return systemPrompt
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get instructions from config and format as XML block
|
||||||
|
*/
|
||||||
|
async #getInstructions(): Promise<string> {
|
||||||
|
const { instructions } = this.config
|
||||||
|
if (!instructions) return ''
|
||||||
|
|
||||||
|
const systemInstructions = instructions.system?.trim()
|
||||||
|
const url = await this.pageController.getCurrentUrl()
|
||||||
|
let pageInstructions: string | undefined
|
||||||
|
|
||||||
|
if (instructions.getPageInstructions) {
|
||||||
|
try {
|
||||||
|
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
chalk.red('[PageAgent] Failed to execute getPageInstructions callback:'),
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!systemInstructions && !pageInstructions) return ''
|
||||||
|
|
||||||
|
let result = '<instructions>\n'
|
||||||
|
|
||||||
|
if (systemInstructions) {
|
||||||
|
result += `<system_instructions>\n${systemInstructions}\n</system_instructions>\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageInstructions) {
|
||||||
|
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
result += '</instructions>\n\n'
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate observations before each step
|
||||||
|
* - URL change detection
|
||||||
|
* - Too many steps warning
|
||||||
|
* @todo loop detection
|
||||||
|
* @todo console error
|
||||||
|
*/
|
||||||
|
async #generateObservations(stepCount: number): Promise<void> {
|
||||||
|
// Detect URL change
|
||||||
|
const currentURL = await this.pageController.getCurrentUrl()
|
||||||
|
if (currentURL !== this.states.lastURL) {
|
||||||
|
this.pushObservation(`Page navigated to → ${currentURL}`)
|
||||||
|
this.states.lastURL = currentURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn about remaining steps
|
||||||
|
const remaining = MAX_STEPS - stepCount
|
||||||
|
if (remaining === 5) {
|
||||||
|
this.pushObservation(
|
||||||
|
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
|
||||||
|
)
|
||||||
|
} else if (remaining === 2) {
|
||||||
|
this.pushObservation(
|
||||||
|
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #assembleUserPrompt(): Promise<string> {
|
||||||
|
let prompt = ''
|
||||||
|
|
||||||
|
// <instructions> (optional)
|
||||||
|
prompt += await this.#getInstructions()
|
||||||
|
|
||||||
|
// <agent_state>
|
||||||
|
// - <user_request>
|
||||||
|
// - <step_info>
|
||||||
|
// <agent_state>
|
||||||
|
|
||||||
|
const stepCount = this.history.filter((e) => e.type === 'step').length
|
||||||
|
|
||||||
|
prompt += `<agent_state>
|
||||||
|
<user_request>
|
||||||
|
${this.task}
|
||||||
|
</user_request>
|
||||||
|
<step_info>
|
||||||
|
Step ${stepCount + 1} of ${MAX_STEPS} max possible steps
|
||||||
|
Current date and time: ${new Date().toISOString()}
|
||||||
|
</step_info>
|
||||||
|
</agent_state>
|
||||||
|
`
|
||||||
|
|
||||||
|
// <agent_history>
|
||||||
|
// - <step_N> for steps
|
||||||
|
// - <sys> for observations and system messages
|
||||||
|
|
||||||
|
prompt += '\n<agent_history>\n'
|
||||||
|
|
||||||
|
let stepIndex = 0
|
||||||
|
for (const event of this.history) {
|
||||||
|
if (event.type === 'step') {
|
||||||
|
stepIndex++
|
||||||
|
prompt += `<step_${stepIndex}>
|
||||||
|
Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
|
||||||
|
Memory: ${event.reflection.memory}
|
||||||
|
Next Goal: ${event.reflection.next_goal}
|
||||||
|
Action Results: ${event.action.output}
|
||||||
|
</step_${stepIndex}>
|
||||||
|
`
|
||||||
|
} else if (event.type === 'observation') {
|
||||||
|
prompt += `<sys>${event.content}</sys>\n`
|
||||||
|
} else if (event.type === 'user_takeover') {
|
||||||
|
prompt += `<sys>User took over control and made changes to the page.</sys>\n`
|
||||||
|
} else if (event.type === 'error') {
|
||||||
|
// Error events are mainly for panel rendering, not included in LLM context
|
||||||
|
// to avoid polluting the agent's reasoning with transient errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += '</agent_history>\n\n'
|
||||||
|
|
||||||
|
// <browser_state>
|
||||||
|
|
||||||
|
prompt += await this.#getBrowserState()
|
||||||
|
|
||||||
|
return trimLines(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#onDone(text: string, success = true) {
|
||||||
|
this.pageController.cleanUpHighlights()
|
||||||
|
this.pageController.hideMask() // No await - fire and forget
|
||||||
|
this.#setStatus(success ? 'completed' : 'error')
|
||||||
|
this.#abortController.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getBrowserState(): Promise<string> {
|
||||||
|
const state = await this.pageController.getBrowserState()
|
||||||
|
|
||||||
|
let content = state.content
|
||||||
|
if (this.config.transformPageContent) {
|
||||||
|
content = await this.config.transformPageContent(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimLines(`<browser_state>
|
||||||
|
Current Page: [${state.title}](${state.url})
|
||||||
|
|
||||||
|
${state.header}
|
||||||
|
${content}
|
||||||
|
${state.footer}
|
||||||
|
|
||||||
|
</browser_state>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(reason?: string) {
|
||||||
|
console.log('Disposing PageAgent...')
|
||||||
|
this.disposed = true
|
||||||
|
this.pageController.dispose()
|
||||||
|
this.history = []
|
||||||
|
this.#abortController.abort(reason ?? 'PageAgent disposed')
|
||||||
|
|
||||||
|
// Emit dispose event for UI cleanup
|
||||||
|
this.dispatchEvent(new Event('dispose'))
|
||||||
|
|
||||||
|
this.config.onDispose?.call(this, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import type { LLMConfig } from '@page-agent/llms'
|
import type { LLMConfig } from '@page-agent/llms'
|
||||||
import type { PageControllerConfig } from '@page-agent/page-controller'
|
import type { PageController, PageControllerConfig } from '@page-agent/page-controller'
|
||||||
import type { SupportedLanguage } from '@page-agent/ui'
|
|
||||||
|
|
||||||
import type { ExecutionResult, HistoryEvent, PageAgent } from '../PageAgent'
|
import type { PageAgentCore } from '../PageAgentCore'
|
||||||
import type { PageAgentTool } from '../tools'
|
import type { PageAgentTool } from '../tools'
|
||||||
|
import type { ExecutionResult, HistoricalEvent } from '../types'
|
||||||
|
|
||||||
export type { LLMConfig }
|
export type { LLMConfig }
|
||||||
|
|
||||||
|
/** Supported UI languages */
|
||||||
|
export type SupportedLanguage = 'en-US' | 'zh-CN'
|
||||||
|
|
||||||
export interface AgentConfig {
|
export interface AgentConfig {
|
||||||
// theme?: 'light' | 'dark'
|
// theme?: 'light' | 'dark'
|
||||||
language?: SupportedLanguage
|
language?: SupportedLanguage
|
||||||
@@ -15,7 +18,7 @@ export interface AgentConfig {
|
|||||||
* Custom tools to extend PageAgent capabilities
|
* Custom tools to extend PageAgent capabilities
|
||||||
* @experimental
|
* @experimental
|
||||||
* @note You can also override or remove internal tools by using the same name.
|
* @note You can also override or remove internal tools by using the same name.
|
||||||
* @see [tools](../tools/index.ts)
|
* @see PageAgentTool
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // override internal tool
|
* // override internal tool
|
||||||
@@ -64,17 +67,16 @@ export interface AgentConfig {
|
|||||||
// @todo: use event instead of hooks
|
// @todo: use event instead of hooks
|
||||||
// @todo: remove `this` binding, pass agent as explicit parameter instead
|
// @todo: remove `this` binding, pass agent as explicit parameter instead
|
||||||
|
|
||||||
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
|
onBeforeStep?: (this: PageAgentCore, stepCnt: number) => Promise<void> | void
|
||||||
onAfterStep?: (this: PageAgent, stepCnt: number, history: HistoryEvent[]) => Promise<void> | void
|
onAfterStep?: (this: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void
|
||||||
onBeforeTask?: (this: PageAgent) => Promise<void> | void
|
onBeforeTask?: (this: PageAgentCore) => Promise<void> | void
|
||||||
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
|
onAfterTask?: (this: PageAgentCore, result: ExecutionResult) => Promise<void> | void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @note this hook can block the disposal process
|
* @note this hook can block the disposal process
|
||||||
* @note when dispose caused by page unload, reason will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut.
|
|
||||||
* @todo remove `this` binding, pass agent as explicit parameter instead
|
* @todo remove `this` binding, pass agent as explicit parameter instead
|
||||||
*/
|
*/
|
||||||
onDispose?: (this: PageAgent, reason?: string) => void
|
onDispose?: (this: PageAgentCore, reason?: string) => void
|
||||||
|
|
||||||
// page behavior hooks
|
// page behavior hooks
|
||||||
|
|
||||||
@@ -108,14 +110,14 @@ export interface AgentConfig {
|
|||||||
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
|
* @note PageAgent will try to detect new pages and decide if it's caused by an action. But not very reliable.
|
||||||
* @todo remove `this` binding, pass agent as explicit parameter instead
|
* @todo remove `this` binding, pass agent as explicit parameter instead
|
||||||
*/
|
*/
|
||||||
onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
|
// onNewPageOpen?: (this: PageAgent, url: string) => Promise<void> | void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: @unimplemented
|
* TODO: @unimplemented
|
||||||
* try to navigate to a new page instead of opening a new tab/window.
|
* 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.
|
* @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
|
// experimentalPreventNewPage?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
|
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.md?raw' {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
system_prompt.md
|
||||||
+1
-2
@@ -92,7 +92,6 @@ Strictly follow these rules while using the browser and navigating the web:
|
|||||||
- User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.
|
- User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.
|
||||||
- Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.
|
- Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.
|
||||||
- Trying to hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted result and harmful side-effects. User would rather you to complete the task with a fail.
|
- Trying to hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted result and harmful side-effects. User would rather you to complete the task with a fail.
|
||||||
- If you are not clear about the request or steps. `ask_user` to clarify it.
|
|
||||||
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
|
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
|
||||||
</capability>
|
</capability>
|
||||||
|
|
||||||
@@ -120,7 +119,7 @@ Exhibit the following reasoning patterns to successfully achieve the <user_reque
|
|||||||
- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.
|
- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.
|
||||||
- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.
|
- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.
|
||||||
- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.
|
- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.
|
||||||
- `ask_user` for help if you have any difficulty. Users want to be kept in the loop.
|
- Ask user for help if you have any difficulty. Keep user in the loop.
|
||||||
- If you see information relevant to <user_request>, plan saving the information to memory.
|
- If you see information relevant to <user_request>, plan saving the information to memory.
|
||||||
- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.
|
- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.
|
||||||
</reasoning_rules>
|
</reasoning_rules>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import zod, { type z } from 'zod'
|
import zod, { type z } from 'zod'
|
||||||
|
|
||||||
import type { PageAgent } from '../PageAgent'
|
import type { PageAgentCore } from '../PageAgentCore'
|
||||||
import { waitFor } from '../utils'
|
import { waitFor } from '../utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +14,7 @@ export interface PageAgentTool<TParams = any> {
|
|||||||
// name: string
|
// name: string
|
||||||
description: string
|
description: string
|
||||||
inputSchema: z.ZodType<TParams>
|
inputSchema: z.ZodType<TParams>
|
||||||
execute: (this: PageAgent, args: TParams) => Promise<string>
|
execute: (this: PageAgentCore, args: TParams) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TParams> {
|
export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TParams> {
|
||||||
@@ -36,7 +36,7 @@ tools.set(
|
|||||||
text: zod.string(),
|
text: zod.string(),
|
||||||
success: zod.boolean().default(true),
|
success: zod.boolean().default(true),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgent, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
// @note main loop will handle this one
|
// @note main loop will handle this one
|
||||||
// this.onDone(input.text, input.success)
|
// this.onDone(input.text, input.success)
|
||||||
return Promise.resolve('Task completed')
|
return Promise.resolve('Task completed')
|
||||||
@@ -52,7 +52,7 @@ tools.set(
|
|||||||
inputSchema: zod.object({
|
inputSchema: zod.object({
|
||||||
seconds: zod.number().min(1).max(10).default(1),
|
seconds: zod.number().min(1).max(10).default(1),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgent, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
|
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
|
||||||
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
|
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
|
||||||
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
|
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
|
||||||
@@ -79,9 +79,12 @@ tools.set(
|
|||||||
inputSchema: zod.object({
|
inputSchema: zod.object({
|
||||||
question: zod.string(),
|
question: zod.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgent, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const answer = await this.panel.askUser(input.question)
|
if (!this.onAskUser) {
|
||||||
return `✅ Received user answer: ${answer}`
|
throw new Error('ask_user tool requires onAskUser callback to be set')
|
||||||
|
}
|
||||||
|
const answer = await this.onAskUser(input.question)
|
||||||
|
return `User answered: ${answer}`
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -93,7 +96,7 @@ tools.set(
|
|||||||
inputSchema: zod.object({
|
inputSchema: zod.object({
|
||||||
index: zod.int().min(0),
|
index: zod.int().min(0),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgent, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.clickElement(input.index)
|
const result = await this.pageController.clickElement(input.index)
|
||||||
return result.message
|
return result.message
|
||||||
},
|
},
|
||||||
@@ -108,7 +111,7 @@ tools.set(
|
|||||||
index: zod.int().min(0),
|
index: zod.int().min(0),
|
||||||
text: zod.string(),
|
text: zod.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgent, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.inputText(input.index, input.text)
|
const result = await this.pageController.inputText(input.index, input.text)
|
||||||
return result.message
|
return result.message
|
||||||
},
|
},
|
||||||
@@ -124,7 +127,7 @@ tools.set(
|
|||||||
index: zod.int().min(0),
|
index: zod.int().min(0),
|
||||||
text: zod.string(),
|
text: zod.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgent, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.selectOption(input.index, input.text)
|
const result = await this.pageController.selectOption(input.index, input.text)
|
||||||
return result.message
|
return result.message
|
||||||
},
|
},
|
||||||
@@ -145,7 +148,7 @@ tools.set(
|
|||||||
pixels: zod.number().int().min(0).optional(),
|
pixels: zod.number().int().min(0).optional(),
|
||||||
index: zod.number().int().min(0).optional(),
|
index: zod.number().int().min(0).optional(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgent, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.scroll({
|
const result = await this.pageController.scroll({
|
||||||
...input,
|
...input,
|
||||||
numPages: input.num_pages,
|
numPages: input.num_pages,
|
||||||
@@ -165,7 +168,7 @@ tools.set(
|
|||||||
pixels: zod.number().int().min(0),
|
pixels: zod.number().int().min(0),
|
||||||
index: zod.number().int().min(0).optional(),
|
index: zod.number().int().min(0).optional(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgent, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.scrollHorizontally(input)
|
const result = await this.pageController.scrollHorizontally(input)
|
||||||
return result.message
|
return result.message
|
||||||
},
|
},
|
||||||
@@ -180,7 +183,7 @@ tools.set(
|
|||||||
inputSchema: zod.object({
|
inputSchema: zod.object({
|
||||||
script: zod.string(),
|
script: zod.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgent, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.executeJavascript(input.script)
|
const result = await this.pageController.executeJavascript(input.script)
|
||||||
return result.message
|
return result.message
|
||||||
},
|
},
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Agent reflection state - the reflection-before-action model
|
||||||
|
*
|
||||||
|
* Every tool call must first reflect on:
|
||||||
|
* - evaluation_previous_goal: How well did the previous action achieve its goal?
|
||||||
|
* - memory: Key information to remember for future steps
|
||||||
|
* - next_goal: What should be accomplished in the next action?
|
||||||
|
*/
|
||||||
|
export interface AgentReflection {
|
||||||
|
evaluation_previous_goal: string
|
||||||
|
memory: string
|
||||||
|
next_goal: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MacroTool input structure
|
||||||
|
*
|
||||||
|
* This is the core abstraction that enforces the "reflection-before-action" mental model.
|
||||||
|
* Before executing any action, the LLM must output its reasoning state.
|
||||||
|
*/
|
||||||
|
export interface MacroToolInput extends Partial<AgentReflection> {
|
||||||
|
action: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MacroTool output structure
|
||||||
|
*/
|
||||||
|
export interface MacroToolResult {
|
||||||
|
input: MacroToolInput
|
||||||
|
output: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single agent step with reflection and action
|
||||||
|
*/
|
||||||
|
export interface AgentStep {
|
||||||
|
type: 'step'
|
||||||
|
reflection: Partial<AgentReflection>
|
||||||
|
action: {
|
||||||
|
name: string
|
||||||
|
input: any
|
||||||
|
output: string
|
||||||
|
}
|
||||||
|
usage: {
|
||||||
|
promptTokens: number
|
||||||
|
completionTokens: number
|
||||||
|
totalTokens: number
|
||||||
|
cachedTokens?: number
|
||||||
|
reasoningTokens?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persistent observation event (stays in memory)
|
||||||
|
*/
|
||||||
|
export interface ObservationEvent {
|
||||||
|
type: 'observation'
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User takeover event
|
||||||
|
*/
|
||||||
|
export interface UserTakeoverEvent {
|
||||||
|
type: 'user_takeover'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error event (retry or error from LLM)
|
||||||
|
*/
|
||||||
|
export interface ErrorEvent {
|
||||||
|
type: 'error'
|
||||||
|
errorType: 'retry' | 'error'
|
||||||
|
message: string
|
||||||
|
attempt?: number
|
||||||
|
maxAttempts?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union type for all history events
|
||||||
|
*/
|
||||||
|
export type HistoricalEvent = AgentStep | ObservationEvent | UserTakeoverEvent | ErrorEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent execution status
|
||||||
|
*/
|
||||||
|
export type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent activity - transient state for immediate UI feedback.
|
||||||
|
*
|
||||||
|
* Unlike historical events (which are persisted), activities are ephemeral
|
||||||
|
* and represent "what the agent is doing right now". UI components should
|
||||||
|
* listen to 'activity' events to show real-time feedback.
|
||||||
|
*
|
||||||
|
* Note: There is no 'idle' activity - absence of activity events means idle.
|
||||||
|
*/
|
||||||
|
export 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 }
|
||||||
|
|
||||||
|
export interface ExecutionResult {
|
||||||
|
success: boolean
|
||||||
|
data: string
|
||||||
|
history: HistoricalEvent[]
|
||||||
|
}
|
||||||
@@ -68,12 +68,13 @@ export function randomID(existingIDs?: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
const _global = globalThis as any
|
||||||
|
|
||||||
if (!window.__PAGE_AGENT_IDS__) {
|
if (!_global.__PAGE_AGENT_IDS__) {
|
||||||
window.__PAGE_AGENT_IDS__ = []
|
_global.__PAGE_AGENT_IDS__ = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = window.__PAGE_AGENT_IDS__
|
const ids = _global.__PAGE_AGENT_IDS__
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random ID.
|
* Generate a random ID.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// @workaround DTS bug
|
||||||
|
// dts do not work with monorepo path mapping
|
||||||
|
// disable path mapping for it
|
||||||
|
"paths": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||||
|
"noEmit": false,
|
||||||
|
"allowImportingTsExtensions": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "dist",
|
||||||
|
"paths": {
|
||||||
|
//
|
||||||
|
"@page-agent/llms": ["../llms/src/index.ts"],
|
||||||
|
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": ["dist", "node_modules"],
|
||||||
|
"references": [
|
||||||
|
//
|
||||||
|
{ "path": "../llms" },
|
||||||
|
{ "path": "../page-controller" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { dirname, resolve } from 'path'
|
||||||
|
import dts from 'unplugin-dts/vite'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
// ES Module for NPM Package
|
||||||
|
export default defineConfig({
|
||||||
|
clearScreen: false,
|
||||||
|
plugins: [
|
||||||
|
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
|
||||||
|
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||||
|
],
|
||||||
|
publicDir: false,
|
||||||
|
esbuild: {
|
||||||
|
keepNames: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'src/PageAgentCore.ts'),
|
||||||
|
name: 'PageAgentCore',
|
||||||
|
fileName: 'page-agent-core',
|
||||||
|
formats: ['es'],
|
||||||
|
},
|
||||||
|
outDir: resolve(__dirname, 'dist', 'esm'),
|
||||||
|
rollupOptions: {
|
||||||
|
external: [
|
||||||
|
'chalk',
|
||||||
|
'zod',
|
||||||
|
// all the internal packages
|
||||||
|
/^@page-agent\//,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
minify: false,
|
||||||
|
sourcemap: true,
|
||||||
|
cssCodeSplit: true,
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'process.env.NODE_ENV': '"production"',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# @page-agent/llms
|
|
||||||
|
|
||||||
LLM client with a **reflection-before-action** mental model for page-agent.
|
|
||||||
|
|
||||||
## Why This Package Exists
|
|
||||||
|
|
||||||
The LLM module and the agent logic are inherently coupled. This package exists not to decouple them, but to **define the interface contract** between the LLM and the agent.
|
|
||||||
|
|
||||||
The core abstraction is the `MacroToolInput` — a structured output format that **forces the model to reflect before acting**.
|
|
||||||
|
|
||||||
## The Reflection-Before-Action Model
|
|
||||||
|
|
||||||
Every tool call must first output its reasoning state before the actual action:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface MacroToolInput {
|
|
||||||
// Reflection (mandatory before any action)
|
|
||||||
evaluation_previous_goal?: string // How well did the previous action work?
|
|
||||||
memory?: string // Key information to remember
|
|
||||||
next_goal?: string // What to accomplish next
|
|
||||||
|
|
||||||
// Action (the actual operation)
|
|
||||||
action: Record<string, any>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This design ensures that:
|
|
||||||
|
|
||||||
1. **The model evaluates its previous action** before deciding the next step
|
|
||||||
2. **Working memory is explicitly maintained** across conversation turns
|
|
||||||
3. **Goals are clearly stated**, making the agent's reasoning transparent and debuggable
|
|
||||||
|
|
||||||
## Key Components
|
|
||||||
|
|
||||||
| Export | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| `LLM` | Main LLM client class with retry logic |
|
|
||||||
| `MacroToolInput` | The reflection-before-action input schema |
|
|
||||||
| `AgentBrain` | Agent's thinking state (eval, memory, goal) |
|
|
||||||
| `LLMConfig` | Configuration for LLM connection |
|
|
||||||
| `parseLLMConfig` | Parse and apply defaults to config |
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-llms.js",
|
"main": "./dist/lib/page-agent-llms.js",
|
||||||
"module": "./dist/lib/page-agent-llms.js",
|
"module": "./dist/lib/page-agent-llms.js",
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ export class LLM extends EventTarget {
|
|||||||
// retry settings
|
// retry settings
|
||||||
{
|
{
|
||||||
maxRetries: this.config.maxRetries,
|
maxRetries: this.config.maxRetries,
|
||||||
onRetry: (current: number) => {
|
onRetry: (attempt: number) => {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('retry', { detail: { current, max: this.config.maxRetries } })
|
new CustomEvent('retry', { detail: { attempt, maxAttempts: this.config.maxRetries } })
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
@@ -73,15 +73,15 @@ async function withRetry<T>(
|
|||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
settings: {
|
settings: {
|
||||||
maxRetries: number
|
maxRetries: number
|
||||||
onRetry: (retries: number) => void
|
onRetry: (attempt: number) => void
|
||||||
onError: (error: Error) => void
|
onError: (error: Error) => void
|
||||||
}
|
}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
let retries = 0
|
let attempt = 0
|
||||||
let lastError: Error | null = null
|
let lastError: Error | null = null
|
||||||
while (retries <= settings.maxRetries) {
|
while (attempt <= settings.maxRetries) {
|
||||||
if (retries > 0) {
|
if (attempt > 0) {
|
||||||
settings.onRetry(retries)
|
settings.onRetry(attempt)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ async function withRetry<T>(
|
|||||||
if (error instanceof InvokeError && !error.retryable) throw error
|
if (error instanceof InvokeError && !error.retryable) throw error
|
||||||
|
|
||||||
lastError = error as Error
|
lastError = error as Error
|
||||||
retries++
|
attempt++
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// @workaround DTS bug
|
// @workaround DTS bug
|
||||||
// dts do not work with monorepo path mapping
|
// dts do not work with monorepo path mapping
|
||||||
// disable path mapping for it
|
// disable path mapping for it
|
||||||
"paths": {}
|
"paths": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-11
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts"],
|
"include": ["**/*.ts"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,4 +34,3 @@ export default defineConfig({
|
|||||||
'process.env.NODE_ENV': '"production"',
|
'process.env.NODE_ENV': '"production"',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "page-agent",
|
"name": "page-agent",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent.js",
|
"main": "./dist/esm/page-agent.js",
|
||||||
"module": "./dist/esm/page-agent.js",
|
"module": "./dist/esm/page-agent.js",
|
||||||
@@ -37,17 +37,18 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://alibaba.github.io/page-agent/",
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build && vite build --config vite.umd.config.js",
|
"build": "vite build && npm run build:demo",
|
||||||
"serve": "npx serve dist/umd -p 5173",
|
"build:demo": "vite build --config vite.iife.config.js",
|
||||||
"dev:umd": "concurrently \"vite build --config vite.umd.config.js --watch\" \"npm run serve\"",
|
"dev:demo": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
|
||||||
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
||||||
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.3.5",
|
"zod": "^4.3.5",
|
||||||
"@page-agent/llms": "0.2.0",
|
"@page-agent/llms": "0.3.0-beta.1",
|
||||||
"@page-agent/page-controller": "0.2.0",
|
"@page-agent/page-controller": "0.3.0-beta.1",
|
||||||
"@page-agent/ui": "0.2.0"
|
"@page-agent/core": "0.3.0-beta.1",
|
||||||
|
"@page-agent/ui": "0.3.0-beta.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,629 +2,25 @@
|
|||||||
* Copyright (C) 2025 Alibaba Group Holding Limited
|
* Copyright (C) 2025 Alibaba Group Holding Limited
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*/
|
*/
|
||||||
import { LLM, type Tool } from '@page-agent/llms'
|
import { type PageAgentConfig, PageAgentCore } from '@page-agent/core'
|
||||||
import { PageController } from '@page-agent/page-controller'
|
import { PageController } from '@page-agent/page-controller'
|
||||||
import { Panel } from '@page-agent/ui'
|
import { Panel } from '@page-agent/ui'
|
||||||
import chalk from 'chalk'
|
|
||||||
import zod from 'zod'
|
|
||||||
|
|
||||||
import type { PageAgentConfig } from './config'
|
|
||||||
import { MAX_STEPS } from './config/constants'
|
|
||||||
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
|
||||||
import { tools } from './tools'
|
|
||||||
import { normalizeResponse, trimLines, uid, waitUntil } from './utils'
|
|
||||||
import { assert } from './utils/assert'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agent reflection state - the reflection-before-action model
|
|
||||||
*
|
|
||||||
* Every tool call must first reflect on:
|
|
||||||
* - evaluation_previous_goal: How well did the previous action achieve its goal?
|
|
||||||
* - memory: Key information to remember for future steps
|
|
||||||
* - next_goal: What should be accomplished in the next action?
|
|
||||||
*/
|
|
||||||
export interface AgentReflection {
|
|
||||||
evaluation_previous_goal: string
|
|
||||||
memory: string
|
|
||||||
next_goal: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MacroTool input structure
|
|
||||||
*
|
|
||||||
* This is the core abstraction that enforces the "reflection-before-action" mental model.
|
|
||||||
* Before executing any action, the LLM must output its reasoning state.
|
|
||||||
*/
|
|
||||||
export interface MacroToolInput extends Partial<AgentReflection> {
|
|
||||||
action: Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MacroTool output structure
|
|
||||||
*/
|
|
||||||
export interface MacroToolResult {
|
|
||||||
input: MacroToolInput
|
|
||||||
output: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { PageAgentConfig }
|
export type { PageAgentConfig }
|
||||||
export { tool, type PageAgentTool } from './tools'
|
|
||||||
|
|
||||||
/**
|
export class PageAgent extends PageAgentCore {
|
||||||
* A single agent step with reflection and action
|
|
||||||
*/
|
|
||||||
export interface AgentStep {
|
|
||||||
type: 'step'
|
|
||||||
reflection: Partial<AgentReflection>
|
|
||||||
action: {
|
|
||||||
name: string
|
|
||||||
input: any
|
|
||||||
output: string
|
|
||||||
}
|
|
||||||
usage: {
|
|
||||||
promptTokens: number
|
|
||||||
completionTokens: number
|
|
||||||
totalTokens: number
|
|
||||||
cachedTokens?: number
|
|
||||||
reasoningTokens?: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persistent observation event (stays in memory)
|
|
||||||
*/
|
|
||||||
export interface ObservationEvent {
|
|
||||||
type: 'observation'
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User takeover event
|
|
||||||
*/
|
|
||||||
export interface UserTakeoverEvent {
|
|
||||||
type: 'user_takeover'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Union type for all history events
|
|
||||||
*/
|
|
||||||
export type HistoryEvent = AgentStep | ObservationEvent | UserTakeoverEvent
|
|
||||||
|
|
||||||
/** @deprecated Use AgentStep instead */
|
|
||||||
export type AgentHistory = AgentStep
|
|
||||||
|
|
||||||
export interface ExecutionResult {
|
|
||||||
success: boolean
|
|
||||||
data: string
|
|
||||||
history: HistoryEvent[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PageAgent extends EventTarget {
|
|
||||||
config: PageAgentConfig
|
|
||||||
id = uid()
|
|
||||||
panel: Panel
|
panel: Panel
|
||||||
tools: typeof tools
|
|
||||||
paused = false
|
|
||||||
disposed = false
|
|
||||||
task = ''
|
|
||||||
taskId = ''
|
|
||||||
|
|
||||||
#llm: LLM
|
|
||||||
#abortController = new AbortController()
|
|
||||||
#llmRetryListener: ((e: Event) => void) | null = null
|
|
||||||
#llmErrorListener: ((e: Event) => void) | null = null
|
|
||||||
#beforeUnloadListener: ((e: Event) => void) | null = null
|
|
||||||
|
|
||||||
/** PageController for DOM operations */
|
|
||||||
pageController: PageController
|
|
||||||
|
|
||||||
/** Runtime states for tracking across steps */
|
|
||||||
states = {
|
|
||||||
/** Accumulated wait time in seconds, used by wait tool */
|
|
||||||
totalWaitTime: 0,
|
|
||||||
/** Last known URL for detecting navigation */
|
|
||||||
lastURL: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
/** History event stream */
|
|
||||||
history: HistoryEvent[] = []
|
|
||||||
|
|
||||||
constructor(config: PageAgentConfig) {
|
constructor(config: PageAgentConfig) {
|
||||||
super()
|
const pageController = new PageController({
|
||||||
|
...config,
|
||||||
this.config = config
|
enableMask: config.enableMask ?? true,
|
||||||
this.#llm = new LLM(this.config)
|
|
||||||
this.panel = new Panel({
|
|
||||||
language: this.config.language,
|
|
||||||
onExecuteTask: (task) => this.execute(task),
|
|
||||||
onStop: () => this.dispose(),
|
|
||||||
onPauseToggle: () => {
|
|
||||||
this.paused = !this.paused
|
|
||||||
return this.paused
|
|
||||||
},
|
|
||||||
getPaused: () => this.paused,
|
|
||||||
})
|
|
||||||
this.tools = new Map(tools)
|
|
||||||
|
|
||||||
// Initialize PageController with config (mask enabled by default)
|
|
||||||
this.pageController = new PageController({
|
|
||||||
...this.config,
|
|
||||||
enableMask: this.config.enableMask ?? true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen to LLM events
|
super({ ...config, pageController })
|
||||||
this.#llmRetryListener = (e) => {
|
|
||||||
const { current, max } = (e as CustomEvent).detail
|
|
||||||
this.panel.update({ type: 'retry', current, max })
|
|
||||||
}
|
|
||||||
this.#llmErrorListener = (e) => {
|
|
||||||
const { error } = (e as CustomEvent).detail
|
|
||||||
this.panel.update({ type: 'error', message: `step failed: ${error.message}` })
|
|
||||||
}
|
|
||||||
this.#llm.addEventListener('retry', this.#llmRetryListener)
|
|
||||||
this.#llm.addEventListener('error', this.#llmErrorListener)
|
|
||||||
|
|
||||||
if (this.config.customTools) {
|
this.panel = new Panel(this, {
|
||||||
for (const [name, tool] of Object.entries(this.config.customTools)) {
|
language: config.language,
|
||||||
if (tool === null) {
|
|
||||||
this.tools.delete(name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
this.tools.set(name, tool)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.config.experimentalScriptExecutionTool) {
|
|
||||||
this.tools.delete('execute_javascript')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#beforeUnloadListener = (e) => {
|
|
||||||
if (!this.disposed) this.dispose('PAGE_UNLOADING')
|
|
||||||
}
|
|
||||||
window.addEventListener('beforeunload', this.#beforeUnloadListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push a persistent observation to the history event stream.
|
|
||||||
* This will be visible in <agent_history> and remain in memory across steps.
|
|
||||||
*/
|
|
||||||
pushObservation(content: string): void {
|
|
||||||
this.history.push({ type: 'observation', content })
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(task: string): Promise<ExecutionResult> {
|
|
||||||
if (!task) throw new Error('Task is required')
|
|
||||||
this.task = task
|
|
||||||
this.taskId = uid()
|
|
||||||
|
|
||||||
const onBeforeStep = this.config.onBeforeStep || (() => void 0)
|
|
||||||
const onAfterStep = this.config.onAfterStep || (() => void 0)
|
|
||||||
const onBeforeTask = this.config.onBeforeTask || (() => void 0)
|
|
||||||
const onAfterTask = this.config.onAfterTask || (() => void 0)
|
|
||||||
|
|
||||||
await onBeforeTask.call(this)
|
|
||||||
|
|
||||||
// Show mask and panel
|
|
||||||
this.pageController.showMask()
|
|
||||||
|
|
||||||
this.panel.show()
|
|
||||||
this.panel.reset()
|
|
||||||
|
|
||||||
this.panel.update({ type: 'input', task: this.task })
|
|
||||||
|
|
||||||
if (this.#abortController) {
|
|
||||||
this.#abortController.abort()
|
|
||||||
this.#abortController = new AbortController()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.history = []
|
|
||||||
|
|
||||||
// Reset states
|
|
||||||
this.states = {
|
|
||||||
totalWaitTime: 0,
|
|
||||||
lastURL: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let step = 0
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
await this.#generateObservations(step)
|
|
||||||
|
|
||||||
await onBeforeStep.call(this, step)
|
|
||||||
|
|
||||||
console.group(`step: ${step}`)
|
|
||||||
|
|
||||||
// abort
|
|
||||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
|
||||||
// pause
|
|
||||||
await waitUntil(() => !this.paused)
|
|
||||||
|
|
||||||
// Update status to thinking
|
|
||||||
console.log(chalk.blue('Thinking...'))
|
|
||||||
this.panel.update({ type: 'thinking' })
|
|
||||||
|
|
||||||
const result = await this.#llm.invoke(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: this.#getSystemPrompt(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: await this.#assembleUserPrompt(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ AgentOutput: this.#packMacroTool() },
|
|
||||||
this.#abortController.signal,
|
|
||||||
{
|
|
||||||
toolChoiceName: 'AgentOutput',
|
|
||||||
normalizeResponse,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const macroResult = result.toolResult as MacroToolResult
|
|
||||||
const input = macroResult.input
|
|
||||||
const output = macroResult.output
|
|
||||||
const reflection: Partial<AgentReflection> = {
|
|
||||||
evaluation_previous_goal: input.evaluation_previous_goal,
|
|
||||||
memory: input.memory,
|
|
||||||
next_goal: input.next_goal,
|
|
||||||
}
|
|
||||||
const actionName = Object.keys(input.action)[0]
|
|
||||||
const action: AgentStep['action'] = {
|
|
||||||
name: actionName,
|
|
||||||
input: input.action[actionName],
|
|
||||||
output: output,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.history.push({
|
|
||||||
type: 'step',
|
|
||||||
reflection,
|
|
||||||
action,
|
|
||||||
usage: result.usage,
|
|
||||||
} as AgentStep)
|
|
||||||
|
|
||||||
console.log(chalk.green('Step finished:'), actionName)
|
|
||||||
console.groupEnd()
|
|
||||||
|
|
||||||
await onAfterStep.call(this, step, this.history)
|
|
||||||
|
|
||||||
step++
|
|
||||||
if (step > MAX_STEPS) {
|
|
||||||
this.#onDone('Step count exceeded maximum limit', false)
|
|
||||||
const result: ExecutionResult = {
|
|
||||||
success: false,
|
|
||||||
data: 'Step count exceeded maximum limit',
|
|
||||||
history: this.history,
|
|
||||||
}
|
|
||||||
await onAfterTask.call(this, result)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
if (actionName === 'done') {
|
|
||||||
const success = action.input?.success ?? false
|
|
||||||
const text = action.input?.text || 'no text provided'
|
|
||||||
console.log(chalk.green.bold('Task completed'), success, text)
|
|
||||||
this.#onDone(text, success)
|
|
||||||
const result: ExecutionResult = {
|
|
||||||
success,
|
|
||||||
data: text,
|
|
||||||
history: this.history,
|
|
||||||
}
|
|
||||||
await onAfterTask.call(this, result)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error('Task failed', error)
|
|
||||||
this.#onDone(String(error), false)
|
|
||||||
const result: ExecutionResult = {
|
|
||||||
success: false,
|
|
||||||
data: String(error),
|
|
||||||
history: this.history,
|
|
||||||
}
|
|
||||||
await onAfterTask.call(this, result)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge all tools into a single MacroTool with the following input:
|
|
||||||
* - thinking: string
|
|
||||||
* - evaluation_previous_goal: string
|
|
||||||
* - memory: string
|
|
||||||
* - next_goal: string
|
|
||||||
* - action: { toolName: toolInput }
|
|
||||||
* where action must be selected from tools defined in this.tools
|
|
||||||
*/
|
|
||||||
#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
|
|
||||||
const tools = this.tools
|
|
||||||
|
|
||||||
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
|
||||||
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const actionSchema = zod.union(
|
|
||||||
actionSchemas as unknown as [zod.ZodType, zod.ZodType, ...zod.ZodType[]]
|
|
||||||
)
|
|
||||||
|
|
||||||
const macroToolSchema = zod.object({
|
|
||||||
// thinking: zod.string().optional(),
|
|
||||||
evaluation_previous_goal: zod.string().optional(),
|
|
||||||
memory: zod.string().optional(),
|
|
||||||
next_goal: zod.string().optional(),
|
|
||||||
action: actionSchema,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
description: 'You MUST call this tool every step. Outputs your reflections and next action.',
|
|
||||||
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
|
|
||||||
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
|
|
||||||
// abort
|
|
||||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
|
||||||
// pause
|
|
||||||
await waitUntil(() => !this.paused)
|
|
||||||
|
|
||||||
console.log(chalk.blue.bold('MacroTool execute'), input)
|
|
||||||
const action = input.action
|
|
||||||
|
|
||||||
const toolName = Object.keys(action)[0]
|
|
||||||
const toolInput = action[toolName]
|
|
||||||
|
|
||||||
// Build reflection text, only include non-empty fields
|
|
||||||
const reflectionLines: string[] = []
|
|
||||||
if (input.evaluation_previous_goal)
|
|
||||||
reflectionLines.push(`✅: ${input.evaluation_previous_goal}`)
|
|
||||||
if (input.memory) reflectionLines.push(`💾: ${input.memory}`)
|
|
||||||
if (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`)
|
|
||||||
|
|
||||||
const reflectionText = reflectionLines.length > 0 ? reflectionLines.join('\n') : ''
|
|
||||||
|
|
||||||
if (reflectionText) {
|
|
||||||
console.log(reflectionText)
|
|
||||||
this.panel.update({ type: 'thinking', text: reflectionText })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the corresponding tool
|
|
||||||
const tool = tools.get(toolName)
|
|
||||||
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
|
|
||||||
|
|
||||||
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
|
|
||||||
this.panel.update({ type: 'toolExecuting', toolName, args: toolInput })
|
|
||||||
|
|
||||||
const startTime = Date.now()
|
|
||||||
|
|
||||||
// Execute tool, bind `this` to PageAgent
|
|
||||||
const result = await tool.execute.bind(this)(toolInput)
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime
|
|
||||||
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
|
|
||||||
|
|
||||||
// Reset wait time for non-wait tools
|
|
||||||
if (toolName !== 'wait') {
|
|
||||||
this.states.totalWaitTime = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Briefly display execution result
|
|
||||||
this.panel.update({
|
|
||||||
type: 'toolCompleted',
|
|
||||||
toolName,
|
|
||||||
args: toolInput,
|
|
||||||
result,
|
|
||||||
duration,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait a moment to let user see the result
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
||||||
|
|
||||||
// Return structured result
|
|
||||||
return {
|
|
||||||
input,
|
|
||||||
output: result,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get system prompt, dynamically replace language settings based on configured language
|
|
||||||
*/
|
|
||||||
#getSystemPrompt(): string {
|
|
||||||
let systemPrompt = SYSTEM_PROMPT
|
|
||||||
|
|
||||||
const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
|
|
||||||
systemPrompt = systemPrompt.replace(
|
|
||||||
/Default working language: \*\*.*?\*\*/,
|
|
||||||
`Default working language: **${targetLanguage}**`
|
|
||||||
)
|
|
||||||
|
|
||||||
return systemPrompt
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get instructions from config and format as XML block
|
|
||||||
*/
|
|
||||||
async #getInstructions(): Promise<string> {
|
|
||||||
const { instructions } = this.config
|
|
||||||
if (!instructions) return ''
|
|
||||||
|
|
||||||
const systemInstructions = instructions.system?.trim()
|
|
||||||
const url = await this.pageController.getCurrentUrl()
|
|
||||||
let pageInstructions: string | undefined
|
|
||||||
|
|
||||||
if (instructions.getPageInstructions) {
|
|
||||||
try {
|
|
||||||
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
chalk.red('[PageAgent] Failed to execute getPageInstructions callback:'),
|
|
||||||
error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!systemInstructions && !pageInstructions) return ''
|
|
||||||
|
|
||||||
let result = '<instructions>\n'
|
|
||||||
|
|
||||||
if (systemInstructions) {
|
|
||||||
result += `<system_instructions>\n${systemInstructions}\n</system_instructions>\n`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageInstructions) {
|
|
||||||
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
|
|
||||||
}
|
|
||||||
|
|
||||||
result += '</instructions>\n\n'
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate observations before each step
|
|
||||||
* - URL change detection
|
|
||||||
* - Too many steps warning
|
|
||||||
* @todo loop detection
|
|
||||||
* @todo console error
|
|
||||||
*/
|
|
||||||
async #generateObservations(stepCount: number): Promise<void> {
|
|
||||||
// Detect URL change
|
|
||||||
const currentURL = await this.pageController.getCurrentUrl()
|
|
||||||
if (currentURL !== this.states.lastURL) {
|
|
||||||
this.pushObservation(`Page navigated to → ${currentURL}`)
|
|
||||||
this.states.lastURL = currentURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn about remaining steps
|
|
||||||
const remaining = MAX_STEPS - stepCount
|
|
||||||
if (remaining === 5) {
|
|
||||||
this.pushObservation(
|
|
||||||
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
|
|
||||||
)
|
|
||||||
} else if (remaining === 2) {
|
|
||||||
this.pushObservation(
|
|
||||||
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async #assembleUserPrompt(): Promise<string> {
|
|
||||||
let prompt = ''
|
|
||||||
|
|
||||||
// <instructions> (optional)
|
|
||||||
prompt += await this.#getInstructions()
|
|
||||||
|
|
||||||
// <agent_state>
|
|
||||||
// - <user_request>
|
|
||||||
// - <step_info>
|
|
||||||
// <agent_state>
|
|
||||||
|
|
||||||
const stepCount = this.history.filter((e) => e.type === 'step').length
|
|
||||||
|
|
||||||
prompt += `<agent_state>
|
|
||||||
<user_request>
|
|
||||||
${this.task}
|
|
||||||
</user_request>
|
|
||||||
<step_info>
|
|
||||||
Step ${stepCount + 1} of ${MAX_STEPS} max possible steps
|
|
||||||
Current date and time: ${new Date().toISOString()}
|
|
||||||
</step_info>
|
|
||||||
</agent_state>
|
|
||||||
`
|
|
||||||
|
|
||||||
// <agent_history>
|
|
||||||
// - <step_N> for steps
|
|
||||||
// - <sys> for observations and system messages
|
|
||||||
|
|
||||||
prompt += '\n<agent_history>\n'
|
|
||||||
|
|
||||||
let stepIndex = 0
|
|
||||||
for (const event of this.history) {
|
|
||||||
if (event.type === 'step') {
|
|
||||||
stepIndex++
|
|
||||||
prompt += `<step_${stepIndex}>
|
|
||||||
Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
|
|
||||||
Memory: ${event.reflection.memory}
|
|
||||||
Next Goal: ${event.reflection.next_goal}
|
|
||||||
Action Results: ${event.action.output}
|
|
||||||
</step_${stepIndex}>
|
|
||||||
`
|
|
||||||
} else if (event.type === 'observation') {
|
|
||||||
prompt += `<sys>${event.content}</sys>\n`
|
|
||||||
} else if (event.type === 'user_takeover') {
|
|
||||||
prompt += `<sys>User took over control and made changes to the page.</sys>\n`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt += '</agent_history>\n\n'
|
|
||||||
|
|
||||||
// <browser_state>
|
|
||||||
|
|
||||||
prompt += await this.#getBrowserState()
|
|
||||||
|
|
||||||
return trimLines(prompt)
|
|
||||||
}
|
|
||||||
|
|
||||||
#onDone(text: string, success = true) {
|
|
||||||
this.pageController.cleanUpHighlights()
|
|
||||||
|
|
||||||
// Update panel status
|
|
||||||
if (success) {
|
|
||||||
this.panel.update({ type: 'output', text })
|
|
||||||
} else {
|
|
||||||
this.panel.update({ type: 'error', message: text })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task completed
|
|
||||||
this.panel.update({ type: 'completed' })
|
|
||||||
|
|
||||||
this.pageController.hideMask()
|
|
||||||
|
|
||||||
this.#abortController.abort()
|
|
||||||
}
|
|
||||||
|
|
||||||
async #getBrowserState(): Promise<string> {
|
|
||||||
const state = await this.pageController.getBrowserState()
|
|
||||||
|
|
||||||
let content = state.content
|
|
||||||
if (this.config.transformPageContent) {
|
|
||||||
content = await this.config.transformPageContent(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimLines(`<browser_state>
|
|
||||||
Current Page: [${state.title}](${state.url})
|
|
||||||
|
|
||||||
${state.header}
|
|
||||||
${content}
|
|
||||||
${state.footer}
|
|
||||||
|
|
||||||
</browser_state>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(reason?: string) {
|
|
||||||
console.log('Disposing PageAgent...')
|
|
||||||
this.disposed = true
|
|
||||||
this.pageController.dispose()
|
|
||||||
this.panel.dispose()
|
|
||||||
this.history = []
|
|
||||||
this.#abortController.abort(reason ?? 'PageAgent disposed')
|
|
||||||
|
|
||||||
// Clean up LLM event listeners
|
|
||||||
if (this.#llmRetryListener) {
|
|
||||||
this.#llm.removeEventListener('retry', this.#llmRetryListener)
|
|
||||||
this.#llmRetryListener = null
|
|
||||||
}
|
|
||||||
if (this.#llmErrorListener) {
|
|
||||||
this.#llm.removeEventListener('error', this.#llmErrorListener)
|
|
||||||
this.#llmErrorListener = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up window event listeners
|
|
||||||
if (this.#beforeUnloadListener) {
|
|
||||||
window.removeEventListener('beforeunload', this.#beforeUnloadListener)
|
|
||||||
this.#beforeUnloadListener = null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.config.onDispose?.call(this, reason)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-run entry for page-agent.js. Insert this script into your page to get page-agent functionality.
|
* IIFE demo entry - auto-initializes with built-in demo API for testing
|
||||||
*/
|
*/
|
||||||
import { PageAgent, type PageAgentConfig } from './PageAgent'
|
import { PageAgent, type PageAgentConfig } from './PageAgent'
|
||||||
|
|
||||||
@@ -11,9 +11,6 @@ if (window.pageAgent) {
|
|||||||
// Mount to global window object
|
// Mount to global window object
|
||||||
window.PageAgent = PageAgent
|
window.PageAgent = PageAgent
|
||||||
|
|
||||||
// Export for ES module usage
|
|
||||||
// export { PageAgent }
|
|
||||||
|
|
||||||
console.log('🚀 page-agent.js loaded!')
|
console.log('🚀 page-agent.js loaded!')
|
||||||
|
|
||||||
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||||
@@ -21,9 +18,10 @@ const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm
|
|||||||
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||||
|
|
||||||
// in case document.x is not ready yet
|
// in case document.x is not ready yet
|
||||||
// @todo give a switch to disable auto-init
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const currentScript = document.currentScript as HTMLScriptElement | null
|
const currentScript = document.currentScript as HTMLScriptElement | null
|
||||||
|
let config: PageAgentConfig
|
||||||
|
|
||||||
if (currentScript) {
|
if (currentScript) {
|
||||||
console.log('🚀 page-agent.js detected current script:', currentScript.src)
|
console.log('🚀 page-agent.js detected current script:', currentScript.src)
|
||||||
const url = new URL(currentScript.src)
|
const url = new URL(currentScript.src)
|
||||||
@@ -31,19 +29,18 @@ setTimeout(() => {
|
|||||||
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
|
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
|
||||||
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
|
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
|
||||||
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
|
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
|
||||||
const config: PageAgentConfig = { model, baseURL, apiKey, language }
|
config = { model, baseURL, apiKey, language }
|
||||||
window.pageAgent = new PageAgent(config)
|
|
||||||
} else {
|
} else {
|
||||||
console.log('🚀 page-agent.js no current script detected, using default demo config')
|
console.log('🚀 page-agent.js no current script detected, using default demo config')
|
||||||
const config: PageAgentConfig = {
|
config = {
|
||||||
model: DEMO_MODEL,
|
model: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,
|
||||||
baseURL: DEMO_BASE_URL,
|
baseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,
|
||||||
apiKey: DEMO_API_KEY,
|
apiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,
|
||||||
}
|
}
|
||||||
window.pageAgent = new PageAgent(config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
// Create agent
|
||||||
|
window.pageAgent = new PageAgent(config)
|
||||||
|
|
||||||
window.pageAgent.panel.show() // Show panel
|
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
||||||
})
|
})
|
||||||
Vendored
-11
@@ -1,20 +1,9 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
import type { PageAgent } from './PageAgent'
|
import type { PageAgent } from './PageAgent'
|
||||||
|
|
||||||
declare module '*.module.css' {
|
|
||||||
const classes: Record<string, string>
|
|
||||||
export default classes
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.md?raw' {
|
|
||||||
const content: string
|
|
||||||
export default content
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
pageAgent?: PageAgent
|
pageAgent?: PageAgent
|
||||||
PageAgent: typeof PageAgent
|
PageAgent: typeof PageAgent
|
||||||
__PAGE_AGENT_IDS__: string[]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
/**
|
|
||||||
* Event mapping definitions
|
|
||||||
* @note Event bus callbacks must be repeatable without errors
|
|
||||||
*/
|
|
||||||
export interface PageAgentEventMap {
|
|
||||||
// PageAgent status events
|
|
||||||
// 'agent:execute': { params: { task: string } }
|
|
||||||
// 'agent:done': { params: { text: string; success: boolean } }
|
|
||||||
// 'agent:paused': { params: undefined }
|
|
||||||
// 'agent:resumed': { params: undefined }
|
|
||||||
// 'agent:disposed': { params: undefined }
|
|
||||||
// 'agent:error': { params: { error: string | Error } }
|
|
||||||
|
|
||||||
// Task status change events
|
|
||||||
'task:start': { params: { task: string } }
|
|
||||||
// 'task:complete': { params: { text: string; success: boolean } }
|
|
||||||
// 'task:error': { params: { error: string | Error } }
|
|
||||||
|
|
||||||
// Index signature for dynamic event names
|
|
||||||
// [key: string]: { params: any }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event handler type definitions
|
|
||||||
*/
|
|
||||||
export type EventHandler<T extends keyof PageAgentEventMap> =
|
|
||||||
PageAgentEventMap[T]['params'] extends undefined
|
|
||||||
? () => void
|
|
||||||
: (params: PageAgentEventMap[T]['params']) => void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Async event handler type definitions
|
|
||||||
*/
|
|
||||||
export type AsyncEventHandler<T extends keyof PageAgentEventMap> =
|
|
||||||
PageAgentEventMap[T]['params'] extends undefined
|
|
||||||
? () => Promise<void>
|
|
||||||
: (params: PageAgentEventMap[T]['params']) => Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type-safe event bus
|
|
||||||
* @note Mainly used to decouple logic and UI
|
|
||||||
* @note All modules of a PageAgent instance share the same EventBus instance for communication
|
|
||||||
* @note Use with caution if delivery guarantee is needed for logic communication
|
|
||||||
* @note `on` `once` `emit` methods handle built-in events with type protection, use `addEventListener` for other events
|
|
||||||
*/
|
|
||||||
class EventBus extends EventTarget {
|
|
||||||
/**
|
|
||||||
* Listen to built-in events
|
|
||||||
*/
|
|
||||||
on<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
|
|
||||||
const wrappedHandler = (e: Event) => {
|
|
||||||
const customEvent = e as CustomEvent
|
|
||||||
const params = customEvent.detail?.[0]
|
|
||||||
return handler(params)
|
|
||||||
}
|
|
||||||
this.addEventListener(event, wrappedHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen to built-in events (one-time)
|
|
||||||
*/
|
|
||||||
once<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
|
|
||||||
const wrappedHandler = (e: Event) => {
|
|
||||||
const customEvent = e as CustomEvent
|
|
||||||
const params = customEvent.detail?.[0]
|
|
||||||
return handler(params)
|
|
||||||
}
|
|
||||||
this.addEventListener(event, wrappedHandler, { once: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit built-in events
|
|
||||||
*/
|
|
||||||
emit<T extends keyof PageAgentEventMap>(
|
|
||||||
event: T,
|
|
||||||
...args: PageAgentEventMap[T]['params'] extends undefined
|
|
||||||
? []
|
|
||||||
: [PageAgentEventMap[T]['params']]
|
|
||||||
): void {
|
|
||||||
const customEvent = new CustomEvent(event, { detail: args })
|
|
||||||
this.dispatchEvent(customEvent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buses = new Map<string, EventBus>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the event bus for a given channel
|
|
||||||
*/
|
|
||||||
export function getEventBus(channel: string) {
|
|
||||||
if (buses.has(channel)) {
|
|
||||||
return buses.get(channel)!
|
|
||||||
}
|
|
||||||
const bus = new EventBus()
|
|
||||||
buses.set(channel, bus)
|
|
||||||
return bus
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { EventBus }
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// @workaround DTS bug
|
// @workaround DTS bug
|
||||||
// dts do not work with monorepo path mapping
|
// dts do not work with monorepo path mapping
|
||||||
// disable path mapping for it
|
// disable path mapping for it
|
||||||
"paths": {}
|
"paths": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"paths": {
|
"paths": {
|
||||||
//
|
//
|
||||||
"@page-agent/llms": ["../llms/src/index.ts"],
|
"@page-agent/llms": ["../llms/src/index.ts"],
|
||||||
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
|
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
|
||||||
"@page-agent/ui": ["../ui/src/index.ts"]
|
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
|
||||||
}
|
"@page-agent/ui": ["../ui/src/index.ts"]
|
||||||
},
|
}
|
||||||
"include": ["**/*.ts"],
|
},
|
||||||
"exclude": ["dist", "node_modules"],
|
"include": ["**/*.ts"],
|
||||||
"references": [
|
"exclude": ["dist", "node_modules"],
|
||||||
//
|
"references": [
|
||||||
{ "path": "../llms" },
|
//
|
||||||
{ "path": "../page-controller" },
|
{ "path": "../llms" },
|
||||||
{ "path": "../ui" }
|
{ "path": "../page-controller" },
|
||||||
]
|
{ "path": "../core" },
|
||||||
|
{ "path": "../ui" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
import { config as dotenvConfig } from 'dotenv'
|
||||||
import { dirname, resolve } from 'path'
|
import { dirname, resolve } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
@@ -6,11 +7,14 @@ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
|||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
// Load .env from repo root
|
||||||
|
dotenvConfig({ path: resolve(__dirname, '../../.env') })
|
||||||
|
|
||||||
// UMD Bundle for CDN
|
// UMD Bundle for CDN
|
||||||
// - alias all local packages so that they can be build in
|
// - alias all local packages so that they can be build in
|
||||||
// - no external
|
// - no external
|
||||||
// - no d.ts. dts does not work with monorepo aliasing
|
// - no d.ts. dts does not work with monorepo aliasing
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => ({
|
||||||
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
esbuild: {
|
||||||
@@ -20,26 +24,34 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
|
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
|
||||||
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
|
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
|
||||||
|
'@page-agent/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),
|
||||||
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
|
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/umd.ts'),
|
entry: resolve(__dirname, 'src/demo.ts'),
|
||||||
name: 'PageAgent',
|
name: 'PageAgent',
|
||||||
fileName: 'page-agent',
|
fileName: () => `page-agent.demo.js`,
|
||||||
formats: ['umd'],
|
formats: ['iife'],
|
||||||
},
|
},
|
||||||
outDir: resolve(__dirname, 'dist', 'umd'),
|
outDir: resolve(__dirname, 'dist', 'iife'),
|
||||||
|
cssCodeSplit: true,
|
||||||
|
// minify: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
// output: {
|
||||||
// force use .js as extension
|
// // force use .js as extension
|
||||||
entryFileNames: 'page-agent.js',
|
// entryFileNames: 'page-agent.js',
|
||||||
|
// },
|
||||||
|
onwarn: function (message, handler) {
|
||||||
|
if (message.code === 'EVAL') return
|
||||||
|
handler(message)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cssCodeSplit: true,
|
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.NODE_ENV': '"production"',
|
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),
|
||||||
|
'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
|
||||||
|
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-controller.js",
|
"main": "./dist/lib/page-controller.js",
|
||||||
"module": "./dist/lib/page-controller.js",
|
"module": "./dist/lib/page-controller.js",
|
||||||
|
|||||||
@@ -18,11 +18,8 @@ import { VIEWPORT_EXPANSION } from './constants'
|
|||||||
import * as dom from './dom'
|
import * as dom from './dom'
|
||||||
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
|
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
|
||||||
import { getPageInfo } from './dom/getPageInfo'
|
import { getPageInfo } from './dom/getPageInfo'
|
||||||
import { SimulatorMask } from './mask/SimulatorMask'
|
|
||||||
import { patchReact } from './patches/react'
|
import { patchReact } from './patches/react'
|
||||||
|
|
||||||
export { SimulatorMask }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for PageController
|
* Configuration for PageController
|
||||||
*/
|
*/
|
||||||
@@ -84,7 +81,8 @@ export class PageController extends EventTarget {
|
|||||||
private lastTimeUpdate = 0
|
private lastTimeUpdate = 0
|
||||||
|
|
||||||
/** Visual mask overlay for blocking user interaction during automation */
|
/** Visual mask overlay for blocking user interaction during automation */
|
||||||
private mask: SimulatorMask | null = null
|
private mask: InstanceType<typeof import('./mask/SimulatorMask').SimulatorMask> | null = null
|
||||||
|
private maskReady: Promise<void> | null = null
|
||||||
|
|
||||||
constructor(config: PageControllerConfig = {}) {
|
constructor(config: PageControllerConfig = {}) {
|
||||||
super()
|
super()
|
||||||
@@ -94,10 +92,17 @@ export class PageController extends EventTarget {
|
|||||||
patchReact(this)
|
patchReact(this)
|
||||||
|
|
||||||
if (config.enableMask) {
|
if (config.enableMask) {
|
||||||
this.mask = new SimulatorMask()
|
this.maskReady = this.initMask()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize mask asynchronously (dynamic import to avoid CSS loading in Node)
|
||||||
|
*/
|
||||||
|
private async initMask(): Promise<void> {
|
||||||
|
const { SimulatorMask } = await import('./mask/SimulatorMask')
|
||||||
|
this.mask = new SimulatorMask()
|
||||||
|
}
|
||||||
// ======= State Queries =======
|
// ======= State Queries =======
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -366,7 +371,8 @@ export class PageController extends EventTarget {
|
|||||||
* Show the visual mask overlay.
|
* Show the visual mask overlay.
|
||||||
* Only works if enableMask was set to true in config.
|
* Only works if enableMask was set to true in config.
|
||||||
*/
|
*/
|
||||||
showMask(): void {
|
async showMask(): Promise<void> {
|
||||||
|
await this.maskReady
|
||||||
this.mask?.show()
|
this.mask?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +380,8 @@ export class PageController extends EventTarget {
|
|||||||
* Hide the visual mask overlay.
|
* Hide the visual mask overlay.
|
||||||
* Only works if enableMask was set to true in config.
|
* Only works if enableMask was set to true in config.
|
||||||
*/
|
*/
|
||||||
hideMask(): void {
|
async hideMask(): Promise<void> {
|
||||||
|
await this.maskReady
|
||||||
this.mask?.hide()
|
this.mask?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,12 +287,52 @@ export async function scrollVertically(
|
|||||||
(document.documentElement as HTMLElement)
|
(document.documentElement as HTMLElement)
|
||||||
|
|
||||||
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
||||||
|
// Page-level scroll
|
||||||
|
const scrollBefore = window.scrollY
|
||||||
|
const scrollMax = document.documentElement.scrollHeight - window.innerHeight
|
||||||
|
|
||||||
window.scrollBy(0, dy)
|
window.scrollBy(0, dy)
|
||||||
return `✅ Scrolled page by ${dy}px.`
|
|
||||||
|
const scrollAfter = window.scrollY
|
||||||
|
const scrolled = scrollAfter - scrollBefore
|
||||||
|
|
||||||
|
if (Math.abs(scrolled) < 1) {
|
||||||
|
return dy > 0
|
||||||
|
? `⚠️ Already at the bottom of the page, cannot scroll down further.`
|
||||||
|
: `⚠️ Already at the top of the page, cannot scroll up further.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
|
||||||
|
const reachedTop = dy < 0 && scrollAfter <= 1
|
||||||
|
|
||||||
|
if (reachedBottom) return `✅ Scrolled page by ${scrolled}px. Reached the bottom of the page.`
|
||||||
|
if (reachedTop) return `✅ Scrolled page by ${scrolled}px. Reached the top of the page.`
|
||||||
|
return `✅ Scrolled page by ${scrolled}px.`
|
||||||
} else {
|
} else {
|
||||||
|
// Container scroll
|
||||||
|
const scrollBefore = el!.scrollTop
|
||||||
|
const scrollMax = el!.scrollHeight - el!.clientHeight
|
||||||
|
|
||||||
el!.scrollBy({ top: dy, behavior: 'smooth' })
|
el!.scrollBy({ top: dy, behavior: 'smooth' })
|
||||||
await waitFor(0.1) // Animation playback
|
await waitFor(0.1)
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${dy}px.`
|
|
||||||
|
const scrollAfter = el!.scrollTop
|
||||||
|
const scrolled = scrollAfter - scrollBefore
|
||||||
|
|
||||||
|
if (Math.abs(scrolled) < 1) {
|
||||||
|
return dy > 0
|
||||||
|
? `⚠️ Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
|
||||||
|
: `⚠️ Already at the top of container (${el!.tagName}), cannot scroll up further.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
|
||||||
|
const reachedTop = dy < 0 && scrollAfter <= 1
|
||||||
|
|
||||||
|
if (reachedBottom)
|
||||||
|
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
|
||||||
|
if (reachedTop)
|
||||||
|
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
|
||||||
|
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,11 +450,52 @@ export async function scrollHorizontally(
|
|||||||
(document.documentElement as HTMLElement)
|
(document.documentElement as HTMLElement)
|
||||||
|
|
||||||
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
||||||
|
// Page-level scroll
|
||||||
|
const scrollBefore = window.scrollX
|
||||||
|
const scrollMax = document.documentElement.scrollWidth - window.innerWidth
|
||||||
|
|
||||||
window.scrollBy(dx, 0)
|
window.scrollBy(dx, 0)
|
||||||
return `✅ Scrolled page horizontally by ${dx}px`
|
|
||||||
|
const scrollAfter = window.scrollX
|
||||||
|
const scrolled = scrollAfter - scrollBefore
|
||||||
|
|
||||||
|
if (Math.abs(scrolled) < 1) {
|
||||||
|
return dx > 0
|
||||||
|
? `⚠️ Already at the right edge of the page, cannot scroll right further.`
|
||||||
|
: `⚠️ Already at the left edge of the page, cannot scroll left further.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
|
||||||
|
const reachedLeft = dx < 0 && scrollAfter <= 1
|
||||||
|
|
||||||
|
if (reachedRight)
|
||||||
|
return `✅ Scrolled page by ${scrolled}px. Reached the right edge of the page.`
|
||||||
|
if (reachedLeft) return `✅ Scrolled page by ${scrolled}px. Reached the left edge of the page.`
|
||||||
|
return `✅ Scrolled page horizontally by ${scrolled}px.`
|
||||||
} else {
|
} else {
|
||||||
|
// Container scroll
|
||||||
|
const scrollBefore = el!.scrollLeft
|
||||||
|
const scrollMax = el!.scrollWidth - el!.clientWidth
|
||||||
|
|
||||||
el!.scrollBy({ left: dx, behavior: 'smooth' })
|
el!.scrollBy({ left: dx, behavior: 'smooth' })
|
||||||
await waitFor(0.1) // Animation playback
|
await waitFor(0.1)
|
||||||
return `✅ Scrolled container (${el!.tagName}) horizontally by ${dx}px`
|
|
||||||
|
const scrollAfter = el!.scrollLeft
|
||||||
|
const scrolled = scrollAfter - scrollBefore
|
||||||
|
|
||||||
|
if (Math.abs(scrolled) < 1) {
|
||||||
|
return dx > 0
|
||||||
|
? `⚠️ Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
|
||||||
|
: `⚠️ Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
|
||||||
|
const reachedLeft = dx < 0 && scrollAfter <= 1
|
||||||
|
|
||||||
|
if (reachedRight)
|
||||||
|
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
|
||||||
|
if (reachedLeft)
|
||||||
|
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
|
||||||
|
return `✅ Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// @workaround DTS bug
|
// @workaround DTS bug
|
||||||
// dts do not work with monorepo path mapping
|
// dts do not work with monorepo path mapping
|
||||||
// disable path mapping for it
|
// disable path mapping for it
|
||||||
"paths": {}
|
"paths": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.js"],
|
"include": ["**/*.ts", "**/*.js"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export default defineConfig({
|
|||||||
outDir: resolve(__dirname, 'dist', 'lib'),
|
outDir: resolve(__dirname, 'dist', 'lib'),
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['@page-agent/*', 'ai-motion'],
|
external: ['@page-agent/*', 'ai-motion'],
|
||||||
|
onwarn: function (message, handler) {
|
||||||
|
if (message.code === 'EVAL') return
|
||||||
|
handler(message)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
minify: false,
|
minify: false,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-ui.js",
|
"main": "./dist/lib/page-agent-ui.js",
|
||||||
"module": "./dist/lib/page-agent-ui.js",
|
"module": "./dist/lib/page-agent-ui.js",
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
/**
|
|
||||||
* Agent execution state management
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface Step {
|
|
||||||
id: string
|
|
||||||
stepNumber: number
|
|
||||||
timestamp: Date
|
|
||||||
type: 'thinking' | 'tool_executing' | 'completed' | 'error' | 'output' | 'input' | 'retry'
|
|
||||||
|
|
||||||
// Tool execution related
|
|
||||||
toolName?: string
|
|
||||||
toolArgs?: any
|
|
||||||
toolResult?: any
|
|
||||||
|
|
||||||
// Display data
|
|
||||||
displayText: string
|
|
||||||
duration?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AgentStatus = 'idle' | 'running' | 'paused' | 'completed' | 'error'
|
|
||||||
|
|
||||||
export class UIState {
|
|
||||||
private steps: Step[] = []
|
|
||||||
private currentStep: Step | null = null
|
|
||||||
private status: AgentStatus = 'idle'
|
|
||||||
private stepCounter = 0
|
|
||||||
|
|
||||||
addStep(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): Step {
|
|
||||||
const step: Step = {
|
|
||||||
id: this.generateId(),
|
|
||||||
stepNumber: ++this.stepCounter,
|
|
||||||
timestamp: new Date(),
|
|
||||||
...stepData,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.steps.push(step)
|
|
||||||
this.currentStep = step
|
|
||||||
|
|
||||||
// Update overall status
|
|
||||||
this.updateStatus(step.type)
|
|
||||||
|
|
||||||
return step
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentStep(updates: Partial<Step>): Step | null {
|
|
||||||
if (!this.currentStep) return null
|
|
||||||
|
|
||||||
Object.assign(this.currentStep, updates)
|
|
||||||
return this.currentStep
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentStep(): Step | null {
|
|
||||||
return this.currentStep
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllSteps(): Step[] {
|
|
||||||
return [...this.steps]
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatus(): AgentStatus {
|
|
||||||
return this.status
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(): void {
|
|
||||||
this.steps = []
|
|
||||||
this.currentStep = null
|
|
||||||
this.status = 'idle'
|
|
||||||
this.stepCounter = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateStatus(stepType: Step['type']): void {
|
|
||||||
switch (stepType) {
|
|
||||||
case 'thinking':
|
|
||||||
case 'tool_executing':
|
|
||||||
case 'output':
|
|
||||||
case 'input':
|
|
||||||
case 'retry':
|
|
||||||
this.status = 'running'
|
|
||||||
break
|
|
||||||
case 'completed':
|
|
||||||
this.status = 'completed'
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
this.status = 'error'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateId(): string {
|
|
||||||
return `step_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,17 +4,13 @@ const enUS = {
|
|||||||
panel: {
|
panel: {
|
||||||
ready: 'Ready',
|
ready: 'Ready',
|
||||||
thinking: 'Thinking...',
|
thinking: 'Thinking...',
|
||||||
paused: 'Paused',
|
|
||||||
taskInput: 'Enter new task, describe steps in detail, press Enter to submit',
|
taskInput: 'Enter new task, describe steps in detail, press Enter to submit',
|
||||||
userAnswerPrompt: 'Please answer the question above, press Enter to submit',
|
userAnswerPrompt: 'Please answer the question above, press Enter to submit',
|
||||||
taskTerminated: 'Task terminated',
|
taskTerminated: 'Task terminated',
|
||||||
taskCompleted: 'Task completed',
|
taskCompleted: 'Task completed',
|
||||||
continueExecution: 'Continue execution',
|
|
||||||
userAnswer: 'User answer: {{input}}',
|
userAnswer: 'User answer: {{input}}',
|
||||||
question: 'Question: {{question}}',
|
question: 'Question: {{question}}',
|
||||||
waitingPlaceholder: 'Waiting for task to start...',
|
waitingPlaceholder: 'Waiting for task to start...',
|
||||||
pause: 'Pause',
|
|
||||||
continue: 'Continue',
|
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
expand: 'Expand history',
|
expand: 'Expand history',
|
||||||
collapse: 'Collapse history',
|
collapse: 'Collapse history',
|
||||||
@@ -26,6 +22,7 @@ const enUS = {
|
|||||||
selecting: 'Selecting option "{{text}}"...',
|
selecting: 'Selecting option "{{text}}"...',
|
||||||
scrolling: 'Scrolling page...',
|
scrolling: 'Scrolling page...',
|
||||||
waiting: 'Waiting {{seconds}} seconds...',
|
waiting: 'Waiting {{seconds}} seconds...',
|
||||||
|
askingUser: 'Asking user...',
|
||||||
done: 'Task done',
|
done: 'Task done',
|
||||||
clicked: '🖱️ Clicked element [{{index}}]',
|
clicked: '🖱️ Clicked element [{{index}}]',
|
||||||
inputted: '⌨️ Inputted text "{{text}}"',
|
inputted: '⌨️ Inputted text "{{text}}"',
|
||||||
@@ -54,17 +51,13 @@ const zhCN = {
|
|||||||
panel: {
|
panel: {
|
||||||
ready: '准备就绪',
|
ready: '准备就绪',
|
||||||
thinking: '正在思考...',
|
thinking: '正在思考...',
|
||||||
paused: '暂停中,稍后',
|
|
||||||
taskInput: '输入新任务,详细描述步骤,回车提交',
|
taskInput: '输入新任务,详细描述步骤,回车提交',
|
||||||
userAnswerPrompt: '请回答上面问题,回车提交',
|
userAnswerPrompt: '请回答上面问题,回车提交',
|
||||||
taskTerminated: '任务已终止',
|
taskTerminated: '任务已终止',
|
||||||
taskCompleted: '任务结束',
|
taskCompleted: '任务结束',
|
||||||
continueExecution: '继续执行',
|
|
||||||
userAnswer: '用户回答: {{input}}',
|
userAnswer: '用户回答: {{input}}',
|
||||||
question: '询问: {{question}}',
|
question: '询问: {{question}}',
|
||||||
waitingPlaceholder: '等待任务开始...',
|
waitingPlaceholder: '等待任务开始...',
|
||||||
pause: '暂停',
|
|
||||||
continue: '继续',
|
|
||||||
stop: '终止',
|
stop: '终止',
|
||||||
expand: '展开历史',
|
expand: '展开历史',
|
||||||
collapse: '收起历史',
|
collapse: '收起历史',
|
||||||
@@ -76,6 +69,7 @@ const zhCN = {
|
|||||||
selecting: '正在选择选项 "{{text}}"...',
|
selecting: '正在选择选项 "{{text}}"...',
|
||||||
scrolling: '正在滚动页面...',
|
scrolling: '正在滚动页面...',
|
||||||
waiting: '等待 {{seconds}} 秒...',
|
waiting: '等待 {{seconds}} 秒...',
|
||||||
|
askingUser: '正在询问用户...',
|
||||||
done: '结束任务',
|
done: '结束任务',
|
||||||
clicked: '🖱️ 已点击元素 [{{index}}]',
|
clicked: '🖱️ 已点击元素 [{{index}}]',
|
||||||
inputted: '⌨️ 已输入文本 "{{text}}"',
|
inputted: '⌨️ 已输入文本 "{{text}}"',
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { Panel, type PanelConfig, type PanelUpdate } from './Panel'
|
export { Panel, type PanelConfig } from './panel/Panel'
|
||||||
export { UIState, type Step, type AgentStatus } from './UIState'
|
export type { AgentActivity, PanelAgentAdapter } from './panel/types'
|
||||||
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
|
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ This is the CSS implementation of ai-motion.
|
|||||||
|
|
||||||
Easy to use but Terrible performance. Causing full screen glitching in some browsers.
|
Easy to use but Terrible performance. Causing full screen glitching in some browsers.
|
||||||
|
|
||||||
Use it only in a small area.
|
Use it only in a small area.
|
||||||
|
|||||||
@@ -209,18 +209,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pauseButton {
|
|
||||||
font-weight: 600;
|
|
||||||
&.paused {
|
|
||||||
background: rgba(34, 197, 94, 0.2); /* 绿色背景表示可以继续 */
|
|
||||||
color: rgb(34, 197, 94);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(34, 197, 94, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stopButton {
|
.stopButton {
|
||||||
background: rgba(239, 68, 68, 0.2);
|
background: rgba(239, 68, 68, 0.2);
|
||||||
color: rgb(255, 41, 41);
|
color: rgb(255, 41, 41);
|
||||||
@@ -364,6 +352,16 @@
|
|||||||
background: linear-gradient(135deg, rgba(255, 214, 0, 0.1), rgba(255, 214, 0, 0.05));
|
background: linear-gradient(135deg, rgba(255, 214, 0, 0.1), rgba(255, 214, 0, 0.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.observation {
|
||||||
|
border-left-color: rgb(147, 51, 234);
|
||||||
|
background: linear-gradient(135deg, rgba(147, 51, 234, 0.1), rgba(147, 51, 234, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.question {
|
||||||
|
border-left-color: rgb(255, 159, 67);
|
||||||
|
background: linear-gradient(135deg, rgba(255, 159, 67, 0.15), rgba(255, 159, 67, 0.08));
|
||||||
|
}
|
||||||
|
|
||||||
/* 突出显示 done 成功结果 */
|
/* 突出显示 done 成功结果 */
|
||||||
&.doneSuccess {
|
&.doneSuccess {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
@@ -446,7 +444,7 @@
|
|||||||
|
|
||||||
.historyContent {
|
.historyContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@@ -460,6 +458,12 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reflectionLines {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.historyMeta {
|
.historyMeta {
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type Step, UIState } from './UIState'
|
import { I18n, type SupportedLanguage } from '../i18n'
|
||||||
import { I18n, type SupportedLanguage } from './i18n'
|
import { truncate } from '../utils'
|
||||||
import { escapeHtml, truncate } from './utils'
|
import { createCard, createReflectionLines, formatTime } from './cards'
|
||||||
|
import type { AgentActivity, PanelAgentAdapter } from './types'
|
||||||
|
|
||||||
import styles from './Panel.module.css'
|
import styles from './Panel.module.css'
|
||||||
|
|
||||||
@@ -9,29 +10,22 @@ import styles from './Panel.module.css'
|
|||||||
*/
|
*/
|
||||||
export interface PanelConfig {
|
export interface PanelConfig {
|
||||||
language?: SupportedLanguage
|
language?: SupportedLanguage
|
||||||
onExecuteTask: (task: string) => void
|
/**
|
||||||
onStop: () => void
|
* Whether to prompt for next task after task completion
|
||||||
onPauseToggle: () => boolean // returns new paused state
|
* @default true
|
||||||
getPaused: () => boolean
|
*/
|
||||||
|
promptForNextTask?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Semantic update types - Panel handles i18n internally
|
|
||||||
*/
|
|
||||||
export type PanelUpdate =
|
|
||||||
| { type: 'thinking'; text?: string } // text is optional, defaults to i18n thinking text
|
|
||||||
| { type: 'input'; task: string }
|
|
||||||
| { type: 'question'; question: string }
|
|
||||||
| { type: 'userAnswer'; input: string }
|
|
||||||
| { type: 'retry'; current: number; max: number }
|
|
||||||
| { type: 'error'; message: string }
|
|
||||||
| { type: 'output'; text: string }
|
|
||||||
| { type: 'completed' }
|
|
||||||
| { type: 'toolExecuting'; toolName: string; args: any }
|
|
||||||
| { type: 'toolCompleted'; toolName: string; args: any; result?: string; duration?: number }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent control panel
|
* Agent control panel
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - History list: renders directly from agent.history (historical events)
|
||||||
|
* - Header bar: shows activity events (transient state) and agent status
|
||||||
|
*
|
||||||
|
* This separation ensures data consistency - history is the single source of truth
|
||||||
|
* for what has been done, while activity shows what is happening now.
|
||||||
*/
|
*/
|
||||||
export class Panel {
|
export class Panel {
|
||||||
#wrapper: HTMLElement
|
#wrapper: HTMLElement
|
||||||
@@ -39,14 +33,13 @@ export class Panel {
|
|||||||
#statusText: HTMLElement
|
#statusText: HTMLElement
|
||||||
#historySection: HTMLElement
|
#historySection: HTMLElement
|
||||||
#expandButton: HTMLElement
|
#expandButton: HTMLElement
|
||||||
#pauseButton: HTMLElement
|
|
||||||
#stopButton: HTMLElement
|
#stopButton: HTMLElement
|
||||||
#inputSection: HTMLElement
|
#inputSection: HTMLElement
|
||||||
#taskInput: HTMLInputElement
|
#taskInput: HTMLInputElement
|
||||||
|
|
||||||
#state = new UIState()
|
#agent: PanelAgentAdapter
|
||||||
#isExpanded = false
|
|
||||||
#config: PanelConfig
|
#config: PanelConfig
|
||||||
|
#isExpanded = false
|
||||||
#i18n: I18n
|
#i18n: I18n
|
||||||
#userAnswerResolver: ((input: string) => void) | null = null
|
#userAnswerResolver: ((input: string) => void) | null = null
|
||||||
#isWaitingForUserAnswer: boolean = false
|
#isWaitingForUserAnswer: boolean = false
|
||||||
@@ -54,47 +47,145 @@ export class Panel {
|
|||||||
#pendingHeaderText: string | null = null
|
#pendingHeaderText: string | null = null
|
||||||
#isAnimating = false
|
#isAnimating = false
|
||||||
|
|
||||||
|
// Event handlers (bound for removal)
|
||||||
|
#onStatusChange = () => this.#handleStatusChange()
|
||||||
|
#onHistoryChange = () => this.#handleHistoryChange()
|
||||||
|
#onActivity = (e: Event) => this.#handleActivity((e as CustomEvent<AgentActivity>).detail)
|
||||||
|
#onAgentDispose = () => this.dispose()
|
||||||
|
|
||||||
get wrapper(): HTMLElement {
|
get wrapper(): HTMLElement {
|
||||||
return this.#wrapper
|
return this.#wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(config: PanelConfig) {
|
/**
|
||||||
|
* Create a Panel bound to an agent
|
||||||
|
* @param agent - Agent instance that implements PanelAgentAdapter
|
||||||
|
* @param config - Optional panel configuration
|
||||||
|
*/
|
||||||
|
constructor(agent: PanelAgentAdapter, config: PanelConfig = {}) {
|
||||||
|
this.#agent = agent
|
||||||
this.#config = config
|
this.#config = config
|
||||||
this.#i18n = new I18n(config.language ?? 'en-US')
|
this.#i18n = new I18n(config.language ?? 'en-US')
|
||||||
|
|
||||||
|
// Set up askUser callback on agent
|
||||||
|
this.#agent.onAskUser = (question) => this.#askUser(question)
|
||||||
|
|
||||||
|
// Create UI elements
|
||||||
this.#wrapper = this.#createWrapper()
|
this.#wrapper = this.#createWrapper()
|
||||||
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!
|
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!
|
||||||
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
|
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
|
||||||
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!
|
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!
|
||||||
this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)!
|
this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)!
|
||||||
this.#pauseButton = this.#wrapper.querySelector(`.${styles.pauseButton}`)!
|
|
||||||
this.#stopButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!
|
this.#stopButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!
|
||||||
this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)!
|
this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)!
|
||||||
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!
|
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!
|
||||||
|
|
||||||
|
// Listen to agent events
|
||||||
|
this.#agent.addEventListener('statuschange', this.#onStatusChange)
|
||||||
|
this.#agent.addEventListener('historychange', this.#onHistoryChange)
|
||||||
|
this.#agent.addEventListener('activity', this.#onActivity)
|
||||||
|
this.#agent.addEventListener('dispose', this.#onAgentDispose)
|
||||||
|
|
||||||
this.#setupEventListeners()
|
this.#setupEventListeners()
|
||||||
this.#startHeaderUpdateLoop()
|
this.#startHeaderUpdateLoop()
|
||||||
|
|
||||||
this.#showInputArea()
|
this.#showInputArea()
|
||||||
|
|
||||||
|
this.hide() // Start hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Agent event handlers ==========
|
||||||
|
|
||||||
|
/** Handle agent status change */
|
||||||
|
#handleStatusChange(): void {
|
||||||
|
const status = this.#agent.status
|
||||||
|
|
||||||
|
// Map agent status to UI indicator type
|
||||||
|
const indicatorType =
|
||||||
|
status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status
|
||||||
|
this.#updateStatusIndicator(indicatorType)
|
||||||
|
|
||||||
|
// Show/hide based on status
|
||||||
|
if (status === 'running') {
|
||||||
|
this.show()
|
||||||
|
this.#hideInputArea() // Hide input while running
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle completion
|
||||||
|
if (status === 'completed' || status === 'error') {
|
||||||
|
if (!this.#isExpanded) {
|
||||||
|
this.#expand()
|
||||||
|
}
|
||||||
|
if (this.#shouldShowInputArea()) {
|
||||||
|
this.#showInputArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle agent history change - re-render history list from agent.history */
|
||||||
|
#handleHistoryChange(): void {
|
||||||
|
this.#renderHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask for user input
|
* Handle agent activity - transient state for immediate UI feedback
|
||||||
|
* Activity events are NOT persisted in history, only used for header bar updates
|
||||||
*/
|
*/
|
||||||
async askUser(question: string): Promise<string> {
|
#handleActivity(activity: AgentActivity): void {
|
||||||
|
switch (activity.type) {
|
||||||
|
case 'thinking':
|
||||||
|
this.#pendingHeaderText = this.#i18n.t('ui.panel.thinking')
|
||||||
|
this.#updateStatusIndicator('thinking')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'executing':
|
||||||
|
this.#pendingHeaderText = this.#getToolExecutingText(activity.tool, activity.input)
|
||||||
|
this.#updateStatusIndicator('executing')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'executed':
|
||||||
|
this.#pendingHeaderText = truncate(activity.output, 50)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'retrying':
|
||||||
|
this.#pendingHeaderText = `Retrying (${activity.attempt}/${activity.maxAttempts})`
|
||||||
|
this.#updateStatusIndicator('retrying')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
this.#pendingHeaderText = truncate(activity.message, 50)
|
||||||
|
this.#updateStatusIndicator('error')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask for user input (internal, called by agent via onAskUser)
|
||||||
|
*/
|
||||||
|
#askUser(question: string): Promise<string> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Set `waiting for user answer` state
|
// Set `waiting for user answer` state
|
||||||
this.#isWaitingForUserAnswer = true
|
this.#isWaitingForUserAnswer = true
|
||||||
this.#userAnswerResolver = resolve
|
this.#userAnswerResolver = resolve
|
||||||
|
|
||||||
// Update state to `running`
|
// Expand history panel
|
||||||
this.#updateInternal({
|
|
||||||
type: 'output',
|
|
||||||
displayText: this.#i18n.t('ui.panel.question', { question }),
|
|
||||||
}) // Expand history panel
|
|
||||||
if (!this.#isExpanded) {
|
if (!this.#isExpanded) {
|
||||||
this.#expand()
|
this.#expand()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add temporary question card so user can see the full question
|
||||||
|
const tempCard = document.createElement('div')
|
||||||
|
tempCard.innerHTML = createCard({
|
||||||
|
icon: '❓',
|
||||||
|
content: `Question: ${question}`,
|
||||||
|
meta: formatTime(this.#config.language ?? 'en-US'),
|
||||||
|
type: 'question',
|
||||||
|
})
|
||||||
|
const cardElement = tempCard.firstElementChild as HTMLElement
|
||||||
|
cardElement.setAttribute('data-temp-card', 'true')
|
||||||
|
this.#historySection.appendChild(cardElement)
|
||||||
|
this.#scrollToBottom()
|
||||||
|
|
||||||
this.#showInputArea(this.#i18n.t('ui.panel.userAnswerPrompt'))
|
this.#showInputArea(this.#i18n.t('ui.panel.userAnswerPrompt'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -115,16 +206,10 @@ export class Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.#state.reset()
|
|
||||||
this.#statusText.textContent = this.#i18n.t('ui.panel.ready')
|
this.#statusText.textContent = this.#i18n.t('ui.panel.ready')
|
||||||
this.#updateStatusIndicator('thinking')
|
this.#updateStatusIndicator('thinking')
|
||||||
this.#updateHistory()
|
this.#renderHistory()
|
||||||
this.#collapse()
|
this.#collapse()
|
||||||
// Reset pause state via callback
|
|
||||||
if (this.#config.getPaused()) {
|
|
||||||
this.#config.onPauseToggle()
|
|
||||||
}
|
|
||||||
this.#updatePauseButton()
|
|
||||||
// Reset user input state
|
// Reset user input state
|
||||||
this.#isWaitingForUserAnswer = false
|
this.#isWaitingForUserAnswer = false
|
||||||
this.#userAnswerResolver = null
|
this.#userAnswerResolver = null
|
||||||
@@ -141,17 +226,16 @@ export class Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update panel with semantic data - i18n handled internally
|
* Dispose panel and clean up event listeners
|
||||||
*/
|
|
||||||
update(data: PanelUpdate): void {
|
|
||||||
const stepData = this.#toStepData(data)
|
|
||||||
this.#updateInternal(stepData)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose panel
|
|
||||||
*/
|
*/
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
|
// Remove agent event listeners
|
||||||
|
this.#agent.removeEventListener('statuschange', this.#onStatusChange)
|
||||||
|
this.#agent.removeEventListener('historychange', this.#onHistoryChange)
|
||||||
|
this.#agent.removeEventListener('activity', this.#onActivity)
|
||||||
|
this.#agent.removeEventListener('dispose', this.#onAgentDispose)
|
||||||
|
|
||||||
|
// Clean up UI
|
||||||
this.#isWaitingForUserAnswer = false
|
this.#isWaitingForUserAnswer = false
|
||||||
this.#stopHeaderUpdateLoop()
|
this.#stopHeaderUpdateLoop()
|
||||||
this.wrapper.remove()
|
this.wrapper.remove()
|
||||||
@@ -159,67 +243,21 @@ export class Panel {
|
|||||||
|
|
||||||
// ========== Private methods ==========
|
// ========== Private methods ==========
|
||||||
|
|
||||||
/**
|
#getToolExecutingText(toolName: string, args: unknown): string {
|
||||||
* Convert semantic update to step data with i18n
|
const a = args as Record<string, string | number>
|
||||||
*/
|
|
||||||
#toStepData(data: PanelUpdate): Omit<Step, 'id' | 'stepNumber' | 'timestamp'> {
|
|
||||||
switch (data.type) {
|
|
||||||
case 'thinking':
|
|
||||||
return { type: 'thinking', displayText: data.text ?? this.#i18n.t('ui.panel.thinking') }
|
|
||||||
case 'input':
|
|
||||||
return { type: 'input', displayText: data.task }
|
|
||||||
case 'question':
|
|
||||||
return {
|
|
||||||
type: 'output',
|
|
||||||
displayText: this.#i18n.t('ui.panel.question', { question: data.question }),
|
|
||||||
}
|
|
||||||
case 'userAnswer':
|
|
||||||
return {
|
|
||||||
type: 'input',
|
|
||||||
displayText: this.#i18n.t('ui.panel.userAnswer', { input: data.input }),
|
|
||||||
}
|
|
||||||
case 'retry':
|
|
||||||
return { type: 'retry', displayText: `retry-ing (${data.current} / ${data.max})` }
|
|
||||||
case 'error':
|
|
||||||
return { type: 'error', displayText: data.message }
|
|
||||||
case 'output':
|
|
||||||
return { type: 'output', displayText: data.text }
|
|
||||||
case 'completed':
|
|
||||||
return { type: 'completed', displayText: this.#i18n.t('ui.panel.taskCompleted') }
|
|
||||||
case 'toolExecuting':
|
|
||||||
return {
|
|
||||||
type: 'tool_executing',
|
|
||||||
toolName: data.toolName,
|
|
||||||
toolArgs: data.args,
|
|
||||||
displayText: this.#getToolExecutingText(data.toolName, data.args),
|
|
||||||
}
|
|
||||||
case 'toolCompleted': {
|
|
||||||
const displayText = this.#getToolCompletedText(data.toolName, data.args)
|
|
||||||
if (!displayText) return { type: 'tool_executing', displayText: '' } // will be filtered
|
|
||||||
return {
|
|
||||||
type: 'tool_executing',
|
|
||||||
toolName: data.toolName,
|
|
||||||
toolArgs: data.args,
|
|
||||||
toolResult: data.result,
|
|
||||||
displayText,
|
|
||||||
duration: data.duration,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#getToolExecutingText(toolName: string, args: any): string {
|
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case 'click_element_by_index':
|
case 'click_element_by_index':
|
||||||
return this.#i18n.t('ui.tools.clicking', { index: args.index })
|
return this.#i18n.t('ui.tools.clicking', { index: a.index })
|
||||||
case 'input_text':
|
case 'input_text':
|
||||||
return this.#i18n.t('ui.tools.inputting', { index: args.index })
|
return this.#i18n.t('ui.tools.inputting', { index: a.index })
|
||||||
case 'select_dropdown_option':
|
case 'select_dropdown_option':
|
||||||
return this.#i18n.t('ui.tools.selecting', { text: args.text })
|
return this.#i18n.t('ui.tools.selecting', { text: a.text })
|
||||||
case 'scroll':
|
case 'scroll':
|
||||||
return this.#i18n.t('ui.tools.scrolling')
|
return this.#i18n.t('ui.tools.scrolling')
|
||||||
case 'wait':
|
case 'wait':
|
||||||
return this.#i18n.t('ui.tools.waiting', { seconds: args.seconds })
|
return this.#i18n.t('ui.tools.waiting', { seconds: a.seconds })
|
||||||
|
case 'ask_user':
|
||||||
|
return this.#i18n.t('ui.tools.askingUser')
|
||||||
case 'done':
|
case 'done':
|
||||||
return this.#i18n.t('ui.tools.done')
|
return this.#i18n.t('ui.tools.done')
|
||||||
default:
|
default:
|
||||||
@@ -227,100 +265,11 @@ export class Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#getToolCompletedText(toolName: string, args: any): string | null {
|
|
||||||
switch (toolName) {
|
|
||||||
case 'click_element_by_index':
|
|
||||||
return this.#i18n.t('ui.tools.clicked', { index: args.index })
|
|
||||||
case 'input_text':
|
|
||||||
return this.#i18n.t('ui.tools.inputted', { text: args.text })
|
|
||||||
case 'select_dropdown_option':
|
|
||||||
return this.#i18n.t('ui.tools.selected', { text: args.text })
|
|
||||||
case 'scroll':
|
|
||||||
return this.#i18n.t('ui.tools.scrolled')
|
|
||||||
case 'wait':
|
|
||||||
return this.#i18n.t('ui.tools.waited')
|
|
||||||
case 'done':
|
|
||||||
return null
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update status (internal)
|
|
||||||
*/
|
|
||||||
#updateInternal(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): void {
|
|
||||||
// Skip empty displayText (filtered toolCompleted for 'done')
|
|
||||||
if (!stepData.displayText) return
|
|
||||||
|
|
||||||
const step = this.#state.addStep(stepData)
|
|
||||||
|
|
||||||
// Queue header text update (will be processed by periodic check)
|
|
||||||
const headerText = truncate(step.displayText, 20)
|
|
||||||
this.#pendingHeaderText = headerText
|
|
||||||
|
|
||||||
this.#updateStatusIndicator(step.type)
|
|
||||||
this.#updateHistory()
|
|
||||||
|
|
||||||
// Auto-expand history after task completion
|
|
||||||
if (step.type === 'completed' || step.type === 'error') {
|
|
||||||
if (!this.#isExpanded) {
|
|
||||||
this.#expand()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Control input area display based on status
|
|
||||||
if (this.#shouldShowInputArea()) {
|
|
||||||
this.#showInputArea()
|
|
||||||
} else {
|
|
||||||
this.#hideInputArea()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle pause state
|
|
||||||
*/
|
|
||||||
#togglePause(): void {
|
|
||||||
const paused = this.#config.onPauseToggle()
|
|
||||||
this.#updatePauseButton()
|
|
||||||
|
|
||||||
// Update status display
|
|
||||||
if (paused) {
|
|
||||||
this.#statusText.textContent = this.#i18n.t('ui.panel.paused')
|
|
||||||
this.#updateStatusIndicator('thinking')
|
|
||||||
} else {
|
|
||||||
this.#statusText.textContent = this.#i18n.t('ui.panel.continueExecution')
|
|
||||||
this.#updateStatusIndicator('tool_executing')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update pause button state
|
|
||||||
*/
|
|
||||||
#updatePauseButton(): void {
|
|
||||||
const paused = this.#config.getPaused()
|
|
||||||
if (paused) {
|
|
||||||
this.#pauseButton.textContent = '▶'
|
|
||||||
this.#pauseButton.title = this.#i18n.t('ui.panel.continue')
|
|
||||||
this.#pauseButton.classList.add(styles.paused)
|
|
||||||
} else {
|
|
||||||
this.#pauseButton.textContent = '⏸︎'
|
|
||||||
this.#pauseButton.title = this.#i18n.t('ui.panel.pause')
|
|
||||||
this.#pauseButton.classList.remove(styles.paused)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop Agent
|
* Stop Agent
|
||||||
*/
|
*/
|
||||||
#stopAgent(): void {
|
#stopAgent(): void {
|
||||||
// Update status display
|
this.#agent.dispose()
|
||||||
this.#updateInternal({
|
|
||||||
type: 'error',
|
|
||||||
displayText: this.#i18n.t('ui.panel.taskTerminated'),
|
|
||||||
})
|
|
||||||
|
|
||||||
this.#config.onStop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -337,7 +286,8 @@ export class Panel {
|
|||||||
// Handle user input mode
|
// Handle user input mode
|
||||||
this.#handleUserAnswer(input)
|
this.#handleUserAnswer(input)
|
||||||
} else {
|
} else {
|
||||||
this.#config.onExecuteTask(input)
|
// Execute task via agent
|
||||||
|
this.#agent.execute(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,10 +295,11 @@ export class Panel {
|
|||||||
* Handle user answer
|
* Handle user answer
|
||||||
*/
|
*/
|
||||||
#handleUserAnswer(input: string): void {
|
#handleUserAnswer(input: string): void {
|
||||||
// Add user input to history
|
// Remove temporary question cards (only direct children for safety)
|
||||||
this.#updateInternal({
|
Array.from(this.#historySection.children).forEach((child) => {
|
||||||
type: 'input',
|
if (child.getAttribute('data-temp-card') === 'true') {
|
||||||
displayText: this.#i18n.t('ui.panel.userAnswer', { input }),
|
child.remove()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
@@ -389,32 +340,38 @@ export class Panel {
|
|||||||
// Always show input area if waiting for user input
|
// Always show input area if waiting for user input
|
||||||
if (this.#isWaitingForUserAnswer) return true
|
if (this.#isWaitingForUserAnswer) return true
|
||||||
|
|
||||||
const steps = this.#state.getAllSteps()
|
const history = this.#agent.history
|
||||||
if (steps.length === 0) {
|
if (history.length === 0) {
|
||||||
return true // Initial state
|
return true // Initial state
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastStep = steps[steps.length - 1]
|
const status = this.#agent.status
|
||||||
return lastStep.type === 'completed' || lastStep.type === 'error'
|
const isTaskEnded = status === 'completed' || status === 'error'
|
||||||
|
|
||||||
|
// Only show input area after task completion if configured to do so
|
||||||
|
if (isTaskEnded) {
|
||||||
|
return this.#config.promptForNextTask ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
#createWrapper(): HTMLElement {
|
#createWrapper(): HTMLElement {
|
||||||
const wrapper = document.createElement('div')
|
const wrapper = document.createElement('div')
|
||||||
wrapper.id = 'page-agent-runtime_agent-panel'
|
wrapper.id = 'page-agent-runtime_agent-panel'
|
||||||
wrapper.className = `${styles.wrapper} ${styles.collapsed}`
|
wrapper.className = styles.wrapper
|
||||||
wrapper.setAttribute('data-browser-use-ignore', 'true')
|
wrapper.setAttribute('data-browser-use-ignore', 'true')
|
||||||
|
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
<div class="${styles.background}"></div>
|
<div class="${styles.background}"></div>
|
||||||
<div class="${styles.historySectionWrapper}">
|
<div class="${styles.historySectionWrapper}">
|
||||||
<div class="${styles.historySection}">
|
<div class="${styles.historySection}">
|
||||||
${this.#createHistoryItem({
|
<div class="${styles.historyItem}">
|
||||||
id: 'placeholder',
|
<div class="${styles.historyContent}">
|
||||||
stepNumber: 0,
|
<span class="${styles.statusIcon}">🧠</span>
|
||||||
timestamp: new Date(),
|
<span>${this.#i18n.t('ui.panel.waitingPlaceholder')}</span>
|
||||||
type: 'thinking',
|
</div>
|
||||||
displayText: this.#i18n.t('ui.panel.waitingPlaceholder'),
|
</div>
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="${styles.header}">
|
<div class="${styles.header}">
|
||||||
@@ -426,9 +383,6 @@ export class Panel {
|
|||||||
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
|
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
|
||||||
▼
|
▼
|
||||||
</button>
|
</button>
|
||||||
<button class="${styles.controlButton} ${styles.pauseButton}" title="${this.#i18n.t('ui.panel.pause')}">
|
|
||||||
⏸︎
|
|
||||||
</button>
|
|
||||||
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
|
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
|
||||||
X
|
X
|
||||||
</button>
|
</button>
|
||||||
@@ -466,12 +420,6 @@ export class Panel {
|
|||||||
this.#toggle()
|
this.#toggle()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Pause/continue button
|
|
||||||
this.#pauseButton.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.#togglePause()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Stop button
|
// Stop button
|
||||||
this.#stopButton.addEventListener('click', (e) => {
|
this.#stopButton.addEventListener('click', (e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -503,7 +451,6 @@ export class Panel {
|
|||||||
|
|
||||||
#expand(): void {
|
#expand(): void {
|
||||||
this.#isExpanded = true
|
this.#isExpanded = true
|
||||||
this.wrapper.classList.remove(styles.collapsed)
|
|
||||||
this.wrapper.classList.add(styles.expanded)
|
this.wrapper.classList.add(styles.expanded)
|
||||||
this.#expandButton.textContent = '▲'
|
this.#expandButton.textContent = '▲'
|
||||||
}
|
}
|
||||||
@@ -511,7 +458,6 @@ export class Panel {
|
|||||||
#collapse(): void {
|
#collapse(): void {
|
||||||
this.#isExpanded = false
|
this.#isExpanded = false
|
||||||
this.wrapper.classList.remove(styles.expanded)
|
this.wrapper.classList.remove(styles.expanded)
|
||||||
this.wrapper.classList.add(styles.collapsed)
|
|
||||||
this.#expandButton.textContent = '▼'
|
this.#expandButton.textContent = '▼'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,7 +526,9 @@ export class Panel {
|
|||||||
}, 150) // Half the duration of fade out animation
|
}, 150) // Half the duration of fade out animation
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateStatusIndicator(type: Step['type']): void {
|
#updateStatusIndicator(
|
||||||
|
type: 'thinking' | 'executing' | 'executed' | 'retrying' | 'completed' | 'error'
|
||||||
|
): void {
|
||||||
// Clear all status classes
|
// Clear all status classes
|
||||||
this.#indicator.className = styles.indicator
|
this.#indicator.className = styles.indicator
|
||||||
|
|
||||||
@@ -588,12 +536,6 @@ export class Panel {
|
|||||||
this.#indicator.classList.add(styles[type])
|
this.#indicator.classList.add(styles[type])
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateHistory(): void {
|
|
||||||
const steps = this.#state.getAllSteps()
|
|
||||||
this.#historySection.innerHTML = steps.map((step) => this.#createHistoryItem(step)).join('')
|
|
||||||
this.#scrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
#scrollToBottom(): void {
|
#scrollToBottom(): void {
|
||||||
// Execute in next event loop to ensure DOM update completion
|
// Execute in next event loop to ensure DOM update completion
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -601,68 +543,107 @@ export class Panel {
|
|||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#createHistoryItem(step: Step): string {
|
/**
|
||||||
const time = step.timestamp.toLocaleTimeString('zh-CN', {
|
* Render history directly from agent.history
|
||||||
hour12: false,
|
*
|
||||||
hour: '2-digit',
|
* Renders:
|
||||||
minute: '2-digit',
|
* 1. Task (first item, from agent.task)
|
||||||
second: '2-digit',
|
* 2. Reflection cards (evaluation, memory, next_goal)
|
||||||
})
|
* 3. Tool execution with output
|
||||||
|
* 4. Observations
|
||||||
|
*/
|
||||||
|
#renderHistory(): void {
|
||||||
|
const items: string[] = []
|
||||||
|
|
||||||
let typeClass = ''
|
// 1. Task card (always first)
|
||||||
let statusIcon = ''
|
const task = this.#agent.task
|
||||||
|
if (task) {
|
||||||
// Set styles and icons based on step type
|
items.push(this.#createTaskCard(task))
|
||||||
if (step.type === 'completed') {
|
|
||||||
// Check if this is a result from done tool
|
|
||||||
if (step.toolName === 'done') {
|
|
||||||
// Judge success or failure based on result
|
|
||||||
const failureKeyword = this.#i18n.t('ui.tools.resultFailure')
|
|
||||||
const errorKeyword = this.#i18n.t('ui.tools.resultError')
|
|
||||||
const isSuccess =
|
|
||||||
!step.toolResult ||
|
|
||||||
(!step.toolResult.includes(failureKeyword) && !step.toolResult.includes(errorKeyword))
|
|
||||||
typeClass = isSuccess ? styles.doneSuccess : styles.doneError
|
|
||||||
statusIcon = isSuccess ? '🎉' : '❌'
|
|
||||||
} else {
|
|
||||||
typeClass = styles.completed
|
|
||||||
statusIcon = '✅'
|
|
||||||
}
|
|
||||||
} else if (step.type === 'error') {
|
|
||||||
typeClass = styles.error
|
|
||||||
statusIcon = '❌'
|
|
||||||
} else if (step.type === 'tool_executing') {
|
|
||||||
statusIcon = '⚙️'
|
|
||||||
} else if (step.type === 'output') {
|
|
||||||
typeClass = styles.output
|
|
||||||
statusIcon = '🤖'
|
|
||||||
} else if (step.type === 'input') {
|
|
||||||
typeClass = styles.input
|
|
||||||
statusIcon = '🎯'
|
|
||||||
} else if (step.type === 'retry') {
|
|
||||||
typeClass = styles.retry
|
|
||||||
statusIcon = '🔄'
|
|
||||||
} else {
|
|
||||||
statusIcon = '🧠'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const durationText = step.duration ? ` · ${step.duration}ms` : ''
|
// 2. Render each history event
|
||||||
const stepLabel = this.#i18n.t('ui.panel.step', {
|
const history = this.#agent.history
|
||||||
number: step.stepNumber.toString(),
|
for (let i = 0; i < history.length; i++) {
|
||||||
|
const event = history[i]
|
||||||
|
items.push(...this.#createHistoryCards(event, i + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#historySection.innerHTML = items.join('')
|
||||||
|
this.#scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
#createTaskCard(task: string): string {
|
||||||
|
return createCard({ icon: '🎯', content: task, type: 'input' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create cards for a history event */
|
||||||
|
#createHistoryCards(event: PanelAgentAdapter['history'][number], stepNumber: number): string[] {
|
||||||
|
const cards: string[] = []
|
||||||
|
const time = formatTime(this.#config.language ?? 'en-US')
|
||||||
|
const meta = this.#i18n.t('ui.panel.step', {
|
||||||
|
number: stepNumber.toString(),
|
||||||
time,
|
time,
|
||||||
duration: durationText || '', // Explicitly pass empty string to replace template
|
duration: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
return `
|
if (event.type === 'step') {
|
||||||
<div class="${styles.historyItem} ${typeClass}">
|
// Reflection card
|
||||||
<div class="${styles.historyContent}">
|
if (event.reflection) {
|
||||||
<span class="${styles.statusIcon}">${statusIcon}</span>
|
const lines = createReflectionLines(event.reflection)
|
||||||
<span>${escapeHtml(step.displayText)}</span>
|
if (lines.length > 0) {
|
||||||
</div>
|
cards.push(createCard({ icon: '🧠', content: lines, meta }))
|
||||||
<div class="${styles.historyMeta}">
|
}
|
||||||
${stepLabel}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
// Action card
|
||||||
`
|
const action = event.action
|
||||||
|
if (action) {
|
||||||
|
cards.push(...this.#createActionCards(action, meta))
|
||||||
|
}
|
||||||
|
} else if (event.type === 'observation') {
|
||||||
|
cards.push(
|
||||||
|
createCard({ icon: '👁️', content: event.content || '', meta, type: 'observation' })
|
||||||
|
)
|
||||||
|
} else if (event.type === 'user_takeover') {
|
||||||
|
cards.push(createCard({ icon: '👤', content: 'User takeover', meta, type: 'input' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create cards for an action */
|
||||||
|
#createActionCards(
|
||||||
|
action: { name: string; input: unknown; output: string },
|
||||||
|
meta: string
|
||||||
|
): string[] {
|
||||||
|
const cards: string[] = []
|
||||||
|
|
||||||
|
if (action.name === 'done') {
|
||||||
|
const input = action.input as { text?: string }
|
||||||
|
const text = input.text || action.output || ''
|
||||||
|
if (text) {
|
||||||
|
cards.push(createCard({ icon: '🤖', content: text, meta, type: 'output' }))
|
||||||
|
}
|
||||||
|
} else if (action.name === 'ask_user') {
|
||||||
|
const input = action.input as { question?: string }
|
||||||
|
const answer = action.output.replace(/^User answered:\s*/i, '')
|
||||||
|
cards.push(
|
||||||
|
createCard({
|
||||||
|
icon: '❓',
|
||||||
|
content: `Question: ${input.question || ''}`,
|
||||||
|
meta,
|
||||||
|
type: 'question',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
cards.push(createCard({ icon: '💬', content: `Answer: ${answer}`, meta, type: 'input' }))
|
||||||
|
} else {
|
||||||
|
const toolText = this.#getToolExecutingText(action.name, action.input)
|
||||||
|
cards.push(createCard({ icon: '🔨', content: toolText, meta }))
|
||||||
|
if (action.output?.length > 0) {
|
||||||
|
cards.push(createCard({ icon: '🔨', content: action.output, meta, type: 'output' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Card HTML generation utilities for Panel
|
||||||
|
*/
|
||||||
|
import { escapeHtml } from '../utils'
|
||||||
|
|
||||||
|
import styles from './Panel.module.css'
|
||||||
|
|
||||||
|
type CardType = 'default' | 'input' | 'output' | 'question' | 'observation'
|
||||||
|
|
||||||
|
interface CardOptions {
|
||||||
|
icon: string
|
||||||
|
content: string | string[]
|
||||||
|
meta?: string
|
||||||
|
type?: CardType
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a single history card */
|
||||||
|
export function createCard({ icon, content, meta, type }: CardOptions): string {
|
||||||
|
const typeClass = type ? styles[type] : ''
|
||||||
|
const contentHtml = Array.isArray(content)
|
||||||
|
? `<div class="${styles.reflectionLines}">${content.join('')}</div>`
|
||||||
|
: `<span>${escapeHtml(content)}</span>`
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="${styles.historyItem} ${typeClass}">
|
||||||
|
<div class="${styles.historyContent}">
|
||||||
|
<span class="${styles.statusIcon}">${icon}</span>
|
||||||
|
${contentHtml}
|
||||||
|
</div>
|
||||||
|
${meta ? `<div class="${styles.historyMeta}">${meta}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format timestamp for cards */
|
||||||
|
export function formatTime(locale: string = 'en-US'): string {
|
||||||
|
return new Date().toLocaleTimeString(locale, {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create reflection lines from reflection object */
|
||||||
|
export function createReflectionLines(reflection: {
|
||||||
|
evaluation_previous_goal?: string
|
||||||
|
memory?: string
|
||||||
|
next_goal?: string
|
||||||
|
}): string[] {
|
||||||
|
const lines: string[] = []
|
||||||
|
if (reflection.evaluation_previous_goal) {
|
||||||
|
lines.push(`<div>🔍 ${escapeHtml(reflection.evaluation_previous_goal)}</div>`)
|
||||||
|
}
|
||||||
|
if (reflection.memory) {
|
||||||
|
lines.push(`<div>💾 ${escapeHtml(reflection.memory)}</div>`)
|
||||||
|
}
|
||||||
|
if (reflection.next_goal) {
|
||||||
|
lines.push(`<div>🎯 ${escapeHtml(reflection.next_goal)}</div>`)
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Agent activity - transient state for immediate UI feedback.
|
||||||
|
*
|
||||||
|
* Unlike historical events (which are persisted), activities are ephemeral
|
||||||
|
* and represent "what the agent is doing right now". UI components should
|
||||||
|
* listen to 'activity' events to show real-time feedback.
|
||||||
|
*
|
||||||
|
* Note: There is no 'idle' activity - absence of activity events means idle.
|
||||||
|
*
|
||||||
|
* Events dispatched: CustomEvent<AgentActivity>
|
||||||
|
*/
|
||||||
|
export 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 }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal interface that Panel expects from an agent.
|
||||||
|
* Panel does not depend on PageAgent directly - it only requires this interface.
|
||||||
|
* This enables decoupling and allows any agent implementation to work with Panel.
|
||||||
|
*
|
||||||
|
* Events:
|
||||||
|
* - 'statuschange': Agent status changed (idle/running/completed/error)
|
||||||
|
* - 'historychange': Historical events updated (persisted)
|
||||||
|
* - 'activity': Transient activity for immediate UI feedback (thinking/executing/etc)
|
||||||
|
* - 'dispose': Agent is being disposed
|
||||||
|
*/
|
||||||
|
export interface PanelAgentAdapter extends EventTarget {
|
||||||
|
/** Current agent status */
|
||||||
|
readonly status: 'idle' | 'running' | 'completed' | 'error'
|
||||||
|
|
||||||
|
/** History of agent events */
|
||||||
|
readonly history: readonly {
|
||||||
|
type: 'step' | 'observation' | 'user_takeover' | 'error'
|
||||||
|
/** For 'step' type */
|
||||||
|
reflection?: {
|
||||||
|
evaluation_previous_goal?: string
|
||||||
|
memory?: string
|
||||||
|
next_goal?: string
|
||||||
|
}
|
||||||
|
/** For 'step' type */
|
||||||
|
action?: {
|
||||||
|
name: string
|
||||||
|
input: unknown
|
||||||
|
output: string
|
||||||
|
}
|
||||||
|
/** For 'observation' type */
|
||||||
|
content?: string
|
||||||
|
}[]
|
||||||
|
|
||||||
|
/** Current task being executed */
|
||||||
|
readonly task: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when agent needs user input.
|
||||||
|
* Panel will set this to handle user questions via its UI.
|
||||||
|
*/
|
||||||
|
onAskUser?: (question: string) => Promise<string>
|
||||||
|
|
||||||
|
/** Execute a task */
|
||||||
|
execute(task: string): Promise<unknown>
|
||||||
|
|
||||||
|
/** Dispose the agent */
|
||||||
|
dispose(): void
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// @workaround DTS bug
|
// @workaround DTS bug
|
||||||
// dts do not work with monorepo path mapping
|
// dts do not work with monorepo path mapping
|
||||||
// disable path mapping for it
|
// disable path mapping for it
|
||||||
"paths": {}
|
"paths": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-11
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.js"],
|
"include": ["**/*.ts", "**/*.js"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
# Landing Page & Docs
|
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "new-york",
|
||||||
"rsc": false,
|
"rsc": false,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/index.css",
|
"css": "src/index.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide",
|
"iconLibrary": "lucide",
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"registries": {
|
"registries": {
|
||||||
"@magicui": "https://magicui.design/r/{name}.json"
|
"@magicui": "https://magicui.design/r/{name}.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.0",
|
"version": "0.3.0-beta.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function Header() {
|
|||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="~/"
|
||||||
className="flex items-center gap-2 sm:gap-3 group shrink-0"
|
className="flex items-center gap-2 sm:gap-3 group shrink-0"
|
||||||
aria-label={t('header.logo_alt')}
|
aria-label={t('header.logo_alt')}
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
@@ -55,7 +55,7 @@ export default function Header() {
|
|||||||
aria-label="Mobile navigation"
|
aria-label="Mobile navigation"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href="/docs/introduction/overview"
|
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"
|
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')}
|
aria-label={t('header.nav_docs')}
|
||||||
>
|
>
|
||||||
@@ -89,7 +89,7 @@ export default function Header() {
|
|||||||
{import.meta.env.VERSION}
|
{import.meta.env.VERSION}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href="/docs/introduction/overview"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<BookOpen className="w-4 h-4" />
|
<BookOpen className="w-4 h-4" />
|
||||||
@@ -137,7 +137,7 @@ export default function Header() {
|
|||||||
role="navigation"
|
role="navigation"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href="/docs/introduction/overview"
|
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"
|
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)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* API Reference component for displaying TypeScript interface definitions
|
||||||
|
*
|
||||||
|
* Provides a beautiful, readable table for documenting API interfaces
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { Badge } from './badge'
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface PropDefinition {
|
||||||
|
/** Property name */
|
||||||
|
name: string
|
||||||
|
/** TypeScript type (can include generics, unions, etc.) */
|
||||||
|
type: string
|
||||||
|
/** Whether the property is required */
|
||||||
|
required?: boolean
|
||||||
|
/** Default value if any */
|
||||||
|
defaultValue?: string
|
||||||
|
/** Description of the property */
|
||||||
|
description: React.ReactNode
|
||||||
|
/** Mark as experimental/deprecated */
|
||||||
|
status?: 'experimental' | 'deprecated'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIReferenceProps {
|
||||||
|
/** Title for the API section */
|
||||||
|
title?: string
|
||||||
|
/** Optional description */
|
||||||
|
description?: React.ReactNode
|
||||||
|
/** Property definitions */
|
||||||
|
properties: PropDefinition[]
|
||||||
|
/** Additional CSS classes */
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export function APIReference({ title, description, properties, className }: APIReferenceProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('my-6', className)}>
|
||||||
|
{title && (
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">{title}</h3>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-50 dark:bg-gray-800/50">
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
|
||||||
|
Property
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300 hidden md:table-cell">
|
||||||
|
Default
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||||
|
{properties.map((prop) => (
|
||||||
|
<PropRow key={prop.name} {...prop} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PropRow({ name, type, required, defaultValue, description, status }: PropDefinition) {
|
||||||
|
return (
|
||||||
|
<tr className="bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
|
||||||
|
{/* Property name */}
|
||||||
|
<td className="px-4 py-3 align-top">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<code className="font-mono text-sm font-medium text-indigo-600 dark:text-indigo-400">
|
||||||
|
{name}
|
||||||
|
</code>
|
||||||
|
{required && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] px-1.5 py-0 border-red-300 text-red-600 dark:border-red-800 dark:text-red-400"
|
||||||
|
>
|
||||||
|
required
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{status === 'experimental' && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] px-1.5 py-0 border-amber-300 text-amber-600 dark:border-amber-800 dark:text-amber-400"
|
||||||
|
>
|
||||||
|
experimental
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{status === 'deprecated' && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] px-1.5 py-0 border-gray-300 text-gray-500 dark:border-gray-700 dark:text-gray-500 line-through"
|
||||||
|
>
|
||||||
|
deprecated
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Type */}
|
||||||
|
<td className="px-4 py-3 align-top">
|
||||||
|
<code className="font-mono text-xs text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded whitespace-nowrap">
|
||||||
|
{type}
|
||||||
|
</code>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Default value */}
|
||||||
|
<td className="px-4 py-3 align-top hidden md:table-cell">
|
||||||
|
{defaultValue ? (
|
||||||
|
<code className="font-mono text-xs text-gray-600 dark:text-gray-400">{defaultValue}</code>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 dark:text-gray-600">-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<td className="px-4 py-3 align-top text-gray-600 dark:text-gray-400">{description}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Utility Components
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Code inline span for type references in descriptions */
|
||||||
|
export function TypeRef({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<code className="font-mono text-xs text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-950/50 px-1 py-0.5 rounded">
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Section divider for grouping related APIs */
|
||||||
|
export function APIDivider({ title }: { title: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-4 my-8">
|
||||||
|
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-gray-200 dark:via-gray-700 to-transparent" />
|
||||||
|
<span className="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-gray-200 dark:via-gray-700 to-transparent" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
// Demo build (auto-init with demo LLM, for quick testing)
|
// Demo build (auto-init with demo LLM, for quick testing)
|
||||||
export const CDN_DEMO_URL = 'https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js'
|
export const CDN_DEMO_URL = 'https://cdn.jsdelivr.net/npm/page-agent/dist/iife/page-agent.demo.js'
|
||||||
export const CDN_DEMO_CN_URL =
|
export const CDN_DEMO_CN_URL =
|
||||||
'https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js'
|
'https://registry.npmmirror.com/page-agent/latest/files/dist/iife/page-agent.demo.js'
|
||||||
|
|
||||||
// Full build (exports PageAgent class, usage identical to npm)
|
|
||||||
export const CDN_FULL_URL = 'https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js'
|
|
||||||
export const CDN_FULL_CN_URL =
|
|
||||||
'https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js'
|
|
||||||
|
|
||||||
// Demo LLM for website testing
|
// Demo LLM for website testing
|
||||||
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ pages/i18n/
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
const { t } = useTranslation('common') // 指定命名空间
|
const { t } = useTranslation('common') // 指定命名空间
|
||||||
|
|
||||||
return <h1>{t('header.nav_docs')}</h1>
|
return <h1>{t('header.nav_docs')}</h1>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -71,4 +71,3 @@ t('common:header.nav_docs')
|
|||||||
- [ ] 文档页翻译(`docs.json`)
|
- [ ] 文档页翻译(`docs.json`)
|
||||||
- [ ] DocsLayout 导航结构国际化
|
- [ ] DocsLayout 导航结构国际化
|
||||||
- [ ] 404 页面国际化
|
- [ ] 404 页面国际化
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default {
|
|||||||
introduction: 'Introduction',
|
introduction: 'Introduction',
|
||||||
features: 'Features',
|
features: 'Features',
|
||||||
integration: 'Integration',
|
integration: 'Integration',
|
||||||
|
advanced: 'Advanced',
|
||||||
overview: 'Overview',
|
overview: 'Overview',
|
||||||
quick_start: 'Quick Start',
|
quick_start: 'Quick Start',
|
||||||
limitations: 'Limitations',
|
limitations: 'Limitations',
|
||||||
@@ -32,9 +33,10 @@ export default {
|
|||||||
knowledge_injection: 'Instructions',
|
knowledge_injection: 'Instructions',
|
||||||
data_masking: 'Data Masking',
|
data_masking: 'Data Masking',
|
||||||
cdn_setup: 'CDN Setup',
|
cdn_setup: 'CDN Setup',
|
||||||
configuration: 'Configuration',
|
|
||||||
best_practices: 'Best Practices',
|
best_practices: 'Best Practices',
|
||||||
third_party_agent: 'Third-party Agent',
|
third_party_agent: 'Third-party Agent',
|
||||||
security_permissions: 'Security & Permissions',
|
security_permissions: 'Security & Permissions',
|
||||||
|
page_agent: 'PageAgent',
|
||||||
|
page_agent_core: 'PageAgentCore',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default {
|
|||||||
introduction: '介绍',
|
introduction: '介绍',
|
||||||
features: '功能特性',
|
features: '功能特性',
|
||||||
integration: '集成指南',
|
integration: '集成指南',
|
||||||
|
advanced: '高级',
|
||||||
overview: '概览',
|
overview: '概览',
|
||||||
quick_start: '快速开始',
|
quick_start: '快速开始',
|
||||||
limitations: '使用限制',
|
limitations: '使用限制',
|
||||||
@@ -31,9 +32,10 @@ export default {
|
|||||||
knowledge_injection: '知识注入',
|
knowledge_injection: '知识注入',
|
||||||
data_masking: '数据脱敏',
|
data_masking: '数据脱敏',
|
||||||
cdn_setup: 'CDN 引入',
|
cdn_setup: 'CDN 引入',
|
||||||
configuration: '配置选项',
|
|
||||||
best_practices: '最佳实践',
|
best_practices: '最佳实践',
|
||||||
third_party_agent: '接入第三方 Agent',
|
third_party_agent: '接入第三方 Agent',
|
||||||
security_permissions: '安全与权限',
|
security_permissions: '安全与权限',
|
||||||
|
page_agent: 'PageAgent',
|
||||||
|
page_agent_core: 'PageAgentCore',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
|
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
|
||||||
import { Bot, Box, MessageSquare, PlayCircle, Shield, Sparkles, Users, Zap } from 'lucide-react'
|
import { Bot, Box, MessageSquare, PlayCircle, Shield, Sparkles, Users, Zap } from 'lucide-react'
|
||||||
import { PageAgent } from 'page-agent'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Link, useSearchParams } from 'wouter'
|
import { Link, useSearchParams } from 'wouter'
|
||||||
@@ -59,13 +58,12 @@ export default function HomePage() {
|
|||||||
const handleExecute = async () => {
|
const handleExecute = async () => {
|
||||||
if (!task.trim()) return
|
if (!task.trim()) return
|
||||||
|
|
||||||
let pageAgent: PageAgent
|
|
||||||
const win = window as any
|
const win = window as any
|
||||||
|
|
||||||
if (win.pageAgent && !win.pageAgent.disposed) {
|
// Lazy load PageAgent only when user clicks execute
|
||||||
pageAgent = win.pageAgent
|
if (!win.pageAgent || win.pageAgent.disposed) {
|
||||||
} else {
|
const { PageAgent } = await import('page-agent')
|
||||||
pageAgent = new PageAgent({
|
win.pageAgent = new PageAgent({
|
||||||
// 把 react 根元素排除掉,挂了很多冒泡时间导致假阳
|
// 把 react 根元素排除掉,挂了很多冒泡时间导致假阳
|
||||||
interactiveBlacklist: [document.getElementById('root')!],
|
interactiveBlacklist: [document.getElementById('root')!],
|
||||||
language: i18n.language as any,
|
language: i18n.language as any,
|
||||||
@@ -91,12 +89,14 @@ export default function HomePage() {
|
|||||||
import.meta.env.DEV && import.meta.env.LLM_API_KEY
|
import.meta.env.DEV && import.meta.env.LLM_API_KEY
|
||||||
? import.meta.env.LLM_API_KEY
|
? import.meta.env.LLM_API_KEY
|
||||||
: DEMO_API_KEY,
|
: DEMO_API_KEY,
|
||||||
|
|
||||||
|
// enableAskUser: false,
|
||||||
|
// promptForNextTask: false,
|
||||||
|
// enablePanel: false,
|
||||||
})
|
})
|
||||||
win.pageAgent = pageAgent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pageAgent.execute(task)
|
const result = await win.pageAgent.execute(task)
|
||||||
|
|
||||||
console.log(result)
|
console.log(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ export default function HomePage() {
|
|||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<main id="main-content">
|
<main id="main-content">
|
||||||
<section
|
<section
|
||||||
className="relative px-6 py-22 lg:py-28 overflow-hidden"
|
className="relative px-6 py-22 pb-18 lg:py-28 overflow-hidden"
|
||||||
aria-labelledby="hero-heading"
|
aria-labelledby="hero-heading"
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto text-center">
|
<div className="max-w-7xl mx-auto text-center">
|
||||||
|
|||||||
@@ -24,38 +24,44 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
|
|||||||
{
|
{
|
||||||
title: t('nav.introduction'),
|
title: t('nav.introduction'),
|
||||||
items: [
|
items: [
|
||||||
{ title: t('nav.overview'), path: '/docs/introduction/overview' },
|
{ title: t('nav.overview'), path: '/introduction/overview' },
|
||||||
{ title: t('nav.quick_start'), path: '/docs/introduction/quick-start' },
|
{ title: t('nav.quick_start'), path: '/introduction/quick-start' },
|
||||||
{ title: t('nav.limitations'), path: '/docs/introduction/limitations' },
|
{ title: t('nav.limitations'), path: '/introduction/limitations' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('nav.features'),
|
title: t('nav.features'),
|
||||||
items: [
|
items: [
|
||||||
{ title: t('nav.models'), path: '/docs/features/models' },
|
{ title: t('nav.models'), path: '/features/models' },
|
||||||
{ title: t('nav.custom_tools'), path: '/docs/features/custom-tools' },
|
{ title: t('nav.custom_tools'), path: '/features/custom-tools' },
|
||||||
{ title: t('nav.knowledge_injection'), path: '/docs/features/custom-instructions' },
|
{ title: t('nav.knowledge_injection'), path: '/features/custom-instructions' },
|
||||||
{ title: t('nav.data_masking'), path: '/docs/features/data-masking' },
|
{ title: t('nav.data_masking'), path: '/features/data-masking' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('nav.integration'),
|
title: t('nav.integration'),
|
||||||
items: [
|
items: [
|
||||||
{ title: t('nav.configuration'), path: '/docs/integration/configuration' },
|
{ title: t('nav.third_party_agent'), path: '/integration/third-party-agent' },
|
||||||
{ title: t('nav.third_party_agent'), path: '/docs/integration/third-party-agent' },
|
{ title: t('nav.cdn_setup'), path: '/integration/cdn-setup' },
|
||||||
{ title: t('nav.cdn_setup'), path: '/docs/integration/cdn-setup' },
|
|
||||||
{
|
{
|
||||||
title: '🚧 ' + t('nav.security_permissions'),
|
title: '🚧 ' + t('nav.security_permissions'),
|
||||||
path: '/docs/integration/security-permissions',
|
path: '/integration/security-permissions',
|
||||||
},
|
},
|
||||||
{ title: '🚧 ' + t('nav.best_practices'), path: '/docs/integration/best-practices' },
|
{ title: '🚧 ' + t('nav.best_practices'), path: '/integration/best-practices' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('nav.advanced'),
|
||||||
|
items: [
|
||||||
|
{ title: t('nav.page_agent'), path: '/advanced/page-agent' },
|
||||||
|
{ title: t('nav.page_agent_core'), path: '/advanced/page-agent-core' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto px-6 py-8 overflow-x-auto">
|
<div className="max-w-7xl mx-auto px-6 py-8 overflow-x-auto">
|
||||||
<div className="flex gap-8 min-w-[900px]">
|
<div className="flex gap-8 min-w-225">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-64 shrink-0" role="complementary" aria-label="文档导航">
|
<aside className="w-64 shrink-0" role="complementary" aria-label="文档导航">
|
||||||
<div className="sticky">
|
<div className="sticky">
|
||||||
|
|||||||
@@ -0,0 +1,514 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { APIDivider, APIReference, TypeRef } from '@/components/ui/api-reference'
|
||||||
|
|
||||||
|
export default function PageAgentCoreDocs() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const isZh = i18n.language === 'zh-CN'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl font-bold mb-6">PageAgentCore</h1>
|
||||||
|
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
||||||
|
{isZh
|
||||||
|
? 'PageAgentCore 是不带 UI 的核心 Agent 类。用于需要自定义 UI 或无头运行的场景。'
|
||||||
|
: 'PageAgentCore is the core Agent class without UI. Use it for custom UI or headless scenarios.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* When to use */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">
|
||||||
|
{isZh ? '何时使用 PageAgentCore' : 'When to Use PageAgentCore'}
|
||||||
|
</h2>
|
||||||
|
<ul className="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2">
|
||||||
|
<li>{isZh ? '需要自定义 UI 界面' : 'Need a custom UI interface'}</li>
|
||||||
|
<li>{isZh ? '在自动化测试中无头运行' : 'Running headless in automated tests'}</li>
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '在非浏览器环境运行(需自定义 PageController)'
|
||||||
|
: 'Running in non-browser environments (requires custom PageController)'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '将 PageAgent 嵌入其他 Agent 系统'
|
||||||
|
: 'Embedding PageAgent in other agent systems'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Basic Usage */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '基本用法' : 'Basic Usage'}</h2>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`import { PageAgentCore } from '@page-agent/core'
|
||||||
|
import { PageController } from '@page-agent/page-controller'
|
||||||
|
|
||||||
|
const agent = new PageAgentCore({
|
||||||
|
pageController: new PageController({ enableMask: true }),
|
||||||
|
baseURL: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
model: 'gpt-5.2',
|
||||||
|
language: 'en-US',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listen to events for UI display
|
||||||
|
|
||||||
|
agent.addEventListener('statuschange', () => {
|
||||||
|
console.log('Status:', agent.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
agent.addEventListener('historychange', () => {
|
||||||
|
console.log('History:', agent.history)
|
||||||
|
})
|
||||||
|
|
||||||
|
agent.addEventListener('activity', (e) => {
|
||||||
|
const activity = (e as CustomEvent).detail
|
||||||
|
console.log('Activity:', activity.type)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute task
|
||||||
|
const result = await agent.execute('Fill in the form with test data')`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<APIDivider title={isZh ? '配置' : 'Configuration'} />
|
||||||
|
|
||||||
|
{/* LLM Configuration */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">LLMConfig</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '配置与大语言模型的连接参数。支持 OpenAI 兼容的 API。'
|
||||||
|
: 'Configure connection parameters for the language model. Supports OpenAI-compatible APIs.'}
|
||||||
|
</p>
|
||||||
|
<APIReference
|
||||||
|
properties={[
|
||||||
|
{
|
||||||
|
name: 'baseURL',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: isZh
|
||||||
|
? 'LLM API 的基础 URL(如 https://api.openai.com/v1)'
|
||||||
|
: 'Base URL of the LLM API (e.g., https://api.openai.com/v1)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apiKey',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: isZh ? 'API 密钥' : 'API key for authentication',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'model',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: isZh
|
||||||
|
? '模型名称(如 gpt-4o, claude-3.5-sonnet)'
|
||||||
|
: 'Model name (e.g., gpt-4o, claude-3.5-sonnet)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'temperature',
|
||||||
|
type: 'number',
|
||||||
|
defaultValue: '0',
|
||||||
|
description: isZh
|
||||||
|
? '模型温度参数,控制输出随机性'
|
||||||
|
: 'Model temperature, controls output randomness',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxRetries',
|
||||||
|
type: 'number',
|
||||||
|
defaultValue: '3',
|
||||||
|
description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customFetch',
|
||||||
|
type: 'typeof fetch',
|
||||||
|
description: isZh
|
||||||
|
? '自定义 fetch 函数,用于定制 headers、credentials、代理等'
|
||||||
|
: 'Custom fetch function for customizing headers, credentials, proxy, etc.',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Agent Configuration */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">AgentConfig</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '配置 Agent 的行为、生命周期钩子和扩展能力。'
|
||||||
|
: 'Configure agent behavior, lifecycle hooks, and extension capabilities.'}
|
||||||
|
</p>
|
||||||
|
<APIReference
|
||||||
|
properties={[
|
||||||
|
{
|
||||||
|
name: 'language',
|
||||||
|
type: "'en-US' | 'zh-CN'",
|
||||||
|
defaultValue: "'en-US'",
|
||||||
|
description: isZh ? 'Agent 输出语言' : 'Agent output language',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customTools',
|
||||||
|
type: 'Record<string, PageAgentTool | null>',
|
||||||
|
status: 'experimental',
|
||||||
|
description: isZh
|
||||||
|
? '自定义工具,可扩展或覆盖内置工具。设为 null 可移除工具。'
|
||||||
|
: 'Custom tools to extend or override built-in tools. Set to null to remove a tool.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'instructions',
|
||||||
|
type: 'InstructionsConfig',
|
||||||
|
description: isZh
|
||||||
|
? '指导 Agent 行为的指令配置'
|
||||||
|
: 'Instructions to guide agent behavior',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'transformPageContent',
|
||||||
|
type: '(content: string) => string | Promise<string>',
|
||||||
|
description: isZh
|
||||||
|
? '发送给 LLM 前转换页面内容,可用于数据脱敏'
|
||||||
|
: 'Transform page content before sending to LLM, useful for data masking',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'experimentalScriptExecutionTool',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: 'false',
|
||||||
|
status: 'experimental',
|
||||||
|
description: isZh
|
||||||
|
? '启用实验性 JavaScript 执行工具'
|
||||||
|
: 'Enable experimental JavaScript execution tool',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold mt-6 mb-3">InstructionsConfig</h3>
|
||||||
|
<APIReference
|
||||||
|
properties={[
|
||||||
|
{
|
||||||
|
name: 'system',
|
||||||
|
type: 'string',
|
||||||
|
description: isZh
|
||||||
|
? '全局系统级指令,应用于所有任务'
|
||||||
|
: 'Global system-level instructions, applied to all tasks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'getPageInstructions',
|
||||||
|
type: '(url: string) => string | undefined | null',
|
||||||
|
description: isZh
|
||||||
|
? '动态页面级指令回调,在每个步骤前调用'
|
||||||
|
: 'Dynamic page-level instructions callback, called before each step',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Lifecycle Hooks */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '生命周期钩子' : 'Lifecycle Hooks'}</h2>
|
||||||
|
<APIReference
|
||||||
|
properties={[
|
||||||
|
{
|
||||||
|
name: 'onBeforeStep',
|
||||||
|
type: '(stepCnt: number) => void | Promise<void>',
|
||||||
|
description: isZh ? '每个步骤执行前调用' : 'Called before each step execution',
|
||||||
|
status: 'experimental',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onAfterStep',
|
||||||
|
type: '(history: HistoricalEvent[]) => void | Promise<void>',
|
||||||
|
description: isZh ? '每个步骤执行后调用' : 'Called after each step execution',
|
||||||
|
status: 'experimental',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onBeforeTask',
|
||||||
|
type: '() => void | Promise<void>',
|
||||||
|
description: isZh ? '任务开始前调用' : 'Called before task starts',
|
||||||
|
status: 'experimental',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onAfterTask',
|
||||||
|
type: '(result: ExecutionResult) => void | Promise<void>',
|
||||||
|
description: isZh ? '任务结束后调用' : 'Called after task ends',
|
||||||
|
status: 'experimental',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onDispose',
|
||||||
|
type: '(reason?: string) => void',
|
||||||
|
description: isZh ? 'Agent 销毁时调用' : 'Called when agent is disposed',
|
||||||
|
status: 'experimental',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* PageController Configuration */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">PageControllerConfig</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '配置 DOM 提取、元素交互和视觉反馈。'
|
||||||
|
: 'Configure DOM extraction, element interaction, and visual feedback.'}
|
||||||
|
</p>
|
||||||
|
<APIReference
|
||||||
|
properties={[
|
||||||
|
{
|
||||||
|
name: 'pageController',
|
||||||
|
type: 'PageController',
|
||||||
|
status: 'experimental',
|
||||||
|
description: isZh
|
||||||
|
? '自定义 PageController 实例。如不提供,将创建默认实例。'
|
||||||
|
: 'Custom PageController instance. If not provided, a default one will be created.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enableMask',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: 'true',
|
||||||
|
description: isZh
|
||||||
|
? '启用视觉遮罩覆盖层,阻止用户在自动化期间操作'
|
||||||
|
: 'Enable visual mask overlay that blocks user interaction during automation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'viewportExpansion',
|
||||||
|
type: 'number',
|
||||||
|
defaultValue: '0',
|
||||||
|
description: isZh
|
||||||
|
? '视口扩展像素数,-1 表示提取整个页面'
|
||||||
|
: 'Viewport expansion in pixels, -1 means extract entire page',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'interactiveBlacklist',
|
||||||
|
type: '(Element | (() => Element))[]',
|
||||||
|
description: isZh ? '要排除的交互元素列表' : 'Elements to exclude from interaction',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'interactiveWhitelist',
|
||||||
|
type: '(Element | (() => Element))[]',
|
||||||
|
description: isZh
|
||||||
|
? '要强制包含的交互元素列表'
|
||||||
|
: 'Elements to force include for interaction',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'include_attributes',
|
||||||
|
type: 'string[]',
|
||||||
|
description: isZh
|
||||||
|
? '在 DOM 提取中包含的额外属性'
|
||||||
|
: 'Additional attributes to include in DOM extraction',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<APIDivider title={isZh ? '属性与方法' : 'Properties & Methods'} />
|
||||||
|
|
||||||
|
{/* Properties */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '属性' : 'Properties'}</h2>
|
||||||
|
<APIReference
|
||||||
|
properties={[
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
type: "'idle' | 'running' | 'completed' | 'error'",
|
||||||
|
description: isZh ? '当前 Agent 执行状态' : 'Current agent execution status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'history',
|
||||||
|
type: 'HistoricalEvent[]',
|
||||||
|
description: isZh
|
||||||
|
? '历史事件数组,构成 Agent 的记忆'
|
||||||
|
: 'Array of historical events, forms agent memory',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'task',
|
||||||
|
type: 'string',
|
||||||
|
description: isZh ? '当前正在执行的任务' : 'Current task being executed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pageController',
|
||||||
|
type: 'PageController',
|
||||||
|
description: isZh
|
||||||
|
? 'PageController 实例,用于 DOM 操作'
|
||||||
|
: 'PageController instance for DOM operations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tools',
|
||||||
|
type: 'Map<string, PageAgentTool>',
|
||||||
|
description: isZh ? '可用工具的 Map' : 'Map of available tools',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onAskUser',
|
||||||
|
type: '(question: string) => Promise<string>',
|
||||||
|
description: isZh
|
||||||
|
? 'Agent 需要用户输入时的回调。未设置则禁用 ask_user 工具。'
|
||||||
|
: 'Callback when agent needs user input. If not set, ask_user tool is disabled.',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Methods */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '方法' : 'Methods'}</h2>
|
||||||
|
<APIReference
|
||||||
|
properties={[
|
||||||
|
{
|
||||||
|
name: 'execute(task: string)',
|
||||||
|
type: 'Promise<ExecutionResult>',
|
||||||
|
description: isZh
|
||||||
|
? '执行任务并返回结果。包含 success、data 和 history 字段。'
|
||||||
|
: 'Execute a task and return result. Contains success, data, and history fields.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pushObservation(content: string)',
|
||||||
|
type: 'void',
|
||||||
|
description: isZh
|
||||||
|
? '向历史流推送一个观察事件,会在下一步时被 LLM 看到'
|
||||||
|
: 'Push an observation to history stream, will be seen by LLM in next step',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'emitActivity(activity: AgentActivity)',
|
||||||
|
type: 'void',
|
||||||
|
description: isZh
|
||||||
|
? '发出活动事件用于 UI 反馈'
|
||||||
|
: 'Emit activity event for UI feedback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dispose(reason?: string)',
|
||||||
|
type: 'void',
|
||||||
|
description: isZh
|
||||||
|
? '销毁 Agent 并清理资源'
|
||||||
|
: 'Dispose the agent and clean up resources',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Events */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '事件' : 'Events'}</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh ? (
|
||||||
|
<>
|
||||||
|
PageAgentCore 继承自 <TypeRef>EventTarget</TypeRef>,提供以下事件:
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
PageAgentCore extends <TypeRef>EventTarget</TypeRef> and provides the following
|
||||||
|
events:
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<APIReference
|
||||||
|
properties={[
|
||||||
|
{
|
||||||
|
name: 'statuschange',
|
||||||
|
type: 'Event',
|
||||||
|
description: isZh
|
||||||
|
? 'Agent 状态变化时触发 (idle → running → completed/error)'
|
||||||
|
: 'Fired when agent status changes (idle → running → completed/error)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'historychange',
|
||||||
|
type: 'Event',
|
||||||
|
description: isZh
|
||||||
|
? '历史事件更新时触发(持久化事件,构成 Agent 记忆)'
|
||||||
|
: 'Fired when history events are updated (persistent, part of agent memory)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'activity',
|
||||||
|
type: 'CustomEvent<AgentActivity>',
|
||||||
|
description: isZh
|
||||||
|
? '实时活动反馈(短暂状态,仅用于 UI)。类型包括:thinking, executing, executed, retrying, error'
|
||||||
|
: 'Real-time activity feedback (transient, UI only). Types: thinking, executing, executed, retrying, error',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dispose',
|
||||||
|
type: 'Event',
|
||||||
|
description: isZh ? 'Agent 被销毁时触发' : 'Fired when agent is disposed',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<APIDivider title={isZh ? '类型定义' : 'Type Definitions'} />
|
||||||
|
|
||||||
|
{/* ExecutionResult */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">ExecutionResult</h2>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`interface ExecutionResult {
|
||||||
|
/** Whether the task completed successfully */
|
||||||
|
success: boolean
|
||||||
|
/** Result description from the agent */
|
||||||
|
data: string
|
||||||
|
/** Full execution history */
|
||||||
|
history: HistoricalEvent[]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* AgentActivity */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">AgentActivity</h2>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`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 }`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<APIDivider title={isZh ? '无头模式' : 'Headless Mode'} />
|
||||||
|
|
||||||
|
{/* Headless Usage */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '无头模式' : 'Headless Mode'}</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '在非 DOM 环境中,你必须实现自定义的 PageController(例如远程操作页面或 Puppeteer)。'
|
||||||
|
: 'In non-DOM environments, you must implement a custom PageController (e.g., remote page control or Puppeteer).'}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`import { PageAgentCore } from '@page-agent/core'
|
||||||
|
import type { PageController } from '@page-agent/page-controller'
|
||||||
|
|
||||||
|
class MyRemotePageController implements PageController {
|
||||||
|
// Implement required methods for DOM extraction and interaction
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = new PageAgentCore({
|
||||||
|
pageController: new MyRemotePageController(),
|
||||||
|
baseURL: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
model: 'gpt-5.2',
|
||||||
|
language: 'en-US',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listen to events for UI display
|
||||||
|
|
||||||
|
agent.addEventListener('statuschange', () => {
|
||||||
|
console.log('Status:', agent.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
agent.addEventListener('historychange', () => {
|
||||||
|
console.log('History:', agent.history)
|
||||||
|
})
|
||||||
|
|
||||||
|
agent.addEventListener('activity', (e) => {
|
||||||
|
const activity = (e as CustomEvent).detail
|
||||||
|
console.log('Activity:', activity.type)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute task
|
||||||
|
const result = await agent.execute('Fill in the form with test data')`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { APIReference, TypeRef } from '@/components/ui/api-reference'
|
||||||
|
|
||||||
|
export default function PageAgentDocs() {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const isZh = i18n.language === 'zh-CN'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl font-bold mb-6">PageAgent</h1>
|
||||||
|
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
||||||
|
{isZh
|
||||||
|
? 'PageAgent 是带有内置 UI 面板的完整 Agent 类。它继承自 PageAgentCore,并自动创建交互面板。'
|
||||||
|
: 'PageAgent is the complete Agent class with built-in UI panel. It extends PageAgentCore and automatically creates an interactive panel.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* When to use */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">
|
||||||
|
{isZh ? '何时使用 PageAgent' : 'When to Use PageAgent'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '在大多数场景下,你应该使用 PageAgent。它提供了开箱即用的完整体验:'
|
||||||
|
: 'In most cases, you should use PageAgent. It provides a complete out-of-the-box experience:'}
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-6">
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '内置 UI 面板,显示任务进度、Agent 思考过程和操作结果'
|
||||||
|
: 'Built-in UI panel showing task progress, agent thinking, and action results'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '支持 ask_user 工具,Agent 可以向用户提问'
|
||||||
|
: 'Supports ask_user tool for agent to ask questions to users'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Basic Usage */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '基本用法' : 'Basic Usage'}</h2>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`import { PageAgent } from 'page-agent'
|
||||||
|
|
||||||
|
const agent = new PageAgent({
|
||||||
|
// LLM Configuration (required)
|
||||||
|
baseURL: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
model: 'gpt-4o',
|
||||||
|
|
||||||
|
// Optional settings
|
||||||
|
language: 'en-US',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute a task
|
||||||
|
const result = await agent.execute('Click the login button')
|
||||||
|
|
||||||
|
console.log(result.success) // true or false
|
||||||
|
console.log(result.data) // Task result description
|
||||||
|
console.log(result.history) // Full execution history`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Class Definition */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '类定义' : 'Class Definition'}</h2>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`class PageAgent extends PageAgentCore {
|
||||||
|
/** The UI panel instance */
|
||||||
|
panel: Panel
|
||||||
|
|
||||||
|
constructor(config: PageAgentConfig)
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-4">
|
||||||
|
{isZh ? (
|
||||||
|
<>
|
||||||
|
PageAgent 继承自{' '}
|
||||||
|
<Link
|
||||||
|
href="/advanced/page-agent-core"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
PageAgentCore
|
||||||
|
</Link>
|
||||||
|
,所有核心方法和事件都可用。详细的 API 参考请查看 PageAgentCore 文档。
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
PageAgent extends{' '}
|
||||||
|
<Link
|
||||||
|
href="/advanced/page-agent-core"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
PageAgentCore
|
||||||
|
</Link>
|
||||||
|
. All core methods and events are available. See PageAgentCore docs for detailed API
|
||||||
|
reference.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Configuration */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? '配置' : 'Configuration'}</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? 'PageAgent 使用与 PageAgentCore 相同的配置接口。'
|
||||||
|
: 'PageAgent uses the same configuration interface as PageAgentCore.'}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh ? (
|
||||||
|
<>
|
||||||
|
完整配置请参考{' '}
|
||||||
|
<Link
|
||||||
|
href="/advanced/page-agent-core"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
PageAgentCore 配置文档
|
||||||
|
</Link>
|
||||||
|
。
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
See{' '}
|
||||||
|
<Link
|
||||||
|
href="/advanced/page-agent-core"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
PageAgentCore configuration docs
|
||||||
|
</Link>{' '}
|
||||||
|
for complete reference.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Panel Property */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">{isZh ? 'Panel 属性' : 'Panel Property'}</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? 'PageAgent 自动创建一个 Panel 实例。你可以通过 panel 属性访问它来控制 UI:'
|
||||||
|
: 'PageAgent automatically creates a Panel instance. You can access it via the panel property to control the UI:'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<APIReference
|
||||||
|
properties={[
|
||||||
|
{
|
||||||
|
name: 'panel',
|
||||||
|
type: 'Panel',
|
||||||
|
required: true,
|
||||||
|
description: isZh
|
||||||
|
? '内置的 UI 面板实例,用于显示任务进度和接收用户输入。'
|
||||||
|
: 'The built-in UI panel instance for displaying task progress and receiving user input.',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold mt-6 mb-3">{isZh ? 'Panel 方法' : 'Panel Methods'}</h3>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`// Show/hide the panel
|
||||||
|
agent.panel.show()
|
||||||
|
agent.panel.hide()
|
||||||
|
|
||||||
|
// Expand/collapse history view
|
||||||
|
agent.panel.expand()
|
||||||
|
agent.panel.collapse()
|
||||||
|
|
||||||
|
// Reset panel state
|
||||||
|
agent.panel.reset()
|
||||||
|
|
||||||
|
// Dispose panel (called automatically when agent disposes)
|
||||||
|
agent.panel.dispose()`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Comparison with PageAgentCore */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">
|
||||||
|
{isZh ? 'PageAgent vs PageAgentCore' : 'PageAgent vs PageAgentCore'}
|
||||||
|
</h2>
|
||||||
|
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-50 dark:bg-gray-800/50">
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
|
||||||
|
{isZh ? '特性' : 'Feature'}
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-center font-medium text-gray-600 dark:text-gray-300">
|
||||||
|
PageAgent
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-center font-medium text-gray-600 dark:text-gray-300">
|
||||||
|
PageAgentCore
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||||
|
<tr className="bg-white dark:bg-gray-900">
|
||||||
|
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">
|
||||||
|
{isZh ? 'UI 面板' : 'UI Panel'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-green-600 dark:text-green-400">✓</td>
|
||||||
|
<td className="px-4 py-3 text-center text-gray-400 dark:text-gray-600">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="bg-white dark:bg-gray-900">
|
||||||
|
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">
|
||||||
|
{isZh ? 'Headless 模式' : 'Headless Mode'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-gray-400 dark:text-gray-600">-</td>
|
||||||
|
<td className="px-4 py-3 text-center text-green-600 dark:text-green-400">✓</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="bg-white dark:bg-gray-900">
|
||||||
|
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">
|
||||||
|
{isZh ? '自定义 PageController' : 'Custom PageController'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-green-600 dark:text-green-400">✓</td>
|
||||||
|
<td className="px-4 py-3 text-center text-green-600 dark:text-green-400">✓</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="bg-white dark:bg-gray-900">
|
||||||
|
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">
|
||||||
|
{isZh ? '适用场景' : 'Use Case'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-gray-600 dark:text-gray-400">
|
||||||
|
{isZh ? '网页集成' : 'Web integration'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center text-gray-600 dark:text-gray-400">
|
||||||
|
{isZh ? '自定义 UI / 无头' : 'Custom UI / Headless'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { Suspense } from 'react'
|
||||||
|
import { Route, Switch } from 'wouter'
|
||||||
|
|
||||||
|
import Header from '../../components/Header'
|
||||||
|
import DocsLayout from './Layout'
|
||||||
|
import PageAgentCoreDocs from './advanced/page-agent-core/page'
|
||||||
|
// Advanced
|
||||||
|
import PageAgentDocs from './advanced/page-agent/page'
|
||||||
|
import Instructions from './features/custom-instructions/page'
|
||||||
|
// Features
|
||||||
|
import CustomTools from './features/custom-tools/page'
|
||||||
|
import DataMasking from './features/data-masking/page'
|
||||||
|
import Models from './features/models/page'
|
||||||
|
import BestPractices from './integration/best-practices/page'
|
||||||
|
// Integration
|
||||||
|
import CdnSetup from './integration/cdn-setup/page'
|
||||||
|
import SecurityPermissions from './integration/security-permissions/page'
|
||||||
|
import ThirdPartyAgent from './integration/third-party-agent/page'
|
||||||
|
import Limitations from './introduction/limitations/page'
|
||||||
|
// Introduction
|
||||||
|
import Overview from './introduction/overview/page'
|
||||||
|
import QuickStart from './introduction/quick-start/page'
|
||||||
|
|
||||||
|
function DocsPage({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||||
|
<Header />
|
||||||
|
<DocsLayout>
|
||||||
|
<Suspense>{children}</Suspense>
|
||||||
|
</DocsLayout>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DocsRouter() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
{/* Introduction */}
|
||||||
|
<Route path="/introduction/overview">
|
||||||
|
<DocsPage>
|
||||||
|
<Overview />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
<Route path="/introduction/quick-start">
|
||||||
|
<DocsPage>
|
||||||
|
<QuickStart />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
<Route path="/introduction/limitations">
|
||||||
|
<DocsPage>
|
||||||
|
<Limitations />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<Route path="/features/custom-tools">
|
||||||
|
<DocsPage>
|
||||||
|
<CustomTools />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
<Route path="/features/data-masking">
|
||||||
|
<DocsPage>
|
||||||
|
<DataMasking />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
<Route path="/features/custom-instructions">
|
||||||
|
<DocsPage>
|
||||||
|
<Instructions />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
<Route path="/features/models">
|
||||||
|
<DocsPage>
|
||||||
|
<Models />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Integration */}
|
||||||
|
<Route path="/integration/cdn-setup">
|
||||||
|
<DocsPage>
|
||||||
|
<CdnSetup />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
<Route path="/integration/security-permissions">
|
||||||
|
<DocsPage>
|
||||||
|
<SecurityPermissions />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
<Route path="/integration/best-practices">
|
||||||
|
<DocsPage>
|
||||||
|
<BestPractices />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
<Route path="/integration/third-party-agent">
|
||||||
|
<DocsPage>
|
||||||
|
<ThirdPartyAgent />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Advanced */}
|
||||||
|
<Route path="/advanced/page-agent">
|
||||||
|
<DocsPage>
|
||||||
|
<PageAgentDocs />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
<Route path="/advanced/page-agent-core">
|
||||||
|
<DocsPage>
|
||||||
|
<PageAgentCoreDocs />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Default redirect or 404 */}
|
||||||
|
<Route path="/docs">
|
||||||
|
<DocsPage>
|
||||||
|
<Overview />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
import { CDN_DEMO_CN_URL, CDN_DEMO_URL, CDN_FULL_CN_URL, CDN_FULL_URL } from '@/constants'
|
import { CDN_DEMO_CN_URL, CDN_DEMO_URL } from '@/constants'
|
||||||
|
|
||||||
export default function CdnSetup() {
|
export default function CdnSetup() {
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
@@ -63,63 +63,6 @@ export default function CdnSetup() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* CDN Build Section */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-bold mb-3">{isZh ? '📦 标准版' : '📦 Standard Build'}</h2>
|
|
||||||
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-3">
|
|
||||||
{isZh
|
|
||||||
? '将 PageAgent 类暴露到 globalThis,用法与 NPM 一致:'
|
|
||||||
: 'Exposes PageAgent class to globalThis, usage identical to NPM:'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
className="mb-4"
|
|
||||||
code={`<script src="${CDN_FULL_URL}" crossorigin="true"></script>
|
|
||||||
<script>
|
|
||||||
const agent = new PageAgent({
|
|
||||||
model: 'deepseek-chat',
|
|
||||||
baseURL: 'https://api.deepseek.com',
|
|
||||||
apiKey: 'YOUR_API_KEY',
|
|
||||||
})
|
|
||||||
await agent.execute('Click the submit button')
|
|
||||||
</script>`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<table className="w-full border-collapse text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<th className="text-left py-2 px-3 font-semibold w-28">
|
|
||||||
{isZh ? '位置' : 'Location'}
|
|
||||||
</th>
|
|
||||||
<th className="text-left py-2 px-3 font-semibold">URL</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr className="border-b border-gray-100 dark:border-gray-800">
|
|
||||||
<td className="py-2 px-3">{isZh ? '全球' : 'Global'}</td>
|
|
||||||
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_FULL_URL}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="py-2 px-3">{isZh ? '中国' : 'China'}</td>
|
|
||||||
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_FULL_CN_URL}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Tips */}
|
|
||||||
<section className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
|
||||||
<h3 className="text-base font-semibold mb-2 text-blue-900 dark:text-blue-300">
|
|
||||||
💡 {isZh ? '提示' : 'Tips'}
|
|
||||||
</h3>
|
|
||||||
<ul className="text-gray-600 dark:text-gray-300 text-sm space-y-1">
|
|
||||||
<li>• {isZh ? '生产环境推荐使用 NPM' : 'NPM is recommended for production'}</li>
|
|
||||||
<li>• {isZh ? '生产环境建议锁定版本号' : 'Lock version number in production'}</li>
|
|
||||||
<li>• {isZh ? '确保 HTTPS 环境' : 'Ensure HTTPS environment'}</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
|
||||||
|
|
||||||
export default function Configuration() {
|
|
||||||
const { i18n } = useTranslation()
|
|
||||||
const isZh = i18n.language === 'zh-CN'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-4xl font-bold mb-6">{isZh ? '配置选项' : 'Configuration'}</h1>
|
|
||||||
|
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
|
||||||
{isZh
|
|
||||||
? 'PageAgent 的完整配置接口定义。'
|
|
||||||
: 'Complete configuration interface for PageAgent.'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* LLM Configuration */}
|
|
||||||
<section className="mb-10">
|
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? 'LLM 配置' : 'LLM Configuration'}</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
{isZh
|
|
||||||
? '配置与大语言模型的连接参数。'
|
|
||||||
: 'Configure connection parameters for the language model.'}
|
|
||||||
</p>
|
|
||||||
<CodeEditor
|
|
||||||
className="mb-4"
|
|
||||||
language="typescript"
|
|
||||||
code={`interface LLMConfig {
|
|
||||||
baseURL?: string
|
|
||||||
apiKey?: string
|
|
||||||
model?: string
|
|
||||||
temperature?: number
|
|
||||||
maxRetries?: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom fetch function for LLM API requests.
|
|
||||||
* Use this to customize headers, credentials, proxy, etc.
|
|
||||||
*/
|
|
||||||
customFetch?: typeof globalThis.fetch
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Agent Configuration */}
|
|
||||||
<section className="mb-10">
|
|
||||||
<h2 className="text-2xl font-semibold mb-4">
|
|
||||||
{isZh ? 'Agent 配置' : 'Agent Configuration'}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
{isZh
|
|
||||||
? '配置 Agent 的行为、生命周期钩子和扩展能力。'
|
|
||||||
: 'Configure agent behavior, lifecycle hooks, and extension capabilities.'}
|
|
||||||
</p>
|
|
||||||
<CodeEditor
|
|
||||||
className="mb-4"
|
|
||||||
language="typescript"
|
|
||||||
code={`interface AgentConfig {
|
|
||||||
language?: 'en-US' | 'zh-CN'
|
|
||||||
|
|
||||||
/** Custom tools to extend or override built-in tools */
|
|
||||||
customTools?: Record<string, PageAgentTool | null>
|
|
||||||
|
|
||||||
/** Instructions to guide the agent's behavior */
|
|
||||||
instructions?: {
|
|
||||||
/** Global system-level instructions, applied to all tasks */
|
|
||||||
system?: string
|
|
||||||
|
|
||||||
/** Dynamic page-level instructions callback */
|
|
||||||
getPageInstructions?: (url: string) => string | undefined | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle hooks
|
|
||||||
onBeforeStep?: (stepCnt: number) => Promise<void> | void
|
|
||||||
onAfterStep?: (stepCnt: number, history: AgentHistory[]) => Promise<void> | void
|
|
||||||
onBeforeTask?: () => Promise<void> | void
|
|
||||||
onAfterTask?: (result: ExecutionResult) => Promise<void> | void
|
|
||||||
onDispose?: (reason?: string) => void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform page content before sending to LLM.
|
|
||||||
* Use cases: inspect extraction results, modify page info, mask sensitive data.
|
|
||||||
*/
|
|
||||||
transformPageContent?: (content: string) => Promise<string> | string
|
|
||||||
|
|
||||||
/** @experimental Enable JavaScript execution tool */
|
|
||||||
experimentalScriptExecutionTool?: boolean
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* PageController Configuration */}
|
|
||||||
<section className="mb-10">
|
|
||||||
<h2 className="text-2xl font-semibold mb-4">
|
|
||||||
{isZh ? 'PageController 配置' : 'PageController Configuration'}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
{isZh
|
|
||||||
? '配置 DOM 提取、元素交互和视觉高亮的细节。'
|
|
||||||
: 'Configure DOM extraction, element interaction, and visual highlighting.'}
|
|
||||||
</p>
|
|
||||||
<CodeEditor
|
|
||||||
className="mb-4"
|
|
||||||
language="typescript"
|
|
||||||
code={`interface PageControllerConfig {
|
|
||||||
/** Elements to exclude from interaction */
|
|
||||||
interactiveBlacklist?: (Element | (() => Element))[]
|
|
||||||
|
|
||||||
/** Elements to force include for interaction */
|
|
||||||
interactiveWhitelist?: (Element | (() => Element))[]
|
|
||||||
|
|
||||||
/** Additional attributes to include in DOM extraction */
|
|
||||||
include_attributes?: string[]
|
|
||||||
|
|
||||||
/** Highlight overlay opacity (0-1) */
|
|
||||||
highlightOpacity?: number
|
|
||||||
|
|
||||||
/** Highlight label opacity (0-1) */
|
|
||||||
highlightLabelOpacity?: number
|
|
||||||
|
|
||||||
/** Viewport expansion in pixels (-1 for full page) */
|
|
||||||
viewportExpansion?: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable visual mask overlay during automation.
|
|
||||||
* Blocks user interaction while agent is running.
|
|
||||||
* Default: false for PageController, true for PageAgent.
|
|
||||||
*/
|
|
||||||
enableMask?: boolean
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Complete Type */}
|
|
||||||
<section className="mb-10">
|
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '完整类型' : 'Complete Type'}</h2>
|
|
||||||
<CodeEditor
|
|
||||||
language="typescript"
|
|
||||||
code={`type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -36,7 +36,7 @@ const pageAgentTool = {
|
|||||||
},
|
},
|
||||||
execute: async (params) => {
|
execute: async (params) => {
|
||||||
const result = await pageAgent.execute(params.instruction)
|
const result = await pageAgent.execute(params.instruction)
|
||||||
return { success: result.success, message: result.message }
|
return { success: result.success, message: result.data }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,6 @@ export default function Overview() {
|
|||||||
|
|
||||||
{/* Status Badges */}
|
{/* Status Badges */}
|
||||||
<div className="flex flex-wrap gap-2 items-center">
|
<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">
|
<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" />
|
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="MIT License" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import BetaNotice from '@/components/BetaNotice'
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
import { CDN_DEMO_CN_URL, CDN_DEMO_URL } from '@/constants'
|
import { CDN_DEMO_CN_URL, CDN_DEMO_URL } from '@/constants'
|
||||||
|
|
||||||
@@ -28,11 +27,29 @@ export default function QuickStart() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`// Global: ${CDN_DEMO_URL}
|
code={`<script src="${isZh ? CDN_DEMO_CN_URL : CDN_DEMO_URL}" crossorigin="true"></script>`}
|
||||||
// China: ${CDN_DEMO_CN_URL}
|
|
||||||
<script src="${isZh ? CDN_DEMO_CN_URL : CDN_DEMO_URL}" crossorigin="true"></script>`}
|
|
||||||
language="html"
|
language="html"
|
||||||
/>
|
/>
|
||||||
|
<table className="w-full border-collapse text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<th className="text-left py-2 px-3 font-semibold w-28">
|
||||||
|
{isZh ? '镜像' : 'Mirrors'}
|
||||||
|
</th>
|
||||||
|
<th className="text-left py-2 px-3 font-semibold">URL</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-b border-gray-100 dark:border-gray-800">
|
||||||
|
<td className="py-2 px-3">{isZh ? '全球' : 'Global'}</td>
|
||||||
|
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_DEMO_URL}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3">{isZh ? '中国' : 'China'}</td>
|
||||||
|
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_DEMO_CN_URL}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* NPM - Recommended */}
|
{/* NPM - Recommended */}
|
||||||
@@ -41,7 +58,7 @@ export default function QuickStart() {
|
|||||||
{isZh ? '📦 NPM 安装(推荐)' : '📦 NPM Install (Recommended)'}
|
{isZh ? '📦 NPM 安装(推荐)' : '📦 NPM Install (Recommended)'}
|
||||||
</h3>
|
</h3>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`npm install page-agent
|
code={`// npm install page-agent
|
||||||
|
|
||||||
import { PageAgent } from 'page-agent'`}
|
import { PageAgent } from 'page-agent'`}
|
||||||
language="bash"
|
language="bash"
|
||||||
@@ -71,7 +88,8 @@ import { PageAgent } from 'page-agent'`}
|
|||||||
code={`// ${isZh ? '程序化执行自然语言指令' : 'Execute natural language instructions programmatically'}
|
code={`// ${isZh ? '程序化执行自然语言指令' : 'Execute natural language instructions programmatically'}
|
||||||
await agent.execute('${isZh ? '点击提交按钮,然后填写用户名为张三' : 'Click submit button, then fill username as John'}');
|
await agent.execute('${isZh ? '点击提交按钮,然后填写用户名为张三' : 'Click submit button, then fill username as John'}');
|
||||||
|
|
||||||
// ${isZh ? '或者显示对话框让用户输入指令' : 'Or show panel for user to input instructions'}
|
// ${isZh ? '或者' : 'Or:'}
|
||||||
|
// ${isZh ? '显示对话框让用户输入指令' : 'Show panel for user to input instructions'}
|
||||||
agent.panel.show()
|
agent.panel.show()
|
||||||
`}
|
`}
|
||||||
language="javascript"
|
language="javascript"
|
||||||
|
|||||||
+24
-137
@@ -1,148 +1,35 @@
|
|||||||
|
import { Suspense, lazy } from 'react'
|
||||||
import { Route, Switch } from 'wouter'
|
import { Route, Switch } from 'wouter'
|
||||||
|
|
||||||
import Header from './components/Header'
|
|
||||||
import HomePage from './pages/Home'
|
import HomePage from './pages/Home'
|
||||||
import DocsLayout from './pages/docs/Layout'
|
import DocsPages from './pages/docs/index'
|
||||||
// Features pages
|
|
||||||
import Instructions from './pages/docs/features/custom-instructions/page'
|
// const DocsPages = lazy(() => import('./pages/docs/index'))
|
||||||
import CustomTools from './pages/docs/features/custom-tools/page'
|
|
||||||
import DataMasking from './pages/docs/features/data-masking/page'
|
|
||||||
import Models from './pages/docs/features/models/page'
|
|
||||||
// Integration pages
|
|
||||||
import BestPractices from './pages/docs/integration/best-practices/page'
|
|
||||||
import CdnSetup from './pages/docs/integration/cdn-setup/page'
|
|
||||||
import Configuration from './pages/docs/integration/configuration/page'
|
|
||||||
import SecurityPermissions from './pages/docs/integration/security-permissions/page'
|
|
||||||
import ThirdPartyAgent from './pages/docs/integration/third-party-agent/page'
|
|
||||||
// Introduction pages
|
|
||||||
import Limitations from './pages/docs/introduction/limitations/page'
|
|
||||||
import Overview from './pages/docs/introduction/overview/page'
|
|
||||||
import QuickStart from './pages/docs/introduction/quick-start/page'
|
|
||||||
|
|
||||||
export default function Router() {
|
export default function Router() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Suspense>
|
||||||
{/* Home page */}
|
<Switch>
|
||||||
<Route path="/" component={HomePage} />
|
{/* Home page */}
|
||||||
|
<Route path="/">
|
||||||
|
<HomePage />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* Documentation pages with layout */}
|
{/* All docs pages */}
|
||||||
<Route path="/docs/introduction/overview">
|
<Route path="/docs" nest>
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
<DocsPages />
|
||||||
<Header />
|
</Route>
|
||||||
<DocsLayout>
|
|
||||||
<Overview />
|
|
||||||
</DocsLayout>
|
|
||||||
</div>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/docs/introduction/quick-start">
|
{/* 404 */}
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
<Route>
|
||||||
<Header />
|
<div className="min-h-screen bg-white dark:bg-gray-900 flex items-center justify-center">
|
||||||
<DocsLayout>
|
<div className="text-center">
|
||||||
<QuickStart />
|
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">404</h1>
|
||||||
</DocsLayout>
|
<p className="text-xl text-gray-600 dark:text-gray-300">页面未找到</p>
|
||||||
</div>
|
</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/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/custom-instructions">
|
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
|
||||||
<Header />
|
|
||||||
<DocsLayout>
|
|
||||||
<Instructions />
|
|
||||||
</DocsLayout>
|
|
||||||
</div>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/docs/features/models">
|
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
|
||||||
<Header />
|
|
||||||
<DocsLayout>
|
|
||||||
<Models />
|
|
||||||
</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/security-permissions">
|
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
|
||||||
<Header />
|
|
||||||
<DocsLayout>
|
|
||||||
<SecurityPermissions />
|
|
||||||
</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>
|
||||||
</div>
|
</Route>
|
||||||
</Route>
|
</Switch>
|
||||||
</Switch>
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"paths": {
|
"paths": {
|
||||||
// Self root
|
// Self root
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
|
|
||||||
// Simplified monorepo solution (raw npm workspace with hoisting)
|
"@page-agent/llms": ["../llms/src/index.ts"],
|
||||||
"page-agent": ["../page-agent/src/PageAgent.ts"],
|
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
|
||||||
"@page-agent/llms": ["../llms/src/index.ts"],
|
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
|
||||||
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
|
"@page-agent/ui": ["../ui/src/index.ts"],
|
||||||
"@page-agent/ui": ["../ui/src/index.ts"]
|
|
||||||
}
|
"page-agent": ["../page-agent/src/PageAgent.ts"]
|
||||||
},
|
}
|
||||||
"include": ["**/*.ts", "**/*.tsx"],
|
},
|
||||||
"exclude": ["dist", "node_modules"],
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
"references": [
|
"exclude": ["dist", "node_modules"],
|
||||||
//
|
"references": [
|
||||||
{ "path": "../llms" },
|
//
|
||||||
{ "path": "../page-agent" },
|
{ "path": "../llms" },
|
||||||
{ "path": "../page-controller" },
|
{ "path": "../page-controller" },
|
||||||
{ "path": "../ui" }
|
{ "path": "../core" },
|
||||||
]
|
{ "path": "../ui" },
|
||||||
|
|
||||||
|
{ "path": "../page-agent" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,26 +16,40 @@ const pageAgentPkg = JSON.parse(
|
|||||||
dotenvConfig({ path: resolve(__dirname, '../../.env') })
|
dotenvConfig({ path: resolve(__dirname, '../../.env') })
|
||||||
|
|
||||||
// Website Config (React Documentation Site)
|
// Website Config (React Documentation Site)
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => ({
|
||||||
base: './',
|
base: './',
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 2000,
|
||||||
|
cssCodeSplit: true,
|
||||||
|
rollupOptions: {
|
||||||
|
onwarn: function (message, handler) {
|
||||||
|
if (message.code === 'EVAL') return
|
||||||
|
handler(message)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
// Self root
|
// Self root
|
||||||
'@': resolve(__dirname, 'src'),
|
'@': resolve(__dirname, 'src'),
|
||||||
|
|
||||||
// Monorepo packages (always bundle local code instead of npm versions)
|
// Monorepo packages (always bundle local code instead of npm versions)
|
||||||
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
|
|
||||||
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
|
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
|
||||||
|
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
|
||||||
|
'@page-agent/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),
|
||||||
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
|
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
|
||||||
|
|
||||||
'page-agent': resolve(__dirname, '../page-agent/src/PageAgent.ts'),
|
'page-agent': resolve(__dirname, '../page-agent/src/PageAgent.ts'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),
|
...(mode === 'development' && {
|
||||||
'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
|
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),
|
||||||
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
|
'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
|
||||||
|
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
|
||||||
|
}),
|
||||||
'import.meta.env.VERSION': JSON.stringify(pageAgentPkg.version),
|
'import.meta.env.VERSION': JSON.stringify(pageAgentPkg.version),
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
|
|||||||
+41
-41
@@ -1,47 +1,47 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"target": "ES2024",
|
"target": "ES2024",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
||||||
// "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
// "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||||
// "baseUrl": "src",
|
// "baseUrl": "src",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
// "incremental": true,
|
// "incremental": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"verbatimModuleSyntax": false,
|
"verbatimModuleSyntax": false,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
|
|
||||||
// "paths": {
|
// "paths": {
|
||||||
// // Simplified monorepo solution (raw npm workspace with hoisting)
|
// // Simplified monorepo solution (raw npm workspace with hoisting)
|
||||||
// "@page-agent/page-controller": ["./packages/page-controller/src/PageController.ts"],
|
// "@page-agent/page-controller": ["./packages/page-controller/src/PageController.ts"],
|
||||||
// "page-agent": ["./packages/page-agent/src/PageAgent.ts"]
|
// "page-agent": ["./packages/page-agent/src/PageAgent.ts"]
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
// "references": [
|
// "references": [
|
||||||
// { "path": "./packages/page-controller" },
|
// { "path": "./packages/page-controller" },
|
||||||
// { "path": "./packages/page-agent" },
|
// { "path": "./packages/page-agent" },
|
||||||
// { "path": "./packages/website" }
|
// { "path": "./packages/website" }
|
||||||
// ],
|
// ],
|
||||||
// "include": ["packages/*/src/**/*.ts", "packages/*/src/**/*.tsx"],
|
// "include": ["packages/*/src/**/*.ts", "packages/*/src/**/*.tsx"],
|
||||||
// "exclude": ["node_modules", "dist", "packages/*/dist"]
|
// "exclude": ["node_modules", "dist", "packages/*/dist"]
|
||||||
// "files": ["env.d.ts"]
|
// "files": ["env.d.ts"]
|
||||||
// "files": []
|
// "files": []
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.base.json",
|
"extends": "./tsconfig.base.json",
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./packages/page-controller" },
|
{ "path": "./packages/page-controller" },
|
||||||
{ "path": "./packages/ui" },
|
{ "path": "./packages/ui" },
|
||||||
{ "path": "./packages/llms" },
|
{ "path": "./packages/llms" },
|
||||||
{ "path": "./packages/page-agent" },
|
{ "path": "./packages/page-agent" },
|
||||||
{ "path": "./packages/website" }
|
{ "path": "./packages/website" }
|
||||||
],
|
],
|
||||||
"include": ["packages/*/src/**/*.ts", "packages/*/src/**/*.tsx"],
|
"include": ["packages/*/src/**/*.ts", "packages/*/src/**/*.tsx"],
|
||||||
"exclude": ["node_modules", "dist", "packages/*/dist"]
|
"exclude": ["node_modules", "dist", "packages/*/dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user