Spaces:
Running
Running
A-Mahla
commited on
Commit
·
304e233
1
Parent(s):
c9554cf
ADD CUA backbone (#1)
Browse files* MOCK backend
* MOCK backend
* Fix pre-commit
* Fix pre-commit
* CHG Step models
* FIX pre-commit
* CHG README.md
* FIX pre-commit
* ADD backend route model
* CHG pre-commit
* FIX pre-commit
- .github/actions/setup-uv/action.yml +16 -0
- .github/workflows/pre-commit.yml +34 -0
- .gitignore +2 -0
- .pre-commit-config.yaml +55 -0
- Makefile +32 -0
- README.md +224 -0
- assets/architecture.png +0 -0
- cua2-core/src/cua2_core/app.py +4 -5
- cua2-core/src/cua2_core/main.py +1 -1
- cua2-core/src/cua2_core/models/__init__.py +0 -1
- cua2-core/src/cua2_core/models/models.py +52 -22
- cua2-core/src/cua2_core/routes/__init__.py +0 -1
- cua2-core/src/cua2_core/routes/routes.py +13 -10
- cua2-core/src/cua2_core/routes/websocket.py +14 -11
- cua2-core/src/cua2_core/services/__init__.py +0 -1
- cua2-core/src/cua2_core/services/agent_service.py +44 -50
- cua2-core/src/cua2_core/services/simulation_metadata/simulated_trace.json +3 -4
- cua2-core/src/cua2_core/websocket/__init__.py +0 -1
- cua2-core/src/cua2_core/websocket/websocket_manager.py +5 -3
- cua2-front/eslint.config.js +26 -0
- cua2-front/index.html +0 -2
- cua2-front/package-lock.json +28 -5
- cua2-front/package.json +5 -4
- cua2-front/src/App.tsx +0 -1
- cua2-front/src/component/poc/ConnectionStatus.tsx +37 -0
- cua2-front/src/component/poc/Header.tsx +47 -0
- cua2-front/src/component/poc/Metadata.tsx +38 -0
- cua2-front/src/component/poc/ProcessingIndicator.tsx +34 -0
- cua2-front/src/component/poc/StackSteps.tsx +29 -0
- cua2-front/src/component/poc/StepCard.tsx +84 -0
- cua2-front/src/component/poc/TaskButton.tsx +76 -0
- cua2-front/src/component/poc/VNCStream.tsx +30 -0
- cua2-front/src/component/poc/index.ts +8 -0
- cua2-front/src/components/mock/ConnectionStatus.tsx +37 -0
- cua2-front/src/components/mock/Header.tsx +47 -0
- cua2-front/src/components/mock/Metadata.tsx +38 -0
- cua2-front/src/components/mock/ProcessingIndicator.tsx +34 -0
- cua2-front/src/components/mock/StackSteps.tsx +29 -0
- cua2-front/src/components/mock/StepCard.tsx +84 -0
- cua2-front/src/components/mock/TaskButton.tsx +76 -0
- cua2-front/src/components/mock/VNCStream.tsx +30 -0
- cua2-front/src/components/mock/index.ts +8 -0
- cua2-front/src/index.css +8 -3
- cua2-front/src/pages/Index.tsx +77 -60
- cua2-front/src/types/agent.ts +64 -16
- cua2-front/tsconfig.app.json +2 -2
- pyproject.toml +89 -0
.github/actions/setup-uv/action.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 'Setup UV'
|
| 2 |
+
description: 'Install UV and set up the virtual environment'
|
| 3 |
+
|
| 4 |
+
runs:
|
| 5 |
+
using: composite
|
| 6 |
+
steps:
|
| 7 |
+
- name: Install uv
|
| 8 |
+
shell: bash
|
| 9 |
+
run: |
|
| 10 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 11 |
+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
| 12 |
+
|
| 13 |
+
- name: Install dependencies
|
| 14 |
+
shell: bash
|
| 15 |
+
run: |
|
| 16 |
+
make sync
|
.github/workflows/pre-commit.yml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Pre-commit
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
pre-commit:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/checkout@v4
|
| 14 |
+
with:
|
| 15 |
+
submodules: true
|
| 16 |
+
|
| 17 |
+
- name: Set up Python
|
| 18 |
+
uses: actions/setup-python@v5
|
| 19 |
+
with:
|
| 20 |
+
python-version: "3.11"
|
| 21 |
+
|
| 22 |
+
- name: Set up Node.js
|
| 23 |
+
uses: actions/setup-node@v4
|
| 24 |
+
with:
|
| 25 |
+
node-version: "20"
|
| 26 |
+
cache: "npm"
|
| 27 |
+
cache-dependency-path: |
|
| 28 |
+
cua2-front/package-lock.json
|
| 29 |
+
|
| 30 |
+
- uses: ./.github/actions/setup-uv
|
| 31 |
+
|
| 32 |
+
- name: Run pre-commit
|
| 33 |
+
run: |
|
| 34 |
+
uv run pre-commit run --all-files --show-diff-on-failure
|
.gitignore
CHANGED
|
@@ -224,3 +224,5 @@ dist-ssr
|
|
| 224 |
*.njsproj
|
| 225 |
*.sln
|
| 226 |
*.sw?
|
|
|
|
|
|
|
|
|
| 224 |
*.njsproj
|
| 225 |
*.sln
|
| 226 |
*.sw?
|
| 227 |
+
|
| 228 |
+
data/
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
repos:
|
| 2 |
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
| 3 |
+
rev: v4.5.0
|
| 4 |
+
hooks:
|
| 5 |
+
- id: trailing-whitespace
|
| 6 |
+
- id: end-of-file-fixer
|
| 7 |
+
- id: check-yaml
|
| 8 |
+
- id: check-added-large-files
|
| 9 |
+
- id: check-ast
|
| 10 |
+
- id: check-json
|
| 11 |
+
exclude: ^(frontend/.*\.json|cua2-front/tsconfig.*\.json)$
|
| 12 |
+
- id: check-merge-conflict
|
| 13 |
+
- id: detect-private-key
|
| 14 |
+
|
| 15 |
+
- repo: https://github.com/pycqa/isort
|
| 16 |
+
rev: 5.13.2
|
| 17 |
+
hooks:
|
| 18 |
+
- id: isort
|
| 19 |
+
args: ["--profile", "black"]
|
| 20 |
+
|
| 21 |
+
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
| 22 |
+
rev: v0.9.0
|
| 23 |
+
hooks:
|
| 24 |
+
- id: ruff
|
| 25 |
+
args: [--fix]
|
| 26 |
+
- id: ruff-format
|
| 27 |
+
|
| 28 |
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
| 29 |
+
rev: v1.14.1
|
| 30 |
+
hooks:
|
| 31 |
+
- id: mypy
|
| 32 |
+
additional_dependencies: [types-PyYAML, types-requests]
|
| 33 |
+
args: [--ignore-missing-imports]
|
| 34 |
+
|
| 35 |
+
- repo: https://github.com/codespell-project/codespell
|
| 36 |
+
rev: v2.3.0
|
| 37 |
+
hooks:
|
| 38 |
+
- id: codespell
|
| 39 |
+
args: ["--skip=*.json,*.jsonl,*.txt,*.md,*.ipynb"]
|
| 40 |
+
|
| 41 |
+
- repo: local
|
| 42 |
+
hooks:
|
| 43 |
+
- id: eslint-cua2-front
|
| 44 |
+
name: ESLint Frontend
|
| 45 |
+
entry: bash -c 'cd cua2-front && npx eslint src/ --config eslint.config.js'
|
| 46 |
+
language: system
|
| 47 |
+
files: ^cua2-front/.*\.(ts|tsx|js|jsx)$
|
| 48 |
+
pass_filenames: false
|
| 49 |
+
|
| 50 |
+
- id: typescript-check
|
| 51 |
+
name: TypeScript Type Check
|
| 52 |
+
entry: bash -c 'cd cua2-front && npx tsc --noEmit --project tsconfig.json'
|
| 53 |
+
language: system
|
| 54 |
+
files: ^cua2-front/.*\.(ts|tsx)$
|
| 55 |
+
pass_filenames: false
|
Makefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: sync setup install dev-backend dev-frontend dev clean
|
| 2 |
+
|
| 3 |
+
# Sync all dependencies (Python + Node.js)
|
| 4 |
+
sync:
|
| 5 |
+
@echo "Syncing Python dependencies..."
|
| 6 |
+
uv sync --all-extras
|
| 7 |
+
@echo "Installing frontend dependencies..."
|
| 8 |
+
cd cua2-front && npm install
|
| 9 |
+
@echo "✓ All dependencies synced!"
|
| 10 |
+
|
| 11 |
+
setup: sync
|
| 12 |
+
|
| 13 |
+
install-frontend:
|
| 14 |
+
cd cua2-front && npm install
|
| 15 |
+
|
| 16 |
+
# Start backend development server
|
| 17 |
+
dev-backend:
|
| 18 |
+
cd cua2-core && uv run uvicorn cua2_core.main:app --reload --host 0.0.0.0 --port 8000
|
| 19 |
+
|
| 20 |
+
# Start frontend development server
|
| 21 |
+
dev-frontend:
|
| 22 |
+
cd cua2-front && npm run dev
|
| 23 |
+
|
| 24 |
+
pre-commit:
|
| 25 |
+
uv run pre-commit run --all-files --show-diff-on-failure
|
| 26 |
+
|
| 27 |
+
clean:
|
| 28 |
+
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
| 29 |
+
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
|
| 30 |
+
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
| 31 |
+
cd cua2-front && rm -rf node_modules dist 2>/dev/null || true
|
| 32 |
+
@echo "✓ Cleaned!"
|
README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CUA2 - Computer Use Agent 2
|
| 2 |
+
|
| 3 |
+
An AI-powered automation interface featuring real-time agent task processing, VNC streaming, and step-by-step execution visualization.
|
| 4 |
+
|
| 5 |
+
## 🚀 Overview
|
| 6 |
+
|
| 7 |
+
CUA2 is a full-stack application that provides a modern web interface for AI agents to perform automated computer tasks. The system features real-time WebSocket communication between a FastAPI backend and React frontend, allowing users to monitor agent execution, view screenshots, track token usage, and stream VNC sessions.
|
| 8 |
+
|
| 9 |
+
## 🏗️ Architecture
|
| 10 |
+
|
| 11 |
+

|
| 12 |
+
|
| 13 |
+
## 🛠️ Tech Stack
|
| 14 |
+
|
| 15 |
+
### Backend (`cua2-core`)
|
| 16 |
+
- **FastAPI**
|
| 17 |
+
- **Uvicorn**
|
| 18 |
+
- **smolagents** - AI agent framework with OpenAI/LiteLLM support
|
| 19 |
+
|
| 20 |
+
### Frontend (`cua2-front`)
|
| 21 |
+
- **React TS**
|
| 22 |
+
- **Vite**
|
| 23 |
+
|
| 24 |
+
## 📋 Prerequisites
|
| 25 |
+
|
| 26 |
+
- **Python** 3.10 or higher
|
| 27 |
+
- **Node.js** 18 or higher
|
| 28 |
+
- **npm**
|
| 29 |
+
- **uv** - Python package manager
|
| 30 |
+
|
| 31 |
+
### Installing uv
|
| 32 |
+
|
| 33 |
+
**macOS/Linux:**
|
| 34 |
+
```bash
|
| 35 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
For more installation options, visit: https://docs.astral.sh/uv/getting-started/installation/
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
## 🚀 Getting Started
|
| 43 |
+
|
| 44 |
+
### 1. Clone the Repository
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
git clone https://github.com/huggingface/CUA2.git
|
| 48 |
+
cd CUA2
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### 2. Install Dependencies
|
| 52 |
+
|
| 53 |
+
Use the Makefile for quick setup:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
make sync
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
This will:
|
| 60 |
+
- Install Python dependencies using `uv`
|
| 61 |
+
- Install Node.js dependencies for the frontend
|
| 62 |
+
|
| 63 |
+
Or install manually:
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
# Backend dependencies
|
| 67 |
+
cd cua2-core
|
| 68 |
+
uv sync --all-extras
|
| 69 |
+
|
| 70 |
+
# Frontend dependencies
|
| 71 |
+
cd ../cua2-front
|
| 72 |
+
npm install
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
### 3. Environment Configuration
|
| 76 |
+
|
| 77 |
+
Copy the example environment file and configure your settings:
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
cd cua2-core
|
| 81 |
+
cp env.example .env
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
Edit `.env` with your configuration:
|
| 85 |
+
- API keys for OpenAI/LiteLLM
|
| 86 |
+
- Database connections (if applicable)
|
| 87 |
+
- Other service credentials
|
| 88 |
+
|
| 89 |
+
### 4. Start Development Servers
|
| 90 |
+
|
| 91 |
+
#### Option 1: Using Makefile (Recommended)
|
| 92 |
+
|
| 93 |
+
Open two terminal windows:
|
| 94 |
+
|
| 95 |
+
**Terminal 1 - Backend:**
|
| 96 |
+
```bash
|
| 97 |
+
make dev-backend
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
**Terminal 2 - Frontend:**
|
| 101 |
+
```bash
|
| 102 |
+
make dev-frontend
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
#### Option 2: Manual Start
|
| 106 |
+
|
| 107 |
+
**Terminal 1 - Backend:**
|
| 108 |
+
```bash
|
| 109 |
+
cd cua2-core
|
| 110 |
+
uv run uvicorn cua2_core.main:app --reload --host 0.0.0.0 --port 8000
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
**Terminal 2 - Frontend:**
|
| 114 |
+
```bash
|
| 115 |
+
cd cua2-front
|
| 116 |
+
npm run dev
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
### 5. Access the Application
|
| 120 |
+
|
| 121 |
+
- **Frontend**: http://localhost:5173
|
| 122 |
+
- **Backend API**: http://localhost:8000
|
| 123 |
+
- **API Documentation**: http://localhost:8000/docs
|
| 124 |
+
- **ReDoc**: http://localhost:8000/redoc
|
| 125 |
+
|
| 126 |
+
## 📁 Project Structure
|
| 127 |
+
|
| 128 |
+
```
|
| 129 |
+
CUA2/
|
| 130 |
+
├── cua2-core/ # Backend application
|
| 131 |
+
│ ├── src/
|
| 132 |
+
│ │ └── cua2_core/
|
| 133 |
+
│ │ ├── app.py # FastAPI application setup
|
| 134 |
+
│ │ ├── main.py # Application entry point
|
| 135 |
+
│ │ ├── models/
|
| 136 |
+
│ │ │ └── models.py # Pydantic models
|
| 137 |
+
│ │ ├── routes/
|
| 138 |
+
│ │ │ ├── routes.py # REST API endpoints
|
| 139 |
+
│ │ │ └── websocket.py # WebSocket endpoint
|
| 140 |
+
│ │ ├── services/
|
| 141 |
+
│ │ │ ├── agent_service.py # Agent task processing
|
| 142 |
+
│ │ │ └── simulation_metadata/ # Demo data
|
| 143 |
+
│ │ └── websocket/
|
| 144 |
+
│ │ └── websocket_manager.py # WebSocket management
|
| 145 |
+
│ ├── pyproject.toml # Python dependencies
|
| 146 |
+
│ └── env.example # Environment variables template
|
| 147 |
+
│
|
| 148 |
+
├── cua2-front/ # Frontend application
|
| 149 |
+
│ ├── src/
|
| 150 |
+
│ │ ├── App.tsx # Main application component
|
| 151 |
+
│ │ ├── pages/
|
| 152 |
+
│ │ │ └── Index.tsx # Main page
|
| 153 |
+
│ │ ├── components/
|
| 154 |
+
│ │ │ └── mock/ # UI components
|
| 155 |
+
│ │ ├── hooks/
|
| 156 |
+
│ │ │ └── useWebSocket.ts # WebSocket hook
|
| 157 |
+
│ │ └── types/
|
| 158 |
+
│ │ └── agent.ts # TypeScript type definitions
|
| 159 |
+
│ ├── package.json # Node dependencies
|
| 160 |
+
│ └── vite.config.ts # Vite configuration
|
| 161 |
+
│
|
| 162 |
+
├── Makefile # Development commands
|
| 163 |
+
└── README.md # This file
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
## 🔌 API Endpoints
|
| 167 |
+
|
| 168 |
+
### REST API
|
| 169 |
+
|
| 170 |
+
| Method | Endpoint | Description |
|
| 171 |
+
|--------|----------|-------------|
|
| 172 |
+
| GET | `/health` | Health check with WebSocket connection count |
|
| 173 |
+
| GET | `/tasks` | Get all active tasks |
|
| 174 |
+
| GET | `/tasks/{task_id}` | Get specific task status |
|
| 175 |
+
| GET | `/docs` | Interactive API documentation (Swagger) |
|
| 176 |
+
| GET | `/redoc` | Alternative API documentation (ReDoc) |
|
| 177 |
+
|
| 178 |
+
### WebSocket
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
#### Client → Server Events
|
| 182 |
+
|
| 183 |
+
- `user_task` - New user task request
|
| 184 |
+
|
| 185 |
+
#### Server → Client Events
|
| 186 |
+
|
| 187 |
+
- `agent_start` - Agent begins processing
|
| 188 |
+
- `agent_progress` - New step completed with image and metadata
|
| 189 |
+
- `agent_complete` - Task finished successfully
|
| 190 |
+
- `agent_error` - Error occurred during processing
|
| 191 |
+
- `vnc_url_set` - VNC stream URL available
|
| 192 |
+
- `vnc_url_unset` - VNC stream ended
|
| 193 |
+
- `heartbeat` - Connection keep-alive
|
| 194 |
+
|
| 195 |
+
## 🧪 Development
|
| 196 |
+
|
| 197 |
+
### Available Make Commands
|
| 198 |
+
|
| 199 |
+
```bash
|
| 200 |
+
make sync # Sync all dependencies (Python + Node.js)
|
| 201 |
+
make dev-backend # Start backend development server
|
| 202 |
+
make dev-frontend # Start frontend development server
|
| 203 |
+
make pre-commit # Run pre-commit hooks
|
| 204 |
+
make clean # Clean build artifacts and caches
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
### Code Quality
|
| 208 |
+
|
| 209 |
+
```bash
|
| 210 |
+
# Backend
|
| 211 |
+
make pre-commit
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
### Build for Production
|
| 215 |
+
|
| 216 |
+
```bash
|
| 217 |
+
# Frontend
|
| 218 |
+
cd cua2-front
|
| 219 |
+
npm run build
|
| 220 |
+
|
| 221 |
+
# The build output will be in cua2-front/dist/
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
**Happy Coding! 🚀**
|
assets/architecture.png
ADDED
|
cua2-core/src/cua2_core/app.py
CHANGED
|
@@ -1,12 +1,11 @@
|
|
| 1 |
from contextlib import asynccontextmanager
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
from fastapi import FastAPI
|
| 5 |
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
|
| 7 |
-
from cua2_core.services.agent_service import AgentService
|
| 8 |
-
from cua2_core.websocket.websocket_manager import WebSocketManager
|
| 9 |
-
|
| 10 |
# Load environment variables
|
| 11 |
load_dotenv()
|
| 12 |
|
|
@@ -39,8 +38,8 @@ async def lifespan(app: FastAPI):
|
|
| 39 |
|
| 40 |
# Create FastAPI app with lifespan
|
| 41 |
app = FastAPI(
|
| 42 |
-
title="Computer Use
|
| 43 |
-
description="Backend API for Computer Use
|
| 44 |
version="1.0.0",
|
| 45 |
docs_url="/docs",
|
| 46 |
redoc_url="/redoc",
|
|
|
|
| 1 |
from contextlib import asynccontextmanager
|
| 2 |
|
| 3 |
+
from cua2_core.services.agent_service import AgentService
|
| 4 |
+
from cua2_core.websocket.websocket_manager import WebSocketManager
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
from fastapi import FastAPI
|
| 7 |
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
|
|
|
|
|
|
|
|
|
|
| 9 |
# Load environment variables
|
| 10 |
load_dotenv()
|
| 11 |
|
|
|
|
| 38 |
|
| 39 |
# Create FastAPI app with lifespan
|
| 40 |
app = FastAPI(
|
| 41 |
+
title="Computer Use Backend",
|
| 42 |
+
description="Backend API for Computer Use - AI-powered automation interface",
|
| 43 |
version="1.0.0",
|
| 44 |
docs_url="/docs",
|
| 45 |
redoc_url="/redoc",
|
cua2-core/src/cua2_core/main.py
CHANGED
|
@@ -22,7 +22,7 @@ if __name__ == "__main__":
|
|
| 22 |
port = int(os.getenv("PORT", 8000))
|
| 23 |
debug = os.getenv("DEBUG", "false").lower() == "true"
|
| 24 |
|
| 25 |
-
print(f"Starting Computer Use
|
| 26 |
print(f"Debug mode: {debug}")
|
| 27 |
print(f"API Documentation: http://{host}:{port}/docs")
|
| 28 |
print(f"WebSocket endpoint: ws://{host}:{port}/ws")
|
|
|
|
| 22 |
port = int(os.getenv("PORT", 8000))
|
| 23 |
debug = os.getenv("DEBUG", "false").lower() == "true"
|
| 24 |
|
| 25 |
+
print(f"Starting Computer Use Backend on {host}:{port}")
|
| 26 |
print(f"Debug mode: {debug}")
|
| 27 |
print(f"API Documentation: http://{host}:{port}/docs")
|
| 28 |
print(f"WebSocket endpoint: ws://{host}:{port}/ws")
|
cua2-core/src/cua2_core/models/__init__.py
CHANGED
|
@@ -1,2 +1 @@
|
|
| 1 |
"""Models module for CUA2 Core"""
|
| 2 |
-
|
|
|
|
| 1 |
"""Models module for CUA2 Core"""
|
|
|
cua2-core/src/cua2_core/models/models.py
CHANGED
|
@@ -1,57 +1,68 @@
|
|
| 1 |
import json
|
| 2 |
import os
|
| 3 |
from datetime import datetime
|
| 4 |
-
from
|
| 5 |
-
from typing import Annotated, Literal, TypeAlias
|
| 6 |
|
| 7 |
from pydantic import BaseModel, Field, field_serializer, model_validator
|
|
|
|
| 8 |
|
| 9 |
#################### Backend -> Frontend ########################
|
| 10 |
|
|
|
|
| 11 |
class AgentAction(BaseModel):
|
| 12 |
"""Agent action structure"""
|
| 13 |
|
| 14 |
-
actionType: Literal[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
actionArguments: dict
|
| 16 |
|
| 17 |
def to_string(self) -> str:
|
| 18 |
"""Convert action to a human-readable string"""
|
| 19 |
action_type = self.actionType
|
| 20 |
args = self.actionArguments
|
| 21 |
-
|
| 22 |
if action_type == "click":
|
| 23 |
x = args.get("x", "?")
|
| 24 |
y = args.get("y", "?")
|
| 25 |
return f"Click at coordinates ({x}, {y})"
|
| 26 |
-
|
| 27 |
elif action_type == "write":
|
| 28 |
text = args.get("text", "")
|
| 29 |
return f"Type text: '{text}'"
|
| 30 |
-
|
| 31 |
elif action_type == "press":
|
| 32 |
key = args.get("key", "")
|
| 33 |
return f"Press key: {key}"
|
| 34 |
-
|
| 35 |
elif action_type == "scroll":
|
| 36 |
direction = args.get("direction", "down")
|
| 37 |
amount = args.get("amount", 2)
|
| 38 |
return f"Scroll {direction} by {amount}"
|
| 39 |
-
|
| 40 |
elif action_type == "wait":
|
| 41 |
seconds = args.get("seconds", 0)
|
| 42 |
return f"Wait for {seconds} seconds"
|
| 43 |
-
|
| 44 |
elif action_type == "open":
|
| 45 |
file_or_url = args.get("file_or_url", "")
|
| 46 |
return f"Open: {file_or_url}"
|
| 47 |
-
|
| 48 |
elif action_type == "launch_app":
|
| 49 |
app_name = args.get("app_name", "")
|
| 50 |
return f"Launch app: {app_name}"
|
| 51 |
-
|
| 52 |
elif action_type == "refresh":
|
| 53 |
return "Refresh the current page"
|
| 54 |
-
|
| 55 |
elif action_type == "go_back":
|
| 56 |
return "Go back one page"
|
| 57 |
|
|
@@ -62,19 +73,19 @@ class AgentStep(BaseModel):
|
|
| 62 |
traceId: str
|
| 63 |
stepId: str
|
| 64 |
image: str
|
| 65 |
-
|
| 66 |
-
actions: list[AgentAction]
|
| 67 |
-
timeTaken: float
|
| 68 |
inputTokensUsed: int
|
| 69 |
outputTokensUsed: int
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
def serialize_actions(self, actions: list[AgentAction], _info):
|
| 75 |
"""Convert actions to list of strings when dumping (controlled by context)"""
|
| 76 |
|
| 77 |
-
if _info.context and _info.context.get(
|
| 78 |
return [action.model_dump(mode="json") for action in actions]
|
| 79 |
|
| 80 |
return [action.to_string() for action in actions]
|
|
@@ -86,8 +97,9 @@ class AgentTraceMetadata(BaseModel):
|
|
| 86 |
traceId: str = ""
|
| 87 |
inputTokensUsed: int = 0
|
| 88 |
outputTokensUsed: int = 0
|
| 89 |
-
|
| 90 |
numberOfSteps: int = 0
|
|
|
|
| 91 |
|
| 92 |
|
| 93 |
class AgentTrace(BaseModel):
|
|
@@ -208,7 +220,11 @@ class ActiveTask(BaseModel):
|
|
| 208 |
self.traceMetadata.traceId = self.message_id
|
| 209 |
os.makedirs(self.trace_path, exist_ok=True)
|
| 210 |
with open(f"{self.trace_path}/tasks.json", "w") as f:
|
| 211 |
-
json.dump(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
return self
|
| 214 |
|
|
@@ -219,3 +235,17 @@ class HealthResponse(BaseModel):
|
|
| 219 |
status: str
|
| 220 |
timestamp: datetime
|
| 221 |
websocket_connections: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import json
|
| 2 |
import os
|
| 3 |
from datetime import datetime
|
| 4 |
+
from typing import Annotated, Literal, Optional
|
|
|
|
| 5 |
|
| 6 |
from pydantic import BaseModel, Field, field_serializer, model_validator
|
| 7 |
+
from typing_extensions import TypeAlias
|
| 8 |
|
| 9 |
#################### Backend -> Frontend ########################
|
| 10 |
|
| 11 |
+
|
| 12 |
class AgentAction(BaseModel):
|
| 13 |
"""Agent action structure"""
|
| 14 |
|
| 15 |
+
actionType: Literal[
|
| 16 |
+
"click",
|
| 17 |
+
"write",
|
| 18 |
+
"press",
|
| 19 |
+
"scroll",
|
| 20 |
+
"wait",
|
| 21 |
+
"open",
|
| 22 |
+
"launch_app",
|
| 23 |
+
"refresh",
|
| 24 |
+
"go_back",
|
| 25 |
+
]
|
| 26 |
actionArguments: dict
|
| 27 |
|
| 28 |
def to_string(self) -> str:
|
| 29 |
"""Convert action to a human-readable string"""
|
| 30 |
action_type = self.actionType
|
| 31 |
args = self.actionArguments
|
| 32 |
+
|
| 33 |
if action_type == "click":
|
| 34 |
x = args.get("x", "?")
|
| 35 |
y = args.get("y", "?")
|
| 36 |
return f"Click at coordinates ({x}, {y})"
|
| 37 |
+
|
| 38 |
elif action_type == "write":
|
| 39 |
text = args.get("text", "")
|
| 40 |
return f"Type text: '{text}'"
|
| 41 |
+
|
| 42 |
elif action_type == "press":
|
| 43 |
key = args.get("key", "")
|
| 44 |
return f"Press key: {key}"
|
| 45 |
+
|
| 46 |
elif action_type == "scroll":
|
| 47 |
direction = args.get("direction", "down")
|
| 48 |
amount = args.get("amount", 2)
|
| 49 |
return f"Scroll {direction} by {amount}"
|
| 50 |
+
|
| 51 |
elif action_type == "wait":
|
| 52 |
seconds = args.get("seconds", 0)
|
| 53 |
return f"Wait for {seconds} seconds"
|
| 54 |
+
|
| 55 |
elif action_type == "open":
|
| 56 |
file_or_url = args.get("file_or_url", "")
|
| 57 |
return f"Open: {file_or_url}"
|
| 58 |
+
|
| 59 |
elif action_type == "launch_app":
|
| 60 |
app_name = args.get("app_name", "")
|
| 61 |
return f"Launch app: {app_name}"
|
| 62 |
+
|
| 63 |
elif action_type == "refresh":
|
| 64 |
return "Refresh the current page"
|
| 65 |
+
|
| 66 |
elif action_type == "go_back":
|
| 67 |
return "Go back one page"
|
| 68 |
|
|
|
|
| 73 |
traceId: str
|
| 74 |
stepId: str
|
| 75 |
image: str
|
| 76 |
+
duration: float
|
|
|
|
|
|
|
| 77 |
inputTokensUsed: int
|
| 78 |
outputTokensUsed: int
|
| 79 |
+
step_evaluation: Literal["like", "dislike", "neutral"]
|
| 80 |
+
error: Optional[str] = None
|
| 81 |
+
thought: Optional[str] = None
|
| 82 |
+
actions: Optional[list[AgentAction]] = None
|
| 83 |
+
|
| 84 |
+
@field_serializer("actions")
|
| 85 |
def serialize_actions(self, actions: list[AgentAction], _info):
|
| 86 |
"""Convert actions to list of strings when dumping (controlled by context)"""
|
| 87 |
|
| 88 |
+
if _info.context and _info.context.get("actions_as_json", False):
|
| 89 |
return [action.model_dump(mode="json") for action in actions]
|
| 90 |
|
| 91 |
return [action.to_string() for action in actions]
|
|
|
|
| 97 |
traceId: str = ""
|
| 98 |
inputTokensUsed: int = 0
|
| 99 |
outputTokensUsed: int = 0
|
| 100 |
+
duration: float = 0.0 # in seconds
|
| 101 |
numberOfSteps: int = 0
|
| 102 |
+
maxSteps: int = 0
|
| 103 |
|
| 104 |
|
| 105 |
class AgentTrace(BaseModel):
|
|
|
|
| 220 |
self.traceMetadata.traceId = self.message_id
|
| 221 |
os.makedirs(self.trace_path, exist_ok=True)
|
| 222 |
with open(f"{self.trace_path}/tasks.json", "w") as f:
|
| 223 |
+
json.dump(
|
| 224 |
+
self.model_dump(mode="json", context={"actions_as_json": True}),
|
| 225 |
+
f,
|
| 226 |
+
indent=2,
|
| 227 |
+
)
|
| 228 |
|
| 229 |
return self
|
| 230 |
|
|
|
|
| 235 |
status: str
|
| 236 |
timestamp: datetime
|
| 237 |
websocket_connections: int
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
class TaskStatusResponse(BaseModel):
|
| 241 |
+
"""Response for a specific task status"""
|
| 242 |
+
|
| 243 |
+
task_id: str
|
| 244 |
+
status: ActiveTask
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
class ActiveTasksResponse(BaseModel):
|
| 248 |
+
"""Response for active tasks"""
|
| 249 |
+
|
| 250 |
+
active_tasks: dict[str, ActiveTask]
|
| 251 |
+
total_connections: int
|
cua2-core/src/cua2_core/routes/__init__.py
CHANGED
|
@@ -1,2 +1 @@
|
|
| 1 |
"""Routes module for CUA2 Core"""
|
| 2 |
-
|
|
|
|
| 1 |
"""Routes module for CUA2 Core"""
|
|
|
cua2-core/src/cua2_core/routes/routes.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
| 1 |
from datetime import datetime
|
| 2 |
|
| 3 |
-
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 4 |
-
|
| 5 |
# Get services from app state
|
| 6 |
-
from cua2_core.models.models import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from cua2_core.services.agent_service import AgentService
|
| 8 |
from cua2_core.websocket.websocket_manager import WebSocketManager
|
|
|
|
| 9 |
|
| 10 |
# Create router
|
| 11 |
router = APIRouter()
|
|
@@ -33,19 +36,19 @@ async def health_check(
|
|
| 33 |
)
|
| 34 |
|
| 35 |
|
| 36 |
-
@router.get("/tasks")
|
| 37 |
async def get_active_tasks(
|
| 38 |
agent_service: AgentService = Depends(get_agent_service),
|
| 39 |
websocket_manager: WebSocketManager = Depends(get_websocket_manager),
|
| 40 |
):
|
| 41 |
"""Get currently active tasks"""
|
| 42 |
-
return
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
|
| 47 |
|
| 48 |
-
@router.get("/tasks/{task_id}")
|
| 49 |
async def get_task_status(
|
| 50 |
task_id: str, agent_service: AgentService = Depends(get_agent_service)
|
| 51 |
):
|
|
@@ -53,4 +56,4 @@ async def get_task_status(
|
|
| 53 |
task_status = agent_service.get_task_status(task_id)
|
| 54 |
if task_status is None:
|
| 55 |
raise HTTPException(status_code=404, detail="Task not found")
|
| 56 |
-
return
|
|
|
|
| 1 |
from datetime import datetime
|
| 2 |
|
|
|
|
|
|
|
| 3 |
# Get services from app state
|
| 4 |
+
from cua2_core.models.models import (
|
| 5 |
+
ActiveTasksResponse,
|
| 6 |
+
HealthResponse,
|
| 7 |
+
TaskStatusResponse,
|
| 8 |
+
)
|
| 9 |
from cua2_core.services.agent_service import AgentService
|
| 10 |
from cua2_core.websocket.websocket_manager import WebSocketManager
|
| 11 |
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 12 |
|
| 13 |
# Create router
|
| 14 |
router = APIRouter()
|
|
|
|
| 36 |
)
|
| 37 |
|
| 38 |
|
| 39 |
+
@router.get("/tasks", response_model=ActiveTasksResponse)
|
| 40 |
async def get_active_tasks(
|
| 41 |
agent_service: AgentService = Depends(get_agent_service),
|
| 42 |
websocket_manager: WebSocketManager = Depends(get_websocket_manager),
|
| 43 |
):
|
| 44 |
"""Get currently active tasks"""
|
| 45 |
+
return ActiveTasksResponse(
|
| 46 |
+
active_tasks=agent_service.get_active_tasks(),
|
| 47 |
+
total_connections=websocket_manager.get_connection_count(),
|
| 48 |
+
)
|
| 49 |
|
| 50 |
|
| 51 |
+
@router.get("/tasks/{task_id}", response_model=TaskStatusResponse)
|
| 52 |
async def get_task_status(
|
| 53 |
task_id: str, agent_service: AgentService = Depends(get_agent_service)
|
| 54 |
):
|
|
|
|
| 56 |
task_status = agent_service.get_task_status(task_id)
|
| 57 |
if task_status is None:
|
| 58 |
raise HTTPException(status_code=404, detail="Task not found")
|
| 59 |
+
return TaskStatusResponse(task_id=task_id, status=task_status)
|
cua2-core/src/cua2_core/routes/websocket.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
import json
|
| 2 |
|
| 3 |
-
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 4 |
-
|
| 5 |
# Get services from app state
|
| 6 |
from cua2_core.app import app
|
| 7 |
-
from cua2_core.models.models import
|
|
|
|
| 8 |
|
| 9 |
# Create router
|
| 10 |
router = APIRouter()
|
|
@@ -34,7 +33,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 34 |
# Parse the message
|
| 35 |
message_data = json.loads(data)
|
| 36 |
print(f"Received message: {message_data}")
|
| 37 |
-
|
| 38 |
# Check if it's a user task message
|
| 39 |
if message_data.get("type") == "user_task":
|
| 40 |
# Extract and parse the trace
|
|
@@ -43,10 +42,13 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 43 |
# Convert timestamp string to datetime if needed
|
| 44 |
if isinstance(trace_data.get("timestamp"), str):
|
| 45 |
from datetime import datetime
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
| 48 |
trace = AgentTrace(**trace_data)
|
| 49 |
-
|
| 50 |
# Process the user task with the trace
|
| 51 |
trace_id = await agent_service.process_user_task(trace)
|
| 52 |
print(f"Started processing trace: {trace_id}")
|
|
@@ -56,9 +58,9 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 56 |
except json.JSONDecodeError as e:
|
| 57 |
print(f"JSON decode error: {e}")
|
| 58 |
from cua2_core.models.models import AgentErrorEvent
|
|
|
|
| 59 |
error_response = AgentErrorEvent(
|
| 60 |
-
type="agent_error",
|
| 61 |
-
error="Invalid JSON format"
|
| 62 |
)
|
| 63 |
await websocket_manager.send_personal_message(
|
| 64 |
error_response, websocket
|
|
@@ -67,11 +69,12 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 67 |
except Exception as e:
|
| 68 |
print(f"Error processing message: {e}")
|
| 69 |
import traceback
|
|
|
|
| 70 |
traceback.print_exc()
|
| 71 |
from cua2_core.models.models import AgentErrorEvent
|
|
|
|
| 72 |
error_response = AgentErrorEvent(
|
| 73 |
-
type="agent_error",
|
| 74 |
-
error=f"Error processing message: {str(e)}"
|
| 75 |
)
|
| 76 |
await websocket_manager.send_personal_message(
|
| 77 |
error_response, websocket
|
|
|
|
| 1 |
import json
|
| 2 |
|
|
|
|
|
|
|
| 3 |
# Get services from app state
|
| 4 |
from cua2_core.app import app
|
| 5 |
+
from cua2_core.models.models import AgentTrace, HeartbeatEvent
|
| 6 |
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 7 |
|
| 8 |
# Create router
|
| 9 |
router = APIRouter()
|
|
|
|
| 33 |
# Parse the message
|
| 34 |
message_data = json.loads(data)
|
| 35 |
print(f"Received message: {message_data}")
|
| 36 |
+
|
| 37 |
# Check if it's a user task message
|
| 38 |
if message_data.get("type") == "user_task":
|
| 39 |
# Extract and parse the trace
|
|
|
|
| 42 |
# Convert timestamp string to datetime if needed
|
| 43 |
if isinstance(trace_data.get("timestamp"), str):
|
| 44 |
from datetime import datetime
|
| 45 |
+
|
| 46 |
+
trace_data["timestamp"] = datetime.fromisoformat(
|
| 47 |
+
trace_data["timestamp"].replace("Z", "+00:00")
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
trace = AgentTrace(**trace_data)
|
| 51 |
+
|
| 52 |
# Process the user task with the trace
|
| 53 |
trace_id = await agent_service.process_user_task(trace)
|
| 54 |
print(f"Started processing trace: {trace_id}")
|
|
|
|
| 58 |
except json.JSONDecodeError as e:
|
| 59 |
print(f"JSON decode error: {e}")
|
| 60 |
from cua2_core.models.models import AgentErrorEvent
|
| 61 |
+
|
| 62 |
error_response = AgentErrorEvent(
|
| 63 |
+
type="agent_error", error="Invalid JSON format"
|
|
|
|
| 64 |
)
|
| 65 |
await websocket_manager.send_personal_message(
|
| 66 |
error_response, websocket
|
|
|
|
| 69 |
except Exception as e:
|
| 70 |
print(f"Error processing message: {e}")
|
| 71 |
import traceback
|
| 72 |
+
|
| 73 |
traceback.print_exc()
|
| 74 |
from cua2_core.models.models import AgentErrorEvent
|
| 75 |
+
|
| 76 |
error_response = AgentErrorEvent(
|
| 77 |
+
type="agent_error", error=f"Error processing message: {str(e)}"
|
|
|
|
| 78 |
)
|
| 79 |
await websocket_manager.send_personal_message(
|
| 80 |
error_response, websocket
|
cua2-core/src/cua2_core/services/__init__.py
CHANGED
|
@@ -1,2 +1 @@
|
|
| 1 |
"""Services module for CUA2 Core"""
|
| 2 |
-
|
|
|
|
| 1 |
"""Services module for CUA2 Core"""
|
|
|
cua2-core/src/cua2_core/services/agent_service.py
CHANGED
|
@@ -1,20 +1,19 @@
|
|
| 1 |
import asyncio
|
| 2 |
-
import json
|
| 3 |
import base64
|
| 4 |
-
|
| 5 |
from pathlib import Path
|
| 6 |
from typing import Optional
|
| 7 |
|
| 8 |
from cua2_core.models.models import (
|
| 9 |
ActiveTask,
|
| 10 |
-
AgentTrace,
|
| 11 |
-
AgentStep,
|
| 12 |
AgentAction,
|
| 13 |
-
AgentTraceMetadata,
|
| 14 |
-
AgentStartEvent,
|
| 15 |
-
AgentProgressEvent,
|
| 16 |
AgentCompleteEvent,
|
| 17 |
AgentErrorEvent,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
VncUrlSetEvent,
|
| 19 |
VncUrlUnsetEvent,
|
| 20 |
)
|
|
@@ -27,8 +26,12 @@ class AgentService:
|
|
| 27 |
def __init__(self, websocket_manager):
|
| 28 |
self.active_tasks: dict[str, ActiveTask] = {}
|
| 29 |
self.websocket_manager: WebSocketManager = websocket_manager
|
| 30 |
-
self.simulation_data_path =
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
async def process_user_task(self, trace: AgentTrace) -> str:
|
| 34 |
"""Process a user task and return the trace ID"""
|
|
@@ -48,59 +51,52 @@ class AgentService:
|
|
| 48 |
)
|
| 49 |
|
| 50 |
# Start the agent processing in the background
|
| 51 |
-
asyncio.create_task(
|
| 52 |
-
self._simulate_agent_processing(trace)
|
| 53 |
-
)
|
| 54 |
|
| 55 |
return trace_id
|
| 56 |
|
| 57 |
-
|
| 58 |
async def _simulate_agent_processing(self, trace: AgentTrace):
|
| 59 |
"""Simulate agent processing using simulated_trace.json data"""
|
| 60 |
trace_id = trace.id
|
| 61 |
-
|
| 62 |
try:
|
| 63 |
# Load simulation data
|
| 64 |
-
with open(self.simulation_data_path,
|
| 65 |
simulation_data = json.load(f)
|
| 66 |
-
|
| 67 |
# Send agent start event with the initial trace
|
| 68 |
-
start_event = AgentStartEvent(
|
| 69 |
-
type="agent_start",
|
| 70 |
-
agentTrace=trace
|
| 71 |
-
)
|
| 72 |
await self.websocket_manager.broadcast(start_event)
|
| 73 |
|
| 74 |
# mock VNC URL
|
| 75 |
vnc_url = "https://www.youtube.com/embed/VCutEsRSJ5A?si=PT0ETJ7zIJ9ywhGW"
|
| 76 |
-
vnc_set_event = VncUrlSetEvent(
|
| 77 |
-
type="vnc_url_set",
|
| 78 |
-
vncUrl=vnc_url
|
| 79 |
-
)
|
| 80 |
await self.websocket_manager.broadcast(vnc_set_event)
|
| 81 |
|
| 82 |
-
trace_metadata = AgentTraceMetadata(traceId=trace_id)
|
| 83 |
-
|
| 84 |
# Process each step from the simulation data
|
| 85 |
for step_data in simulation_data["steps"]:
|
| 86 |
# Wait before sending the next step to simulate processing time
|
| 87 |
-
await asyncio.sleep(step_data["
|
| 88 |
-
|
| 89 |
# Load and encode the image
|
| 90 |
-
image_path =
|
| 91 |
-
|
|
|
|
|
|
|
| 92 |
image_bytes = img_file.read()
|
| 93 |
image_base64 = f"data:image/png;base64,{base64.b64encode(image_bytes).decode('utf-8')}"
|
| 94 |
-
|
| 95 |
# Convert actions to AgentAction objects
|
| 96 |
actions = [
|
| 97 |
AgentAction(
|
| 98 |
actionType=action["actionType"],
|
| 99 |
-
actionArguments=action["actionArguments"]
|
| 100 |
)
|
| 101 |
for action in step_data["actions"]
|
| 102 |
]
|
| 103 |
-
|
| 104 |
# Create agent step
|
| 105 |
agent_step = AgentStep(
|
| 106 |
traceId=trace_id,
|
|
@@ -108,57 +104,55 @@ class AgentService:
|
|
| 108 |
image=image_base64,
|
| 109 |
thought=step_data["thought"],
|
| 110 |
actions=actions,
|
| 111 |
-
|
|
|
|
| 112 |
inputTokensUsed=step_data["inputTokensUsed"],
|
| 113 |
outputTokensUsed=step_data["outputTokensUsed"],
|
| 114 |
-
|
| 115 |
-
step_evaluation=step_data["step_evaluation"]
|
| 116 |
)
|
| 117 |
|
| 118 |
trace_metadata.numberOfSteps += 1
|
| 119 |
-
trace_metadata.
|
| 120 |
trace_metadata.inputTokensUsed += step_data["inputTokensUsed"]
|
| 121 |
trace_metadata.outputTokensUsed += step_data["outputTokensUsed"]
|
| 122 |
-
|
| 123 |
# Send progress event
|
| 124 |
progress_event = AgentProgressEvent(
|
| 125 |
type="agent_progress",
|
| 126 |
agentStep=agent_step,
|
| 127 |
-
traceMetadata=trace_metadata
|
| 128 |
)
|
| 129 |
await self.websocket_manager.broadcast(progress_event)
|
| 130 |
-
|
| 131 |
# Update active task
|
| 132 |
self.active_tasks[trace_id].steps.append(agent_step)
|
| 133 |
-
|
| 134 |
# Unset VNC URL before completion
|
| 135 |
vnc_unset_event = VncUrlUnsetEvent(type="vnc_url_unset")
|
| 136 |
await self.websocket_manager.broadcast(vnc_unset_event)
|
| 137 |
-
|
| 138 |
# Send completion event
|
| 139 |
complete_event = AgentCompleteEvent(
|
| 140 |
-
type="agent_complete",
|
| 141 |
-
traceMetadata=trace_metadata
|
| 142 |
)
|
| 143 |
await self.websocket_manager.broadcast(complete_event)
|
| 144 |
-
|
| 145 |
# Update active task with final metadata
|
| 146 |
self.active_tasks[trace_id].traceMetadata = trace_metadata
|
| 147 |
-
|
| 148 |
# Clean up after a delay
|
| 149 |
await asyncio.sleep(1)
|
| 150 |
if trace_id in self.active_tasks:
|
| 151 |
del self.active_tasks[trace_id]
|
| 152 |
-
|
| 153 |
except Exception as e:
|
| 154 |
print(f"Error in agent simulation: {str(e)}")
|
| 155 |
# Send error event
|
| 156 |
error_event = AgentErrorEvent(
|
| 157 |
-
type="agent_error",
|
| 158 |
-
error=f"Error processing task: {str(e)}"
|
| 159 |
)
|
| 160 |
await self.websocket_manager.broadcast(error_event)
|
| 161 |
-
|
| 162 |
# Clean up
|
| 163 |
if trace_id in self.active_tasks:
|
| 164 |
del self.active_tasks[trace_id]
|
|
|
|
| 1 |
import asyncio
|
|
|
|
| 2 |
import base64
|
| 3 |
+
import json
|
| 4 |
from pathlib import Path
|
| 5 |
from typing import Optional
|
| 6 |
|
| 7 |
from cua2_core.models.models import (
|
| 8 |
ActiveTask,
|
|
|
|
|
|
|
| 9 |
AgentAction,
|
|
|
|
|
|
|
|
|
|
| 10 |
AgentCompleteEvent,
|
| 11 |
AgentErrorEvent,
|
| 12 |
+
AgentProgressEvent,
|
| 13 |
+
AgentStartEvent,
|
| 14 |
+
AgentStep,
|
| 15 |
+
AgentTrace,
|
| 16 |
+
AgentTraceMetadata,
|
| 17 |
VncUrlSetEvent,
|
| 18 |
VncUrlUnsetEvent,
|
| 19 |
)
|
|
|
|
| 26 |
def __init__(self, websocket_manager):
|
| 27 |
self.active_tasks: dict[str, ActiveTask] = {}
|
| 28 |
self.websocket_manager: WebSocketManager = websocket_manager
|
| 29 |
+
self.simulation_data_path = (
|
| 30 |
+
Path(__file__).parent / "simulation_metadata" / "simulated_trace.json"
|
| 31 |
+
)
|
| 32 |
+
self.simulation_images_path = (
|
| 33 |
+
Path(__file__).parent / "simulation_metadata" / "images"
|
| 34 |
+
)
|
| 35 |
|
| 36 |
async def process_user_task(self, trace: AgentTrace) -> str:
|
| 37 |
"""Process a user task and return the trace ID"""
|
|
|
|
| 51 |
)
|
| 52 |
|
| 53 |
# Start the agent processing in the background
|
| 54 |
+
asyncio.create_task(self._simulate_agent_processing(trace))
|
|
|
|
|
|
|
| 55 |
|
| 56 |
return trace_id
|
| 57 |
|
|
|
|
| 58 |
async def _simulate_agent_processing(self, trace: AgentTrace):
|
| 59 |
"""Simulate agent processing using simulated_trace.json data"""
|
| 60 |
trace_id = trace.id
|
| 61 |
+
|
| 62 |
try:
|
| 63 |
# Load simulation data
|
| 64 |
+
with open(self.simulation_data_path, "r") as f:
|
| 65 |
simulation_data = json.load(f)
|
| 66 |
+
|
| 67 |
# Send agent start event with the initial trace
|
| 68 |
+
start_event = AgentStartEvent(type="agent_start", agentTrace=trace)
|
|
|
|
|
|
|
|
|
|
| 69 |
await self.websocket_manager.broadcast(start_event)
|
| 70 |
|
| 71 |
# mock VNC URL
|
| 72 |
vnc_url = "https://www.youtube.com/embed/VCutEsRSJ5A?si=PT0ETJ7zIJ9ywhGW"
|
| 73 |
+
vnc_set_event = VncUrlSetEvent(type="vnc_url_set", vncUrl=vnc_url)
|
|
|
|
|
|
|
|
|
|
| 74 |
await self.websocket_manager.broadcast(vnc_set_event)
|
| 75 |
|
| 76 |
+
trace_metadata = AgentTraceMetadata(traceId=trace_id, maxSteps=20)
|
| 77 |
+
|
| 78 |
# Process each step from the simulation data
|
| 79 |
for step_data in simulation_data["steps"]:
|
| 80 |
# Wait before sending the next step to simulate processing time
|
| 81 |
+
await asyncio.sleep(step_data["duration"])
|
| 82 |
+
|
| 83 |
# Load and encode the image
|
| 84 |
+
image_path = (
|
| 85 |
+
self.simulation_images_path / step_data["image"].split("/")[-1]
|
| 86 |
+
)
|
| 87 |
+
with open(image_path, "rb") as img_file:
|
| 88 |
image_bytes = img_file.read()
|
| 89 |
image_base64 = f"data:image/png;base64,{base64.b64encode(image_bytes).decode('utf-8')}"
|
| 90 |
+
|
| 91 |
# Convert actions to AgentAction objects
|
| 92 |
actions = [
|
| 93 |
AgentAction(
|
| 94 |
actionType=action["actionType"],
|
| 95 |
+
actionArguments=action["actionArguments"],
|
| 96 |
)
|
| 97 |
for action in step_data["actions"]
|
| 98 |
]
|
| 99 |
+
|
| 100 |
# Create agent step
|
| 101 |
agent_step = AgentStep(
|
| 102 |
traceId=trace_id,
|
|
|
|
| 104 |
image=image_base64,
|
| 105 |
thought=step_data["thought"],
|
| 106 |
actions=actions,
|
| 107 |
+
error="",
|
| 108 |
+
duration=step_data["duration"],
|
| 109 |
inputTokensUsed=step_data["inputTokensUsed"],
|
| 110 |
outputTokensUsed=step_data["outputTokensUsed"],
|
| 111 |
+
step_evaluation=step_data["step_evaluation"],
|
|
|
|
| 112 |
)
|
| 113 |
|
| 114 |
trace_metadata.numberOfSteps += 1
|
| 115 |
+
trace_metadata.duration += step_data["duration"]
|
| 116 |
trace_metadata.inputTokensUsed += step_data["inputTokensUsed"]
|
| 117 |
trace_metadata.outputTokensUsed += step_data["outputTokensUsed"]
|
| 118 |
+
|
| 119 |
# Send progress event
|
| 120 |
progress_event = AgentProgressEvent(
|
| 121 |
type="agent_progress",
|
| 122 |
agentStep=agent_step,
|
| 123 |
+
traceMetadata=trace_metadata,
|
| 124 |
)
|
| 125 |
await self.websocket_manager.broadcast(progress_event)
|
| 126 |
+
|
| 127 |
# Update active task
|
| 128 |
self.active_tasks[trace_id].steps.append(agent_step)
|
| 129 |
+
|
| 130 |
# Unset VNC URL before completion
|
| 131 |
vnc_unset_event = VncUrlUnsetEvent(type="vnc_url_unset")
|
| 132 |
await self.websocket_manager.broadcast(vnc_unset_event)
|
| 133 |
+
|
| 134 |
# Send completion event
|
| 135 |
complete_event = AgentCompleteEvent(
|
| 136 |
+
type="agent_complete", traceMetadata=trace_metadata
|
|
|
|
| 137 |
)
|
| 138 |
await self.websocket_manager.broadcast(complete_event)
|
| 139 |
+
|
| 140 |
# Update active task with final metadata
|
| 141 |
self.active_tasks[trace_id].traceMetadata = trace_metadata
|
| 142 |
+
|
| 143 |
# Clean up after a delay
|
| 144 |
await asyncio.sleep(1)
|
| 145 |
if trace_id in self.active_tasks:
|
| 146 |
del self.active_tasks[trace_id]
|
| 147 |
+
|
| 148 |
except Exception as e:
|
| 149 |
print(f"Error in agent simulation: {str(e)}")
|
| 150 |
# Send error event
|
| 151 |
error_event = AgentErrorEvent(
|
| 152 |
+
type="agent_error", error=f"Error processing task: {str(e)}"
|
|
|
|
| 153 |
)
|
| 154 |
await self.websocket_manager.broadcast(error_event)
|
| 155 |
+
|
| 156 |
# Clean up
|
| 157 |
if trace_id in self.active_tasks:
|
| 158 |
del self.active_tasks[trace_id]
|
cua2-core/src/cua2_core/services/simulation_metadata/simulated_trace.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
| 13 |
}
|
| 14 |
}
|
| 15 |
],
|
| 16 |
-
"
|
| 17 |
"inputTokensUsed": 1250,
|
| 18 |
"outputTokensUsed": 85,
|
| 19 |
"timestamp": "2025-10-17T14:30:02.300Z",
|
|
@@ -32,7 +32,7 @@
|
|
| 32 |
}
|
| 33 |
}
|
| 34 |
],
|
| 35 |
-
"
|
| 36 |
"inputTokensUsed": 1180,
|
| 37 |
"outputTokensUsed": 72,
|
| 38 |
"timestamp": "2025-10-17T14:30:04.100Z",
|
|
@@ -51,7 +51,7 @@
|
|
| 51 |
}
|
| 52 |
}
|
| 53 |
],
|
| 54 |
-
"
|
| 55 |
"inputTokensUsed": 1100,
|
| 56 |
"outputTokensUsed": 68,
|
| 57 |
"timestamp": "2025-10-17T14:30:05.600Z",
|
|
@@ -59,4 +59,3 @@
|
|
| 59 |
}
|
| 60 |
]
|
| 61 |
}
|
| 62 |
-
|
|
|
|
| 13 |
}
|
| 14 |
}
|
| 15 |
],
|
| 16 |
+
"duration": 2.3,
|
| 17 |
"inputTokensUsed": 1250,
|
| 18 |
"outputTokensUsed": 85,
|
| 19 |
"timestamp": "2025-10-17T14:30:02.300Z",
|
|
|
|
| 32 |
}
|
| 33 |
}
|
| 34 |
],
|
| 35 |
+
"duration": 1.8,
|
| 36 |
"inputTokensUsed": 1180,
|
| 37 |
"outputTokensUsed": 72,
|
| 38 |
"timestamp": "2025-10-17T14:30:04.100Z",
|
|
|
|
| 51 |
}
|
| 52 |
}
|
| 53 |
],
|
| 54 |
+
"duration": 1.5,
|
| 55 |
"inputTokensUsed": 1100,
|
| 56 |
"outputTokensUsed": 68,
|
| 57 |
"timestamp": "2025-10-17T14:30:05.600Z",
|
|
|
|
| 59 |
}
|
| 60 |
]
|
| 61 |
}
|
|
|
cua2-core/src/cua2_core/websocket/__init__.py
CHANGED
|
@@ -1,2 +1 @@
|
|
| 1 |
"""WebSocket module for CUA2 Core"""
|
| 2 |
-
|
|
|
|
| 1 |
"""WebSocket module for CUA2 Core"""
|
|
|
cua2-core/src/cua2_core/websocket/websocket_manager.py
CHANGED
|
@@ -2,9 +2,8 @@ import asyncio
|
|
| 2 |
import json
|
| 3 |
from typing import Dict, Optional, Set
|
| 4 |
|
| 5 |
-
from fastapi import WebSocket
|
| 6 |
-
|
| 7 |
from cua2_core.models.models import AgentTraceMetadata, WebSocketEvent
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
class WebSocketManager:
|
|
@@ -77,7 +76,10 @@ class WebSocketManager:
|
|
| 77 |
await self.broadcast(event)
|
| 78 |
|
| 79 |
async def send_agent_complete(
|
| 80 |
-
self,
|
|
|
|
|
|
|
|
|
|
| 81 |
):
|
| 82 |
"""Send agent complete event"""
|
| 83 |
event = WebSocketEvent(
|
|
|
|
| 2 |
import json
|
| 3 |
from typing import Dict, Optional, Set
|
| 4 |
|
|
|
|
|
|
|
| 5 |
from cua2_core.models.models import AgentTraceMetadata, WebSocketEvent
|
| 6 |
+
from fastapi import WebSocket
|
| 7 |
|
| 8 |
|
| 9 |
class WebSocketManager:
|
|
|
|
| 76 |
await self.broadcast(event)
|
| 77 |
|
| 78 |
async def send_agent_complete(
|
| 79 |
+
self,
|
| 80 |
+
content: str,
|
| 81 |
+
message_id: str,
|
| 82 |
+
metadata: Optional[AgentTraceMetadata] = None,
|
| 83 |
):
|
| 84 |
"""Send agent complete event"""
|
| 85 |
event = WebSocketEvent(
|
cua2-front/eslint.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from "@eslint/js";
|
| 2 |
+
import globals from "globals";
|
| 3 |
+
import reactHooks from "eslint-plugin-react-hooks";
|
| 4 |
+
import reactRefresh from "eslint-plugin-react-refresh";
|
| 5 |
+
import tseslint from "typescript-eslint";
|
| 6 |
+
|
| 7 |
+
export default tseslint.config(
|
| 8 |
+
{ ignores: ["dist"] },
|
| 9 |
+
{
|
| 10 |
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
| 11 |
+
files: ["**/*.{ts,tsx}"],
|
| 12 |
+
languageOptions: {
|
| 13 |
+
ecmaVersion: 2020,
|
| 14 |
+
globals: globals.browser,
|
| 15 |
+
},
|
| 16 |
+
plugins: {
|
| 17 |
+
"react-hooks": reactHooks,
|
| 18 |
+
"react-refresh": reactRefresh,
|
| 19 |
+
},
|
| 20 |
+
rules: {
|
| 21 |
+
...reactHooks.configs.recommended.rules,
|
| 22 |
+
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
| 23 |
+
"@typescript-eslint/no-unused-vars": "off",
|
| 24 |
+
},
|
| 25 |
+
},
|
| 26 |
+
);
|
cua2-front/index.html
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
-
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<title>CUA2</title>
|
| 8 |
</head>
|
|
@@ -11,4 +10,3 @@
|
|
| 11 |
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
</body>
|
| 13 |
</html>
|
| 14 |
-
|
|
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>CUA2</title>
|
| 7 |
</head>
|
|
|
|
| 10 |
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
</body>
|
| 12 |
</html>
|
|
|
cua2-front/package-lock.json
CHANGED
|
@@ -10,10 +10,11 @@
|
|
| 10 |
"dependencies": {
|
| 11 |
"react": "^18.3.1",
|
| 12 |
"react-dom": "^18.3.1",
|
| 13 |
-
"react-router-dom": "^6.30.1"
|
|
|
|
| 14 |
},
|
| 15 |
"devDependencies": {
|
| 16 |
-
"@eslint/js": "^9.
|
| 17 |
"@types/node": "^22.16.5",
|
| 18 |
"@types/react": "^18.3.23",
|
| 19 |
"@types/react-dom": "^18.3.7",
|
|
@@ -539,9 +540,9 @@
|
|
| 539 |
}
|
| 540 |
},
|
| 541 |
"node_modules/@eslint/js": {
|
| 542 |
-
"version": "9.
|
| 543 |
-
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.
|
| 544 |
-
"integrity": "sha512-
|
| 545 |
"dev": true,
|
| 546 |
"license": "MIT",
|
| 547 |
"engines": {
|
|
@@ -2020,6 +2021,19 @@
|
|
| 2020 |
"url": "https://opencollective.com/eslint"
|
| 2021 |
}
|
| 2022 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2023 |
"node_modules/espree": {
|
| 2024 |
"version": "10.4.0",
|
| 2025 |
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
|
@@ -3027,6 +3041,15 @@
|
|
| 3027 |
"typescript": ">=4.8.4 <6.0.0"
|
| 3028 |
}
|
| 3029 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3030 |
"node_modules/undici-types": {
|
| 3031 |
"version": "6.21.0",
|
| 3032 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
|
|
|
| 10 |
"dependencies": {
|
| 11 |
"react": "^18.3.1",
|
| 12 |
"react-dom": "^18.3.1",
|
| 13 |
+
"react-router-dom": "^6.30.1",
|
| 14 |
+
"ulid": "^3.0.1"
|
| 15 |
},
|
| 16 |
"devDependencies": {
|
| 17 |
+
"@eslint/js": "^9.38.0",
|
| 18 |
"@types/node": "^22.16.5",
|
| 19 |
"@types/react": "^18.3.23",
|
| 20 |
"@types/react-dom": "^18.3.7",
|
|
|
|
| 540 |
}
|
| 541 |
},
|
| 542 |
"node_modules/@eslint/js": {
|
| 543 |
+
"version": "9.38.0",
|
| 544 |
+
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
|
| 545 |
+
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
|
| 546 |
"dev": true,
|
| 547 |
"license": "MIT",
|
| 548 |
"engines": {
|
|
|
|
| 2021 |
"url": "https://opencollective.com/eslint"
|
| 2022 |
}
|
| 2023 |
},
|
| 2024 |
+
"node_modules/eslint/node_modules/@eslint/js": {
|
| 2025 |
+
"version": "9.37.0",
|
| 2026 |
+
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
|
| 2027 |
+
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
|
| 2028 |
+
"dev": true,
|
| 2029 |
+
"license": "MIT",
|
| 2030 |
+
"engines": {
|
| 2031 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 2032 |
+
},
|
| 2033 |
+
"funding": {
|
| 2034 |
+
"url": "https://eslint.org/donate"
|
| 2035 |
+
}
|
| 2036 |
+
},
|
| 2037 |
"node_modules/espree": {
|
| 2038 |
"version": "10.4.0",
|
| 2039 |
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
|
|
|
| 3041 |
"typescript": ">=4.8.4 <6.0.0"
|
| 3042 |
}
|
| 3043 |
},
|
| 3044 |
+
"node_modules/ulid": {
|
| 3045 |
+
"version": "3.0.1",
|
| 3046 |
+
"resolved": "https://registry.npmjs.org/ulid/-/ulid-3.0.1.tgz",
|
| 3047 |
+
"integrity": "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q==",
|
| 3048 |
+
"license": "MIT",
|
| 3049 |
+
"bin": {
|
| 3050 |
+
"ulid": "dist/cli.js"
|
| 3051 |
+
}
|
| 3052 |
+
},
|
| 3053 |
"node_modules/undici-types": {
|
| 3054 |
"version": "6.21.0",
|
| 3055 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
cua2-front/package.json
CHANGED
|
@@ -7,17 +7,18 @@
|
|
| 7 |
"dev": "vite",
|
| 8 |
"build": "vite build",
|
| 9 |
"build:dev": "vite build --mode development",
|
| 10 |
-
"lint": "eslint src/ --config
|
| 11 |
-
"type-check": "tsc --noEmit --project
|
| 12 |
"preview": "vite preview"
|
| 13 |
},
|
| 14 |
"dependencies": {
|
| 15 |
"react": "^18.3.1",
|
| 16 |
"react-router-dom": "^6.30.1",
|
| 17 |
-
"react-dom": "^18.3.1"
|
|
|
|
| 18 |
},
|
| 19 |
"devDependencies": {
|
| 20 |
-
"@eslint/js": "^9.
|
| 21 |
"@types/node": "^22.16.5",
|
| 22 |
"@types/react": "^18.3.23",
|
| 23 |
"@types/react-dom": "^18.3.7",
|
|
|
|
| 7 |
"dev": "vite",
|
| 8 |
"build": "vite build",
|
| 9 |
"build:dev": "vite build --mode development",
|
| 10 |
+
"lint": "eslint src/ --config eslint.config.js",
|
| 11 |
+
"type-check": "tsc --noEmit --project tsconfig.json",
|
| 12 |
"preview": "vite preview"
|
| 13 |
},
|
| 14 |
"dependencies": {
|
| 15 |
"react": "^18.3.1",
|
| 16 |
"react-router-dom": "^6.30.1",
|
| 17 |
+
"react-dom": "^18.3.1",
|
| 18 |
+
"ulid": "^3.0.1"
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
| 21 |
+
"@eslint/js": "^9.38.0",
|
| 22 |
"@types/node": "^22.16.5",
|
| 23 |
"@types/react": "^18.3.23",
|
| 24 |
"@types/react-dom": "^18.3.7",
|
cua2-front/src/App.tsx
CHANGED
|
@@ -9,7 +9,6 @@ const App = () => (
|
|
| 9 |
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
| 10 |
</Routes>
|
| 11 |
</BrowserRouter>
|
| 12 |
-
|
| 13 |
);
|
| 14 |
|
| 15 |
export default App;
|
|
|
|
| 9 |
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
| 10 |
</Routes>
|
| 11 |
</BrowserRouter>
|
|
|
|
| 12 |
);
|
| 13 |
|
| 14 |
export default App;
|
cua2-front/src/component/poc/ConnectionStatus.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface ConnectionStatusProps {
|
| 4 |
+
isConnected: boolean;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ isConnected }) => {
|
| 8 |
+
return (
|
| 9 |
+
<div style={{
|
| 10 |
+
display: 'flex',
|
| 11 |
+
alignItems: 'center',
|
| 12 |
+
gap: '8px',
|
| 13 |
+
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
| 14 |
+
padding: '8px 16px',
|
| 15 |
+
borderRadius: '20px',
|
| 16 |
+
backdropFilter: 'blur(10px)',
|
| 17 |
+
border: '1px solid rgba(255, 255, 255, 0.3)'
|
| 18 |
+
}}>
|
| 19 |
+
<div style={{
|
| 20 |
+
width: '8px',
|
| 21 |
+
height: '8px',
|
| 22 |
+
borderRadius: '50%',
|
| 23 |
+
backgroundColor: isConnected ? '#10b981' : '#ef4444',
|
| 24 |
+
boxShadow: isConnected ? '0 0 8px #10b981' : '0 0 8px #ef4444',
|
| 25 |
+
animation: isConnected ? 'pulse 2s infinite' : 'none'
|
| 26 |
+
}}></div>
|
| 27 |
+
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
| 28 |
+
<span className="text-xs font-semibold text-white" style={{ lineHeight: '1.2' }}>
|
| 29 |
+
{isConnected ? 'Connected' : 'Disconnected'}
|
| 30 |
+
</span>
|
| 31 |
+
<span className="text-xs text-white" style={{ opacity: 0.7, fontSize: '10px', lineHeight: '1.2' }}>
|
| 32 |
+
WebSocket
|
| 33 |
+
</span>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
);
|
| 37 |
+
};
|
cua2-front/src/component/poc/Header.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { ConnectionStatus } from './ConnectionStatus';
|
| 3 |
+
import { ProcessingIndicator } from './ProcessingIndicator';
|
| 4 |
+
import { TaskButton } from './TaskButton';
|
| 5 |
+
|
| 6 |
+
interface HeaderProps {
|
| 7 |
+
isConnected: boolean;
|
| 8 |
+
isAgentProcessing: boolean;
|
| 9 |
+
onSendTask: (content: string, modelId: string) => void;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export const Header: React.FC<HeaderProps> = ({ isConnected, isAgentProcessing, onSendTask }) => {
|
| 13 |
+
return (
|
| 14 |
+
<>
|
| 15 |
+
<div style={{
|
| 16 |
+
flexShrink: 0,
|
| 17 |
+
}}>
|
| 18 |
+
<div style={{ maxWidth: '1400px', margin: '0 auto', padding: '20px 32px' }}>
|
| 19 |
+
<div className="flex items-center justify-between">
|
| 20 |
+
<div className="flex items-center gap-6">
|
| 21 |
+
<ConnectionStatus isConnected={isConnected} />
|
| 22 |
+
<h1 className="text-3xl font-bold text-white" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
|
| 23 |
+
CUA2 Agent
|
| 24 |
+
</h1>
|
| 25 |
+
</div>
|
| 26 |
+
<ProcessingIndicator isAgentProcessing={isAgentProcessing} />
|
| 27 |
+
</div>
|
| 28 |
+
<TaskButton
|
| 29 |
+
isAgentProcessing={isAgentProcessing}
|
| 30 |
+
isConnected={isConnected}
|
| 31 |
+
onSendTask={onSendTask}
|
| 32 |
+
/>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<style>{`
|
| 37 |
+
@keyframes spin {
|
| 38 |
+
to { transform: rotate(360deg); }
|
| 39 |
+
}
|
| 40 |
+
@keyframes pulse {
|
| 41 |
+
0%, 100% { opacity: 1; }
|
| 42 |
+
50% { opacity: 0.5; }
|
| 43 |
+
}
|
| 44 |
+
`}</style>
|
| 45 |
+
</>
|
| 46 |
+
);
|
| 47 |
+
};
|
cua2-front/src/component/poc/Metadata.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AgentTrace } from '@/types/agent';
|
| 2 |
+
import React from 'react';
|
| 3 |
+
|
| 4 |
+
interface MetadataProps {
|
| 5 |
+
trace?: AgentTrace;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export const Metadata: React.FC<MetadataProps> = ({ trace }) => {
|
| 9 |
+
return (
|
| 10 |
+
<div style={{ flexShrink: 0 }} className="bg-white rounded-lg shadow-md border border-gray-200 p-5">
|
| 11 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-4">Metadata</h3>
|
| 12 |
+
{trace?.metadata ? (
|
| 13 |
+
<div style={{ display: 'flex', gap: '12px' }}>
|
| 14 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '8px', padding: '12px', border: '1px solid #bfdbfe', boxShadow: '0 2px 4px rgba(59, 130, 246, 0.1)' }}>
|
| 15 |
+
<span style={{ fontSize: '10px', fontWeight: 600, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Time</span>
|
| 16 |
+
<span style={{ fontSize: '24px', fontWeight: 700, color: '#1e40af' }}>{trace.metadata.duration.toFixed(2)}s</span>
|
| 17 |
+
</div>
|
| 18 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '8px', padding: '12px', border: '1px solid #bbf7d0', boxShadow: '0 2px 4px rgba(16, 185, 129, 0.1)' }}>
|
| 19 |
+
<span style={{ fontSize: '10px', fontWeight: 600, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>In Tokens</span>
|
| 20 |
+
<span style={{ fontSize: '24px', fontWeight: 700, color: '#166534' }}>{trace.metadata.inputTokensUsed.toLocaleString()}</span>
|
| 21 |
+
</div>
|
| 22 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '8px', padding: '12px', border: '1px solid #e9d5ff', boxShadow: '0 2px 4px rgba(139, 92, 246, 0.1)' }}>
|
| 23 |
+
<span style={{ fontSize: '10px', fontWeight: 600, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Out Tokens</span>
|
| 24 |
+
<span style={{ fontSize: '24px', fontWeight: 700, color: '#6b21a8' }}>{trace.metadata.outputTokensUsed.toLocaleString()}</span>
|
| 25 |
+
</div>
|
| 26 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#fff7ed', borderRadius: '8px', padding: '12px', border: '1px solid #fed7aa', boxShadow: '0 2px 4px rgba(249, 115, 22, 0.1)' }}>
|
| 27 |
+
<span style={{ fontSize: '10px', fontWeight: 600, color: '#ea580c', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Steps</span>
|
| 28 |
+
<span style={{ fontSize: '24px', fontWeight: 700, color: '#c2410c' }}>{trace.metadata.numberOfSteps}</span>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
) : (
|
| 32 |
+
<div className="text-gray-400 text-sm py-2">
|
| 33 |
+
{trace ? 'Waiting for completion...' : 'No task started yet'}
|
| 34 |
+
</div>
|
| 35 |
+
)}
|
| 36 |
+
</div>
|
| 37 |
+
);
|
| 38 |
+
};
|
cua2-front/src/component/poc/ProcessingIndicator.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface ProcessingIndicatorProps {
|
| 4 |
+
isAgentProcessing: boolean;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export const ProcessingIndicator: React.FC<ProcessingIndicatorProps> = ({ isAgentProcessing }) => {
|
| 8 |
+
if (!isAgentProcessing) return null;
|
| 9 |
+
|
| 10 |
+
return (
|
| 11 |
+
<div style={{
|
| 12 |
+
display: 'flex',
|
| 13 |
+
alignItems: 'center',
|
| 14 |
+
gap: '10px',
|
| 15 |
+
padding: '10px 20px',
|
| 16 |
+
backgroundColor: 'rgba(251, 191, 36, 0.2)',
|
| 17 |
+
borderRadius: '10px',
|
| 18 |
+
border: '1px solid rgba(251, 191, 36, 0.4)'
|
| 19 |
+
}}>
|
| 20 |
+
<span style={{
|
| 21 |
+
width: '16px',
|
| 22 |
+
height: '16px',
|
| 23 |
+
border: '2px solid #fbbf24',
|
| 24 |
+
borderTopColor: 'transparent',
|
| 25 |
+
borderRadius: '50%',
|
| 26 |
+
animation: 'spin 1s linear infinite',
|
| 27 |
+
display: 'inline-block'
|
| 28 |
+
}}></span>
|
| 29 |
+
<span style={{ fontSize: '14px', fontWeight: 600, color: '#fbbf24', letterSpacing: '0.5px' }}>
|
| 30 |
+
PROCESSING...
|
| 31 |
+
</span>
|
| 32 |
+
</div>
|
| 33 |
+
);
|
| 34 |
+
};
|
cua2-front/src/component/poc/StackSteps.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AgentTrace } from '@/types/agent';
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { StepCard } from './StepCard';
|
| 4 |
+
|
| 5 |
+
interface StackStepsProps {
|
| 6 |
+
trace?: AgentTrace;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const StackSteps: React.FC<StackStepsProps> = ({ trace }) => {
|
| 10 |
+
return (
|
| 11 |
+
<div style={{ width: '360px', flexShrink: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', marginLeft: '12px', marginTop: '20px', marginBottom: '20px', boxShadow: '0 2px 12px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb' }}>
|
| 12 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-4">Stack Steps</h3>
|
| 13 |
+
<div style={{ flex: 1, overflowY: 'auto', minHeight: 0, padding: '16px' }}>
|
| 14 |
+
{trace?.steps && trace.steps.length > 0 ? (
|
| 15 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
| 16 |
+
{trace.steps.map((step, index) => (
|
| 17 |
+
<StepCard key={step.stepId} step={step} index={index} />
|
| 18 |
+
))}
|
| 19 |
+
</div>
|
| 20 |
+
) : (
|
| 21 |
+
<div className="flex flex-col items-center justify-center h-full text-gray-400 p-6 text-center">
|
| 22 |
+
<p className="font-medium">No steps yet</p>
|
| 23 |
+
<p className="text-xs mt-1">Steps will appear as agent progresses</p>
|
| 24 |
+
</div>
|
| 25 |
+
)}
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
);
|
| 29 |
+
};
|
cua2-front/src/component/poc/StepCard.tsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AgentStep } from '@/types/agent';
|
| 2 |
+
import React from 'react';
|
| 3 |
+
|
| 4 |
+
interface StepCardProps {
|
| 5 |
+
step: AgentStep;
|
| 6 |
+
index: number;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const StepCard: React.FC<StepCardProps> = ({ step, index }) => {
|
| 10 |
+
return (
|
| 11 |
+
<div
|
| 12 |
+
key={step.stepId}
|
| 13 |
+
style={{ backgroundColor: '#f9fafb', borderRadius: '8px', border: '1px solid #d1d5db', padding: '12px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)' }}
|
| 14 |
+
className="hover:border-blue-400 transition-all"
|
| 15 |
+
>
|
| 16 |
+
{/* Step Header */}
|
| 17 |
+
<div className="mb-6">
|
| 18 |
+
<span className="text-xs font-bold text-blue-600 uppercase tracking-wide">Step {index + 1}</span>
|
| 19 |
+
<hr style={{ margin: '12px 0', border: 'none', borderTop: '2px solid #d1d5db' }} />
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
{/* Step Image */}
|
| 23 |
+
{step.image && (
|
| 24 |
+
<div className="mb-6">
|
| 25 |
+
<div className="rounded-md overflow-hidden border border-gray-300 bg-white" style={{ maxHeight: '140px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
| 26 |
+
<img
|
| 27 |
+
src={step.image}
|
| 28 |
+
alt={`Step ${index + 1}`}
|
| 29 |
+
style={{ width: '100%', height: 'auto', maxHeight: '140px', objectFit: 'contain' }}
|
| 30 |
+
/>
|
| 31 |
+
</div>
|
| 32 |
+
<hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
|
| 33 |
+
</div>
|
| 34 |
+
)}
|
| 35 |
+
|
| 36 |
+
{/* Thought */}
|
| 37 |
+
<div className="mb-6">
|
| 38 |
+
<div className="bg-white rounded-md p-2.5 border border-gray-200">
|
| 39 |
+
<h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
|
| 40 |
+
<span>💭</span>
|
| 41 |
+
<span>Thought</span>
|
| 42 |
+
</h4>
|
| 43 |
+
<p className="text-xs text-gray-600 leading-relaxed">{step.thought}</p>
|
| 44 |
+
</div>
|
| 45 |
+
<hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
{/* Actions */}
|
| 49 |
+
<div className="mb-6">
|
| 50 |
+
<div className="bg-white rounded-md p-2.5 border border-gray-200">
|
| 51 |
+
<h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
|
| 52 |
+
<span>⚡</span>
|
| 53 |
+
<span>Actions</span>
|
| 54 |
+
</h4>
|
| 55 |
+
<ul className="space-y-1" style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
| 56 |
+
{step.actions.map((action, actionIndex) => (
|
| 57 |
+
<li key={actionIndex} className="text-xs text-gray-600 flex items-start leading-snug">
|
| 58 |
+
<span className="mr-1.5 text-blue-500 flex-shrink-0">→</span>
|
| 59 |
+
<span className="break-words">{action}</span>
|
| 60 |
+
</li>
|
| 61 |
+
))}
|
| 62 |
+
</ul>
|
| 63 |
+
</div>
|
| 64 |
+
<hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
{/* Step Metadata Footer */}
|
| 68 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
| 69 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bfdbfe' }}>
|
| 70 |
+
<span style={{ fontSize: '9px', fontWeight: 500, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Time</span>
|
| 71 |
+
<span style={{ fontSize: '12px', fontWeight: 700, color: '#1e40af' }}>{step.duration.toFixed(2)}s</span>
|
| 72 |
+
</div>
|
| 73 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bbf7d0' }}>
|
| 74 |
+
<span style={{ fontSize: '9px', fontWeight: 500, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.5px' }}>In Tokens</span>
|
| 75 |
+
<span style={{ fontSize: '12px', fontWeight: 700, color: '#166534' }}>{step.inputTokensUsed}</span>
|
| 76 |
+
</div>
|
| 77 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #e9d5ff' }}>
|
| 78 |
+
<span style={{ fontSize: '9px', fontWeight: 500, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Out Tokens</span>
|
| 79 |
+
<span style={{ fontSize: '12px', fontWeight: 700, color: '#6b21a8' }}>{step.outputTokensUsed}</span>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
);
|
| 84 |
+
};
|
cua2-front/src/component/poc/TaskButton.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface TaskButtonProps {
|
| 4 |
+
isAgentProcessing: boolean;
|
| 5 |
+
isConnected: boolean;
|
| 6 |
+
onSendTask: (content: string, modelId: string) => void;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const TaskButton: React.FC<TaskButtonProps> = ({ isAgentProcessing, isConnected, onSendTask }) => {
|
| 10 |
+
return (
|
| 11 |
+
<div
|
| 12 |
+
onClick={() => {
|
| 13 |
+
if (!isAgentProcessing && isConnected) {
|
| 14 |
+
onSendTask(
|
| 15 |
+
"Complete the online form by clicking through the required fields",
|
| 16 |
+
"anthropic/claude-sonnet-4-5-20250929"
|
| 17 |
+
);
|
| 18 |
+
}
|
| 19 |
+
}}
|
| 20 |
+
style={{
|
| 21 |
+
marginTop: '16px',
|
| 22 |
+
padding: '14px 18px',
|
| 23 |
+
background: isAgentProcessing || !isConnected
|
| 24 |
+
? 'rgba(255, 255, 255, 0.1)'
|
| 25 |
+
: 'rgba(255, 255, 255, 0.15)',
|
| 26 |
+
borderRadius: '10px',
|
| 27 |
+
backdropFilter: 'blur(10px)',
|
| 28 |
+
border: '2px solid rgba(0, 0, 0, 0.3)',
|
| 29 |
+
cursor: isAgentProcessing || !isConnected ? 'not-allowed' : 'pointer',
|
| 30 |
+
transition: 'all 0.3s ease',
|
| 31 |
+
opacity: isAgentProcessing || !isConnected ? 0.6 : 1,
|
| 32 |
+
}}
|
| 33 |
+
onMouseEnter={(e) => {
|
| 34 |
+
if (!isAgentProcessing && isConnected) {
|
| 35 |
+
e.currentTarget.style.background = 'rgba(200, 200, 200, 0.3)';
|
| 36 |
+
e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.5)';
|
| 37 |
+
e.currentTarget.style.transform = 'translateY(-2px)';
|
| 38 |
+
e.currentTarget.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.2)';
|
| 39 |
+
}
|
| 40 |
+
}}
|
| 41 |
+
onMouseLeave={(e) => {
|
| 42 |
+
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
|
| 43 |
+
e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.3)';
|
| 44 |
+
e.currentTarget.style.transform = 'translateY(0)';
|
| 45 |
+
e.currentTarget.style.boxShadow = 'none';
|
| 46 |
+
}}
|
| 47 |
+
>
|
| 48 |
+
<div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>
|
| 49 |
+
<div style={{ flex: 1 }}>
|
| 50 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
| 51 |
+
<span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.7)', textTransform: 'uppercase', letterSpacing: '1px' }}>Task</span>
|
| 52 |
+
{!isAgentProcessing && isConnected && (
|
| 53 |
+
<span style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.5)', fontStyle: 'italic' }}>
|
| 54 |
+
(click to run)
|
| 55 |
+
</span>
|
| 56 |
+
)}
|
| 57 |
+
</div>
|
| 58 |
+
<p style={{ fontSize: '15px', fontWeight: 500, color: '#1f2937' }}>
|
| 59 |
+
Complete the online form by clicking through the required fields
|
| 60 |
+
</p>
|
| 61 |
+
</div>
|
| 62 |
+
<div style={{
|
| 63 |
+
padding: '8px 16px',
|
| 64 |
+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
| 65 |
+
borderRadius: '6px',
|
| 66 |
+
border: '1px solid rgba(0, 0, 0, 0.2)'
|
| 67 |
+
}}>
|
| 68 |
+
<span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.6)', textTransform: 'uppercase', letterSpacing: '1px' }}>Model</span>
|
| 69 |
+
<p style={{ fontSize: '12px', fontWeight: 600, color: '#1f2937', marginTop: '2px', whiteSpace: 'nowrap' }}>
|
| 70 |
+
claude-sonnet-4-5-20250929
|
| 71 |
+
</p>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
);
|
| 76 |
+
};
|
cua2-front/src/component/poc/VNCStream.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface VNCStreamProps {
|
| 4 |
+
vncUrl: string;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export const VNCStream: React.FC<VNCStreamProps> = ({ vncUrl }) => {
|
| 8 |
+
return (
|
| 9 |
+
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb', overflow: 'hidden', padding: '20px' }}>
|
| 10 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-4">VNC Stream</h3>
|
| 11 |
+
<div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
| 12 |
+
{vncUrl ? (
|
| 13 |
+
<iframe
|
| 14 |
+
src={vncUrl}
|
| 15 |
+
style={{ width: '100%', height: '100%', border: 'none' }}
|
| 16 |
+
title="VNC Stream"
|
| 17 |
+
/>
|
| 18 |
+
) : (
|
| 19 |
+
<div className="text-gray-400 text-center p-8">
|
| 20 |
+
<svg className="w-16 h-16 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 21 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
| 22 |
+
</svg>
|
| 23 |
+
<p className="font-medium">No VNC stream available</p>
|
| 24 |
+
<p className="text-sm mt-1 text-gray-500">Stream will appear when agent starts</p>
|
| 25 |
+
</div>
|
| 26 |
+
)}
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
);
|
| 30 |
+
};
|
cua2-front/src/component/poc/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { ConnectionStatus } from './ConnectionStatus';
|
| 2 |
+
export { Header } from './Header';
|
| 3 |
+
export { Metadata } from './Metadata';
|
| 4 |
+
export { ProcessingIndicator } from './ProcessingIndicator';
|
| 5 |
+
export { StackSteps } from './StackSteps';
|
| 6 |
+
export { StepCard } from './StepCard';
|
| 7 |
+
export { TaskButton } from './TaskButton';
|
| 8 |
+
export { VNCStream } from './VNCStream';
|
cua2-front/src/components/mock/ConnectionStatus.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface ConnectionStatusProps {
|
| 4 |
+
isConnected: boolean;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ isConnected }) => {
|
| 8 |
+
return (
|
| 9 |
+
<div style={{
|
| 10 |
+
display: 'flex',
|
| 11 |
+
alignItems: 'center',
|
| 12 |
+
gap: '8px',
|
| 13 |
+
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
| 14 |
+
padding: '8px 16px',
|
| 15 |
+
borderRadius: '20px',
|
| 16 |
+
backdropFilter: 'blur(10px)',
|
| 17 |
+
border: '1px solid rgba(255, 255, 255, 0.3)'
|
| 18 |
+
}}>
|
| 19 |
+
<div style={{
|
| 20 |
+
width: '8px',
|
| 21 |
+
height: '8px',
|
| 22 |
+
borderRadius: '50%',
|
| 23 |
+
backgroundColor: isConnected ? '#10b981' : '#ef4444',
|
| 24 |
+
boxShadow: isConnected ? '0 0 8px #10b981' : '0 0 8px #ef4444',
|
| 25 |
+
animation: isConnected ? 'pulse 2s infinite' : 'none'
|
| 26 |
+
}}></div>
|
| 27 |
+
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
| 28 |
+
<span className="text-xs font-semibold text-white" style={{ lineHeight: '1.2' }}>
|
| 29 |
+
{isConnected ? 'Connected' : 'Disconnected'}
|
| 30 |
+
</span>
|
| 31 |
+
<span className="text-xs text-white" style={{ opacity: 0.7, fontSize: '10px', lineHeight: '1.2' }}>
|
| 32 |
+
WebSocket
|
| 33 |
+
</span>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
);
|
| 37 |
+
};
|
cua2-front/src/components/mock/Header.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { ConnectionStatus } from './ConnectionStatus';
|
| 3 |
+
import { ProcessingIndicator } from './ProcessingIndicator';
|
| 4 |
+
import { TaskButton } from './TaskButton';
|
| 5 |
+
|
| 6 |
+
interface HeaderProps {
|
| 7 |
+
isConnected: boolean;
|
| 8 |
+
isAgentProcessing: boolean;
|
| 9 |
+
onSendTask: (content: string, modelId: string) => void;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export const Header: React.FC<HeaderProps> = ({ isConnected, isAgentProcessing, onSendTask }) => {
|
| 13 |
+
return (
|
| 14 |
+
<>
|
| 15 |
+
<div style={{
|
| 16 |
+
flexShrink: 0,
|
| 17 |
+
}}>
|
| 18 |
+
<div style={{ maxWidth: '1400px', margin: '0 auto', padding: '20px 32px' }}>
|
| 19 |
+
<div className="flex items-center justify-between">
|
| 20 |
+
<div className="flex items-center gap-6">
|
| 21 |
+
<ConnectionStatus isConnected={isConnected} />
|
| 22 |
+
<h1 className="text-3xl font-bold text-white" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
|
| 23 |
+
CUA2 Agent
|
| 24 |
+
</h1>
|
| 25 |
+
</div>
|
| 26 |
+
<ProcessingIndicator isAgentProcessing={isAgentProcessing} />
|
| 27 |
+
</div>
|
| 28 |
+
<TaskButton
|
| 29 |
+
isAgentProcessing={isAgentProcessing}
|
| 30 |
+
isConnected={isConnected}
|
| 31 |
+
onSendTask={onSendTask}
|
| 32 |
+
/>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<style>{`
|
| 37 |
+
@keyframes spin {
|
| 38 |
+
to { transform: rotate(360deg); }
|
| 39 |
+
}
|
| 40 |
+
@keyframes pulse {
|
| 41 |
+
0%, 100% { opacity: 1; }
|
| 42 |
+
50% { opacity: 0.5; }
|
| 43 |
+
}
|
| 44 |
+
`}</style>
|
| 45 |
+
</>
|
| 46 |
+
);
|
| 47 |
+
};
|
cua2-front/src/components/mock/Metadata.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AgentTrace } from '@/types/agent';
|
| 2 |
+
import React from 'react';
|
| 3 |
+
|
| 4 |
+
interface MetadataProps {
|
| 5 |
+
trace?: AgentTrace;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export const Metadata: React.FC<MetadataProps> = ({ trace }) => {
|
| 9 |
+
return (
|
| 10 |
+
<div style={{ flexShrink: 0 }} className="bg-white rounded-lg shadow-md border border-gray-200 p-5">
|
| 11 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-4">Metadata</h3>
|
| 12 |
+
{trace?.metadata ? (
|
| 13 |
+
<div style={{ display: 'flex', gap: '12px' }}>
|
| 14 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '8px', padding: '12px', border: '1px solid #bfdbfe', boxShadow: '0 2px 4px rgba(59, 130, 246, 0.1)' }}>
|
| 15 |
+
<span style={{ fontSize: '10px', fontWeight: 600, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Time</span>
|
| 16 |
+
<span style={{ fontSize: '24px', fontWeight: 700, color: '#1e40af' }}>{trace.metadata.duration.toFixed(2)}s</span>
|
| 17 |
+
</div>
|
| 18 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '8px', padding: '12px', border: '1px solid #bbf7d0', boxShadow: '0 2px 4px rgba(16, 185, 129, 0.1)' }}>
|
| 19 |
+
<span style={{ fontSize: '10px', fontWeight: 600, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>In Tokens</span>
|
| 20 |
+
<span style={{ fontSize: '24px', fontWeight: 700, color: '#166534' }}>{trace.metadata.inputTokensUsed.toLocaleString()}</span>
|
| 21 |
+
</div>
|
| 22 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '8px', padding: '12px', border: '1px solid #e9d5ff', boxShadow: '0 2px 4px rgba(139, 92, 246, 0.1)' }}>
|
| 23 |
+
<span style={{ fontSize: '10px', fontWeight: 600, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Out Tokens</span>
|
| 24 |
+
<span style={{ fontSize: '24px', fontWeight: 700, color: '#6b21a8' }}>{trace.metadata.outputTokensUsed.toLocaleString()}</span>
|
| 25 |
+
</div>
|
| 26 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#fff7ed', borderRadius: '8px', padding: '12px', border: '1px solid #fed7aa', boxShadow: '0 2px 4px rgba(249, 115, 22, 0.1)' }}>
|
| 27 |
+
<span style={{ fontSize: '10px', fontWeight: 600, color: '#ea580c', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Steps</span>
|
| 28 |
+
<span style={{ fontSize: '24px', fontWeight: 700, color: '#c2410c' }}>{trace.metadata.numberOfSteps}</span>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
) : (
|
| 32 |
+
<div className="text-gray-400 text-sm py-2">
|
| 33 |
+
{trace ? 'Waiting for completion...' : 'No task started yet'}
|
| 34 |
+
</div>
|
| 35 |
+
)}
|
| 36 |
+
</div>
|
| 37 |
+
);
|
| 38 |
+
};
|
cua2-front/src/components/mock/ProcessingIndicator.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface ProcessingIndicatorProps {
|
| 4 |
+
isAgentProcessing: boolean;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export const ProcessingIndicator: React.FC<ProcessingIndicatorProps> = ({ isAgentProcessing }) => {
|
| 8 |
+
if (!isAgentProcessing) return null;
|
| 9 |
+
|
| 10 |
+
return (
|
| 11 |
+
<div style={{
|
| 12 |
+
display: 'flex',
|
| 13 |
+
alignItems: 'center',
|
| 14 |
+
gap: '10px',
|
| 15 |
+
padding: '10px 20px',
|
| 16 |
+
backgroundColor: 'rgba(251, 191, 36, 0.2)',
|
| 17 |
+
borderRadius: '10px',
|
| 18 |
+
border: '1px solid rgba(251, 191, 36, 0.4)'
|
| 19 |
+
}}>
|
| 20 |
+
<span style={{
|
| 21 |
+
width: '16px',
|
| 22 |
+
height: '16px',
|
| 23 |
+
border: '2px solid #fbbf24',
|
| 24 |
+
borderTopColor: 'transparent',
|
| 25 |
+
borderRadius: '50%',
|
| 26 |
+
animation: 'spin 1s linear infinite',
|
| 27 |
+
display: 'inline-block'
|
| 28 |
+
}}></span>
|
| 29 |
+
<span style={{ fontSize: '14px', fontWeight: 600, color: '#fbbf24', letterSpacing: '0.5px' }}>
|
| 30 |
+
PROCESSING...
|
| 31 |
+
</span>
|
| 32 |
+
</div>
|
| 33 |
+
);
|
| 34 |
+
};
|
cua2-front/src/components/mock/StackSteps.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { AgentTrace } from '@/types/agent';
|
| 3 |
+
import { StepCard } from './StepCard';
|
| 4 |
+
|
| 5 |
+
interface StackStepsProps {
|
| 6 |
+
trace?: AgentTrace;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const StackSteps: React.FC<StackStepsProps> = ({ trace }) => {
|
| 10 |
+
return (
|
| 11 |
+
<div style={{ width: '360px', flexShrink: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', marginLeft: '12px', marginTop: '20px', marginBottom: '20px', boxShadow: '0 2px 12px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb' }}>
|
| 12 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-4">Stack Steps</h3>
|
| 13 |
+
<div style={{ flex: 1, overflowY: 'auto', minHeight: 0, padding: '16px' }}>
|
| 14 |
+
{trace?.steps && trace.steps.length > 0 ? (
|
| 15 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
| 16 |
+
{trace.steps.map((step, index) => (
|
| 17 |
+
<StepCard key={step.stepId} step={step} index={index} />
|
| 18 |
+
))}
|
| 19 |
+
</div>
|
| 20 |
+
) : (
|
| 21 |
+
<div className="flex flex-col items-center justify-center h-full text-gray-400 p-6 text-center">
|
| 22 |
+
<p className="font-medium">No steps yet</p>
|
| 23 |
+
<p className="text-xs mt-1">Steps will appear as agent progresses</p>
|
| 24 |
+
</div>
|
| 25 |
+
)}
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
);
|
| 29 |
+
};
|
cua2-front/src/components/mock/StepCard.tsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { AgentStep } from '@/types/agent';
|
| 2 |
+
import React from 'react';
|
| 3 |
+
|
| 4 |
+
interface StepCardProps {
|
| 5 |
+
step: AgentStep;
|
| 6 |
+
index: number;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const StepCard: React.FC<StepCardProps> = ({ step, index }) => {
|
| 10 |
+
return (
|
| 11 |
+
<div
|
| 12 |
+
key={step.stepId}
|
| 13 |
+
style={{ backgroundColor: '#f9fafb', borderRadius: '8px', border: '1px solid #d1d5db', padding: '12px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)' }}
|
| 14 |
+
className="hover:border-blue-400 transition-all"
|
| 15 |
+
>
|
| 16 |
+
{/* Step Header */}
|
| 17 |
+
<div className="mb-6">
|
| 18 |
+
<span className="text-xs font-bold text-blue-600 uppercase tracking-wide">Step {index + 1}</span>
|
| 19 |
+
<hr style={{ margin: '12px 0', border: 'none', borderTop: '2px solid #d1d5db' }} />
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
{/* Step Image */}
|
| 23 |
+
{step.image && (
|
| 24 |
+
<div className="mb-6">
|
| 25 |
+
<div className="rounded-md overflow-hidden border border-gray-300 bg-white" style={{ maxHeight: '140px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
| 26 |
+
<img
|
| 27 |
+
src={step.image}
|
| 28 |
+
alt={`Step ${index + 1}`}
|
| 29 |
+
style={{ width: '100%', height: 'auto', maxHeight: '140px', objectFit: 'contain' }}
|
| 30 |
+
/>
|
| 31 |
+
</div>
|
| 32 |
+
<hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
|
| 33 |
+
</div>
|
| 34 |
+
)}
|
| 35 |
+
|
| 36 |
+
{/* Thought */}
|
| 37 |
+
<div className="mb-6">
|
| 38 |
+
<div className="bg-white rounded-md p-2.5 border border-gray-200">
|
| 39 |
+
<h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
|
| 40 |
+
<span>💭</span>
|
| 41 |
+
<span>Thought</span>
|
| 42 |
+
</h4>
|
| 43 |
+
<p className="text-xs text-gray-600 leading-relaxed">{step.thought}</p>
|
| 44 |
+
</div>
|
| 45 |
+
<hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
{/* Actions */}
|
| 49 |
+
<div className="mb-6">
|
| 50 |
+
<div className="bg-white rounded-md p-2.5 border border-gray-200">
|
| 51 |
+
<h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
|
| 52 |
+
<span>⚡</span>
|
| 53 |
+
<span>Actions</span>
|
| 54 |
+
</h4>
|
| 55 |
+
<ul className="space-y-1" style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
| 56 |
+
{step.actions.map((action, actionIndex) => (
|
| 57 |
+
<li key={actionIndex} className="text-xs text-gray-600 flex items-start leading-snug">
|
| 58 |
+
<span className="mr-1.5 text-blue-500 flex-shrink-0">→</span>
|
| 59 |
+
<span className="break-words">{action}</span>
|
| 60 |
+
</li>
|
| 61 |
+
))}
|
| 62 |
+
</ul>
|
| 63 |
+
</div>
|
| 64 |
+
<hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
{/* Step Metadata Footer */}
|
| 68 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
| 69 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bfdbfe' }}>
|
| 70 |
+
<span style={{ fontSize: '9px', fontWeight: 500, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Time</span>
|
| 71 |
+
<span style={{ fontSize: '12px', fontWeight: 700, color: '#1e40af' }}>{step.duration.toFixed(2)}s</span>
|
| 72 |
+
</div>
|
| 73 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bbf7d0' }}>
|
| 74 |
+
<span style={{ fontSize: '9px', fontWeight: 500, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.5px' }}>In Tokens</span>
|
| 75 |
+
<span style={{ fontSize: '12px', fontWeight: 700, color: '#166534' }}>{step.inputTokensUsed}</span>
|
| 76 |
+
</div>
|
| 77 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #e9d5ff' }}>
|
| 78 |
+
<span style={{ fontSize: '9px', fontWeight: 500, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Out Tokens</span>
|
| 79 |
+
<span style={{ fontSize: '12px', fontWeight: 700, color: '#6b21a8' }}>{step.outputTokensUsed}</span>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
);
|
| 84 |
+
};
|
cua2-front/src/components/mock/TaskButton.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface TaskButtonProps {
|
| 4 |
+
isAgentProcessing: boolean;
|
| 5 |
+
isConnected: boolean;
|
| 6 |
+
onSendTask: (content: string, modelId: string) => void;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const TaskButton: React.FC<TaskButtonProps> = ({ isAgentProcessing, isConnected, onSendTask }) => {
|
| 10 |
+
return (
|
| 11 |
+
<div
|
| 12 |
+
onClick={() => {
|
| 13 |
+
if (!isAgentProcessing && isConnected) {
|
| 14 |
+
onSendTask(
|
| 15 |
+
"Complete the online form by clicking through the required fields",
|
| 16 |
+
"anthropic/claude-sonnet-4-5-20250929"
|
| 17 |
+
);
|
| 18 |
+
}
|
| 19 |
+
}}
|
| 20 |
+
style={{
|
| 21 |
+
marginTop: '16px',
|
| 22 |
+
padding: '14px 18px',
|
| 23 |
+
background: isAgentProcessing || !isConnected
|
| 24 |
+
? 'rgba(255, 255, 255, 0.1)'
|
| 25 |
+
: 'rgba(255, 255, 255, 0.15)',
|
| 26 |
+
borderRadius: '10px',
|
| 27 |
+
backdropFilter: 'blur(10px)',
|
| 28 |
+
border: '2px solid rgba(0, 0, 0, 0.3)',
|
| 29 |
+
cursor: isAgentProcessing || !isConnected ? 'not-allowed' : 'pointer',
|
| 30 |
+
transition: 'all 0.3s ease',
|
| 31 |
+
opacity: isAgentProcessing || !isConnected ? 0.6 : 1,
|
| 32 |
+
}}
|
| 33 |
+
onMouseEnter={(e) => {
|
| 34 |
+
if (!isAgentProcessing && isConnected) {
|
| 35 |
+
e.currentTarget.style.background = 'rgba(200, 200, 200, 0.3)';
|
| 36 |
+
e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.5)';
|
| 37 |
+
e.currentTarget.style.transform = 'translateY(-2px)';
|
| 38 |
+
e.currentTarget.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.2)';
|
| 39 |
+
}
|
| 40 |
+
}}
|
| 41 |
+
onMouseLeave={(e) => {
|
| 42 |
+
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
|
| 43 |
+
e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.3)';
|
| 44 |
+
e.currentTarget.style.transform = 'translateY(0)';
|
| 45 |
+
e.currentTarget.style.boxShadow = 'none';
|
| 46 |
+
}}
|
| 47 |
+
>
|
| 48 |
+
<div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>
|
| 49 |
+
<div style={{ flex: 1 }}>
|
| 50 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
| 51 |
+
<span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.7)', textTransform: 'uppercase', letterSpacing: '1px' }}>Task</span>
|
| 52 |
+
{!isAgentProcessing && isConnected && (
|
| 53 |
+
<span style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.5)', fontStyle: 'italic' }}>
|
| 54 |
+
(click to run)
|
| 55 |
+
</span>
|
| 56 |
+
)}
|
| 57 |
+
</div>
|
| 58 |
+
<p style={{ fontSize: '15px', fontWeight: 500, color: '#1f2937' }}>
|
| 59 |
+
Complete the online form by clicking through the required fields
|
| 60 |
+
</p>
|
| 61 |
+
</div>
|
| 62 |
+
<div style={{
|
| 63 |
+
padding: '8px 16px',
|
| 64 |
+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
| 65 |
+
borderRadius: '6px',
|
| 66 |
+
border: '1px solid rgba(0, 0, 0, 0.2)'
|
| 67 |
+
}}>
|
| 68 |
+
<span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.6)', textTransform: 'uppercase', letterSpacing: '1px' }}>Model</span>
|
| 69 |
+
<p style={{ fontSize: '12px', fontWeight: 600, color: '#1f2937', marginTop: '2px', whiteSpace: 'nowrap' }}>
|
| 70 |
+
claude-sonnet-4-5-20250929
|
| 71 |
+
</p>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
);
|
| 76 |
+
};
|
cua2-front/src/components/mock/VNCStream.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface VNCStreamProps {
|
| 4 |
+
vncUrl: string;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export const VNCStream: React.FC<VNCStreamProps> = ({ vncUrl }) => {
|
| 8 |
+
return (
|
| 9 |
+
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb', overflow: 'hidden', padding: '20px' }}>
|
| 10 |
+
<h3 className="text-lg font-semibold text-gray-800 mb-4">VNC Stream</h3>
|
| 11 |
+
<div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
| 12 |
+
{vncUrl ? (
|
| 13 |
+
<iframe
|
| 14 |
+
src={vncUrl}
|
| 15 |
+
style={{ width: '100%', height: '100%', border: 'none' }}
|
| 16 |
+
title="VNC Stream"
|
| 17 |
+
/>
|
| 18 |
+
) : (
|
| 19 |
+
<div className="text-gray-400 text-center p-8">
|
| 20 |
+
<svg className="w-16 h-16 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 21 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
| 22 |
+
</svg>
|
| 23 |
+
<p className="font-medium">No VNC stream available</p>
|
| 24 |
+
<p className="text-sm mt-1 text-gray-500">Stream will appear when agent starts</p>
|
| 25 |
+
</div>
|
| 26 |
+
)}
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
);
|
| 30 |
+
};
|
cua2-front/src/components/mock/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { ConnectionStatus } from './ConnectionStatus';
|
| 2 |
+
export { ProcessingIndicator } from './ProcessingIndicator';
|
| 3 |
+
export { TaskButton } from './TaskButton';
|
| 4 |
+
export { Header } from './Header';
|
| 5 |
+
export { VNCStream } from './VNCStream';
|
| 6 |
+
export { Metadata } from './Metadata';
|
| 7 |
+
export { StepCard } from './StepCard';
|
| 8 |
+
export { StackSteps } from './StackSteps';
|
cua2-front/src/index.css
CHANGED
|
@@ -4,8 +4,12 @@
|
|
| 4 |
box-sizing: border-box;
|
| 5 |
}
|
| 6 |
|
| 7 |
-
body {
|
| 8 |
margin: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
| 10 |
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
| 11 |
sans-serif;
|
|
@@ -15,6 +19,7 @@ body {
|
|
| 15 |
|
| 16 |
#root {
|
| 17 |
width: 100%;
|
| 18 |
-
height:
|
|
|
|
|
|
|
| 19 |
}
|
| 20 |
-
|
|
|
|
| 4 |
box-sizing: border-box;
|
| 5 |
}
|
| 6 |
|
| 7 |
+
html, body {
|
| 8 |
margin: 0;
|
| 9 |
+
padding: 0;
|
| 10 |
+
height: 100%;
|
| 11 |
+
width: 100%;
|
| 12 |
+
overflow: hidden;
|
| 13 |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
| 14 |
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
| 15 |
sans-serif;
|
|
|
|
| 19 |
|
| 20 |
#root {
|
| 21 |
width: 100%;
|
| 22 |
+
height: 100%;
|
| 23 |
+
display: flex;
|
| 24 |
+
flex-direction: column;
|
| 25 |
}
|
|
|
cua2-front/src/pages/Index.tsx
CHANGED
|
@@ -1,14 +1,20 @@
|
|
| 1 |
import React from 'react';
|
| 2 |
import { useWebSocket } from '@/hooks/useWebSocket';
|
| 3 |
-
import {
|
| 4 |
-
import {
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
const Index = () => {
|
| 7 |
-
const [
|
| 8 |
const [isAgentProcessing, setIsAgentProcessing] = useState(false);
|
| 9 |
const [vncUrl, setVncUrl] = useState<string>('');
|
|
|
|
| 10 |
|
| 11 |
-
// WebSocket
|
|
|
|
|
|
|
| 12 |
// const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000/ws';
|
| 13 |
const WS_URL = 'ws://localhost:8000/ws';
|
| 14 |
|
|
@@ -18,71 +24,59 @@ const Index = () => {
|
|
| 18 |
switch (event.type) {
|
| 19 |
case 'agent_start':
|
| 20 |
setIsAgentProcessing(true);
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
id: event.messageId,
|
| 24 |
-
type: 'agent',
|
| 25 |
-
instructions: event.instructions,
|
| 26 |
-
modelId: event.modelId,
|
| 27 |
-
timestamp: new Date(),
|
| 28 |
-
isLoading: true,
|
| 29 |
-
};
|
| 30 |
-
setMessages(prev => [...prev, newMessage]);
|
| 31 |
-
}
|
| 32 |
break;
|
| 33 |
|
| 34 |
case 'agent_progress':
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
}
|
| 52 |
break;
|
| 53 |
|
| 54 |
case 'agent_complete':
|
| 55 |
setIsAgentProcessing(false);
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
)
|
| 67 |
-
);
|
| 68 |
-
}
|
| 69 |
break;
|
| 70 |
|
| 71 |
case 'agent_error':
|
| 72 |
setIsAgentProcessing(false);
|
| 73 |
// TODO: Handle agent error
|
|
|
|
| 74 |
break;
|
| 75 |
|
| 76 |
case 'vnc_url_set':
|
| 77 |
-
|
| 78 |
-
setVncUrl(event.vncUrl);
|
| 79 |
-
}
|
| 80 |
// TODO: Handle VNC URL set
|
|
|
|
| 81 |
break;
|
| 82 |
|
| 83 |
case 'vnc_url_unset':
|
| 84 |
setVncUrl('');
|
| 85 |
// TODO: Handle VNC URL unset
|
|
|
|
| 86 |
break;
|
| 87 |
|
| 88 |
case 'heartbeat':
|
|
@@ -92,7 +86,7 @@ const Index = () => {
|
|
| 92 |
};
|
| 93 |
|
| 94 |
const handleWebSocketError = () => {
|
| 95 |
-
// Error handling
|
| 96 |
|
| 97 |
};
|
| 98 |
|
|
@@ -102,29 +96,52 @@ const Index = () => {
|
|
| 102 |
onError: handleWebSocketError,
|
| 103 |
});
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
timestamp: new Date(),
|
|
|
|
| 111 |
};
|
| 112 |
|
| 113 |
-
|
| 114 |
|
| 115 |
// Send message to Python backend via WebSocket
|
| 116 |
sendMessage({
|
| 117 |
type: 'user_task',
|
| 118 |
-
|
| 119 |
-
model_id: "anthropic/claude-sonnet-4-5-20250929",
|
| 120 |
-
timestamp: new Date().toISOString(),
|
| 121 |
});
|
| 122 |
};
|
| 123 |
|
|
|
|
| 124 |
|
| 125 |
return (
|
| 126 |
-
<div>
|
| 127 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
</div>
|
| 129 |
);
|
| 130 |
};
|
|
|
|
| 1 |
import React from 'react';
|
| 2 |
import { useWebSocket } from '@/hooks/useWebSocket';
|
| 3 |
+
import { WebSocketEvent } from '@/types/agent';
|
| 4 |
+
import { useState } from 'react';
|
| 5 |
+
import { AgentTrace, AgentStep } from '@/types/agent';
|
| 6 |
+
import { ulid } from 'ulid';
|
| 7 |
+
import { Header, VNCStream, Metadata, StackSteps } from '@/components/mock';
|
| 8 |
|
| 9 |
const Index = () => {
|
| 10 |
+
const [trace, setTrace] = useState<AgentTrace>();
|
| 11 |
const [isAgentProcessing, setIsAgentProcessing] = useState(false);
|
| 12 |
const [vncUrl, setVncUrl] = useState<string>('');
|
| 13 |
+
const [selectedModelId, setSelectedModelId] = useState<string>("claude-sonnet-4-5-20250929");
|
| 14 |
|
| 15 |
+
// #################### WebSocket Connection ########################
|
| 16 |
+
|
| 17 |
+
// WebSocket connection - Use environment variable
|
| 18 |
// const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000/ws';
|
| 19 |
const WS_URL = 'ws://localhost:8000/ws';
|
| 20 |
|
|
|
|
| 24 |
switch (event.type) {
|
| 25 |
case 'agent_start':
|
| 26 |
setIsAgentProcessing(true);
|
| 27 |
+
setTrace(event.agentTrace);
|
| 28 |
+
console.log('Agent start received:', event.agentTrace);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
break;
|
| 30 |
|
| 31 |
case 'agent_progress':
|
| 32 |
+
// Add new step from a agent trace run with image, generated text, actions, tokens and timestamp
|
| 33 |
+
setTrace(prev => {
|
| 34 |
+
const existingSteps = prev?.steps || [] as AgentStep[];
|
| 35 |
+
const stepExists = existingSteps.some(step => step.stepId === event.agentStep.stepId);
|
| 36 |
+
|
| 37 |
+
if (!stepExists) {
|
| 38 |
+
return {
|
| 39 |
+
...prev,
|
| 40 |
+
steps: [...existingSteps, event.agentStep],
|
| 41 |
+
traceMetadata: event.traceMetadata,
|
| 42 |
+
isRunning: true
|
| 43 |
+
};
|
| 44 |
+
}
|
| 45 |
+
return prev;
|
| 46 |
+
});
|
| 47 |
+
console.log('Agent progress received:', event.agentStep);
|
|
|
|
| 48 |
break;
|
| 49 |
|
| 50 |
case 'agent_complete':
|
| 51 |
setIsAgentProcessing(false);
|
| 52 |
+
setTrace(trace => {
|
| 53 |
+
return trace.id === event.traceMetadata.traceId
|
| 54 |
+
? {
|
| 55 |
+
...trace,
|
| 56 |
+
isRunning: false,
|
| 57 |
+
metadata: event.traceMetadata,
|
| 58 |
+
}
|
| 59 |
+
: trace;
|
| 60 |
+
});
|
| 61 |
+
console.log('Agent complete received:', event.traceMetadata);
|
|
|
|
|
|
|
|
|
|
| 62 |
break;
|
| 63 |
|
| 64 |
case 'agent_error':
|
| 65 |
setIsAgentProcessing(false);
|
| 66 |
// TODO: Handle agent error
|
| 67 |
+
console.log('Agent error received:', event.error);
|
| 68 |
break;
|
| 69 |
|
| 70 |
case 'vnc_url_set':
|
| 71 |
+
setVncUrl(event.vncUrl);
|
|
|
|
|
|
|
| 72 |
// TODO: Handle VNC URL set
|
| 73 |
+
console.log('VNC URL set received:', event.vncUrl);
|
| 74 |
break;
|
| 75 |
|
| 76 |
case 'vnc_url_unset':
|
| 77 |
setVncUrl('');
|
| 78 |
// TODO: Handle VNC URL unset
|
| 79 |
+
console.log('VNC URL unset received:');
|
| 80 |
break;
|
| 81 |
|
| 82 |
case 'heartbeat':
|
|
|
|
| 86 |
};
|
| 87 |
|
| 88 |
const handleWebSocketError = () => {
|
| 89 |
+
// WebSocket Frontend Error handling
|
| 90 |
|
| 91 |
};
|
| 92 |
|
|
|
|
| 96 |
onError: handleWebSocketError,
|
| 97 |
});
|
| 98 |
|
| 99 |
+
// #################### Frontend Functionality ########################
|
| 100 |
+
|
| 101 |
+
const handleModelId = (modelId: string) => {
|
| 102 |
+
setSelectedModelId(modelId);
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
const handleSendNewTask = (content: string, modelId: string) => {
|
| 106 |
+
const trace: AgentTrace = {
|
| 107 |
+
id: ulid(),
|
| 108 |
+
instruction: content,
|
| 109 |
+
modelId: selectedModelId,
|
| 110 |
timestamp: new Date(),
|
| 111 |
+
isRunning: true,
|
| 112 |
};
|
| 113 |
|
| 114 |
+
setTrace(trace);
|
| 115 |
|
| 116 |
// Send message to Python backend via WebSocket
|
| 117 |
sendMessage({
|
| 118 |
type: 'user_task',
|
| 119 |
+
trace: trace,
|
|
|
|
|
|
|
| 120 |
});
|
| 121 |
};
|
| 122 |
|
| 123 |
+
// #################### Mock Frontend Rendering ########################
|
| 124 |
|
| 125 |
return (
|
| 126 |
+
<div style={{ height: '100%', width: '100%', display: 'flex', flexDirection: 'column', backgroundColor: '#f3f4f6' }}>
|
| 127 |
+
<Header
|
| 128 |
+
isConnected={isConnected}
|
| 129 |
+
isAgentProcessing={isAgentProcessing}
|
| 130 |
+
onSendTask={handleSendNewTask}
|
| 131 |
+
/>
|
| 132 |
+
|
| 133 |
+
<div style={{ flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', overflow: 'hidden', minHeight: 0, padding: '32px' }}>
|
| 134 |
+
<div style={{ width: '100%', height: '100%', maxWidth: '1400px', maxHeight: '900px', display: 'flex', flexDirection: 'row', overflow: 'hidden' }}>
|
| 135 |
+
{/* Left Side: VNC Stream + Metadata */}
|
| 136 |
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: '20px 12px', gap: '20px', minWidth: 0 }}>
|
| 137 |
+
<VNCStream vncUrl={vncUrl} />
|
| 138 |
+
<Metadata trace={trace} />
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
{/* Right Side: Stack Steps */}
|
| 142 |
+
<StackSteps trace={trace} />
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
</div>
|
| 146 |
);
|
| 147 |
};
|
cua2-front/src/types/agent.ts
CHANGED
|
@@ -1,36 +1,84 @@
|
|
| 1 |
-
|
|
|
|
| 2 |
id: string;
|
| 3 |
-
type: 'user' | 'agent';
|
| 4 |
timestamp: Date;
|
| 5 |
-
|
| 6 |
modelId: string;
|
|
|
|
| 7 |
steps?: AgentStep[];
|
| 8 |
-
metadata?:
|
| 9 |
-
isLoading?: boolean;
|
| 10 |
}
|
| 11 |
|
| 12 |
export interface AgentStep {
|
| 13 |
-
|
| 14 |
stepId: string;
|
|
|
|
| 15 |
image: string;
|
| 16 |
-
|
| 17 |
actions: string[];
|
|
|
|
| 18 |
inputTokensUsed: number;
|
| 19 |
outputTokensUsed: number;
|
| 20 |
-
|
| 21 |
}
|
| 22 |
|
| 23 |
-
export interface
|
| 24 |
-
|
| 25 |
inputTokensUsed: number;
|
| 26 |
outputTokensUsed: number;
|
| 27 |
-
|
| 28 |
numberOfSteps: number;
|
| 29 |
}
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
|
|
|
| 1 |
+
|
| 2 |
+
export interface AgentTrace {
|
| 3 |
id: string;
|
|
|
|
| 4 |
timestamp: Date;
|
| 5 |
+
instruction: string;
|
| 6 |
modelId: string;
|
| 7 |
+
isRunning: boolean;
|
| 8 |
steps?: AgentStep[];
|
| 9 |
+
metadata?: AgentTraceMetadata;
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
export interface AgentStep {
|
| 13 |
+
traceId: string;
|
| 14 |
stepId: string;
|
| 15 |
+
error: string;
|
| 16 |
image: string;
|
| 17 |
+
thought: string;
|
| 18 |
actions: string[];
|
| 19 |
+
duration: number;
|
| 20 |
inputTokensUsed: number;
|
| 21 |
outputTokensUsed: number;
|
| 22 |
+
step_evaluation: 'like' | 'dislike' | 'neutral';
|
| 23 |
}
|
| 24 |
|
| 25 |
+
export interface AgentTraceMetadata {
|
| 26 |
+
traceId: string;
|
| 27 |
inputTokensUsed: number;
|
| 28 |
outputTokensUsed: number;
|
| 29 |
+
duration: number;
|
| 30 |
numberOfSteps: number;
|
| 31 |
}
|
| 32 |
|
| 33 |
+
// #################### WebSocket Events Types - Server to Client ########################
|
| 34 |
+
|
| 35 |
+
interface AgentStartEvent {
|
| 36 |
+
type: 'agent_start';
|
| 37 |
+
agentTrace: AgentTrace;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
interface AgentProgressEvent {
|
| 41 |
+
type: 'agent_progress';
|
| 42 |
+
agentStep: AgentStep;
|
| 43 |
+
traceMetadata: AgentTraceMetadata;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
interface AgentCompleteEvent {
|
| 47 |
+
type: 'agent_complete';
|
| 48 |
+
traceMetadata: AgentTraceMetadata;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
interface AgentErrorEvent {
|
| 52 |
+
type: 'agent_error';
|
| 53 |
+
error: string;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
interface VncUrlSetEvent {
|
| 57 |
+
type: 'vnc_url_set';
|
| 58 |
+
vncUrl: string;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
interface VncUrlUnsetEvent {
|
| 62 |
+
type: 'vnc_url_unset';
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
interface HeartbeatEvent {
|
| 66 |
+
type: 'heartbeat';
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
export type WebSocketEvent =
|
| 70 |
+
| AgentStartEvent
|
| 71 |
+
| AgentProgressEvent
|
| 72 |
+
| AgentCompleteEvent
|
| 73 |
+
| AgentErrorEvent
|
| 74 |
+
| VncUrlSetEvent
|
| 75 |
+
| VncUrlUnsetEvent
|
| 76 |
+
| HeartbeatEvent;
|
| 77 |
+
|
| 78 |
+
// #################### User Task Message Type (Through WebSocket) - Client to Server ########################
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
export interface UserTaskMessage {
|
| 82 |
+
type: 'user_task';
|
| 83 |
+
trace: AgentTrace;
|
| 84 |
}
|
cua2-front/tsconfig.app.json
CHANGED
|
@@ -30,6 +30,6 @@
|
|
| 30 |
}
|
| 31 |
},
|
| 32 |
"include": [
|
| 33 |
-
"src"
|
| 34 |
]
|
| 35 |
-
}
|
|
|
|
| 30 |
}
|
| 31 |
},
|
| 32 |
"include": [
|
| 33 |
+
"src"
|
| 34 |
]
|
| 35 |
+
}
|
pyproject.toml
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["hatchling"]
|
| 3 |
+
build-backend = "hatchling.build"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "cua2-workspace"
|
| 7 |
+
version = "0.0.0-dev.0"
|
| 8 |
+
description = "CUA2 Workspace - A comprehensive platform for AI automation with desktop environments"
|
| 9 |
+
# readme = "README.md"
|
| 10 |
+
authors = [{ name = "Amir Mahla", email = "amir.mahla@icloud.com" }]
|
| 11 |
+
keywords = ["docker", "automation", "gui", "sandbox", "desktop", "playwright", "ai", "workspace"]
|
| 12 |
+
classifiers = [
|
| 13 |
+
"Development Status :: 4 - Beta",
|
| 14 |
+
"Intended Audience :: Developers",
|
| 15 |
+
"License :: OSI Approved :: MIT License",
|
| 16 |
+
"Operating System :: OS Independent",
|
| 17 |
+
"Programming Language :: Python :: 3",
|
| 18 |
+
"Programming Language :: Python :: 3.10",
|
| 19 |
+
"Programming Language :: Python :: 3.11",
|
| 20 |
+
"Programming Language :: Python :: 3.12",
|
| 21 |
+
"Topic :: Software Development :: Testing",
|
| 22 |
+
"Topic :: System :: Emulators",
|
| 23 |
+
"Topic :: Desktop Environment",
|
| 24 |
+
]
|
| 25 |
+
requires-python = ">=3.10"
|
| 26 |
+
|
| 27 |
+
dependencies = [
|
| 28 |
+
"cua2-core"
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
[tool.uv.workspace]
|
| 33 |
+
members = [
|
| 34 |
+
"cua2-core",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
[tool.uv.sources]
|
| 38 |
+
cua2-core = { workspace = true }
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
[project.optional-dependencies]
|
| 42 |
+
dev = [
|
| 43 |
+
"pytest>=7.0.0",
|
| 44 |
+
"pytest-asyncio>=0.21.0",
|
| 45 |
+
"pytest-cov>=4.0.0",
|
| 46 |
+
"black>=23.0.0",
|
| 47 |
+
"isort>=5.12.0",
|
| 48 |
+
"flake8>=6.0.0",
|
| 49 |
+
"mypy>=1.0.0",
|
| 50 |
+
"pre-commit>=3.0.0",
|
| 51 |
+
]
|
| 52 |
+
test = [
|
| 53 |
+
"pytest>=7.0.0",
|
| 54 |
+
"pytest-asyncio>=0.21.0",
|
| 55 |
+
"pytest-cov>=4.0.0",
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
[tool.hatch.build.targets.wheel]
|
| 59 |
+
packages = ["cua2-core/src/cua2-core"]
|
| 60 |
+
|
| 61 |
+
[tool.hatch.build.targets.sdist]
|
| 62 |
+
include = [
|
| 63 |
+
"/cua2-core",
|
| 64 |
+
"/README.md",
|
| 65 |
+
"/LICENSE",
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
[tool.coverage.run]
|
| 69 |
+
source = ["cua2-core"]
|
| 70 |
+
omit = [
|
| 71 |
+
"*/tests/*",
|
| 72 |
+
"*/test_*",
|
| 73 |
+
"*/__pycache__/*",
|
| 74 |
+
"*/migrations/*",
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
[tool.coverage.report]
|
| 78 |
+
exclude_lines = [
|
| 79 |
+
"pragma: no cover",
|
| 80 |
+
"def __repr__",
|
| 81 |
+
"if self.debug:",
|
| 82 |
+
"if settings.DEBUG",
|
| 83 |
+
"raise AssertionError",
|
| 84 |
+
"raise NotImplementedError",
|
| 85 |
+
"if 0:",
|
| 86 |
+
"if __name__ == .__main__.:",
|
| 87 |
+
"class .*\\bProtocol\\):",
|
| 88 |
+
"@(abc\\.)?abstractmethod",
|
| 89 |
+
]
|