Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fe6ae6862 | |||
| 66be1e56e3 | |||
| 0fd4eaf9af | |||
| a0712baa91 | |||
| 49af3a9c58 | |||
| 20ad3d04d2 | |||
| c7e0538836 | |||
| 59fcfaa503 | |||
| 17befe4bab | |||
| 7911bac052 | |||
| 87a8da5783 | |||
| 0aec35aee3 | |||
| 35fe51427c | |||
| 9e0b5bec8b | |||
| 70d33a9756 | |||
| 1c99d21c19 | |||
| e3c612c338 | |||
| 87596469b6 | |||
| 566b2d7022 | |||
| f521bd9beb | |||
| 35db15460e | |||
| a34d86ba75 | |||
| 3144e8d44d | |||
| 32be34bcb1 | |||
| a774bd94dc | |||
| 7063b8199d | |||
| 64f1a8c443 | |||
| 635416f964 | |||
| 7c2d000e29 | |||
| b36a0c0261 | |||
| 339b066d2d | |||
| c8efffa80a | |||
| 59235263ad | |||
| bc1e8fea3c | |||
| 85a4096b2d | |||
| 0ef84bac13 | |||
| a7834c77e1 | |||
| 030e5cfc54 | |||
| aa694e3b85 | |||
| 2fee2d5ea3 | |||
| 3082843810 | |||
| 34098e2f66 | |||
| 8461006c91 | |||
| 2e76a35188 | |||
| abc96b415f | |||
| 77ce3b0b49 | |||
| 3742f75d14 | |||
| c2e9e6a571 | |||
| 54eb50f7be | |||
| 9351948794 | |||
| 68d1a10dd0 | |||
| b9fe8146b6 | |||
| 2a727226bc | |||
| 0167377b2f |
@@ -25,7 +25,7 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Build demo
|
||||
run: npm run build
|
||||
run: npm run build:website
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
run: npm run build:libs
|
||||
|
||||
- name: Publish all public packages
|
||||
run: npm publish --workspaces --access public
|
||||
Vendored
+11
-1
@@ -1,6 +1,16 @@
|
||||
{
|
||||
"editor.fontLigatures": true,
|
||||
"cSpell.words": ["HITL", "innerhtml", "opensource", "retryable", "wouter"],
|
||||
"cSpell.words": [
|
||||
"deepseek",
|
||||
"HITL",
|
||||
"innerhtml",
|
||||
"llms",
|
||||
"opensource",
|
||||
"qwen",
|
||||
"retryable",
|
||||
"shadcn",
|
||||
"wouter"
|
||||
],
|
||||
"markdownlint.config": {
|
||||
// "comment": "Relaxed rules",
|
||||
"default": true,
|
||||
|
||||
@@ -1,95 +1,59 @@
|
||||
# Instructions for coding assistants
|
||||
# Instructions for Coding Assistants
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **monorepo** with npm workspaces containing **two main packages**:
|
||||
This is a **monorepo** with npm workspaces:
|
||||
|
||||
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, 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 Library** (`packages/page-agent/`) - AI agent for browser DOM automation, published as `page-agent` on npm
|
||||
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
|
||||
|
||||
And other internal packages. Such as:
|
||||
Internal packages:
|
||||
|
||||
- **Page Controller** (`packages/page-controller/`) - DOM operations and element interactions module. Independent of LLM, can be tested in unit tests.
|
||||
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
|
||||
- **Page Controller** (`packages/page-controller/`) - DOM operations, independent of LLM
|
||||
- **UI** (`packages/ui/`) - Panel, SimulatorMask, i18n. Decoupled from PageAgent
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Core Commands
|
||||
|
||||
```bash
|
||||
npm start # Start website dev server
|
||||
npm run build # Build all packages
|
||||
npm run build:lib # Build page-agent library only
|
||||
npm run build:libs # Build all libraries
|
||||
npm run lint # ESLint with TypeScript strict rules
|
||||
```
|
||||
|
||||
### Package-specific Commands
|
||||
|
||||
```bash
|
||||
# Core library
|
||||
npm run build --workspace=page-agent
|
||||
npm run build:watch --workspace=page-agent
|
||||
|
||||
# Website
|
||||
npm run dev --workspace=@page-agent/website
|
||||
npm run build --workspace=@page-agent/website
|
||||
```
|
||||
|
||||
## Architecture & Critical Patterns
|
||||
## Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
|
||||
We adopt a very simple monorepo solution: ts reference + vite alias.
|
||||
Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig and vite config when adding/removing packages.
|
||||
|
||||
You must update tsconfig and vite config if you add/remove/rename a package.
|
||||
|
||||
```bash
|
||||
```
|
||||
packages/
|
||||
├── page-agent/ # npm: "page-agent" ⭐ MAIN
|
||||
│ ├── src/ # AI agent source
|
||||
│ │ ├── PageAgent.ts # Main AI agent class
|
||||
│ │ ├── tools/ # LLM tool definitions
|
||||
│ │ ├── llms/ # LLM integration
|
||||
│ │ └── ui/ # UI components
|
||||
│ ├── vite.config.js # Library build (ES + UMD)
|
||||
│ └── package.json
|
||||
├── website/ # npm: "@page-agent/website" (private) ⭐ MAIN
|
||||
│ ├── src/ # Website source
|
||||
│ └── index.html # Entry of vite webpage
|
||||
│
|
||||
│ # ...internal packages below...
|
||||
│
|
||||
└── page-controller/ # npm: "@page-agent/page-controller"
|
||||
└── src/ # DOM operations source
|
||||
├── PageController.ts # Main controller class
|
||||
├── actions.ts # Element interaction actions
|
||||
└── dom/ # DOM tree extraction
|
||||
├── website/ # @page-agent/website (private)
|
||||
├── llms/ # @page-agent/llms
|
||||
├── page-controller/ # @page-agent/page-controller
|
||||
└── ui/ # @page-agent/ui
|
||||
```
|
||||
|
||||
`workspaces` must be written in topological order to guarantee build order.
|
||||
`workspaces` in `package.json` must be in topological order.
|
||||
|
||||
```json
|
||||
"workspaces": [
|
||||
// internal deps ...
|
||||
"packages/page-controller",
|
||||
"packages/page-agent",
|
||||
"packages/website"
|
||||
],
|
||||
```
|
||||
### Module Boundaries
|
||||
|
||||
### Module Boundaries (Critical)
|
||||
|
||||
- **Website** (`packages/website/`): CAN import from `page-agent` for demos. Alias `@/` → `website/src/`
|
||||
- **Page Agent** (`packages/page-agent/`): The core lib. Imports from all internal packages. Never import from website.
|
||||
- **Page Controller** (`packages/page-controller/`): Internal lib. Pure DOM operations, NO LLM dependency. Never import from page-agent.
|
||||
- **Page Agent**: Core lib. Imports from `@page-agent/llms`, `@page-agent/page-controller`, `@page-agent/ui`
|
||||
- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent
|
||||
- **UI**: Panel, Mask, i18n. No dependency on page-agent
|
||||
- **Page Controller**: Pure DOM operations. No LLM or UI dependency
|
||||
|
||||
### PageController ↔ PageAgent Communication
|
||||
|
||||
All communication between PageAgent and PageController is async and isolated:
|
||||
All communication is async and isolated:
|
||||
|
||||
```typescript
|
||||
// PageAgent delegates DOM operations to PageController
|
||||
await this.pageController.updateTree() // Refresh DOM state
|
||||
await this.pageController.clickElement(index) // Click by index
|
||||
await this.pageController.updateTree()
|
||||
await this.pageController.clickElement(index)
|
||||
await this.pageController.inputText(index, text)
|
||||
await this.pageController.scroll({ down: true, numPages: 1 })
|
||||
|
||||
@@ -98,32 +62,22 @@ const simplifiedHTML = await this.pageController.getSimplifiedHTML()
|
||||
const pageInfo = await this.pageController.getPageInfo()
|
||||
```
|
||||
|
||||
DOM element references and internal state (selectorMap, elementTextMap) are encapsulated in PageController.
|
||||
|
||||
### DOM Pipeline
|
||||
|
||||
1. **DOM Extraction**: Convert live DOM to `FlatDomTree` via `page-controller/src/dom/dom_tree/`
|
||||
2. **Dehydration**: DOM tree → simplified text for LLM processing
|
||||
3. **LLM Processing**: AI model returns action plans (in page-agent)
|
||||
4. **Indexed Operations**: PageAgent calls PageController methods by element index
|
||||
1. **DOM Extraction**: Live DOM → `FlatDomTree` via `page-controller/src/dom/dom_tree/`
|
||||
2. **Dehydration**: DOM tree → simplified text for LLM
|
||||
3. **LLM Processing**: AI returns action plans (page-agent)
|
||||
4. **Indexed Operations**: PageAgent calls PageController by element index
|
||||
|
||||
### Hash Routing Requirement
|
||||
### CDN Auto-Injection
|
||||
|
||||
Uses wouter with `useHashLocation` for static hosting:
|
||||
|
||||
```tsx
|
||||
<Router hook={useHashLocation}> // Always hash-based routes
|
||||
```
|
||||
|
||||
### CDN Auto-Injection Pattern
|
||||
|
||||
Library auto-initializes when injected via script tag:
|
||||
Library auto-initializes via script tag:
|
||||
|
||||
```html
|
||||
<script src="page-agent.js?model=gpt-4"></script>
|
||||
```
|
||||
|
||||
Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
||||
Query params configure `PageAgentConfig` in `src/umd.ts`.
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
@@ -131,87 +85,42 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `src/PageAgent.ts` | ⭐ Main AI agent class orchestrating tools and LLM |
|
||||
| `src/entry.ts` | CDN/UMD entry point with auto-initialization |
|
||||
| `src/tools/` | Tool definitions that call PageController methods |
|
||||
| `src/ui/` | UI components (Panel, SimulatorMask) with CSS modules |
|
||||
| `src/llms/` | LLM integration and communication layer |
|
||||
| `vite.config.js` | Library build configuration (ES + UMD) |
|
||||
| `src/PageAgent.ts` | ⭐ Main AI agent class |
|
||||
| `src/umd.ts` | CDN/UMD entry with auto-init |
|
||||
| `src/tools/` | Tool definitions calling PageController |
|
||||
|
||||
### LLMs (`packages/llms/`)
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `src/index.ts` | ⭐ LLM class with retry logic |
|
||||
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig |
|
||||
| `src/OpenAILenientClient.ts` | OpenAI-compatible client |
|
||||
|
||||
### Page Controller (`packages/page-controller/`)
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `src/PageController.ts` | ⭐ Main controller class managing DOM state and actions |
|
||||
| `src/actions.ts` | Element interaction implementations (click, input, scroll) |
|
||||
| `src/dom/dom_tree/index.js` | Core DOM extraction engine (ported from browser-use) |
|
||||
| `src/dom/getPageInfo.ts` | Page scroll/size information |
|
||||
| `src/patches/` | Framework-specific optimizations (React, Antd) |
|
||||
| `src/types.ts` | TypeScript interfaces for controller |
|
||||
|
||||
### Website (`packages/website/`)
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `src/router.tsx` | ⭐ Central routing (manual registration required) |
|
||||
| `src/components/DocsLayout.tsx` | Navigation structure (hardcoded nav items) |
|
||||
| `src/main.tsx` | Site entry with hash routing setup |
|
||||
| `src/docs/[section]/[topic]/page.tsx` | Documentation pages |
|
||||
| `src/test-pages/` | Library integration test pages |
|
||||
| `vite.config.js` | Website build configuration |
|
||||
| `src/PageController.ts` | ⭐ Main controller class |
|
||||
| `src/actions.ts` | Element interactions (click, input, scroll) |
|
||||
| `src/dom/dom_tree/index.js` | Core DOM extraction engine |
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### New Documentation Page
|
||||
|
||||
1. Create `packages/website/src/docs/<section>/<slug>/page.tsx`
|
||||
2. Add route to `packages/website/src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
||||
3. Add navigation item to `DocsLayout.tsx`
|
||||
|
||||
### New Agent Tool
|
||||
|
||||
1. Implement tool in `packages/page-agent/src/tools/index.ts`
|
||||
2. If tool needs DOM operations, add method to PageController first
|
||||
1. Implement in `packages/page-agent/src/tools/index.ts`
|
||||
2. If tool needs DOM ops, add method to PageController first
|
||||
3. Tool calls `this.pageController.methodName()` for DOM interactions
|
||||
|
||||
### New PageController Action
|
||||
|
||||
1. Add action implementation in `packages/page-controller/src/actions.ts`
|
||||
1. Add implementation in `packages/page-controller/src/actions.ts`
|
||||
2. Expose via async method in `PageController.ts`
|
||||
3. Export from `packages/page-controller/src/index.ts`
|
||||
|
||||
## Code Standards
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Explicit typing for exported/public APIs
|
||||
- ESLint relaxes some unsafe rules for rapid iteration
|
||||
|
||||
### CSS & Styling
|
||||
|
||||
- **Prefer Tailwind CSS over custom CSS**
|
||||
- Custom CSS variables for theme gradients in `src/index.css`
|
||||
- Dark mode support via `dark:` classes
|
||||
- CSS modules for component-specific styles
|
||||
|
||||
### Import Organization
|
||||
|
||||
- External libraries first
|
||||
- Internal modules (`@/`, `@pages/`)
|
||||
- Relative imports last
|
||||
- Blank lines between groups
|
||||
|
||||
## Debugging Common Issues
|
||||
|
||||
### Blank Documentation Pages
|
||||
|
||||
1. Verify route exists in `packages/website/src/router.tsx`
|
||||
2. Check component import path
|
||||
3. Verify CSS isn't hiding content (check dark mode classes)
|
||||
4. Test with minimal component first
|
||||
|
||||
### Library Integration Issues
|
||||
|
||||
1. Check `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly
|
||||
2. Test CDN injection with query params
|
||||
4. Use `packages/website/src/test-pages/` for isolated testing
|
||||
- All code and comments must be in English.
|
||||
|
||||
+3
-3
@@ -112,16 +112,16 @@ npm start # React development server
|
||||
> @TODO this part is outdated. Update this.
|
||||
|
||||
- Config your LLM API
|
||||
- Start a local script
|
||||
- Start and serve a local umd script
|
||||
|
||||
```bash
|
||||
npm run build:watch --workspace=page-agent # UMD with auto rebuild
|
||||
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:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/dist/lib/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
|
||||
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);})();
|
||||
```
|
||||
|
||||
### Adding Documentation
|
||||
|
||||
+9
-10
@@ -31,10 +31,11 @@
|
||||
### CDN 集成
|
||||
|
||||
```html
|
||||
<!-- 临时 CDN URL. 未来会变更 -->
|
||||
<script
|
||||
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
|
||||
crossorigin="true"
|
||||
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js
|
||||
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
|
||||
<script
|
||||
src="https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js"
|
||||
crossorigin="true"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
```
|
||||
@@ -72,12 +73,10 @@ PageAgent adopts a simplified monorepo structure:
|
||||
```
|
||||
packages/
|
||||
├── page-agent/ # AI agent (npm: page-agent)
|
||||
│ ├── PageAgent # Agent main loop
|
||||
│ ├── tools/ # LLM tool definitions
|
||||
│ ├── ui/ # UI components & panels
|
||||
│ └── llms/ # LLM integration layer
|
||||
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
|
||||
└── website/ # Documentation site
|
||||
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
|
||||
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller)
|
||||
├── ui/ # 面板 & 蒙层 & 模拟鼠标 (npm: @page-agent/ui)
|
||||
└── website/ # 文档站点
|
||||
```
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PageAgent 🤖🪄
|
||||
|
||||

|
||||

|
||||
|
||||
[](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)
|
||||
|
||||
@@ -31,10 +31,11 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
|
||||
### CDN Integration
|
||||
|
||||
```html
|
||||
<!-- temporary CDN URL. May change in the future -->
|
||||
<script
|
||||
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
|
||||
crossorigin="true"
|
||||
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js
|
||||
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js"
|
||||
crossorigin="true"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
```
|
||||
@@ -72,11 +73,9 @@ PageAgent adopts a simplified monorepo structure:
|
||||
```
|
||||
packages/
|
||||
├── page-agent/ # AI agent (npm: page-agent)
|
||||
│ ├── PageAgent # Agent main loop
|
||||
│ ├── tools/ # LLM tool definitions
|
||||
│ ├── ui/ # UI components & panels
|
||||
│ └── llms/ # LLM integration layer
|
||||
├── llms/ # LLM client (npm: @page-agent/llms)
|
||||
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
|
||||
├── ui/ # Panel & Mask & Mouse Animation (npm: @page-agent/ui)
|
||||
└── website/ # Demo & Documentation site
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ The development progress and future plans for PageAgent.
|
||||
- [ ] **Tools for more complex tasks**
|
||||
- todo list
|
||||
- file sys
|
||||
- [ ] **Support custom llm fetch**
|
||||
- [x] **Support custom llm fetch**
|
||||
- [ ] **Testing suits**
|
||||
- [ ] **Same-origin multi-page-app rally**
|
||||
- [ ] **Local MCP proxy**
|
||||
|
||||
+6
-1
@@ -8,7 +8,12 @@ import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['**/dist', '**/test-pages', '**/node_modules']),
|
||||
globalIgnores([
|
||||
'**/dist',
|
||||
'**/test-pages',
|
||||
'**/node_modules',
|
||||
'packages/website/src/components/ui',
|
||||
]),
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
|
||||
Generated
+1645
-461
File diff suppressed because it is too large
Load Diff
+20
-14
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.18",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/page-controller",
|
||||
"packages/ui",
|
||||
"packages/llms",
|
||||
"packages/page-agent",
|
||||
"packages/website"
|
||||
],
|
||||
@@ -22,34 +23,39 @@
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"cleanup": "rm -rf packages/*/dist",
|
||||
"start": "npm run dev --workspace=@page-agent/website",
|
||||
"build": "npm run build --workspaces --if-present",
|
||||
"build:lib": "npm run build:lib --workspace=page-agent",
|
||||
"build:website": "npm run build:website --workspace=@page-agent/website",
|
||||
"build:libs": "npm run build --workspaces --if-present",
|
||||
"build": "npm run build:libs && npm run build:website",
|
||||
"dev:umd": "npm run dev:umd --workspace=page-agent",
|
||||
"version": "node scripts/sync-version.js",
|
||||
"lint": "eslint .",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.1.0",
|
||||
"@commitlint/config-conventional": "^20.0.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@commitlint/cli": "^20.2.0",
|
||||
"@commitlint/config-conventional": "^20.2.0",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@microsoft/api-extractor": "^7.55.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
||||
"@types/node": "^25.0.3",
|
||||
"concurrently": "^9.2.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-react-dom": "^2.3.9",
|
||||
"eslint-plugin-react-dom": "^2.3.13",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"eslint-plugin-react-x": "^2.3.9",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"eslint-plugin-react-x": "^2.3.13",
|
||||
"globals": "^16.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.4",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"vite": "^7.2.6",
|
||||
"@microsoft/api-extractor": "^7.55.1",
|
||||
"typescript-eslint": "^8.50.1",
|
||||
"unplugin-dts": "^1.0.0-beta.6",
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# @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 |
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@page-agent/llms",
|
||||
"version": "0.0.18",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-agent-llms.js",
|
||||
"module": "./dist/lib/page-agent-llms.js",
|
||||
"types": "./dist/lib/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/lib/index.d.ts",
|
||||
"import": "./dist/lib/page-agent-llms.js",
|
||||
"default": "./dist/lib/page-agent-llms.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"description": "LLM client with reflection-before-action mental model for page-agent",
|
||||
"keywords": [
|
||||
"page-agent",
|
||||
"llm",
|
||||
"openai",
|
||||
"tool-calling",
|
||||
"agent"
|
||||
],
|
||||
"author": "Simon<gaomeng1900>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alibaba/page-agent.git",
|
||||
"directory": "packages/llms"
|
||||
},
|
||||
"homepage": "https://alibaba.github.io/page-agent/",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
||||
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.6.2",
|
||||
"zod": "^4.2.1"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
/**
|
||||
* OpenAI Client implementation
|
||||
* @note This client is only for demonstrating how to implement a LLM client.
|
||||
* @note Use OpenAILenientClient instead.
|
||||
*/
|
||||
import { InvokeError, InvokeErrorType } from './errors'
|
||||
import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types'
|
||||
import type { InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||
import { modelPatch, zodToOpenAITool } from './utils'
|
||||
|
||||
/**
|
||||
* @deprecated Use OpenAILenientClient instead.
|
||||
*/
|
||||
export class OpenAIClient implements LLMClient {
|
||||
config: OpenAIClientConfig
|
||||
config: LLMConfig
|
||||
|
||||
constructor(config: OpenAIClientConfig) {
|
||||
constructor(config: LLMConfig) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
+7
-10
@@ -1,16 +1,17 @@
|
||||
/**
|
||||
* OpenAI Client implementation
|
||||
*/
|
||||
import type { MacroToolInput } from '../PageAgent'
|
||||
import { InvokeError, InvokeErrorType } from './errors'
|
||||
import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types'
|
||||
import type { InvokeResult, LLMClient, LLMConfig, MacroToolInput, Message, Tool } from './types'
|
||||
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
|
||||
|
||||
export class OpenAIClient implements LLMClient {
|
||||
config: OpenAIClientConfig
|
||||
config: Required<LLMConfig>
|
||||
private fetch: typeof globalThis.fetch
|
||||
|
||||
constructor(config: OpenAIClientConfig) {
|
||||
constructor(config: Required<LLMConfig>) {
|
||||
this.config = config
|
||||
this.fetch = config.customFetch
|
||||
}
|
||||
|
||||
async invoke(
|
||||
@@ -24,7 +25,7 @@ export class OpenAIClient implements LLMClient {
|
||||
// 2. Call API
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(`${this.config.baseURL}/chat/completions`, {
|
||||
response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -40,11 +41,6 @@ export class OpenAIClient implements LLMClient {
|
||||
tools: openaiTools,
|
||||
// tool_choice: 'required',
|
||||
tool_choice: { type: 'function', function: { name: 'AgentOutput' } },
|
||||
|
||||
// model specific params
|
||||
|
||||
// reasoning_effort: 'minimal',
|
||||
// verbosity: 'low',
|
||||
parallel_tool_calls: false,
|
||||
})
|
||||
),
|
||||
@@ -52,6 +48,7 @@ export class OpenAIClient implements LLMClient {
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
// Network error
|
||||
console.error(error)
|
||||
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Dev environment: use .env config if available, otherwise fallback to testing api
|
||||
export const DEFAULT_MODEL_NAME: string =
|
||||
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
|
||||
? import.meta.env.LLM_MODEL_NAME
|
||||
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||
|
||||
export const DEFAULT_API_KEY: string =
|
||||
import.meta.env.DEV && import.meta.env.LLM_API_KEY
|
||||
? import.meta.env.LLM_API_KEY
|
||||
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||
|
||||
export const DEFAULT_BASE_URL: string =
|
||||
import.meta.env.DEV && import.meta.env.LLM_BASE_URL
|
||||
? import.meta.env.LLM_BASE_URL
|
||||
: 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
|
||||
|
||||
// internal
|
||||
|
||||
export const LLM_MAX_RETRIES = 2
|
||||
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
|
||||
export const DEFAULT_MAX_TOKENS = 16000
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -31,13 +31,49 @@
|
||||
* - 永远使用 tool call 来返回结构化数据,禁止模型直接返回(视为出错)
|
||||
* - 不能假设 tool 参数合法,必须有修复机制,而且修复也应该使用 tool call 返回
|
||||
*/
|
||||
import type { LLMConfig } from '../config'
|
||||
import { parseLLMConfig } from '../config'
|
||||
import { OpenAIClient } from './OpenAILenientClient'
|
||||
import {
|
||||
DEFAULT_API_KEY,
|
||||
DEFAULT_BASE_URL,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_MODEL_NAME,
|
||||
DEFAULT_TEMPERATURE,
|
||||
LLM_MAX_RETRIES,
|
||||
} from './constants'
|
||||
import { InvokeError } from './errors'
|
||||
import type { InvokeResult, LLMClient, Message, Tool } from './types'
|
||||
import type {
|
||||
AgentBrain,
|
||||
InvokeResult,
|
||||
LLMClient,
|
||||
LLMConfig,
|
||||
MacroToolInput,
|
||||
MacroToolResult,
|
||||
Message,
|
||||
Tool,
|
||||
} from './types'
|
||||
|
||||
export type { Message, Tool, InvokeResult, LLMClient }
|
||||
export type {
|
||||
AgentBrain,
|
||||
InvokeResult,
|
||||
LLMClient,
|
||||
LLMConfig,
|
||||
MacroToolInput,
|
||||
MacroToolResult,
|
||||
Message,
|
||||
Tool,
|
||||
}
|
||||
|
||||
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
||||
return {
|
||||
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
|
||||
apiKey: config.apiKey ?? DEFAULT_API_KEY,
|
||||
model: config.model ?? DEFAULT_MODEL_NAME,
|
||||
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
||||
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
|
||||
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
||||
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
||||
}
|
||||
}
|
||||
|
||||
export class LLM extends EventTarget {
|
||||
config: Required<LLMConfig>
|
||||
@@ -48,13 +84,7 @@ export class LLM extends EventTarget {
|
||||
this.config = parseLLMConfig(config)
|
||||
|
||||
// Default to OpenAI client
|
||||
this.client = new OpenAIClient({
|
||||
model: this.config.model,
|
||||
apiKey: this.config.apiKey,
|
||||
baseURL: this.config.baseURL,
|
||||
temperature: this.config.temperature,
|
||||
maxTokens: this.config.maxTokens,
|
||||
})
|
||||
this.client = new OpenAIClient(this.config)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,13 +65,54 @@ export interface InvokeResult<TResult = unknown> {
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI Client config
|
||||
* LLM configuration
|
||||
*/
|
||||
export interface OpenAIClientConfig {
|
||||
model: string
|
||||
apiKey: string
|
||||
baseURL: string
|
||||
export interface LLMConfig {
|
||||
baseURL?: string
|
||||
apiKey?: string
|
||||
model?: string
|
||||
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
maxRetries?: number
|
||||
|
||||
/**
|
||||
* Custom fetch function for LLM API requests.
|
||||
* Use this to customize headers, credentials, proxy, etc.
|
||||
* The response should follow OpenAI API format.
|
||||
*/
|
||||
customFetch?: typeof globalThis.fetch
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent brain 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 AgentBrain {
|
||||
// thinking?: string
|
||||
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 AgentBrain {
|
||||
action: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* MacroTool output structure
|
||||
*/
|
||||
export interface MacroToolResult {
|
||||
input: MacroToolInput
|
||||
output: string
|
||||
}
|
||||
@@ -4,9 +4,12 @@
|
||||
import chalk from 'chalk'
|
||||
import { z } from 'zod'
|
||||
|
||||
import type { MacroToolInput } from '../PageAgent'
|
||||
import { InvokeError, InvokeErrorType } from './errors'
|
||||
import type { Tool } from './types'
|
||||
import type { MacroToolInput, Tool } from './types'
|
||||
|
||||
function debug(message: string) {
|
||||
console.debug(chalk.gray('[LLM]'), message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Zod schema to OpenAI tool format
|
||||
@@ -193,22 +196,82 @@ export function lenientParseMacroToolCall(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch model specific parameters
|
||||
*/
|
||||
export function modelPatch(body: Record<string, any>) {
|
||||
const model: string = body.model || ''
|
||||
if (!model) return body
|
||||
|
||||
if (model.toLowerCase().startsWith('claude')) {
|
||||
const modelName = normalizeModelName(model)
|
||||
|
||||
if (modelName.startsWith('claude')) {
|
||||
debug('Applying Claude patch: change tool_choice and disable thinking')
|
||||
body.tool_choice = { type: 'tool', name: 'AgentOutput' }
|
||||
body.thinking = { type: 'disabled' }
|
||||
// body.reasoning = { enabled: 'disabled' }
|
||||
}
|
||||
|
||||
if (model.toLowerCase().includes('grok')) {
|
||||
console.log('Applying Grok patch: removing tool_choice')
|
||||
if (modelName.startsWith('grok')) {
|
||||
debug('Applying Grok patch: removing tool_choice')
|
||||
delete body.tool_choice
|
||||
console.log('Applying Grok patch: disable reasoning and thinking')
|
||||
debug('Applying Grok patch: disable reasoning and thinking')
|
||||
body.thinking = { type: 'disabled', effort: 'minimal' }
|
||||
body.reasoning = { enabled: false, effort: 'low' }
|
||||
}
|
||||
|
||||
if (modelName.startsWith('gpt')) {
|
||||
debug('Applying GPT patch: set verbosity to low')
|
||||
body.verbosity = 'low'
|
||||
|
||||
if (modelName.startsWith('gpt-52')) {
|
||||
debug('Applying GPT-52 patch: disable reasoning')
|
||||
body.reasoning_effort = 'none'
|
||||
} else if (modelName.startsWith('gpt-51')) {
|
||||
debug('Applying GPT-51 patch: disable reasoning')
|
||||
body.reasoning_effort = 'none'
|
||||
} else if (modelName.startsWith('gpt-5')) {
|
||||
debug('Applying GPT-5 patch: set reasoning effort to low')
|
||||
body.reasoning_effort = 'low'
|
||||
}
|
||||
}
|
||||
|
||||
if (modelName.startsWith('gemini')) {
|
||||
debug('Applying Gemini patch: set reasoning effort to minimal')
|
||||
body.reasoning_effort = 'minimal'
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
/**
|
||||
* check if a given model ID fits a specific model name
|
||||
*
|
||||
* @note
|
||||
* Different model providers may use different model IDs for the same model.
|
||||
* For example, openai's `gpt-5.2` may called:
|
||||
*
|
||||
* - `gpt-5.2-version`
|
||||
* - `gpt-5_2-date`
|
||||
* - `GPT-52-version-date`
|
||||
* - `openai/gpt-5.2-chat`
|
||||
*
|
||||
* They should be treated as the same model.
|
||||
* Normalize them to `gpt-52`
|
||||
*/
|
||||
function normalizeModelName(modelName: string): string {
|
||||
let normalizedName = modelName.toLowerCase()
|
||||
|
||||
// remove prefix before '/'
|
||||
if (normalizedName.includes('/')) {
|
||||
normalizedName = normalizedName.split('/')[1]
|
||||
}
|
||||
|
||||
// remove '_'
|
||||
normalizedName = normalizedName.replace(/_/g, '')
|
||||
|
||||
// remove '.'
|
||||
normalizedName = normalizedName.replace(/\./g, '')
|
||||
|
||||
return normalizedName
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// @workaround DTS bug
|
||||
// dts do not work with monorepo path mapping
|
||||
// disable path mapping for it
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"noEmit": false,
|
||||
"allowImportingTsExtensions": false,
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// @ts-check
|
||||
import chalk from 'chalk'
|
||||
import { dirname, resolve } from 'path'
|
||||
import dts from 'unplugin-dts/vite'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
console.log(chalk.cyan(`📦 Building @page-agent/llms`))
|
||||
|
||||
export default defineConfig({
|
||||
clearScreen: false,
|
||||
plugins: [dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true })],
|
||||
publicDir: false,
|
||||
esbuild: {
|
||||
keepNames: true,
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
name: 'PageAgentLLMs',
|
||||
fileName: 'page-agent-llms',
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: resolve(__dirname, 'dist', 'lib'),
|
||||
rollupOptions: {
|
||||
external: ['chalk', 'zod'],
|
||||
},
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "page-agent",
|
||||
"private": false,
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.18",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-agent.js",
|
||||
"module": "./dist/lib/page-agent.js",
|
||||
"types": "./dist/lib/PageAgent.d.ts",
|
||||
"main": "./dist/esm/page-agent.js",
|
||||
"module": "./dist/esm/page-agent.js",
|
||||
"types": "./dist/esm/PageAgent.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/lib/PageAgent.d.ts",
|
||||
"import": "./dist/lib/page-agent.js",
|
||||
"default": "./dist/lib/page-agent.js"
|
||||
"types": "./dist/esm/PageAgent.d.ts",
|
||||
"import": "./dist/esm/page-agent.js",
|
||||
"default": "./dist/esm/page-agent.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -37,17 +37,17 @@
|
||||
},
|
||||
"homepage": "https://alibaba.github.io/page-agent/",
|
||||
"scripts": {
|
||||
"build": "npm run build:lib && npm run build:umd",
|
||||
"build:lib": "MODE=lib vite build",
|
||||
"build:umd": "MODE=umd vite build",
|
||||
"build:watch": "MODE=umd vite build --watch",
|
||||
"build": "vite build && vite build --config vite.umd.config.js",
|
||||
"serve": "npx serve dist/umd -p 5173",
|
||||
"dev:umd": "concurrently \"vite build --config vite.umd.config.js --watch\" \"npm run serve\"",
|
||||
"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.1.12",
|
||||
"@page-agent/page-controller": "0.0.8",
|
||||
"@page-agent/ui": "0.0.8"
|
||||
"zod": "^4.2.1",
|
||||
"@page-agent/llms": "0.0.18",
|
||||
"@page-agent/page-controller": "0.0.18",
|
||||
"@page-agent/ui": "0.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
* Copyright (C) 2025 Alibaba Group Holding Limited
|
||||
* All rights reserved.
|
||||
*/
|
||||
import {
|
||||
type AgentBrain,
|
||||
LLM,
|
||||
type MacroToolInput,
|
||||
type MacroToolResult,
|
||||
type Tool,
|
||||
} from '@page-agent/llms'
|
||||
import { PageController } from '@page-agent/page-controller'
|
||||
import { Panel, SimulatorMask } from '@page-agent/ui'
|
||||
import chalk from 'chalk'
|
||||
@@ -9,7 +16,6 @@ import zod from 'zod'
|
||||
|
||||
import type { PageAgentConfig } from './config'
|
||||
import { MAX_STEPS } from './config/constants'
|
||||
import { LLM, type Tool } from './llms'
|
||||
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
||||
import { tools } from './tools'
|
||||
import { trimLines, uid, waitUntil } from './utils'
|
||||
@@ -17,31 +23,7 @@ import { assert } from './utils/assert'
|
||||
|
||||
export type { PageAgentConfig }
|
||||
export { tool, type PageAgentTool } from './tools'
|
||||
|
||||
export interface AgentBrain {
|
||||
// thinking?: string
|
||||
evaluation_previous_goal: string
|
||||
memory: string
|
||||
next_goal: string
|
||||
}
|
||||
|
||||
/**
|
||||
* MacroTool input structure
|
||||
*/
|
||||
export interface MacroToolInput {
|
||||
evaluation_previous_goal?: string
|
||||
memory?: string
|
||||
next_goal?: string
|
||||
action: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* MacroTool output structure
|
||||
*/
|
||||
export interface MacroToolResult {
|
||||
input: MacroToolInput
|
||||
output: string
|
||||
}
|
||||
export type { AgentBrain, MacroToolInput, MacroToolResult }
|
||||
|
||||
export interface AgentHistory {
|
||||
brain: AgentBrain
|
||||
@@ -281,9 +263,7 @@ export class PageAgent extends EventTarget {
|
||||
const tools = this.tools
|
||||
|
||||
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
||||
return zod.object({
|
||||
[toolName]: tool.inputSchema,
|
||||
})
|
||||
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
|
||||
})
|
||||
|
||||
const actionSchema = zod.union(
|
||||
|
||||
@@ -1,22 +1,2 @@
|
||||
// Dev environment: use .env config if available, otherwise fallback to testing api
|
||||
export const DEFAULT_MODEL_NAME: string =
|
||||
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
|
||||
? import.meta.env.LLM_MODEL_NAME
|
||||
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||
|
||||
export const DEFAULT_API_KEY: string =
|
||||
import.meta.env.DEV && import.meta.env.LLM_API_KEY
|
||||
? import.meta.env.LLM_API_KEY
|
||||
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||
|
||||
export const DEFAULT_BASE_URL: string =
|
||||
import.meta.env.DEV && import.meta.env.LLM_BASE_URL
|
||||
? import.meta.env.LLM_BASE_URL
|
||||
: 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
|
||||
|
||||
// internal
|
||||
|
||||
export const LLM_MAX_RETRIES = 2
|
||||
// Agent-specific constants (LLM constants moved to @page-agent/llms)
|
||||
export const MAX_STEPS = 20
|
||||
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
|
||||
export const DEFAULT_MAX_TOKENS = 4096
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
import type { LLMConfig } from '@page-agent/llms'
|
||||
import type { PageControllerConfig } from '@page-agent/page-controller'
|
||||
import type { SupportedLanguage } from '@page-agent/ui'
|
||||
|
||||
import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent'
|
||||
import type { PageAgentTool } from '../tools'
|
||||
import {
|
||||
DEFAULT_API_KEY,
|
||||
DEFAULT_BASE_URL,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_MODEL_NAME,
|
||||
DEFAULT_TEMPERATURE,
|
||||
LLM_MAX_RETRIES,
|
||||
} from './constants'
|
||||
|
||||
export interface LLMConfig {
|
||||
baseURL?: string
|
||||
apiKey?: string
|
||||
model?: string
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
maxRetries?: number
|
||||
}
|
||||
export type { LLMConfig }
|
||||
|
||||
export interface AgentConfig {
|
||||
// theme?: 'light' | 'dark'
|
||||
@@ -96,14 +82,3 @@ export interface AgentConfig {
|
||||
}
|
||||
|
||||
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
|
||||
|
||||
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
||||
return {
|
||||
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
|
||||
apiKey: config.apiKey ?? DEFAULT_API_KEY,
|
||||
model: config.model ?? DEFAULT_MODEL_NAME,
|
||||
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
||||
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
|
||||
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,21 +20,25 @@ const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
|
||||
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||
|
||||
const currentScript = document.currentScript as HTMLScriptElement | null
|
||||
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') as 'zh-CN' | 'en-US') || 'zh-CN'
|
||||
const config: PageAgentConfig = { model, baseURL, apiKey, language }
|
||||
window.pageAgent = new PageAgent(config)
|
||||
} else {
|
||||
console.log('🚀 page-agent.js no current script detected, using default demo config')
|
||||
window.pageAgent = new PageAgent()
|
||||
}
|
||||
// in case document.x is not ready yet
|
||||
// @todo give a switch to disable auto-init
|
||||
setTimeout(() => {
|
||||
const currentScript = document.currentScript as HTMLScriptElement | null
|
||||
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') as 'zh-CN' | 'en-US') || 'zh-CN'
|
||||
const config: PageAgentConfig = { model, baseURL, apiKey, language }
|
||||
window.pageAgent = new PageAgent(config)
|
||||
} else {
|
||||
console.log('🚀 page-agent.js no current script detected, using default demo config')
|
||||
window.pageAgent = new PageAgent()
|
||||
}
|
||||
|
||||
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
||||
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
||||
|
||||
window.pageAgent.panel.show() // Show panel
|
||||
window.pageAgent.panel.show() // Show panel
|
||||
})
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"paths": {
|
||||
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
|
||||
//
|
||||
"@page-agent/llms": ["../llms/src/index.ts"],
|
||||
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
|
||||
"@page-agent/ui": ["../ui/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"references": [{ "path": "../page-controller" }]
|
||||
"references": [
|
||||
//
|
||||
{ "path": "../llms" },
|
||||
{ "path": "../page-controller" },
|
||||
{ "path": "../ui" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// @ts-check
|
||||
import chalk from 'chalk'
|
||||
import 'dotenv/config'
|
||||
import process from 'node:process'
|
||||
import { dirname, resolve } from 'path'
|
||||
import dts from 'unplugin-dts/vite'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -10,15 +7,11 @@ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// ============================================================================
|
||||
// Library Config (ES Module for NPM Package)
|
||||
// ============================================================================
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const libConfig = {
|
||||
// ES Module for NPM Package
|
||||
export default defineConfig({
|
||||
clearScreen: false,
|
||||
plugins: [
|
||||
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
|
||||
// dts({ tsconfigPath: './tsconfig.json', bundleTypes: true, compilerOptions: { paths: {} } }),
|
||||
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||
],
|
||||
publicDir: false,
|
||||
@@ -32,9 +25,14 @@ const libConfig = {
|
||||
fileName: 'page-agent',
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: resolve(__dirname, 'dist', 'lib'),
|
||||
outDir: resolve(__dirname, 'dist', 'esm'),
|
||||
rollupOptions: {
|
||||
external: ['ai', 'ai-motion', 'chalk', 'zod', '@page-agent/*'],
|
||||
external: [
|
||||
'chalk',
|
||||
'zod',
|
||||
// all the internal packages
|
||||
/^@page-agent\//,
|
||||
],
|
||||
},
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
@@ -43,53 +41,4 @@ const libConfig = {
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UMD Config (Browser Bundle for CDN)
|
||||
// - alias all local packages so that they can be build in
|
||||
// - no external
|
||||
// - no d.ts. dts does not work with monorepo aliasing
|
||||
// ============================================================================
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const umdConfig = {
|
||||
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
||||
publicDir: false,
|
||||
esbuild: {
|
||||
keepNames: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
|
||||
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/umd.ts'),
|
||||
name: 'PageAgent',
|
||||
fileName: 'page-agent',
|
||||
formats: ['umd'],
|
||||
},
|
||||
outDir: resolve(__dirname, 'dist', 'umd'),
|
||||
cssCodeSplit: true,
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
const MODE = process.env.MODE
|
||||
|
||||
console.log(chalk.cyan(`📦 Build mode: ${chalk.bold(MODE || 'lib')}`))
|
||||
|
||||
let config
|
||||
if (MODE === 'umd') {
|
||||
config = umdConfig
|
||||
} else {
|
||||
config = libConfig
|
||||
}
|
||||
|
||||
export default defineConfig(config)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// @ts-check
|
||||
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))
|
||||
|
||||
// UMD Bundle for CDN
|
||||
// - alias all local packages so that they can be build in
|
||||
// - no external
|
||||
// - no d.ts. dts does not work with monorepo aliasing
|
||||
export default defineConfig({
|
||||
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
||||
publicDir: false,
|
||||
esbuild: {
|
||||
keepNames: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
|
||||
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
|
||||
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/umd.ts'),
|
||||
name: 'PageAgent',
|
||||
fileName: 'page-agent',
|
||||
formats: ['umd'],
|
||||
},
|
||||
outDir: resolve(__dirname, 'dist', 'umd'),
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// force use .js as extension
|
||||
entryFileNames: 'page-agent.js',
|
||||
},
|
||||
},
|
||||
cssCodeSplit: true,
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
},
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@page-agent/page-controller",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.18",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-controller.js",
|
||||
"module": "./dist/lib/page-controller.js",
|
||||
@@ -32,7 +32,6 @@
|
||||
"homepage": "https://alibaba.github.io/page-agent/",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
||||
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@page-agent/ui",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.18",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-agent-ui.js",
|
||||
"module": "./dist/lib/page-agent-ui.js",
|
||||
@@ -32,7 +32,6 @@
|
||||
"homepage": "https://alibaba.github.io/page-agent/",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
||||
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||
},
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
* @returns {boolean} - True if a common dark mode class is found.
|
||||
*/
|
||||
function hasDarkModeClass() {
|
||||
const DFEAULT_DARK_MODE_CLASSES = ['dark', 'dark-mode', 'theme-dark', 'night', 'night-mode']
|
||||
const DEFAULT_DARK_MODE_CLASSES = ['dark', 'dark-mode', 'theme-dark', 'night', 'night-mode']
|
||||
|
||||
const htmlElement = document.documentElement
|
||||
const bodyElement = document.body
|
||||
const bodyElement = document.body || document.documentElement // can be null in some cases
|
||||
|
||||
// Check class names on <html> and <body>
|
||||
for (const className of DFEAULT_DARK_MODE_CLASSES) {
|
||||
if (htmlElement.classList.contains(className) || bodyElement.classList.contains(className)) {
|
||||
for (const className of DEFAULT_DARK_MODE_CLASSES) {
|
||||
if (htmlElement.classList.contains(className) || bodyElement?.classList.contains(className)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ function isColorDark(colorString: string, threshold = 128) {
|
||||
function isBackgroundDark() {
|
||||
// We check both <html> and <body> because some pages set the color on <html>
|
||||
const htmlStyle = window.getComputedStyle(document.documentElement)
|
||||
const bodyStyle = window.getComputedStyle(document.body)
|
||||
const bodyStyle = window.getComputedStyle(document.body || document.documentElement)
|
||||
|
||||
// Get background colors
|
||||
const htmlBgColor = htmlStyle.backgroundColor
|
||||
@@ -93,18 +93,23 @@ function isBackgroundDark() {
|
||||
* @returns {boolean} - True if the page is likely dark.
|
||||
*/
|
||||
export function isPageDark() {
|
||||
// Strategy 1: Check for common dark mode classes
|
||||
if (hasDarkModeClass()) {
|
||||
return true
|
||||
try {
|
||||
// Strategy 1: Check for common dark mode classes
|
||||
if (hasDarkModeClass()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Strategy 2: Analyze the computed background color
|
||||
if (isBackgroundDark()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// @TODO add more checks here, e.g., analyzing text color,
|
||||
// or checking the background of major layout elements like <main> or #app.
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.warn('Error determining if page is dark:', error)
|
||||
return false
|
||||
}
|
||||
|
||||
// Strategy 2: Analyze the computed background color
|
||||
if (isBackgroundDark()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// @TODO add more checks here, e.g., analyzing text color,
|
||||
// or checking the background of major layout elements like <main> or #app.
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Website Package - Instructions for Coding Assistants
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **React** with TypeScript
|
||||
- **Vite** for dev server and build
|
||||
- **Tailwind CSS** for styling
|
||||
- **shadcn/ui** (new-york style) for UI components
|
||||
- **Magic UI** for animations and effects
|
||||
- **wouter** with hash routing for static hosting
|
||||
- **lucide-react** for icons
|
||||
|
||||
## Component Guidelines
|
||||
|
||||
### Use shadcn/ui Components First
|
||||
|
||||
**ALWAYS prefer shadcn/ui components over custom implementations.**
|
||||
|
||||
Before creating any UI component, check if shadcn already provides it:
|
||||
|
||||
```bash
|
||||
# IMPORTANT: Run from packages/website/, NOT from repo root
|
||||
cd packages/website
|
||||
|
||||
# Add a new shadcn component
|
||||
npx shadcn@latest add <component-name>
|
||||
|
||||
# Add a Magic UI component
|
||||
npx shadcn@latest add "@magicui/<component-name>"
|
||||
```
|
||||
|
||||
Available shadcn components: https://ui.shadcn.com/docs/components
|
||||
Available Magic UI components: https://magicui.design/docs/components
|
||||
|
||||
### Current UI Components
|
||||
|
||||
Located in `src/components/ui/`:
|
||||
|
||||
**From shadcn/ui:**
|
||||
- `alert`, `badge`, `button`, `separator`, `sonner`, `switch`, `tooltip`
|
||||
|
||||
**From Magic UI:**
|
||||
- `animated-gradient-text`, `animated-shiny-text`, `aurora-text`
|
||||
- `hyper-text`, `magic-card`, `neon-gradient-card`, `particles`
|
||||
- `sparkles-text`, `text-animate`, `typing-animation`
|
||||
|
||||
**Custom:**
|
||||
- `highlighter`, `kbd`, `spinner`
|
||||
|
||||
### Styling Rules
|
||||
|
||||
1. **Prefer Tailwind classes** over custom CSS
|
||||
2. Use CSS modules only for complex component-specific styles
|
||||
3. Support dark mode via `dark:` classes
|
||||
4. Use CSS variables from `src/index.css` for theme colors
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── ui/ # shadcn/ui + Magic UI components
|
||||
│ ├── Header.tsx # Site header
|
||||
│ ├── Footer.tsx # Site footer
|
||||
│ └── DocsLayout.tsx # Documentation sidebar
|
||||
├── docs/ # Documentation pages
|
||||
│ └── [section]/[topic]/page.tsx
|
||||
├── test-pages/ # Library integration tests
|
||||
├── i18n/ # Internationalization
|
||||
├── router.tsx # Central routing
|
||||
├── page.tsx # Homepage
|
||||
└── main.tsx # App entry
|
||||
```
|
||||
|
||||
## Adding New Pages
|
||||
|
||||
### Documentation Page
|
||||
|
||||
1. Create `src/docs/<section>/<slug>/page.tsx`
|
||||
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
||||
3. Add navigation item to `DocsLayout.tsx`
|
||||
|
||||
### Test Page
|
||||
|
||||
1. Create `src/test-pages/<name>.tsx`
|
||||
2. Add route to `src/test-pages/router.tsx`
|
||||
|
||||
## Routing
|
||||
|
||||
Uses hash-based routing for static hosting:
|
||||
|
||||
```tsx
|
||||
import { Router } from 'wouter'
|
||||
import { useHashLocation } from 'wouter/use-hash-location'
|
||||
|
||||
<Router hook={useHashLocation}>
|
||||
{/* routes */}
|
||||
</Router>
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `components.json` | shadcn/ui configuration |
|
||||
| `vite.config.js` | Vite build settings |
|
||||
| `tsconfig.json` | TypeScript config |
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm start # Dev server (from root)
|
||||
npm run build:website # Build website (from root)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {
|
||||
"@magicui": "https://magicui.design/r/{name}.json"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,42 @@
|
||||
{
|
||||
"name": "@page-agent/website",
|
||||
"private": true,
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.18",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:website": "vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"i18next": "^25.7.1",
|
||||
"i18next": "^25.7.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.1.4",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-i18next": "^16.5.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"wouter": "^3.7.1"
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"wouter": "^3.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.23.26",
|
||||
"next-themes": "^0.4.6",
|
||||
"rough-notation": "^0.5.1",
|
||||
"simple-icons": "^16.2.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { siGithub } from 'simple-icons'
|
||||
|
||||
export default function Footer() {
|
||||
const { t } = useTranslation('common')
|
||||
@@ -19,8 +20,13 @@ export default function Footer() {
|
||||
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200"
|
||||
aria-label={t('footer.github_label')}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-5 h-5 fill-current"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d={siGithub.path} />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { BookOpen, Menu, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { siGithub } from 'simple-icons'
|
||||
import { Link } from 'wouter'
|
||||
|
||||
import LanguageSwitcher from './LanguageSwitcher'
|
||||
import ThemeSwitcher from './ThemeSwitcher'
|
||||
import { BookIcon, CloseIcon, GithubIcon, MenuIcon } from './icons'
|
||||
import { HyperText } from './ui/hyper-text'
|
||||
|
||||
export default function Header() {
|
||||
const { t } = useTranslation('common')
|
||||
@@ -34,9 +36,14 @@ export default function Header() {
|
||||
<span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white block leading-tight">
|
||||
page-agent
|
||||
</span>
|
||||
<p className="hidden sm:block text-xs text-gray-600 dark:text-gray-300">
|
||||
<HyperText
|
||||
as="p"
|
||||
className="hidden sm:block text-xs text-gray-600 dark:text-gray-300 py-0 font-normal overflow-visible"
|
||||
duration={600}
|
||||
animateOnHover={true}
|
||||
>
|
||||
{t('header.slogan')}
|
||||
</p>
|
||||
</HyperText>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -51,7 +58,7 @@ export default function Header() {
|
||||
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
|
||||
aria-label={t('header.nav_docs')}
|
||||
>
|
||||
<BookIcon className="w-5 h-5" />
|
||||
<BookOpen className="w-5 h-5" />
|
||||
</Link>
|
||||
<a
|
||||
href="https://github.com/alibaba/page-agent"
|
||||
@@ -60,7 +67,14 @@ export default function Header() {
|
||||
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
|
||||
aria-label={t('header.nav_source')}
|
||||
>
|
||||
<GithubIcon className="w-5 h-5" />
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-5 h-5 fill-current"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d={siGithub.path} />
|
||||
</svg>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@@ -74,7 +88,7 @@ export default function Header() {
|
||||
href="/docs/introduction/overview"
|
||||
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||
>
|
||||
<BookIcon />
|
||||
<BookOpen className="w-4 h-4" />
|
||||
{t('header.nav_docs')}
|
||||
</Link>
|
||||
<a
|
||||
@@ -84,7 +98,14 @@ export default function Header() {
|
||||
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||
aria-label={t('header.nav_source')}
|
||||
>
|
||||
<GithubIcon />
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-4 h-4 fill-current"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d={siGithub.path} />
|
||||
</svg>
|
||||
{t('header.nav_source')}
|
||||
</a>
|
||||
<ThemeSwitcher />
|
||||
@@ -100,7 +121,7 @@ export default function Header() {
|
||||
aria-controls="mobile-menu"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
||||
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +137,7 @@ export default function Header() {
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<BookIcon className="w-5 h-5" />
|
||||
<BookOpen className="w-5 h-5" />
|
||||
{t('header.nav_docs')}
|
||||
</Link>
|
||||
<a
|
||||
@@ -126,7 +147,14 @@ export default function Header() {
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||
aria-label={t('header.nav_source')}
|
||||
>
|
||||
<GithubIcon className="w-5 h-5" />
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-5 h-5 fill-current"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d={siGithub.path} />
|
||||
</svg>
|
||||
{t('header.nav_source')}
|
||||
</a>
|
||||
<div className="flex items-center gap-3 px-3 py-2">
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
// SVG图标组件集合,用于Header等地方复用
|
||||
|
||||
interface IconProps {
|
||||
className?: string
|
||||
'aria-hidden'?: boolean
|
||||
}
|
||||
|
||||
export function BookIcon({ className = 'w-4 h-4', ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GithubIcon({ className = 'w-4 h-4', ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MenuIcon({ className = 'w-6 h-6', ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CloseIcon({ className = 'w-6 h-6', ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-card text-card-foreground',
|
||||
destructive:
|
||||
'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ComponentPropsWithoutRef } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface AnimatedGradientTextProps extends ComponentPropsWithoutRef<'div'> {
|
||||
speed?: number
|
||||
colorFrom?: string
|
||||
colorTo?: string
|
||||
}
|
||||
|
||||
export function AnimatedGradientText({
|
||||
children,
|
||||
className,
|
||||
speed = 1,
|
||||
colorFrom = '#ffaa40',
|
||||
colorTo = '#9c40ff',
|
||||
...props
|
||||
}: AnimatedGradientTextProps) {
|
||||
return (
|
||||
<span
|
||||
style={
|
||||
{
|
||||
'--bg-size': `${speed * 300}%`,
|
||||
'--color-from': colorFrom,
|
||||
'--color-to': colorTo,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
`animate-gradient inline bg-gradient-to-r from-[var(--color-from)] via-[var(--color-to)] to-[var(--color-from)] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { CSSProperties, ComponentPropsWithoutRef, FC } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<'span'> {
|
||||
shimmerWidth?: number
|
||||
}
|
||||
|
||||
export const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||
children,
|
||||
className,
|
||||
shimmerWidth = 100,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
style={
|
||||
{
|
||||
'--shiny-width': `${shimmerWidth}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
'mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70',
|
||||
|
||||
// Shine effect
|
||||
'animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
|
||||
|
||||
// Shine gradient
|
||||
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
|
||||
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { memo } from 'react'
|
||||
|
||||
interface AuroraTextProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
colors?: string[]
|
||||
speed?: number
|
||||
}
|
||||
|
||||
export const AuroraText = memo(
|
||||
({
|
||||
children,
|
||||
className = '',
|
||||
colors = ['#FF0080', '#7928CA', '#0070F3', '#38bdf8'],
|
||||
speed = 1,
|
||||
}: AuroraTextProps) => {
|
||||
const gradientStyle = {
|
||||
backgroundImage: `linear-gradient(135deg, ${colors.join(', ')}, ${colors[0]})`,
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
animationDuration: `${10 / speed}s`,
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`relative inline-block ${className}`}>
|
||||
<span className="sr-only">{children}</span>
|
||||
<span
|
||||
className="animate-aurora relative bg-[length:200%_auto] bg-clip-text text-transparent"
|
||||
style={gradientStyle}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
AuroraText.displayName = 'AuroraText'
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'span'
|
||||
|
||||
return <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ArrowRightIcon } from '@radix-ui/react-icons'
|
||||
import { ComponentPropsWithoutRef, ReactNode } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface BentoGridProps extends ComponentPropsWithoutRef<'div'> {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface BentoCardProps extends ComponentPropsWithoutRef<'div'> {
|
||||
name: string
|
||||
className: string
|
||||
background: ReactNode
|
||||
Icon: React.ElementType
|
||||
description: string
|
||||
href: string
|
||||
cta: string
|
||||
}
|
||||
|
||||
const BentoGrid = ({ children, className, ...props }: BentoGridProps) => {
|
||||
return (
|
||||
<div className={cn('grid w-full auto-rows-[22rem] grid-cols-3 gap-4', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BentoCard = ({
|
||||
name,
|
||||
className,
|
||||
background,
|
||||
Icon,
|
||||
description,
|
||||
href,
|
||||
cta,
|
||||
...props
|
||||
}: BentoCardProps) => (
|
||||
<div
|
||||
key={name}
|
||||
className={cn(
|
||||
'group relative col-span-3 flex flex-col justify-between overflow-hidden rounded-xl',
|
||||
// light styles
|
||||
'bg-background [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]',
|
||||
// dark styles
|
||||
'dark:bg-background transform-gpu dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset] dark:[border:1px_solid_rgba(255,255,255,.1)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div>{background}</div>
|
||||
<div className="p-4">
|
||||
<div className="pointer-events-none z-10 flex transform-gpu flex-col gap-1 transition-all duration-300 lg:group-hover:-translate-y-10">
|
||||
<Icon className="h-12 w-12 origin-left transform-gpu text-neutral-700 transition-all duration-300 ease-in-out group-hover:scale-75" />
|
||||
<h3 className="text-xl font-semibold text-neutral-700 dark:text-neutral-300">{name}</h3>
|
||||
<p className="max-w-lg text-neutral-400">{description}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none flex w-full translate-y-0 transform-gpu flex-row items-center transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 lg:hidden'
|
||||
)}
|
||||
>
|
||||
<Button variant="link" asChild size="sm" className="pointer-events-auto p-0">
|
||||
<a href={href}>
|
||||
{cta}
|
||||
<ArrowRightIcon className="ms-2 h-4 w-4 rtl:rotate-180" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute bottom-0 hidden w-full translate-y-10 transform-gpu flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 lg:flex'
|
||||
)}
|
||||
>
|
||||
<Button variant="link" asChild size="sm" className="pointer-events-auto p-0">
|
||||
<a href={href}>
|
||||
{cta}
|
||||
<ArrowRightIcon className="ms-2 h-4 w-4 rtl:rotate-180" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export { BentoCard, BentoGrid }
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
AnimatePresence,
|
||||
MotionProps,
|
||||
UseInViewOptions,
|
||||
Variants,
|
||||
motion,
|
||||
useInView,
|
||||
} from 'motion/react'
|
||||
import { useRef } from 'react'
|
||||
|
||||
type MarginType = UseInViewOptions['margin']
|
||||
|
||||
interface BlurFadeProps extends MotionProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
variant?: {
|
||||
hidden: { y: number }
|
||||
visible: { y: number }
|
||||
}
|
||||
duration?: number
|
||||
delay?: number
|
||||
offset?: number
|
||||
direction?: 'up' | 'down' | 'left' | 'right'
|
||||
inView?: boolean
|
||||
inViewMargin?: MarginType
|
||||
blur?: string
|
||||
}
|
||||
|
||||
export function BlurFade({
|
||||
children,
|
||||
className,
|
||||
variant,
|
||||
duration = 0.4,
|
||||
delay = 0,
|
||||
offset = 6,
|
||||
direction = 'down',
|
||||
inView = false,
|
||||
inViewMargin = '-50px',
|
||||
blur = '6px',
|
||||
...props
|
||||
}: BlurFadeProps) {
|
||||
const ref = useRef(null)
|
||||
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
|
||||
const isInView = !inView || inViewResult
|
||||
const defaultVariants: Variants = {
|
||||
hidden: {
|
||||
[direction === 'left' || direction === 'right' ? 'x' : 'y']:
|
||||
direction === 'right' || direction === 'down' ? -offset : offset,
|
||||
opacity: 0,
|
||||
filter: `blur(${blur})`,
|
||||
},
|
||||
visible: {
|
||||
[direction === 'left' || direction === 'right' ? 'x' : 'y']: 0,
|
||||
opacity: 1,
|
||||
filter: `blur(0px)`,
|
||||
},
|
||||
}
|
||||
const combinedVariants = variant || defaultVariants
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
exit="hidden"
|
||||
variants={combinedVariants}
|
||||
transition={{
|
||||
delay: 0.04 + delay,
|
||||
duration,
|
||||
ease: 'easeOut',
|
||||
}}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useInView } from 'motion/react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type React from 'react'
|
||||
import { annotate } from 'rough-notation'
|
||||
import { type RoughAnnotation } from 'rough-notation/lib/model'
|
||||
|
||||
type AnnotationAction =
|
||||
| 'highlight'
|
||||
| 'underline'
|
||||
| 'box'
|
||||
| 'circle'
|
||||
| 'strike-through'
|
||||
| 'crossed-off'
|
||||
| 'bracket'
|
||||
|
||||
interface HighlighterProps {
|
||||
children: React.ReactNode
|
||||
action?: AnnotationAction
|
||||
color?: string
|
||||
strokeWidth?: number
|
||||
animationDuration?: number
|
||||
iterations?: number
|
||||
padding?: number
|
||||
multiline?: boolean
|
||||
isView?: boolean
|
||||
}
|
||||
|
||||
export function Highlighter({
|
||||
children,
|
||||
action = 'highlight',
|
||||
color = '#ffd1dc',
|
||||
strokeWidth = 1.5,
|
||||
animationDuration = 600,
|
||||
iterations = 2,
|
||||
padding = 2,
|
||||
multiline = true,
|
||||
isView = false,
|
||||
}: HighlighterProps) {
|
||||
const elementRef = useRef<HTMLSpanElement>(null)
|
||||
const annotationRef = useRef<RoughAnnotation | null>(null)
|
||||
|
||||
const isInView = useInView(elementRef, {
|
||||
once: true,
|
||||
margin: '-10%',
|
||||
})
|
||||
|
||||
// If isView is false, always show. If isView is true, wait for inView
|
||||
const shouldShow = !isView || isInView
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldShow) return
|
||||
|
||||
const element = elementRef.current
|
||||
if (!element) return
|
||||
|
||||
const annotationConfig = {
|
||||
type: action,
|
||||
color,
|
||||
strokeWidth,
|
||||
animationDuration,
|
||||
iterations,
|
||||
padding,
|
||||
multiline,
|
||||
}
|
||||
|
||||
const annotation = annotate(element, annotationConfig)
|
||||
|
||||
annotationRef.current = annotation
|
||||
annotationRef.current.show()
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
annotation.hide()
|
||||
annotation.show()
|
||||
})
|
||||
|
||||
resizeObserver.observe(element)
|
||||
resizeObserver.observe(document.body)
|
||||
|
||||
return () => {
|
||||
if (element) {
|
||||
annotate(element, { type: action }).remove()
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
}, [shouldShow, action, color, strokeWidth, animationDuration, iterations, padding, multiline])
|
||||
|
||||
return (
|
||||
<span ref={elementRef} className="relative inline-block bg-transparent">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { AnimatePresence, MotionProps, motion } from 'motion/react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type CharacterSet = string[] | readonly string[]
|
||||
|
||||
interface HyperTextProps extends MotionProps {
|
||||
/** The text content to be animated */
|
||||
children: string
|
||||
/** Optional className for styling */
|
||||
className?: string
|
||||
/** Duration of the animation in milliseconds */
|
||||
duration?: number
|
||||
/** Delay before animation starts in milliseconds */
|
||||
delay?: number
|
||||
/** Component to render as - defaults to div */
|
||||
as?: React.ElementType
|
||||
/** Whether to start animation when element comes into view */
|
||||
startOnView?: boolean
|
||||
/** Whether to trigger animation on hover */
|
||||
animateOnHover?: boolean
|
||||
/** Custom character set for scramble effect. Defaults to uppercase alphabet */
|
||||
characterSet?: CharacterSet
|
||||
}
|
||||
|
||||
const DEFAULT_CHARACTER_SET = Object.freeze(
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
|
||||
) as readonly string[]
|
||||
|
||||
const getRandomInt = (max: number): number => Math.floor(Math.random() * max)
|
||||
|
||||
export function HyperText({
|
||||
children,
|
||||
className,
|
||||
duration = 800,
|
||||
delay = 0,
|
||||
as: Component = 'div',
|
||||
startOnView = false,
|
||||
animateOnHover = true,
|
||||
characterSet = DEFAULT_CHARACTER_SET,
|
||||
...props
|
||||
}: HyperTextProps) {
|
||||
const MotionComponent = motion.create(Component, {
|
||||
forwardMotionProps: true,
|
||||
})
|
||||
|
||||
const [displayText, setDisplayText] = useState<string[]>(() => children.split(''))
|
||||
const [isAnimating, setIsAnimating] = useState(false)
|
||||
const iterationCount = useRef(0)
|
||||
const elementRef = useRef<HTMLElement>(null)
|
||||
|
||||
const handleAnimationTrigger = () => {
|
||||
if (animateOnHover && !isAnimating) {
|
||||
iterationCount.current = 0
|
||||
setIsAnimating(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle animation start based on view or delay
|
||||
useEffect(() => {
|
||||
if (!startOnView) {
|
||||
const startTimeout = setTimeout(() => {
|
||||
setIsAnimating(true)
|
||||
}, delay)
|
||||
return () => clearTimeout(startTimeout)
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
setIsAnimating(true)
|
||||
}, delay)
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '-30% 0px -30% 0px' }
|
||||
)
|
||||
|
||||
if (elementRef.current) {
|
||||
observer.observe(elementRef.current)
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [delay, startOnView])
|
||||
|
||||
// Handle scramble animation
|
||||
useEffect(() => {
|
||||
if (!isAnimating) return
|
||||
|
||||
const maxIterations = children.length
|
||||
const startTime = performance.now()
|
||||
let animationFrameId: number
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
iterationCount.current = progress * maxIterations
|
||||
|
||||
setDisplayText((currentText) =>
|
||||
currentText.map((letter, index) =>
|
||||
letter === ' '
|
||||
? letter
|
||||
: index <= iterationCount.current
|
||||
? children[index]
|
||||
: characterSet[getRandomInt(characterSet.length)]
|
||||
)
|
||||
)
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
} else {
|
||||
setIsAnimating(false)
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
|
||||
return () => cancelAnimationFrame(animationFrameId)
|
||||
}, [children, duration, isAnimating, characterSet])
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
ref={elementRef}
|
||||
className={cn('overflow-hidden py-2 text-4xl font-bold', className)}
|
||||
onMouseEnter={handleAnimationTrigger}
|
||||
{...props}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{displayText.map((letter, index) => (
|
||||
<motion.span key={index} className={cn('font-mono', letter === ' ' ? 'w-3' : '')}>
|
||||
{letter.toUpperCase()}
|
||||
</motion.span>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</MotionComponent>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd"
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
|
||||
"[&_svg:not([class*='size-'])]:size-3",
|
||||
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd-group"
|
||||
className={cn('inline-flex items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Kbd, KbdGroup }
|
||||
@@ -0,0 +1,101 @@
|
||||
import { motion, useMotionTemplate, useMotionValue } from 'motion/react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface MagicCardProps {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
gradientSize?: number
|
||||
gradientColor?: string
|
||||
gradientOpacity?: number
|
||||
gradientFrom?: string
|
||||
gradientTo?: string
|
||||
}
|
||||
|
||||
export function MagicCard({
|
||||
children,
|
||||
className,
|
||||
gradientSize = 200,
|
||||
gradientColor = '#262626',
|
||||
gradientOpacity = 0.8,
|
||||
gradientFrom = '#9E7AFF',
|
||||
gradientTo = '#FE8BBB',
|
||||
}: MagicCardProps) {
|
||||
const mouseX = useMotionValue(-gradientSize)
|
||||
const mouseY = useMotionValue(-gradientSize)
|
||||
const reset = useCallback(() => {
|
||||
mouseX.set(-gradientSize)
|
||||
mouseY.set(-gradientSize)
|
||||
}, [gradientSize, mouseX, mouseY])
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
mouseX.set(e.clientX - rect.left)
|
||||
mouseY.set(e.clientY - rect.top)
|
||||
},
|
||||
[mouseX, mouseY]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
reset()
|
||||
}, [reset])
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalPointerOut = (e: PointerEvent) => {
|
||||
if (!e.relatedTarget) {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('pointerout', handleGlobalPointerOut)
|
||||
window.addEventListener('blur', reset)
|
||||
document.addEventListener('visibilitychange', handleVisibility)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('pointerout', handleGlobalPointerOut)
|
||||
window.removeEventListener('blur', reset)
|
||||
document.removeEventListener('visibilitychange', handleVisibility)
|
||||
}
|
||||
}, [reset])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group relative rounded-[inherit]', className)}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerLeave={reset}
|
||||
onPointerEnter={reset}
|
||||
>
|
||||
<motion.div
|
||||
className="bg-border pointer-events-none absolute inset-0 rounded-[inherit] duration-300 group-hover:opacity-100"
|
||||
style={{
|
||||
background: useMotionTemplate`
|
||||
radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px,
|
||||
${gradientFrom},
|
||||
${gradientTo},
|
||||
var(--border) 100%
|
||||
)
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div className="bg-background absolute inset-px rounded-[inherit]" />
|
||||
<motion.div
|
||||
className="pointer-events-none absolute inset-px rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
style={{
|
||||
background: useMotionTemplate`
|
||||
radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, ${gradientColor}, transparent 100%)
|
||||
`,
|
||||
opacity: gradientOpacity,
|
||||
}}
|
||||
/>
|
||||
<div className="relative">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { CSSProperties, ReactElement, ReactNode, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface NeonColorsProps {
|
||||
firstColor: string
|
||||
secondColor: string
|
||||
}
|
||||
|
||||
interface NeonGradientCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* @default <div />
|
||||
* @type ReactElement
|
||||
* @description
|
||||
* The component to be rendered as the card
|
||||
* */
|
||||
as?: ReactElement
|
||||
/**
|
||||
* @default ""
|
||||
* @type string
|
||||
* @description
|
||||
* The className of the card
|
||||
*/
|
||||
className?: string
|
||||
|
||||
/**
|
||||
* @default ""
|
||||
* @type ReactNode
|
||||
* @description
|
||||
* The children of the card
|
||||
* */
|
||||
children?: ReactNode
|
||||
|
||||
/**
|
||||
* @default 5
|
||||
* @type number
|
||||
* @description
|
||||
* The size of the border in pixels
|
||||
* */
|
||||
borderSize?: number
|
||||
|
||||
/**
|
||||
* @default 20
|
||||
* @type number
|
||||
* @description
|
||||
* The size of the radius in pixels
|
||||
* */
|
||||
borderRadius?: number
|
||||
|
||||
/**
|
||||
* @default "{ firstColor: '#ff00aa', secondColor: '#00FFF1' }"
|
||||
* @type string
|
||||
* @description
|
||||
* The colors of the neon gradient
|
||||
* */
|
||||
neonColors?: NeonColorsProps
|
||||
}
|
||||
|
||||
export const NeonGradientCard: React.FC<NeonGradientCardProps> = ({
|
||||
className,
|
||||
children,
|
||||
borderSize = 2,
|
||||
borderRadius = 20,
|
||||
neonColors = {
|
||||
firstColor: '#ff00aa',
|
||||
secondColor: '#00FFF1',
|
||||
},
|
||||
...props
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
const { offsetWidth, offsetHeight } = containerRef.current
|
||||
setDimensions({ width: offsetWidth, height: offsetHeight })
|
||||
}
|
||||
}
|
||||
|
||||
updateDimensions()
|
||||
window.addEventListener('resize', updateDimensions)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateDimensions)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const { offsetWidth, offsetHeight } = containerRef.current
|
||||
setDimensions({ width: offsetWidth, height: offsetHeight })
|
||||
}
|
||||
}, [children])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={
|
||||
{
|
||||
'--border-size': `${borderSize}px`,
|
||||
'--border-radius': `${borderRadius}px`,
|
||||
'--neon-first-color': neonColors.firstColor,
|
||||
'--neon-second-color': neonColors.secondColor,
|
||||
'--card-width': `${dimensions.width}px`,
|
||||
'--card-height': `${dimensions.height}px`,
|
||||
'--card-content-radius': `${borderRadius - borderSize}px`,
|
||||
'--pseudo-element-background-image': `linear-gradient(0deg, ${neonColors.firstColor}, ${neonColors.secondColor})`,
|
||||
'--pseudo-element-width': `${dimensions.width + borderSize * 2}px`,
|
||||
'--pseudo-element-height': `${dimensions.height + borderSize * 2}px`,
|
||||
'--after-blur': `${dimensions.width / 6}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
className={cn('relative z-10 size-full rounded-[var(--border-radius)]', className)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative size-full min-h-[inherit] rounded-[var(--card-content-radius)] bg-gray-100',
|
||||
'before:absolute before:-top-[var(--border-size)] before:-left-[var(--border-size)] before:-z-10 before:block',
|
||||
"before:h-[var(--pseudo-element-height)] before:w-[var(--pseudo-element-width)] before:rounded-[var(--border-radius)] before:content-['']",
|
||||
'before:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] before:bg-[length:100%_200%]',
|
||||
'before:animate-background-position-spin',
|
||||
'after:absolute after:-top-[var(--border-size)] after:-left-[var(--border-size)] after:-z-10 after:block',
|
||||
"after:h-[var(--pseudo-element-height)] after:w-[var(--pseudo-element-width)] after:rounded-[var(--border-radius)] after:blur-[var(--after-blur)] after:content-['']",
|
||||
'after:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] after:bg-[length:100%_200%] after:opacity-80',
|
||||
'after:animate-background-position-spin',
|
||||
'dark:bg-neutral-900',
|
||||
'break-words'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
import React, { ComponentPropsWithoutRef, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface MousePosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
function MousePosition(): MousePosition {
|
||||
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
setMousePosition({ x: event.clientX, y: event.clientY })
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return mousePosition
|
||||
}
|
||||
|
||||
interface ParticlesProps extends ComponentPropsWithoutRef<'div'> {
|
||||
className?: string
|
||||
quantity?: number
|
||||
staticity?: number
|
||||
ease?: number
|
||||
size?: number
|
||||
refresh?: boolean
|
||||
color?: string
|
||||
vx?: number
|
||||
vy?: number
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): number[] {
|
||||
hex = hex.replace('#', '')
|
||||
|
||||
if (hex.length === 3) {
|
||||
hex = hex
|
||||
.split('')
|
||||
.map((char) => char + char)
|
||||
.join('')
|
||||
}
|
||||
|
||||
const hexInt = parseInt(hex, 16)
|
||||
const red = (hexInt >> 16) & 255
|
||||
const green = (hexInt >> 8) & 255
|
||||
const blue = hexInt & 255
|
||||
return [red, green, blue]
|
||||
}
|
||||
|
||||
type Circle = {
|
||||
x: number
|
||||
y: number
|
||||
translateX: number
|
||||
translateY: number
|
||||
size: number
|
||||
alpha: number
|
||||
targetAlpha: number
|
||||
dx: number
|
||||
dy: number
|
||||
magnetism: number
|
||||
}
|
||||
|
||||
export const Particles: React.FC<ParticlesProps> = ({
|
||||
className = '',
|
||||
quantity = 100,
|
||||
staticity = 50,
|
||||
ease = 50,
|
||||
size = 0.4,
|
||||
refresh = false,
|
||||
color = '#ffffff',
|
||||
vx = 0,
|
||||
vy = 0,
|
||||
...props
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const canvasContainerRef = useRef<HTMLDivElement>(null)
|
||||
const context = useRef<CanvasRenderingContext2D | null>(null)
|
||||
const circles = useRef<Circle[]>([])
|
||||
const mousePosition = MousePosition()
|
||||
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 })
|
||||
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1
|
||||
const rafID = useRef<number | null>(null)
|
||||
const resizeTimeout = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
context.current = canvasRef.current.getContext('2d')
|
||||
}
|
||||
initCanvas()
|
||||
animate()
|
||||
|
||||
const handleResize = () => {
|
||||
if (resizeTimeout.current) {
|
||||
clearTimeout(resizeTimeout.current)
|
||||
}
|
||||
resizeTimeout.current = setTimeout(() => {
|
||||
initCanvas()
|
||||
}, 200)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
if (rafID.current != null) {
|
||||
window.cancelAnimationFrame(rafID.current)
|
||||
}
|
||||
if (resizeTimeout.current) {
|
||||
clearTimeout(resizeTimeout.current)
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [color])
|
||||
|
||||
useEffect(() => {
|
||||
onMouseMove()
|
||||
}, [mousePosition.x, mousePosition.y])
|
||||
|
||||
useEffect(() => {
|
||||
initCanvas()
|
||||
}, [refresh])
|
||||
|
||||
const initCanvas = () => {
|
||||
resizeCanvas()
|
||||
drawParticles()
|
||||
}
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (canvasRef.current) {
|
||||
const rect = canvasRef.current.getBoundingClientRect()
|
||||
const { w, h } = canvasSize.current
|
||||
const x = mousePosition.x - rect.left - w / 2
|
||||
const y = mousePosition.y - rect.top - h / 2
|
||||
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
|
||||
if (inside) {
|
||||
mouse.current.x = x
|
||||
mouse.current.y = y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resizeCanvas = () => {
|
||||
if (canvasContainerRef.current && canvasRef.current && context.current) {
|
||||
canvasSize.current.w = canvasContainerRef.current.offsetWidth
|
||||
canvasSize.current.h = canvasContainerRef.current.offsetHeight
|
||||
|
||||
canvasRef.current.width = canvasSize.current.w * dpr
|
||||
canvasRef.current.height = canvasSize.current.h * dpr
|
||||
canvasRef.current.style.width = `${canvasSize.current.w}px`
|
||||
canvasRef.current.style.height = `${canvasSize.current.h}px`
|
||||
context.current.scale(dpr, dpr)
|
||||
|
||||
// Clear existing particles and create new ones with exact quantity
|
||||
circles.current = []
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const circle = circleParams()
|
||||
drawCircle(circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const circleParams = (): Circle => {
|
||||
const x = Math.floor(Math.random() * canvasSize.current.w)
|
||||
const y = Math.floor(Math.random() * canvasSize.current.h)
|
||||
const translateX = 0
|
||||
const translateY = 0
|
||||
const pSize = Math.floor(Math.random() * 2) + size
|
||||
const alpha = 0
|
||||
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
|
||||
const dx = (Math.random() - 0.5) * 0.1
|
||||
const dy = (Math.random() - 0.5) * 0.1
|
||||
const magnetism = 0.1 + Math.random() * 4
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
translateX,
|
||||
translateY,
|
||||
size: pSize,
|
||||
alpha,
|
||||
targetAlpha,
|
||||
dx,
|
||||
dy,
|
||||
magnetism,
|
||||
}
|
||||
}
|
||||
|
||||
const rgb = hexToRgb(color)
|
||||
|
||||
const drawCircle = (circle: Circle, update = false) => {
|
||||
if (context.current) {
|
||||
const { x, y, translateX, translateY, size, alpha } = circle
|
||||
context.current.translate(translateX, translateY)
|
||||
context.current.beginPath()
|
||||
context.current.arc(x, y, size, 0, 2 * Math.PI)
|
||||
context.current.fillStyle = `rgba(${rgb.join(', ')}, ${alpha})`
|
||||
context.current.fill()
|
||||
context.current.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
|
||||
if (!update) {
|
||||
circles.current.push(circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clearContext = () => {
|
||||
if (context.current) {
|
||||
context.current.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h)
|
||||
}
|
||||
}
|
||||
|
||||
const drawParticles = () => {
|
||||
clearContext()
|
||||
const particleCount = quantity
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const circle = circleParams()
|
||||
drawCircle(circle)
|
||||
}
|
||||
}
|
||||
|
||||
const remapValue = (
|
||||
value: number,
|
||||
start1: number,
|
||||
end1: number,
|
||||
start2: number,
|
||||
end2: number
|
||||
): number => {
|
||||
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
|
||||
return remapped > 0 ? remapped : 0
|
||||
}
|
||||
|
||||
const animate = () => {
|
||||
clearContext()
|
||||
circles.current.forEach((circle: Circle, i: number) => {
|
||||
// Handle the alpha value
|
||||
const edge = [
|
||||
circle.x + circle.translateX - circle.size, // distance from left edge
|
||||
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
||||
circle.y + circle.translateY - circle.size, // distance from top edge
|
||||
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
||||
]
|
||||
const closestEdge = edge.reduce((a, b) => Math.min(a, b))
|
||||
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
|
||||
if (remapClosestEdge > 1) {
|
||||
circle.alpha += 0.02
|
||||
if (circle.alpha > circle.targetAlpha) {
|
||||
circle.alpha = circle.targetAlpha
|
||||
}
|
||||
} else {
|
||||
circle.alpha = circle.targetAlpha * remapClosestEdge
|
||||
}
|
||||
circle.x += circle.dx + vx
|
||||
circle.y += circle.dy + vy
|
||||
circle.translateX +=
|
||||
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / ease
|
||||
circle.translateY +=
|
||||
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / ease
|
||||
|
||||
drawCircle(circle, true)
|
||||
|
||||
// circle gets out of the canvas
|
||||
if (
|
||||
circle.x < -circle.size ||
|
||||
circle.x > canvasSize.current.w + circle.size ||
|
||||
circle.y < -circle.size ||
|
||||
circle.y > canvasSize.current.h + circle.size
|
||||
) {
|
||||
// remove the circle from the array
|
||||
circles.current.splice(i, 1)
|
||||
// create a new circle
|
||||
const newCircle = circleParams()
|
||||
drawCircle(newCircle)
|
||||
}
|
||||
})
|
||||
rafID.current = window.requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('pointer-events-none', className)}
|
||||
ref={canvasContainerRef}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<canvas ref={canvasRef} className="size-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = 'horizontal',
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
} from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { Toaster as Sonner, type ToasterProps } from 'sonner'
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = 'system' } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps['theme']}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
'--border-radius': 'var(--radius)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -0,0 +1,150 @@
|
||||
import { motion } from 'motion/react'
|
||||
import { CSSProperties, ReactElement, useEffect, useState } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Sparkle {
|
||||
id: string
|
||||
x: string
|
||||
y: string
|
||||
color: string
|
||||
delay: number
|
||||
scale: number
|
||||
lifespan: number
|
||||
}
|
||||
|
||||
const Sparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
|
||||
return (
|
||||
<motion.svg
|
||||
key={id}
|
||||
className="pointer-events-none absolute z-20"
|
||||
initial={{ opacity: 0, left: x, top: y }}
|
||||
animate={{
|
||||
opacity: [0, 1, 0],
|
||||
scale: [0, scale, 0],
|
||||
rotate: [75, 120, 150],
|
||||
}}
|
||||
transition={{ duration: 0.8, repeat: Infinity, delay }}
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
>
|
||||
<path
|
||||
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
|
||||
fill={color}
|
||||
/>
|
||||
</motion.svg>
|
||||
)
|
||||
}
|
||||
|
||||
interface SparklesTextProps {
|
||||
/**
|
||||
* @default <div />
|
||||
* @type ReactElement
|
||||
* @description
|
||||
* The component to be rendered as the text
|
||||
* */
|
||||
as?: ReactElement
|
||||
|
||||
/**
|
||||
* @default ""
|
||||
* @type string
|
||||
* @description
|
||||
* The className of the text
|
||||
*/
|
||||
className?: string
|
||||
|
||||
/**
|
||||
* @required
|
||||
* @type ReactNode
|
||||
* @description
|
||||
* The content to be displayed
|
||||
* */
|
||||
children: React.ReactNode
|
||||
|
||||
/**
|
||||
* @default 10
|
||||
* @type number
|
||||
* @description
|
||||
* The count of sparkles
|
||||
* */
|
||||
sparklesCount?: number
|
||||
|
||||
/**
|
||||
* @default "{first: '#9E7AFF', second: '#FE8BBB'}"
|
||||
* @type string
|
||||
* @description
|
||||
* The colors of the sparkles
|
||||
* */
|
||||
colors?: {
|
||||
first: string
|
||||
second: string
|
||||
}
|
||||
}
|
||||
|
||||
export const SparklesText: React.FC<SparklesTextProps> = ({
|
||||
children,
|
||||
colors = { first: '#9E7AFF', second: '#FE8BBB' },
|
||||
className,
|
||||
sparklesCount = 10,
|
||||
...props
|
||||
}) => {
|
||||
const [sparkles, setSparkles] = useState<Sparkle[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const generateStar = (): Sparkle => {
|
||||
const starX = `${Math.random() * 100}%`
|
||||
const starY = `${Math.random() * 100}%`
|
||||
const color = Math.random() > 0.5 ? colors.first : colors.second
|
||||
const delay = Math.random() * 2
|
||||
const scale = Math.random() * 1 + 0.3
|
||||
const lifespan = Math.random() * 10 + 5
|
||||
const id = `${starX}-${starY}-${Date.now()}`
|
||||
return { id, x: starX, y: starY, color, delay, scale, lifespan }
|
||||
}
|
||||
|
||||
const initializeStars = () => {
|
||||
const newSparkles = Array.from({ length: sparklesCount }, generateStar)
|
||||
setSparkles(newSparkles)
|
||||
}
|
||||
|
||||
const updateStars = () => {
|
||||
setSparkles((currentSparkles) =>
|
||||
currentSparkles.map((star) => {
|
||||
if (star.lifespan <= 0) {
|
||||
return generateStar()
|
||||
} else {
|
||||
return { ...star, lifespan: star.lifespan - 0.1 }
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
initializeStars()
|
||||
const interval = setInterval(updateStars, 100)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [colors.first, colors.second, sparklesCount])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('text-6xl font-bold', className)}
|
||||
{...props}
|
||||
style={
|
||||
{
|
||||
'--sparkles-first-color': `${colors.first}`,
|
||||
'--sparkles-second-color': `${colors.second}`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<span className="relative inline-block">
|
||||
{sparkles.map((sparkle) => (
|
||||
<Sparkle key={sparkle.id} {...sparkle} />
|
||||
))}
|
||||
<strong className="bg-linear-to-r from-[var(--sparkles-first-color)] to-[var(--sparkles-second-color)] bg-clip-text text-transparent">
|
||||
{children}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Loader2Icon } from 'lucide-react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cn('size-4 animate-spin', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Spinner }
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as SwitchPrimitive from '@radix-ui/react-switch'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0'
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,417 @@
|
||||
import { AnimatePresence, MotionProps, Variants, motion } from 'motion/react'
|
||||
import { ElementType, memo } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type AnimationType = 'text' | 'word' | 'character' | 'line'
|
||||
type AnimationVariant =
|
||||
| 'fadeIn'
|
||||
| 'blurIn'
|
||||
| 'blurInUp'
|
||||
| 'blurInDown'
|
||||
| 'slideUp'
|
||||
| 'slideDown'
|
||||
| 'slideLeft'
|
||||
| 'slideRight'
|
||||
| 'scaleUp'
|
||||
| 'scaleDown'
|
||||
|
||||
interface TextAnimateProps extends MotionProps {
|
||||
/**
|
||||
* The text content to animate
|
||||
*/
|
||||
children: string
|
||||
/**
|
||||
* The class name to be applied to the component
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* The class name to be applied to each segment
|
||||
*/
|
||||
segmentClassName?: string
|
||||
/**
|
||||
* The delay before the animation starts
|
||||
*/
|
||||
delay?: number
|
||||
/**
|
||||
* The duration of the animation
|
||||
*/
|
||||
duration?: number
|
||||
/**
|
||||
* Custom motion variants for the animation
|
||||
*/
|
||||
variants?: Variants
|
||||
/**
|
||||
* The element type to render
|
||||
*/
|
||||
as?: ElementType
|
||||
/**
|
||||
* How to split the text ("text", "word", "character")
|
||||
*/
|
||||
by?: AnimationType
|
||||
/**
|
||||
* Whether to start animation when component enters viewport
|
||||
*/
|
||||
startOnView?: boolean
|
||||
/**
|
||||
* Whether to animate only once
|
||||
*/
|
||||
once?: boolean
|
||||
/**
|
||||
* The animation preset to use
|
||||
*/
|
||||
animation?: AnimationVariant
|
||||
/**
|
||||
* Whether to enable accessibility features (default: true)
|
||||
*/
|
||||
accessible?: boolean
|
||||
}
|
||||
|
||||
const staggerTimings: Record<AnimationType, number> = {
|
||||
text: 0.06,
|
||||
word: 0.05,
|
||||
character: 0.03,
|
||||
line: 0.06,
|
||||
}
|
||||
|
||||
const defaultContainerVariants = {
|
||||
hidden: { opacity: 1 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
delayChildren: 0,
|
||||
staggerChildren: 0.05,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transition: {
|
||||
staggerChildren: 0.05,
|
||||
staggerDirection: -1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const defaultItemVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
},
|
||||
}
|
||||
|
||||
const defaultItemAnimationVariants: Record<
|
||||
AnimationVariant,
|
||||
{ container: Variants; item: Variants }
|
||||
> = {
|
||||
fadeIn: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
blurIn: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, filter: 'blur(10px)' },
|
||||
show: {
|
||||
opacity: 1,
|
||||
filter: 'blur(0px)',
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
filter: 'blur(10px)',
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
blurInUp: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, filter: 'blur(10px)', y: 20 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
filter: 'blur(0px)',
|
||||
y: 0,
|
||||
transition: {
|
||||
y: { duration: 0.3 },
|
||||
opacity: { duration: 0.4 },
|
||||
filter: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
filter: 'blur(10px)',
|
||||
y: 20,
|
||||
transition: {
|
||||
y: { duration: 0.3 },
|
||||
opacity: { duration: 0.4 },
|
||||
filter: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
blurInDown: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, filter: 'blur(10px)', y: -20 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
filter: 'blur(0px)',
|
||||
y: 0,
|
||||
transition: {
|
||||
y: { duration: 0.3 },
|
||||
opacity: { duration: 0.4 },
|
||||
filter: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
slideUp: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
show: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
y: -20,
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
slideDown: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { y: -20, opacity: 0 },
|
||||
show: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
exit: {
|
||||
y: 20,
|
||||
opacity: 0,
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
slideLeft: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { x: 20, opacity: 0 },
|
||||
show: {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
exit: {
|
||||
x: -20,
|
||||
opacity: 0,
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
slideRight: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { x: -20, opacity: 0 },
|
||||
show: {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
exit: {
|
||||
x: 20,
|
||||
opacity: 0,
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
scaleUp: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { scale: 0.5, opacity: 0 },
|
||||
show: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
scale: {
|
||||
type: 'spring',
|
||||
damping: 15,
|
||||
stiffness: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
scale: 0.5,
|
||||
opacity: 0,
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
scaleDown: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { scale: 1.5, opacity: 0 },
|
||||
show: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
scale: {
|
||||
type: 'spring',
|
||||
damping: 15,
|
||||
stiffness: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
scale: 1.5,
|
||||
opacity: 0,
|
||||
transition: { duration: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const TextAnimateBase = ({
|
||||
children,
|
||||
delay = 0,
|
||||
duration = 0.3,
|
||||
variants,
|
||||
className,
|
||||
segmentClassName,
|
||||
as: Component = 'p',
|
||||
startOnView = true,
|
||||
once = false,
|
||||
by = 'word',
|
||||
animation = 'fadeIn',
|
||||
accessible = true,
|
||||
...props
|
||||
}: TextAnimateProps) => {
|
||||
const MotionComponent = motion.create(Component)
|
||||
|
||||
let segments: string[] = []
|
||||
switch (by) {
|
||||
case 'word':
|
||||
segments = children.split(/(\s+)/)
|
||||
break
|
||||
case 'character':
|
||||
segments = children.split('')
|
||||
break
|
||||
case 'line':
|
||||
segments = children.split('\n')
|
||||
break
|
||||
case 'text':
|
||||
default:
|
||||
segments = [children]
|
||||
break
|
||||
}
|
||||
|
||||
const finalVariants = variants
|
||||
? {
|
||||
container: {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
opacity: { duration: 0.01, delay },
|
||||
delayChildren: delay,
|
||||
staggerChildren: duration / segments.length,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transition: {
|
||||
staggerChildren: duration / segments.length,
|
||||
staggerDirection: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
item: variants,
|
||||
}
|
||||
: animation
|
||||
? {
|
||||
container: {
|
||||
...defaultItemAnimationVariants[animation].container,
|
||||
show: {
|
||||
...defaultItemAnimationVariants[animation].container.show,
|
||||
transition: {
|
||||
delayChildren: delay,
|
||||
staggerChildren: duration / segments.length,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
...defaultItemAnimationVariants[animation].container.exit,
|
||||
transition: {
|
||||
staggerChildren: duration / segments.length,
|
||||
staggerDirection: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
item: defaultItemAnimationVariants[animation].item,
|
||||
}
|
||||
: { container: defaultContainerVariants, item: defaultItemVariants }
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="popLayout">
|
||||
<MotionComponent
|
||||
variants={finalVariants.container as Variants}
|
||||
initial="hidden"
|
||||
whileInView={startOnView ? 'show' : undefined}
|
||||
animate={startOnView ? undefined : 'show'}
|
||||
exit="exit"
|
||||
className={cn('whitespace-pre-wrap', className)}
|
||||
viewport={{ once }}
|
||||
aria-label={accessible ? children : undefined}
|
||||
{...props}
|
||||
>
|
||||
{accessible && <span className="sr-only">{children}</span>}
|
||||
{segments.map((segment, i) => (
|
||||
<motion.span
|
||||
key={`${by}-${segment}-${i}`}
|
||||
variants={finalVariants.item}
|
||||
custom={i * staggerTimings[by]}
|
||||
className={cn(
|
||||
by === 'line' ? 'block' : 'inline-block whitespace-pre',
|
||||
by === 'character' && '',
|
||||
segmentClassName
|
||||
)}
|
||||
aria-hidden={accessible ? true : undefined}
|
||||
>
|
||||
{segment}
|
||||
</motion.span>
|
||||
))}
|
||||
</MotionComponent>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
// Export the memoized version
|
||||
export const TextAnimate = memo(TextAnimateBase)
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -0,0 +1,164 @@
|
||||
import { MotionProps, motion, useInView } from 'motion/react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface TypingAnimationProps extends MotionProps {
|
||||
children?: string
|
||||
words?: string[]
|
||||
className?: string
|
||||
duration?: number
|
||||
typeSpeed?: number
|
||||
deleteSpeed?: number
|
||||
delay?: number
|
||||
pauseDelay?: number
|
||||
loop?: boolean
|
||||
as?: React.ElementType
|
||||
startOnView?: boolean
|
||||
showCursor?: boolean
|
||||
blinkCursor?: boolean
|
||||
cursorStyle?: 'line' | 'block' | 'underscore'
|
||||
}
|
||||
|
||||
export function TypingAnimation({
|
||||
children,
|
||||
words,
|
||||
className,
|
||||
duration = 100,
|
||||
typeSpeed,
|
||||
deleteSpeed,
|
||||
delay = 0,
|
||||
pauseDelay = 1000,
|
||||
loop = false,
|
||||
as: Component = 'span',
|
||||
startOnView = true,
|
||||
showCursor = true,
|
||||
blinkCursor = true,
|
||||
cursorStyle = 'line',
|
||||
...props
|
||||
}: TypingAnimationProps) {
|
||||
const MotionComponent = motion.create(Component, {
|
||||
forwardMotionProps: true,
|
||||
})
|
||||
|
||||
const [displayedText, setDisplayedText] = useState<string>('')
|
||||
const [currentWordIndex, setCurrentWordIndex] = useState(0)
|
||||
const [currentCharIndex, setCurrentCharIndex] = useState(0)
|
||||
const [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')
|
||||
const elementRef = useRef<HTMLElement | null>(null)
|
||||
const isInView = useInView(elementRef as React.RefObject<Element>, {
|
||||
amount: 0.3,
|
||||
once: true,
|
||||
})
|
||||
|
||||
const wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])
|
||||
const hasMultipleWords = wordsToAnimate.length > 1
|
||||
|
||||
const typingSpeed = typeSpeed || duration
|
||||
const deletingSpeed = deleteSpeed || typingSpeed / 2
|
||||
|
||||
const shouldStart = startOnView ? isInView : true
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldStart || wordsToAnimate.length === 0) return
|
||||
|
||||
const timeoutDelay =
|
||||
delay > 0 && displayedText === ''
|
||||
? delay
|
||||
: phase === 'typing'
|
||||
? typingSpeed
|
||||
: phase === 'deleting'
|
||||
? deletingSpeed
|
||||
: pauseDelay
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const currentWord = wordsToAnimate[currentWordIndex] || ''
|
||||
const graphemes = Array.from(currentWord)
|
||||
|
||||
switch (phase) {
|
||||
case 'typing':
|
||||
if (currentCharIndex < graphemes.length) {
|
||||
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))
|
||||
setCurrentCharIndex(currentCharIndex + 1)
|
||||
} else {
|
||||
if (hasMultipleWords || loop) {
|
||||
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
|
||||
if (!isLastWord || loop) {
|
||||
setPhase('pause')
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'pause':
|
||||
setPhase('deleting')
|
||||
break
|
||||
|
||||
case 'deleting':
|
||||
if (currentCharIndex > 0) {
|
||||
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))
|
||||
setCurrentCharIndex(currentCharIndex - 1)
|
||||
} else {
|
||||
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
|
||||
setCurrentWordIndex(nextIndex)
|
||||
setPhase('typing')
|
||||
}
|
||||
break
|
||||
}
|
||||
}, timeoutDelay)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [
|
||||
shouldStart,
|
||||
phase,
|
||||
currentCharIndex,
|
||||
currentWordIndex,
|
||||
displayedText,
|
||||
wordsToAnimate,
|
||||
hasMultipleWords,
|
||||
loop,
|
||||
typingSpeed,
|
||||
deletingSpeed,
|
||||
pauseDelay,
|
||||
delay,
|
||||
])
|
||||
|
||||
const currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')
|
||||
const isComplete =
|
||||
!loop &&
|
||||
currentWordIndex === wordsToAnimate.length - 1 &&
|
||||
currentCharIndex >= currentWordGraphemes.length &&
|
||||
phase !== 'deleting'
|
||||
|
||||
const shouldShowCursor =
|
||||
showCursor &&
|
||||
!isComplete &&
|
||||
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
|
||||
|
||||
const getCursorChar = () => {
|
||||
switch (cursorStyle) {
|
||||
case 'block':
|
||||
return '▌'
|
||||
case 'underscore':
|
||||
return '_'
|
||||
case 'line':
|
||||
default:
|
||||
return '|'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
ref={elementRef}
|
||||
className={cn('leading-[5rem] tracking-[-0.02em]', className)}
|
||||
{...props}
|
||||
>
|
||||
{displayedText}
|
||||
{shouldShowCursor && (
|
||||
<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>
|
||||
{getCursorChar()}
|
||||
</span>
|
||||
)}
|
||||
</MotionComponent>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const CDN_URL = 'https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js'
|
||||
export const CDN_CN_URL =
|
||||
'https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js'
|
||||
@@ -1,164 +1,127 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
// Recommended models: lightweight with excellent tool call capabilities
|
||||
const MODELS = {
|
||||
recommended: ['gpt-4.1-mini', 'claude-haiku-4.5', 'gemini-3-flash', 'deepseek-3.2', 'gpt-5.2'],
|
||||
verified: [
|
||||
'qwen-3-max',
|
||||
'gpt-4.1',
|
||||
'gpt-5',
|
||||
'gpt-5-mini',
|
||||
'gpt-5.1',
|
||||
'grok-4',
|
||||
'grok-code-fast',
|
||||
'claude-sonnet-3.5',
|
||||
'claude-sonnet-4.5',
|
||||
'claude-opus-4.5',
|
||||
'gemini-2.5',
|
||||
'gemini-3-pro',
|
||||
],
|
||||
}
|
||||
|
||||
export default function ModelIntegration() {
|
||||
const { t } = useTranslation('docs')
|
||||
const allModels = [...MODELS.recommended, ...MODELS.verified]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-6">{t('model_integration.title')}</h1>
|
||||
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
|
||||
<div className="max-w-4xl">
|
||||
<h1 className="text-4xl font-bold mb-4">{t('model_integration.title')}</h1>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">
|
||||
{t('model_integration.subtitle')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3">{t('model_integration.recommended')}</h2>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
|
||||
{t('model_integration.model_gpt4_title')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||
{t('model_integration.model_gpt4_badge')}
|
||||
</p>
|
||||
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li>{t('model_integration.model_gpt4_1')}</li>
|
||||
<li>{t('model_integration.model_gpt4_2')}</li>
|
||||
<li>{t('model_integration.model_gpt4_3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
|
||||
{t('model_integration.model_deepseek_title')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||
{t('model_integration.model_deepseek_badge')}
|
||||
</p>
|
||||
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li>{t('model_integration.model_deepseek_1')}</li>
|
||||
<li>{t('model_integration.model_deepseek_2')}</li>
|
||||
<li>{t('model_integration.model_deepseek_3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
|
||||
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
|
||||
{t('model_integration.model_qwen_title')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||
{t('model_integration.model_qwen_badge')}
|
||||
</p>
|
||||
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li>{t('model_integration.model_qwen_1')}</li>
|
||||
<li>{t('model_integration.model_qwen_2')}</li>
|
||||
<li>{t('model_integration.model_qwen_3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
|
||||
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
|
||||
{t('model_integration.model_gemini_title')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||
{t('model_integration.model_gemini_badge')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3">{t('model_integration.available')}</h2>
|
||||
|
||||
<div className="p-4 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg mb-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-emerald-900 dark:text-emerald-300">
|
||||
{t('model_integration.available_verified')}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
|
||||
gpt-4.1-mini/4.1/5
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
|
||||
grok-4/grok-code-fast
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
|
||||
qwen3
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
|
||||
deepseek-v3.1/3.2
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
|
||||
claude-sonnet-4/4.5/haiku-4.5
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full bg-emerald-100 dark:bg-emerald-900/40 text-emerald-900 dark:text-emerald-200 px-3 py-1 text-sm">
|
||||
gemini-2.5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3">{t('model_integration.tips')}</h2>
|
||||
|
||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg mb-6">
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-disc pl-5">
|
||||
<li>{t('model_integration.tip_1')}</li>
|
||||
<li>{t('model_integration.tip_2')}</li>
|
||||
<li>{t('model_integration.tip_3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3">{t('model_integration.security')}</h2>
|
||||
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-500 mb-4">
|
||||
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
|
||||
{t('model_integration.security_warning')}
|
||||
{/* Models Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-2xl font-semibold mb-3">{t('model_integration.available')}</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{t('model_integration.recommendation_logic')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allModels.map((model) => {
|
||||
const isRecommended = MODELS.recommended.includes(model)
|
||||
return (
|
||||
<div
|
||||
key={model}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium font-mono transition-colors ${
|
||||
isRecommended
|
||||
? 'bg-emerald-500 text-white shadow-sm'
|
||||
: 'bg-white/80 dark:bg-gray-800/80 text-gray-800 dark:text-gray-200 border border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
{model}
|
||||
{isRecommended && <span className="ml-1">⭐</span>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-5">⭐ baseline models</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{t('model_integration.security_desc')}
|
||||
</p>
|
||||
{/* Tips Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-2xl font-semibold mb-4">{t('model_integration.tips')}</h2>
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
||||
<li>{t('model_integration.tip_2')}</li>
|
||||
<li>{t('model_integration.tip_3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-blue-900 dark:text-blue-300">
|
||||
{t('model_integration.security_backend_proxy')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
{t('model_integration.security_backend_desc')}
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-none pl-0">
|
||||
<li>{t('model_integration.security_method_1')}</li>
|
||||
<li>{t('model_integration.security_method_2')}</li>
|
||||
<li>{t('model_integration.security_method_3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/* Security Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-2xl font-semibold mb-4">{t('model_integration.security')}</h2>
|
||||
<div className="bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-5 rounded-r-lg mb-4">
|
||||
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
|
||||
{t('model_integration.security_warning')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{t('model_integration.security_backend_proxy')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
{t('model_integration.security_backend_desc')}
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<li>{t('model_integration.security_method_1')}</li>
|
||||
<li>{t('model_integration.security_method_2')}</li>
|
||||
<li>{t('model_integration.security_method_3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3">{t('model_integration.configuration')}</h2>
|
||||
|
||||
<CodeEditor
|
||||
code={`
|
||||
// 百炼等其他兼容服务
|
||||
{/* Configuration Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-2xl font-semibold mb-4">{t('model_integration.configuration')}</h2>
|
||||
<CodeEditor
|
||||
code={`// OpenAI-compatible services (e.g., Alibaba Bailian)
|
||||
const pageAgent = new PageAgent({
|
||||
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
apiKey: 'your-api-key',
|
||||
model: 'qwen-plus'
|
||||
});
|
||||
|
||||
// 私有部署模型
|
||||
// Self-hosted models (e.g., Ollama)
|
||||
const pageAgent = new PageAgent({
|
||||
baseURL: 'http://localhost:11434/v1',
|
||||
apiKey: 'N/A', // Ollama 通常使用任意值
|
||||
apiKey: 'N/A', // Ollama typically accepts any value
|
||||
model: 'qwen3:latest'
|
||||
});
|
||||
|
||||
// 测试接口
|
||||
// @note: 限流,限制 prompt 内容,限制来源,随时变更,请替换成你自己的
|
||||
// @note: 使用 DeepSeek-chat(3.2) 官方版本,使用协议和隐私策略见 DeepSeek 网站
|
||||
// Free testing endpoint
|
||||
// Note: Rate-limited, content-filtered, subject to change. Replace with your own.
|
||||
// Note: Uses official DeepSeek-chat (3.2). See DeepSeek website for terms & privacy.
|
||||
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'
|
||||
`}
|
||||
/>
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
import { CDN_CN_URL, CDN_URL } from '@/constants'
|
||||
|
||||
export default function CdnSetup() {
|
||||
return (
|
||||
@@ -15,11 +16,16 @@ export default function CdnSetup() {
|
||||
<CodeEditor
|
||||
className="mb-8"
|
||||
code={`
|
||||
// 仅供测试使用,稳定 CDN 待定
|
||||
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>
|
||||
// CDN: \t${CDN_URL}
|
||||
// Mirror: \t${CDN_CN_URL}
|
||||
<script
|
||||
src="${CDN_URL}"
|
||||
crossorigin="true"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script>
|
||||
window.pageAgent.panel.show()
|
||||
// window.pageAgent
|
||||
</script>`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ interface LLMConfig {
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
maxRetries?: number
|
||||
|
||||
/**
|
||||
* Custom fetch function for LLM API requests.
|
||||
* Use this to customize headers, credentials, proxy, etc.
|
||||
* The response should follow OpenAI API format.
|
||||
*/
|
||||
customFetch?: typeof globalThis.fetch
|
||||
}
|
||||
|
||||
interface AgentConfig {
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function Overview() {
|
||||
{/* 头图 */}
|
||||
<figure className="mb-8 rounded-xl overflow-hidden">
|
||||
<img
|
||||
src="https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg"
|
||||
src="https://img.alicdn.com/imgextra/i3/O1CN014xLeic1gnF2iWdzjJ_!!6000000004186-49-tps-1280-353.webp"
|
||||
alt="page-agent"
|
||||
className="w-full h-64 object-cover"
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
import { CDN_CN_URL, CDN_URL } from '@/constants'
|
||||
|
||||
export default function QuickStart() {
|
||||
const { t } = useTranslation('docs')
|
||||
@@ -23,8 +24,9 @@ export default function QuickStart() {
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">{t('quick_start.step1_cdn')}</p>
|
||||
<CodeEditor
|
||||
code={`// 仅供测试使用
|
||||
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>`}
|
||||
code={`// CDN: \t${CDN_URL}
|
||||
// Mirror: \t${CDN_CN_URL}
|
||||
<script src="${CDN_URL}" crossorigin="true" type="text/javascript"></script>`}
|
||||
language="html"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -115,34 +115,19 @@ export default {
|
||||
model_integration: {
|
||||
title: 'Model Integration',
|
||||
subtitle:
|
||||
'Supports OpenAI-compatible models with tool call support, including public cloud services and private deployments.',
|
||||
'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.',
|
||||
recommendation_logic: 'Recommended: Lightweight models with strong ToolCall capabilities.',
|
||||
recommended: 'Recommended Models',
|
||||
model_gpt4_title: '⚡ gpt-4.1-mini',
|
||||
model_gpt4_badge: 'Evaluation Baseline ✅',
|
||||
model_gpt4_1: '• Cost-effective',
|
||||
model_gpt4_2: '• Fast',
|
||||
model_gpt4_3: '• High success rate',
|
||||
model_deepseek_title: '💰 DeepSeek-3.2',
|
||||
model_deepseek_badge: 'Economical',
|
||||
model_deepseek_1: '• Much cheaper than similar models',
|
||||
model_deepseek_2: '• ToolCall may error but usually auto-recovers',
|
||||
model_deepseek_3: "• This site's free demo uses DeepSeek",
|
||||
model_qwen_title: '🛡️ qwen3',
|
||||
model_qwen_badge: 'Secure & Compliant',
|
||||
model_qwen_1: '• Controllable, decent results, reasonable price',
|
||||
model_qwen_2: '• ToolCall may error but usually auto-recovers',
|
||||
model_qwen_3: '• Best for scenarios with detailed steps',
|
||||
model_gemini_title: '⚡ gemini-2.5-flash',
|
||||
model_gemini_badge: 'Highly efficient, high success rate, reasonable price',
|
||||
available: 'Available Models',
|
||||
available_verified: '✅ Verified Working',
|
||||
available: 'Tested Models',
|
||||
available_verified: 'Verified Working Models',
|
||||
tips: 'Tips',
|
||||
tip_1: 'Reasoning models (like GPT-5) are slower with no advantage',
|
||||
tip_2:
|
||||
"Non-OpenAI models don't guarantee JSON schema compliance—tool call may error but usually recovers. Higher temperature recommended",
|
||||
tip_3: 'Small/nano models perform poorly',
|
||||
'Models with weaker ToolCall capabilities may return incorrect formats. Common errors usually auto-recover. Higher temperature recommended',
|
||||
tip_3:
|
||||
'Small models or those unable to handle complex tool definitions typically perform poorly',
|
||||
security: '🔐 Production Authentication',
|
||||
security_warning: '⚠️ Never commit real LLM API Keys to your frontend codebase',
|
||||
security_warning: '⚠️ Never commit real LLM API Keys to your frontend code',
|
||||
security_desc:
|
||||
'In production environments, to hide the real LLM API Keys, we recommend the following architecture:',
|
||||
security_backend_proxy: 'Backend Proxy Pattern',
|
||||
|
||||
@@ -3,7 +3,7 @@ export default {
|
||||
badge: 'GUI Agent in your webpage',
|
||||
title_line1: 'The AI Operator',
|
||||
title_line2: 'Living in Your Web App',
|
||||
subtitle_emoji: '🪄 One line of CDN',
|
||||
subtitle_emoji: '🪄One line of code',
|
||||
subtitle_main: ' adds intelligent GUI Agents to your website.',
|
||||
subtitle_detail: 'Users give natural language commands, AI handles the rest.',
|
||||
tab_try: '🚀 Try It Now',
|
||||
@@ -18,6 +18,9 @@ export default {
|
||||
step1_content: 'Show your bookmarks bar',
|
||||
step2_title: 'Step 2:',
|
||||
step2_content: 'Drag this button to your bookmarks',
|
||||
cdn_label: 'CDN Source',
|
||||
cdn_international: 'International',
|
||||
cdn_china: 'China Mirror',
|
||||
step3_title: 'Step 3:',
|
||||
step3_content: 'Click the bookmark on any site to activate',
|
||||
notice_title: '⚠️ Heads Up',
|
||||
|
||||
@@ -114,33 +114,17 @@ export default {
|
||||
model_integration: {
|
||||
title: '模型接入',
|
||||
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
|
||||
recommendation_logic: '推荐使用 ToolCall 能力强的轻量级模型。',
|
||||
recommended: '推荐模型',
|
||||
model_gpt4_title: '⚡ gpt-4.1-mini',
|
||||
model_gpt4_badge: '评估基准 ✅',
|
||||
model_gpt4_1: '• 性价比高',
|
||||
model_gpt4_2: '• 速度快',
|
||||
model_gpt4_3: '• 成功率高',
|
||||
model_deepseek_title: '💰 DeepSeek-3.2',
|
||||
model_deepseek_badge: '经济实惠',
|
||||
model_deepseek_1: '• 价格远低于同等级其他模型',
|
||||
model_deepseek_2: '• ToolCall 有出错率,通常能够自动修复',
|
||||
model_deepseek_3: '• 本网站提供的免费试用为 DeepSeek',
|
||||
model_qwen_title: '🛡️ qwen3',
|
||||
model_qwen_badge: '安全合规',
|
||||
model_qwen_1: '• 可控、效果尚可,价格合理',
|
||||
model_qwen_2: '• ToolCall 有出错率,通常能够自动修复',
|
||||
model_qwen_3: '• 适合能给出详细步骤的场景',
|
||||
model_gemini_title: '⚡ gemini-2.5-flash',
|
||||
model_gemini_badge: '极其高效,成功率高,价格合理',
|
||||
available: '可用模型',
|
||||
available_verified: '✅ 已验证可用',
|
||||
available: '已测试模型',
|
||||
available_verified: '已验证可用模型',
|
||||
tips: '提示',
|
||||
tip_1: 'reasoning 模型(如 GPT-5),速度偏慢,没有必要',
|
||||
tip_2:
|
||||
'不保证 json schema 的模型(openAI 以外的几乎所有模型),tool call 有概率出错,通常能自动修复,建议 temperature 设置高一些',
|
||||
tip_3: '小模型、nano 模型,效果不佳',
|
||||
'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复,建议设置较高的 temperature',
|
||||
tip_3: '小模型或者无法适应复杂 Tool 定义的模型,通常效果不佳',
|
||||
security: '🔐 生产环境鉴权建议',
|
||||
security_warning: '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码库',
|
||||
security_warning: '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码',
|
||||
security_desc: '在实际应用中,为了隐藏真实的 LLM API Key,建议采用以下架构:',
|
||||
security_backend_proxy: '后端代理转发',
|
||||
security_backend_desc:
|
||||
|
||||
@@ -3,7 +3,7 @@ export default {
|
||||
badge: 'GUI Agent in your webpage',
|
||||
title_line1: '让你的 Web 应用',
|
||||
title_line2: '拥有 AI 操作员',
|
||||
subtitle_emoji: '🪄 一行 CDN 引入',
|
||||
subtitle_emoji: '🪄一行代码',
|
||||
subtitle_main: ',为你的网站添加 GUI Agent。',
|
||||
subtitle_detail: '用户/答疑机器人给出文字指示,AI 帮你操作页面。',
|
||||
tab_try: '🚀 立即尝试',
|
||||
@@ -17,6 +17,9 @@ export default {
|
||||
step1_content: '显示收藏夹栏',
|
||||
step2_title: '步骤 2:',
|
||||
step2_content: '拖拽下面按钮到收藏夹栏',
|
||||
cdn_label: 'CDN 源',
|
||||
cdn_international: '国际',
|
||||
cdn_china: '国内镜像',
|
||||
step3_title: '步骤 3:',
|
||||
step3_content: '在其他网站点击收藏夹中的按钮即可使用',
|
||||
notice_title: '⚠️ 注意',
|
||||
|
||||
@@ -1,14 +1,49 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* 启用 class-based dark mode for Tailwind v4 */
|
||||
@variant dark (.dark &);
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
/* 主题色渐变 */
|
||||
--theme-color-1: rgb(88, 192, 252);
|
||||
--theme-color-2: rgb(189, 69, 251);
|
||||
|
||||
/* shadcn */
|
||||
--radius: 0.625rem;
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
/* class-based dark mode - 应用到 html.dark */
|
||||
@@ -202,3 +237,153 @@ td {
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
/* shadcn */
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
/* magic ui */
|
||||
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
|
||||
@keyframes blink-cursor {
|
||||
0%,
|
||||
49% {
|
||||
opacity: 1;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
--animate-aurora: aurora 8s ease-in-out infinite alternate;
|
||||
@keyframes aurora {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
transform: rotate(-5deg) scale(0.9);
|
||||
}
|
||||
25% {
|
||||
background-position: 50% 100%;
|
||||
transform: rotate(5deg) scale(1.1);
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
transform: rotate(-3deg) scale(0.95);
|
||||
}
|
||||
75% {
|
||||
background-position: 50% 0%;
|
||||
transform: rotate(3deg) scale(1.05);
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
transform: rotate(-5deg) scale(0.9);
|
||||
}
|
||||
}
|
||||
--animate-shiny-text: shiny-text 8s infinite;
|
||||
@keyframes shiny-text {
|
||||
0%,
|
||||
90%,
|
||||
100% {
|
||||
background-position: calc(-100% - var(--shiny-width)) 0;
|
||||
}
|
||||
30%,
|
||||
60% {
|
||||
background-position: calc(100% + var(--shiny-width)) 0;
|
||||
}
|
||||
}
|
||||
--animate-gradient: gradient 8s linear infinite;
|
||||
@keyframes gradient {
|
||||
to {
|
||||
background-position: var(--bg-size, 300%) 0;
|
||||
}
|
||||
}
|
||||
--animate-background-position-spin: background-position-spin 3000ms infinite alternate;
|
||||
@keyframes background-position-spin {
|
||||
0% {
|
||||
background-position: top center;
|
||||
}
|
||||
100% {
|
||||
background-position: bottom center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* shadcn dark mode */
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
/* shadcn base */
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { useHashLocation } from 'wouter/use-hash-location'
|
||||
|
||||
import './i18n/config'
|
||||
import './i18n/types'
|
||||
import { default as PagesRouter } from './router.tsx'
|
||||
import { default as TestPagesRouter } from './test-pages/router.tsx'
|
||||
import { default as PagesRouter } from './router'
|
||||
import { default as TestPagesRouter } from './test-pages/router'
|
||||
|
||||
import './index.css'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
|
||||
import { Bot, Box, MessageSquare, PlayCircle, Shield, Sparkles, Users, Zap } from 'lucide-react'
|
||||
import { PageAgent } from 'page-agent'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -6,23 +7,32 @@ import { Link, useSearchParams } from 'wouter'
|
||||
|
||||
import Footer from './components/Footer'
|
||||
import Header from './components/Header'
|
||||
import { AnimatedGradientText } from './components/ui/animated-gradient-text'
|
||||
import { Highlighter } from './components/ui/highlighter'
|
||||
import { NeonGradientCard } from './components/ui/neon-gradient-card'
|
||||
import { Particles } from './components/ui/particles'
|
||||
import { SparklesText } from './components/ui/sparkles-text'
|
||||
import { CDN_CN_URL, CDN_URL } from './constants'
|
||||
|
||||
const injection = encodeURI(
|
||||
"javascript:(function(){var s=document.createElement('script');s.src=`https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js?t=${Math.random()}`;s.setAttribute('crossorigin', true);s.type=`text/javascript`;s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();"
|
||||
)
|
||||
function getInjection(useCN?: boolean) {
|
||||
const cdn = useCN ? CDN_CN_URL : CDN_URL
|
||||
|
||||
const injectionA = `
|
||||
<a
|
||||
href=${injection}
|
||||
class="inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
|
||||
draggable="true"
|
||||
onclick="return false;"
|
||||
title="Drag me to your bookmarks bar!"
|
||||
>
|
||||
✨PageAgent
|
||||
</a>
|
||||
const injection = encodeURI(
|
||||
`javascript:(function(){var s=document.createElement('script');s.src=\`${cdn}?t=\${Math.random()}\`;s.setAttribute('crossorigin', true);s.type="text/javascript";s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();`
|
||||
)
|
||||
|
||||
`
|
||||
return `
|
||||
<a
|
||||
href=${injection}
|
||||
class="inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
|
||||
draggable="true"
|
||||
onclick="return false;"
|
||||
title="Drag me to your bookmarks bar!"
|
||||
>
|
||||
✨PageAgent
|
||||
</a>
|
||||
`
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const { t, i18n } = useTranslation(['home', 'common'])
|
||||
@@ -38,6 +48,7 @@ export default function HomePage() {
|
||||
const isOther = params.has('try_other')
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'try' | 'other'>(isOther ? 'other' : 'try')
|
||||
const [cdnSource, setCdnSource] = useState<'international' | 'china'>('international')
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (!task.trim()) return
|
||||
@@ -77,19 +88,28 @@ export default function HomePage() {
|
||||
<main id="main-content">
|
||||
<section className="relative px-6 py-22 lg:py-28" aria-labelledby="hero-heading">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
{/* Background Pattern */}
|
||||
{/* Background Pattern + Particles */}
|
||||
<div className="absolute inset-0 opacity-30" aria-hidden="true">
|
||||
<div className="absolute inset-0 bg-linear-to-r from-blue-400/20 to-purple-400/20 rounded-3xl transform rotate-1"></div>
|
||||
<div className="absolute inset-0 bg-linear-to-l from-purple-400/20 to-blue-400/20 rounded-3xl transform -rotate-1"></div>
|
||||
</div>
|
||||
<Particles
|
||||
className="absolute inset-0"
|
||||
quantity={80}
|
||||
staticity={30}
|
||||
ease={80}
|
||||
color="#6366f1"
|
||||
/>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="inline-flex items-center px-4 py-2 mb-8 text-sm font-medium text-blue-700 bg-blue-100 rounded-full dark:text-blue-300 dark:bg-blue-900/30">
|
||||
<div className="inline-flex items-center px-4 py-2 mb-8 text-sm font-medium bg-white/90 dark:bg-gray-800/90 rounded-full shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<span
|
||||
className="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{t('home:hero.badge')}
|
||||
<AnimatedGradientText colorFrom="#3b82f6" colorTo="#8b5cf6">
|
||||
{t('home:hero.badge')}
|
||||
</AnimatedGradientText>
|
||||
</div>
|
||||
|
||||
<h1
|
||||
@@ -102,23 +122,29 @@ export default function HomePage() {
|
||||
</h1>
|
||||
|
||||
<p className="text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-4xl mx-auto leading-relaxed">
|
||||
<span className="bg-linear-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold">
|
||||
{t('home:hero.subtitle_emoji')}
|
||||
</span>
|
||||
<Highlighter action="underline" color="#8b5cf6" strokeWidth={2}>
|
||||
<span className="bg-linear-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold">
|
||||
{t('home:hero.subtitle_emoji')}
|
||||
</span>
|
||||
</Highlighter>
|
||||
{t('home:hero.subtitle_main')}
|
||||
<br />
|
||||
{t('home:hero.subtitle_detail')}
|
||||
</p>
|
||||
|
||||
{/* Try It Now Section - Tab Card */}
|
||||
<div className="mt-8 mb-6">
|
||||
<div className="mb-12">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<NeonGradientCard
|
||||
borderSize={2}
|
||||
borderRadius={20}
|
||||
neonColors={{ firstColor: '#ff00aa', secondColor: '#00FFF1' }}
|
||||
>
|
||||
{/* Tab Headers */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => setActiveTab('try')}
|
||||
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 ${
|
||||
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 rounded-tl-2xl ${
|
||||
activeTab === 'try'
|
||||
? 'bg-linear-to-r from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
@@ -128,7 +154,7 @@ export default function HomePage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('other')}
|
||||
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 ${
|
||||
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 rounded-tr-2xl ${
|
||||
activeTab === 'other'
|
||||
? 'bg-linear-to-r from-green-50 to-blue-50 dark:from-green-900/30 dark:to-blue-900/30 text-green-700 dark:text-green-300 border-b-2 border-green-500'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
@@ -194,10 +220,25 @@ export default function HomePage() {
|
||||
</span>{' '}
|
||||
{t('home:try_other.step2_content')}
|
||||
</p>
|
||||
<div
|
||||
className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400"
|
||||
dangerouslySetInnerHTML={{ __html: injectionA }}
|
||||
></div>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<select
|
||||
value={cdnSource}
|
||||
onChange={(e) =>
|
||||
setCdnSource(e.target.value as 'international' | 'china')
|
||||
}
|
||||
className="px-2 py-1.5 text-xs border border-gray-300 dark:border-gray-500 rounded bg-white dark:bg-gray-600 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
<option value="international">
|
||||
{t('home:try_other.cdn_international')}
|
||||
</option>
|
||||
<option value="china">{t('home:try_other.cdn_china')}</option>
|
||||
</select>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getInjection(cdnSource === 'china'),
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Instructions */}
|
||||
@@ -252,7 +293,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NeonGradientCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -306,10 +347,10 @@ export default function HomePage() {
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
className="w-12 h-12 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
|
||||
className="w-14 h-14 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="text-white text-xl">📦</span>
|
||||
<Box className="w-7 h-7 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
||||
{t('home:features.in_page.title')}
|
||||
@@ -325,10 +366,10 @@ export default function HomePage() {
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
className="w-12 h-12 bg-linear-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
|
||||
className="w-14 h-14 bg-linear-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="text-white text-xl">⚡</span>
|
||||
<Zap className="w-7 h-7 text-white fill-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
||||
{t('home:features.zero_backend.title')}
|
||||
@@ -344,10 +385,10 @@ export default function HomePage() {
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
className="w-12 h-12 bg-linear-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
|
||||
className="w-14 h-14 bg-linear-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="text-white text-xl">🌈</span>
|
||||
<MessageSquare className="w-7 h-7 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
||||
{t('home:features.accessible.title')}
|
||||
@@ -363,10 +404,10 @@ export default function HomePage() {
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
className="w-12 h-12 bg-linear-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300"
|
||||
className="w-14 h-14 bg-linear-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="text-white text-xl">🔒</span>
|
||||
<Shield className="w-7 h-7 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
||||
{t('home:features.secure_integration.title')}
|
||||
@@ -383,12 +424,12 @@ export default function HomePage() {
|
||||
<section className="px-6 py-20" aria-labelledby="use-cases-heading">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2
|
||||
id="use-cases-heading"
|
||||
className="text-4xl lg:text-5xl font-bold mb-6 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
|
||||
<SparklesText
|
||||
className="text-4xl lg:text-5xl mb-6"
|
||||
colors={{ first: '#3b82f6', second: '#8b5cf6' }}
|
||||
>
|
||||
{t('home:use_cases.section_title')}
|
||||
</h2>
|
||||
</SparklesText>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
{t('home:use_cases.section_subtitle')}
|
||||
</p>
|
||||
@@ -396,10 +437,10 @@ export default function HomePage() {
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-12" role="list">
|
||||
{/* Use Case 1 */}
|
||||
<div className="bg-linear-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
|
||||
<div className="group bg-linear-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center shrink-0">
|
||||
<span className="text-white font-bold">1</span>
|
||||
<div className="w-12 h-12 bg-linear-to-br from-blue-500 to-purple-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
|
||||
<Bot className="w-6 h-6 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -414,10 +455,10 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* Use Case 2 */}
|
||||
<div className="bg-linear-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
|
||||
<div className="group bg-linear-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center shrink-0">
|
||||
<span className="text-white font-bold">2</span>
|
||||
<div className="w-12 h-12 bg-linear-to-br from-green-500 to-blue-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
|
||||
<Sparkles className="w-6 h-6 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
|
||||
@@ -431,10 +472,10 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* Use Case 3 */}
|
||||
<div className="bg-linear-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
|
||||
<div className="group bg-linear-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-10 h-10 bg-purple-500 rounded-lg flex items-center justify-center shrink-0">
|
||||
<span className="text-white font-bold">3</span>
|
||||
<div className="w-12 h-12 bg-linear-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
|
||||
<PlayCircle className="w-6 h-6 text-white fill-white/30" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
|
||||
@@ -448,10 +489,10 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* Use Case 4 */}
|
||||
<div className="bg-linear-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl">
|
||||
<div className="group bg-linear-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center shrink-0">
|
||||
<span className="text-white font-bold">4</span>
|
||||
<div className="w-12 h-12 bg-linear-to-br from-orange-500 to-red-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
|
||||
<Users className="w-6 h-6 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
|
||||
|
||||
@@ -11,11 +11,19 @@
|
||||
"@/*": ["src/*"],
|
||||
|
||||
// Simplified monorepo solution (raw npm workspace with hoisting)
|
||||
"page-agent": ["../page-agent/src/PageAgent.ts"],
|
||||
"@page-agent/llms": ["../llms/src/index.ts"],
|
||||
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
|
||||
"page-agent": ["../page-agent/src/PageAgent.ts"]
|
||||
"@page-agent/ui": ["../ui/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"references": [{ "path": "../page-controller" }, { "path": "../page-agent" }]
|
||||
"references": [
|
||||
//
|
||||
{ "path": "../llms" },
|
||||
{ "path": "../page-agent" },
|
||||
{ "path": "../page-controller" },
|
||||
{ "path": "../ui" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import 'dotenv/config'
|
||||
import { config as dotenvConfig } from 'dotenv'
|
||||
import process from 'node:process'
|
||||
import { dirname, resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -8,6 +8,9 @@ import { defineConfig } from 'vite'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// Load .env from repo root
|
||||
dotenvConfig({ path: resolve(__dirname, '../../.env') })
|
||||
|
||||
// Website Config (React Documentation Site)
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
@@ -19,6 +22,7 @@ export default defineConfig({
|
||||
'@': resolve(__dirname, 'src'),
|
||||
|
||||
// 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/ui': resolve(__dirname, '../ui/src/index.ts'),
|
||||
'page-agent': resolve(__dirname, '../page-agent/src/PageAgent.ts'),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"references": [
|
||||
{ "path": "./packages/page-controller" },
|
||||
{ "path": "./packages/ui" },
|
||||
{ "path": "./packages/llms" },
|
||||
{ "path": "./packages/page-agent" },
|
||||
{ "path": "./packages/website" }
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user