diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9c1b03b3460bed432baaab7957b8a2c891c12a5b..e07cc1c9cdaff34806c3fb8048589d00c85590e9 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -31,4 +31,4 @@ jobs: - name: Run pre-commit run: | - make pre-commit + uv run pre-commit run --all-files --show-diff-on-failure diff --git a/Makefile b/Makefile index 8631b0a735872f3358de565f777c35138131a6e6..c2917eeef97e3980946345a98c2a0b5490493a30 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: sync setup install dev-backend dev-frontend dev clean docker-build docker-run docker-stop docker-clean docker-logs +.PHONY: sync setup install dev-backend dev-frontend dev clean # Sync all dependencies (Python + Node.js) sync: @@ -23,14 +23,6 @@ dev-frontend: pre-commit: uv run pre-commit run --all-files --show-diff-on-failure - make test - -# Run tests -test: - cd cua2-core && uv run pytest tests/ -v - -test-coverage: - cd cua2-core && uv run pytest tests/ -v --cov=cua2_core --cov-report=html --cov-report=term clean: find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true @@ -38,42 +30,3 @@ clean: find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true cd cua2-front && rm -rf node_modules dist 2>/dev/null || true @echo "✓ Cleaned!" - -# Docker commands -docker-build: - @echo "Building Docker image..." - make docker-stop - docker build -t cua2:latest . - @echo "✓ Docker image built successfully!" - -docker-run: - @echo "Starting CUA2 container..." - @if [ -z "$$E2B_API_KEY" ]; then \ - echo "Error: E2B_API_KEY environment variable is not set"; \ - echo "Please set it with: export E2B_API_KEY=your-key"; \ - exit 1; \ - fi - @if [ -z "$$HF_TOKEN" ]; then \ - echo "Error: HF_TOKEN environment variable is not set"; \ - echo "Please set it with: export HF_TOKEN=your-token"; \ - exit 1; \ - fi - docker run -d --name cua2-app -p 7860:7860 \ - -e E2B_API_KEY="$$E2B_API_KEY" \ - -e HF_TOKEN="$$HF_TOKEN" \ - cua2:latest - @echo "✓ Container started! Access at http://localhost:7860" - -docker-stop: - @echo "Stopping CUA2 container..." - docker stop cua2-app || true - docker rm cua2-app || true - @echo "✓ Container stopped!" - -docker-clean: - @echo "Removing CUA2 Docker images..." - docker rmi cua2:latest || true - @echo "✓ Docker images removed!" - -docker-logs: - docker logs -f cua2-app diff --git a/cua2-core/src/cua2_core/app.py b/cua2-core/src/cua2_core/app.py index 81ce4c7b725b3621920fe2410b10f6190d80e57b..d20f507f00f72fb00410ca56e311f8ea2a43579a 100644 --- a/cua2-core/src/cua2_core/app.py +++ b/cua2-core/src/cua2_core/app.py @@ -1,4 +1,3 @@ -import os from contextlib import asynccontextmanager from cua2_core.services.agent_service import AgentService @@ -18,9 +17,6 @@ async def lifespan(app: FastAPI): # Startup: Initialize services print("Initializing services...") - if not os.getenv("HF_TOKEN"): - raise ValueError("HF_TOKEN is not set") - websocket_manager = WebSocketManager() sandbox_service = SandboxService() diff --git a/cua2-core/src/cua2_core/websocket/websocket_manager.py b/cua2-core/src/cua2_core/websocket/websocket_manager.py index ab7dd622ae6479811c6c052b2513ab731f02d713..f6d64da4f31bb2c307ca865069c154773b982741 100644 --- a/cua2-core/src/cua2_core/websocket/websocket_manager.py +++ b/cua2-core/src/cua2_core/websocket/websocket_manager.py @@ -52,7 +52,7 @@ class WebSocketManager: try: await websocket.send_text( json.dumps( - message.model_dump(mode="json", context={"actions_as_json": False}) + message.model_dump(mode="json", context={"actions_as_json": True}) ) ) except Exception as e: diff --git a/cua2-front/package-lock.json b/cua2-front/package-lock.json index 3dec41c7cf4f73b6b2156cbdf4fd9a053210daff..d6510e87eb00ed29a2b5a90416930267cf589246 100644 --- a/cua2-front/package-lock.json +++ b/cua2-front/package-lock.json @@ -8,10 +8,17 @@ "name": "cua2-front", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.4", + "@mui/lab": "^7.0.1-beta.19", + "@mui/material": "^7.3.4", + "gifshot": "^0.4.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.30.1", - "ulid": "^3.0.1" + "ulid": "^3.0.1", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/js": "^9.38.0", @@ -28,6 +35,291 @@ "vite": "^5.4.19" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -628,6 +920,318 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz", + "integrity": "sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.4.tgz", + "integrity": "sha512-9n6Xcq7molXWYb680N2Qx+FRW8oT6j/LXF5PZFH3ph9X/Rct0B/BlLAsFI7iL9ySI6LVLuQIVtrLiPT82R7OZw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.3.4", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/lab": { + "version": "7.0.1-beta.19", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-7.0.1-beta.19.tgz", + "integrity": "sha512-Ekxd2mPnr5iKwrMXjN/y2xgpxPX8ithBBcDenjqNdBt/ZQumrmBl0ifVoqAHsL6lxN6DOgRsWTRc4eOdDiB+0Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/system": "^7.3.5", + "@mui/types": "^7.4.8", + "@mui/utils": "^7.3.5", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": "^7.3.5", + "@mui/material-pigment-css": "^7.3.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", + "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/core-downloads-tracker": "^7.3.5", + "@mui/system": "^7.3.5", + "@mui/types": "^7.4.8", + "@mui/utils": "^7.3.5", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.5.tgz", + "integrity": "sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.5.tgz", + "integrity": "sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz", + "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/private-theming": "^7.3.5", + "@mui/styled-engine": "^7.3.5", + "@mui/types": "^7.4.8", + "@mui/utils": "^7.3.5", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.8", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.8.tgz", + "integrity": "sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.5.tgz", + "integrity": "sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.8", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -666,6 +1270,16 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@remix-run/router": { "version": "1.23.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", @@ -1240,18 +1854,22 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1268,6 +1886,15 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", @@ -1641,6 +2268,21 @@ "postcss": "^8.1.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1720,7 +2362,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1764,6 +2405,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1791,6 +2441,28 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1810,14 +2482,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1838,6 +2508,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.237", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", @@ -1845,6 +2525,15 @@ "dev": true, "license": "ISC" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1898,7 +2587,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2185,6 +2873,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2252,6 +2946,21 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gifshot": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/gifshot/-/gifshot-0.4.5.tgz", + "integrity": "sha512-oaOTT7patjxFFv7ptR0R0NNhqy3ZAmcLUQCjM/sTsvsQaUAlB2fHirLajcNAKJ6ufoVhdP+ZkXYvmUycHP1FNg==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2295,6 +3004,33 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2309,7 +3045,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2332,6 +3067,27 @@ "node": ">=0.8.19" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2391,6 +3147,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2398,6 +3166,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2436,6 +3210,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2512,7 +3292,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2558,6 +3337,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2612,7 +3400,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -2621,6 +3408,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2641,11 +3446,25 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2707,6 +3526,23 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2763,6 +3599,12 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT" + }, "node_modules/react-router": { "version": "6.30.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", @@ -2795,11 +3637,46 @@ "react-dom": ">=16.8" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2927,6 +3804,15 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2950,6 +3836,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2963,6 +3855,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3184,6 +4088,15 @@ "node": ">=0.10.0" } }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -3196,6 +4109,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/cua2-front/package.json b/cua2-front/package.json index c6b760888606dc1e8a34e12aec1e9f0b6738a928..4804968edfce379a007a1ad17374232efb1529ac 100644 --- a/cua2-front/package.json +++ b/cua2-front/package.json @@ -12,10 +12,17 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.4", + "@mui/lab": "^7.0.1-beta.19", + "@mui/material": "^7.3.4", + "gifshot": "^0.4.5", "react": "^18.3.1", - "react-router-dom": "^6.30.1", "react-dom": "^18.3.1", - "ulid": "^3.0.1" + "react-router-dom": "^6.30.1", + "ulid": "^3.0.1", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/js": "^9.38.0", diff --git a/cua2-front/src/App.tsx b/cua2-front/src/App.tsx index 04acaae7159e60caa8f1adedeca332cf1dd2cafd..7ee68e26a677279a1efbdf1aa1ffef3c49f96044 100644 --- a/cua2-front/src/App.tsx +++ b/cua2-front/src/App.tsx @@ -1,14 +1,31 @@ -import React from 'react'; +import { useMemo } from 'react'; import { BrowserRouter, Routes, Route } from "react-router-dom"; -import Index from "./pages/Index"; - -const App = () => ( - - - } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - - -); +import { ThemeProvider, CssBaseline } from '@mui/material'; +import getTheme from './theme'; +import Welcome from "./pages/Welcome"; +import Task from "./pages/Task"; +import { useAgentStore, selectIsDarkMode } from './stores/agentStore'; +import { useAgentWebSocket } from './hooks/useAgentWebSocket'; +import { config } from './config'; + +const App = () => { + const isDarkMode = useAgentStore(selectIsDarkMode); + const theme = useMemo(() => getTheme(isDarkMode ? 'dark' : 'light'), [isDarkMode]); + + // Initialize WebSocket connection at app level so it persists across route changes + useAgentWebSocket({ url: config.wsUrl }); + + return ( + + + + + } /> + } /> + + + + ); +}; export default App; diff --git a/cua2-front/src/components/ConnectionStatus.tsx b/cua2-front/src/components/ConnectionStatus.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7bfbd8f28b0fc1e5a4fa4e971a21b847a936b74a --- /dev/null +++ b/cua2-front/src/components/ConnectionStatus.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Box, Chip, keyframes } from '@mui/material'; +import CircleIcon from '@mui/icons-material/Circle'; + +interface ConnectionStatusProps { + isConnected: boolean; +} + +// Pulse animation for connected indicator +const pulse = keyframes` + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +`; + +export const ConnectionStatus: React.FC = ({ isConnected }) => { + return ( + + } + onDelete={() => {}} // Required for deleteIcon to show + size="small" + sx={{ + backgroundColor: 'action.hover', + border: '1px solid', + borderColor: 'divider', + color: 'text.primary', + fontSize: '0.7rem', + fontWeight: 500, + height: 'auto', + '& .MuiChip-label': { + px: 1, + py: 0.5, + }, + '& .MuiChip-deleteIcon': { + color: isConnected ? '#10b981' : '#ef4444', + marginRight: 0.5, + '&:hover': { + color: isConnected ? '#10b981' : '#ef4444', + }, + }, + }} + /> + ); +}; diff --git a/cua2-front/src/components/Header.tsx b/cua2-front/src/components/Header.tsx new file mode 100644 index 0000000000000000000000000000000000000000..14ed56898ebf5dcb51956458f63c2ca81ca8c19a --- /dev/null +++ b/cua2-front/src/components/Header.tsx @@ -0,0 +1,409 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { AppBar, Toolbar, Box, Typography, Chip, IconButton, CircularProgress, keyframes } from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import LightModeOutlined from '@mui/icons-material/LightModeOutlined'; +import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'; +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import InputIcon from '@mui/icons-material/Input'; +import OutputIcon from '@mui/icons-material/Output'; +import SmartToyIcon from '@mui/icons-material/SmartToy'; +import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; +import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; +import { useAgentStore, selectTrace, selectError, selectIsDarkMode, selectMetadata, selectIsConnectingToE2B, selectFinalStep } from '@/stores/agentStore'; + +interface HeaderProps { + isAgentProcessing: boolean; + onBackToHome?: () => void; +} + +// Animation for the running task border - smooth oscillation (primary) +const borderPulse = keyframes` + 0%, 100% { + border-color: rgba(79, 134, 198, 0.5); + box-shadow: 0 0 0 0 rgba(79, 134, 198, 0.3); + } + 50% { + border-color: rgba(79, 134, 198, 1); + box-shadow: 0 0 8px 2px rgba(79, 134, 198, 0.4); + } +`; + +// Animation for the background glow (primary) +const backgroundPulse = keyframes` + 0%, 100% { + background-color: rgba(79, 134, 198, 0.08); + } + 50% { + background-color: rgba(79, 134, 198, 0.15); + } +`; + +// Animation for token flash - smooth glow effect +const tokenFlash = keyframes` + 0% { + filter: brightness(1); + text-shadow: none; + } + 25% { + filter: brightness(1.4); + text-shadow: 0 0 8px rgba(79, 134, 198, 0.6); + } + 100% { + filter: brightness(1); + text-shadow: none; + } +`; + +// Animation for token icon flash +const iconFlash = keyframes` + 0% { + filter: brightness(1); + transform: scale(1); + } + 25% { + filter: brightness(1.6); + transform: scale(1.15); + } + 100% { + filter: brightness(1); + transform: scale(1); + } +`; + +export const Header: React.FC = ({ isAgentProcessing, onBackToHome }) => { + const trace = useAgentStore(selectTrace); + const error = useAgentStore(selectError); + const finalStep = useAgentStore(selectFinalStep); + const isDarkMode = useAgentStore(selectIsDarkMode); + const toggleDarkMode = useAgentStore((state) => state.toggleDarkMode); + const metadata = useAgentStore(selectMetadata); + const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B); + const [elapsedTime, setElapsedTime] = useState(0); + const [inputTokenFlash, setInputTokenFlash] = useState(false); + const [outputTokenFlash, setOutputTokenFlash] = useState(false); + const prevInputTokens = useRef(0); + const prevOutputTokens = useRef(0); + + // Update elapsed time every 100ms when agent is processing + useEffect(() => { + if (isAgentProcessing && trace?.timestamp) { + const interval = setInterval(() => { + const now = new Date(); + const startTime = new Date(trace.timestamp); + const elapsed = (now.getTime() - startTime.getTime()) / 1000; + setElapsedTime(elapsed); + }, 100); + + return () => clearInterval(interval); + } else if (metadata && metadata.duration > 0) { + setElapsedTime(metadata.duration); + } + }, [isAgentProcessing, trace?.timestamp, metadata]); + + // Detect token changes and trigger flash animation + useEffect(() => { + if (metadata) { + // Input tokens changed + if (metadata.inputTokensUsed > prevInputTokens.current && prevInputTokens.current > 0) { + setInputTokenFlash(true); + setTimeout(() => setInputTokenFlash(false), 800); + } + prevInputTokens.current = metadata.inputTokensUsed; + + // Output tokens changed + if (metadata.outputTokensUsed > prevOutputTokens.current && prevOutputTokens.current > 0) { + setOutputTokenFlash(true); + setTimeout(() => setOutputTokenFlash(false), 800); + } + prevOutputTokens.current = metadata.outputTokensUsed; + } + }, [metadata?.inputTokensUsed, metadata?.outputTokensUsed]); + + // Determine task status - Use finalStep as source of truth + const getTaskStatus = () => { + // If we have a final step, use its type + if (finalStep) { + if (finalStep.type === 'failure') { + return { label: 'Task failed', color: 'error', icon: }; + } + return { label: 'Completed', color: 'success', icon: }; + } + // Otherwise check running states + if (isConnectingToE2B) return { label: 'Connecting to E2B...', color: 'primary', icon: }; + if (isAgentProcessing || trace?.isRunning) return { label: 'Running', color: 'primary', icon: }; + return { label: 'Ready', color: 'default', icon: }; + }; + + const taskStatus = getTaskStatus(); + + // Extract model name from modelId (e.g., "Qwen/Qwen3-VL-8B-Instruct" -> "Qwen3-VL-8B-Instruct") + const modelName = trace?.modelId?.split('/').pop() || 'Unknown Model'; + + return ( + + + {/* First row: Back button + Task info + Connection Status */} + + {/* Left side: Back button + Task info */} + + + + + + {trace?.instruction || 'No task running'} + + + + {/* Right side: Dark Mode */} + + + {isDarkMode ? : } + + + + + {/* Second row: Status + Model + Metadata - Only show when we have trace data */} + {trace && ( + + {/* Status Badge - Compact */} + + {taskStatus.icon} + + {taskStatus.label} + + + + {/* Divider */} + + + {/* Model */} + + + + {modelName} + + + + {/* Steps Count */} + {metadata && ( + <> + + + + {metadata.numberOfSteps} + + + {metadata.numberOfSteps === 1 ? 'Step' : 'Steps'} + + + > + )} + + {/* Time */} + {(isAgentProcessing || metadata) && ( + <> + + + + + {elapsedTime.toFixed(1)}s + + + > + )} + + {/* Input Tokens */} + {metadata && metadata.inputTokensUsed > 0 && ( + <> + + + + + + {metadata.inputTokensUsed.toLocaleString()} + + + + > + )} + + {/* Output Tokens */} + {metadata && metadata.outputTokensUsed > 0 && ( + <> + + + + + + {metadata.outputTokensUsed.toLocaleString()} + + + + > + )} + + )} + + + ); +}; diff --git a/cua2-front/src/components/ProcessingIndicator.tsx b/cua2-front/src/components/ProcessingIndicator.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0346df6d21405a73abe8b0274e4fcb6888e35e13 --- /dev/null +++ b/cua2-front/src/components/ProcessingIndicator.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Box, CircularProgress, Typography } from '@mui/material'; + +interface ProcessingIndicatorProps { + isAgentProcessing: boolean; +} + +export const ProcessingIndicator: React.FC = ({ isAgentProcessing }) => { + if (!isAgentProcessing) return null; + + return ( + + + + Agent is running... + + + ); +}; diff --git a/cua2-front/src/components/WelcomeScreen.tsx b/cua2-front/src/components/WelcomeScreen.tsx new file mode 100644 index 0000000000000000000000000000000000000000..eb64e22683f1ac546370e0befce7be5daff6b519 --- /dev/null +++ b/cua2-front/src/components/WelcomeScreen.tsx @@ -0,0 +1,452 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Box, Typography, Button, Container, Paper, TextField, IconButton, Select, MenuItem, FormControl, InputLabel, CircularProgress } from '@mui/material'; +import ShuffleIcon from '@mui/icons-material/Shuffle'; +import SendIcon from '@mui/icons-material/Send'; +import LightModeOutlined from '@mui/icons-material/LightModeOutlined'; +import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'; +import SmartToyIcon from '@mui/icons-material/SmartToy'; +import { useAgentStore, selectSelectedModelId, selectIsDarkMode, selectAvailableModels, selectIsLoadingModels } from '@/stores/agentStore'; +import { fetchAvailableModels, generateRandomQuestion } from '@/services/api'; + +interface WelcomeScreenProps { + onStartTask: (instruction: string, modelId: string) => void; + isConnected: boolean; +} + +export const WelcomeScreen: React.FC = ({ onStartTask, isConnected }) => { + const [customTask, setCustomTask] = useState(''); + const [isTyping, setIsTyping] = useState(false); + const [isGeneratingQuestion, setIsGeneratingQuestion] = useState(false); + const typingIntervalRef = useRef(null); + + const isDarkMode = useAgentStore(selectIsDarkMode); + const toggleDarkMode = useAgentStore((state) => state.toggleDarkMode); + const selectedModelId = useAgentStore(selectSelectedModelId); + const setSelectedModelId = useAgentStore((state) => state.setSelectedModelId); + const availableModels = useAgentStore(selectAvailableModels); + const isLoadingModels = useAgentStore(selectIsLoadingModels); + const setAvailableModels = useAgentStore((state) => state.setAvailableModels); + const setIsLoadingModels = useAgentStore((state) => state.setIsLoadingModels); + + // Load available models on mount + useEffect(() => { + const loadModels = async () => { + setIsLoadingModels(true); + try { + const models = await fetchAvailableModels(); + setAvailableModels(models); + + // Set first model as default if current selection is not in the list + if (models.length > 0 && !models.includes(selectedModelId)) { + setSelectedModelId(models[0]); + } + } catch (error) { + console.error('Failed to load models:', error); + // Fallback to empty array on error + setAvailableModels([]); + } finally { + setIsLoadingModels(false); + } + }; + + loadModels(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // Clean up typing interval on unmount + useEffect(() => { + return () => { + if (typingIntervalRef.current) { + clearInterval(typingIntervalRef.current); + } + }; + }, []); + + const handleWriteRandomTask = async () => { + // Clear any existing typing interval + if (typingIntervalRef.current) { + clearInterval(typingIntervalRef.current); + typingIntervalRef.current = null; + } + + setIsGeneratingQuestion(true); + try { + const randomTask = await generateRandomQuestion(selectedModelId); + + // Clear current text + setCustomTask(''); + setIsTyping(true); + + // Type effect + let currentIndex = 0; + typingIntervalRef.current = setInterval(() => { + if (currentIndex < randomTask.length) { + setCustomTask(randomTask.substring(0, currentIndex + 1)); + currentIndex++; + } else { + if (typingIntervalRef.current) { + clearInterval(typingIntervalRef.current); + typingIntervalRef.current = null; + } + setIsTyping(false); + } + }, 30); // 30ms per character + } catch (error) { + console.error('Failed to generate question:', error); + setIsTyping(false); + } finally { + setIsGeneratingQuestion(false); + } + }; + + const handleCustomTask = () => { + if (customTask.trim() && !isTyping) { + onStartTask(customTask.trim(), selectedModelId); + } + }; + + return ( + <> + {/* Dark Mode Toggle - Top Right (Absolute to viewport) */} + + + {isDarkMode ? : } + + + + + {/* Title */} + + CUA2 Agent + + + {/* Powered by smolagents */} + + + Powered by + + + {/* Hugging Face Official Logo */} + + + + smolagents + + + {/* GitHub stars badge */} + + theme.palette.mode === 'dark' + ? 'rgba(144, 202, 249, 0.08)' + : 'rgba(25, 118, 210, 0.08)', + borderRadius: 1, + border: '1px solid', + borderColor: 'primary.main', + }} + > + ⭐ + + 23.7k + + + + + + {/* Subtitle */} + + AI-Powered Computer Use Automation + + + {/* Description */} + + Watch in real-time as AI agents write and execute Python code to complete tasks. + Built by Hugging Face, smolagents is LLM-agnostic and uses 30% fewer steps than traditional agents. + + + {/* Task Input Section */} + `0 4px 16px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.3)' : 'rgba(79, 134, 198, 0.15)'}`, + } : {}, + }} + > + {/* Input Field */} + setCustomTask(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && !e.shiftKey && isConnected && customTask.trim() && !isTyping) { + handleCustomTask(); + } + }} + disabled={!isConnected || isTyping} + multiline + rows={3} + sx={{ + mb: 2, + '& .MuiOutlinedInput-root': { + borderRadius: 1.5, + backgroundColor: 'action.hover', + color: 'text.primary', + '& fieldset': { + borderColor: 'divider', + }, + '&:hover fieldset': { + borderColor: 'text.secondary', + }, + '&.Mui-focused fieldset': { + borderColor: 'primary.main', + borderWidth: '2px', + }, + }, + '& .MuiInputBase-input': { + color: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important', + fontWeight: 500, + WebkitTextFillColor: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important', + }, + '& .MuiInputBase-input.Mui-disabled': { + color: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important', + WebkitTextFillColor: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important', + }, + '& .MuiInputBase-input::placeholder': { + color: 'text.secondary', + opacity: 0.7, + }, + }} + /> + + {/* Model Selection + Buttons Row */} + + {/* Model Select */} + + Model + 0 && availableModels.includes(selectedModelId) ? selectedModelId : ''} + label="Model" + onChange={(e) => setSelectedModelId(e.target.value)} + disabled={!isConnected || isTyping || isLoadingModels} + sx={{ + borderRadius: 1.5, + '& .MuiOutlinedInput-notchedOutline': { + borderWidth: 2, + }, + }} + > + {isLoadingModels ? ( + + + + Loading models... + + + ) : availableModels.length === 0 ? ( + + + No models available + + + ) : ( + availableModels.map((modelId) => ( + + + + + {modelId.split('/').pop()} + + + + )) + )} + + + + {/* Buttons on the right */} + + : } + sx={{ + borderRadius: 1.5, + textTransform: 'none', + fontWeight: 600, + borderWidth: 2, + px: 3, + '&:hover': { + borderWidth: 2, + }, + }} + > + {isGeneratingQuestion ? 'Generating...' : isTyping ? 'Writing...' : 'Write random task'} + + + } + > + Run Task + + + + + + {/* Connection status hint */} + {!isConnected && ( + + + Make sure the backend is running on port 8000 + + )} + + > + ); +}; diff --git a/cua2-front/src/components/index.ts b/cua2-front/src/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1bfc970d05a4af4d8f5c43a5d37f40f9e472d969 --- /dev/null +++ b/cua2-front/src/components/index.ts @@ -0,0 +1,14 @@ +// General components +export { Header } from './Header'; +export { ConnectionStatus } from './ConnectionStatus'; +export { ProcessingIndicator } from './ProcessingIndicator'; +export { WelcomeScreen } from './WelcomeScreen'; + +// Sandbox components +export { SandboxViewer, CompletionView, DownloadGifButton, DownloadJsonButton } from './sandbox'; + +// Timeline components +export { Timeline } from './timeline'; + +// Steps components +export { StepsList, StepCard, FinalStepCard, ThinkingStepCard, ConnectionStepCard } from './steps'; diff --git a/cua2-front/src/components/mock/ConnectionStatus.tsx b/cua2-front/src/components/mock/ConnectionStatus.tsx deleted file mode 100644 index 7c8bc50ea2ca843ea613d7295ba2813c42f9229b..0000000000000000000000000000000000000000 --- a/cua2-front/src/components/mock/ConnectionStatus.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -interface ConnectionStatusProps { - isConnected: boolean; -} - -export const ConnectionStatus: React.FC = ({ isConnected }) => { - return ( - - - - - {isConnected ? 'Connected' : 'Disconnected'} - - - WebSocket - - - - ); -}; diff --git a/cua2-front/src/components/mock/Header.tsx b/cua2-front/src/components/mock/Header.tsx deleted file mode 100644 index 9c9067f35206719cd04381903387d6b082bf63f8..0000000000000000000000000000000000000000 --- a/cua2-front/src/components/mock/Header.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { ConnectionStatus } from './ConnectionStatus'; -import { ProcessingIndicator } from './ProcessingIndicator'; -import { TaskButton } from './TaskButton'; - -interface HeaderProps { - isConnected: boolean; - isAgentProcessing: boolean; - onSendTask: (content: string, modelId: string) => void; -} - -export const Header: React.FC = ({ isConnected, isAgentProcessing, onSendTask }) => { - return ( - <> - - - - - - - CUA2 Agent - - - - - - - - - - > - ); -}; diff --git a/cua2-front/src/components/mock/Metadata.tsx b/cua2-front/src/components/mock/Metadata.tsx deleted file mode 100644 index 9bd1d95db52bb7b1879b0d0f2484a054508fd437..0000000000000000000000000000000000000000 --- a/cua2-front/src/components/mock/Metadata.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { AgentTrace } from '@/types/agent'; -import React from 'react'; - -interface MetadataProps { - trace?: AgentTrace; -} - -export const Metadata: React.FC = ({ trace }) => { - return ( - - Metadata - {trace?.metadata ? ( - - - Total Time - {trace.metadata.duration.toFixed(2)}s - - - In Tokens - {trace.metadata.inputTokensUsed.toLocaleString()} - - - Out Tokens - {trace.metadata.outputTokensUsed.toLocaleString()} - - - Total Steps - {trace.metadata.numberOfSteps} - - - ) : ( - - {trace ? 'Waiting for completion...' : 'No task started yet'} - - )} - - ); -}; diff --git a/cua2-front/src/components/mock/ProcessingIndicator.tsx b/cua2-front/src/components/mock/ProcessingIndicator.tsx deleted file mode 100644 index ad4c0761819d4fe9b47b718d63324cc64b3fb400..0000000000000000000000000000000000000000 --- a/cua2-front/src/components/mock/ProcessingIndicator.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; - -interface ProcessingIndicatorProps { - isAgentProcessing: boolean; -} - -export const ProcessingIndicator: React.FC = ({ isAgentProcessing }) => { - if (!isAgentProcessing) return null; - - return ( - - - - PROCESSING... - - - ); -}; diff --git a/cua2-front/src/components/mock/StackSteps.tsx b/cua2-front/src/components/mock/StackSteps.tsx deleted file mode 100644 index 9fbf768552b20222f0022f35a01981a8290e9c1f..0000000000000000000000000000000000000000 --- a/cua2-front/src/components/mock/StackSteps.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { AgentTrace } from '@/types/agent'; -import { StepCard } from './StepCard'; - -interface StackStepsProps { - trace?: AgentTrace; -} - -export const StackSteps: React.FC = ({ trace }) => { - return ( - - Stack Steps - - {trace?.steps && trace.steps.length > 0 ? ( - - {trace.steps.map((step, index) => ( - - ))} - - ) : ( - - No steps yet - Steps will appear as agent progresses - - )} - - - ); -}; diff --git a/cua2-front/src/components/mock/StepCard.tsx b/cua2-front/src/components/mock/StepCard.tsx deleted file mode 100644 index fc74b3eafb1127bf7da424931f077787d02bad73..0000000000000000000000000000000000000000 --- a/cua2-front/src/components/mock/StepCard.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { AgentStep } from '@/types/agent'; -import React from 'react'; - -interface StepCardProps { - step: AgentStep; - index: number; -} - -export const StepCard: React.FC = ({ step, index }) => { - return ( - - {/* Step Header */} - - Step {index + 1} - - - - {/* Step Image */} - {step.image && ( - - - - - - - )} - - {/* Thought */} - - - - 💭 - Thought - - {step.thought} - - - - - {/* Actions */} - - - - ⚡ - Actions - - - {step.actions.map((action, actionIndex) => ( - - → - {action} - - ))} - - - - - - {/* Step Metadata Footer */} - - - Time - {step.duration.toFixed(2)}s - - - In Tokens - {step.inputTokensUsed} - - - Out Tokens - {step.outputTokensUsed} - - - - ); -}; diff --git a/cua2-front/src/components/mock/TaskButton.tsx b/cua2-front/src/components/mock/TaskButton.tsx deleted file mode 100644 index 34a8d98854028f26ec021e9ea042e54ceb3dae94..0000000000000000000000000000000000000000 --- a/cua2-front/src/components/mock/TaskButton.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; - -interface TaskButtonProps { - isAgentProcessing: boolean; - isConnected: boolean; - onSendTask: (content: string, modelId: string) => void; -} - -export const TaskButton: React.FC = ({ isAgentProcessing, isConnected, onSendTask }) => { - return ( - { - if (!isAgentProcessing && isConnected) { - onSendTask( - "Find the price of a NVIDIA RTX 4090 GPU", - "Qwen/Qwen3-VL-30B-A3B-Instruct" - ); - } - }} - style={{ - marginTop: '16px', - padding: '14px 18px', - background: isAgentProcessing || !isConnected - ? 'rgba(255, 255, 255, 0.1)' - : 'rgba(255, 255, 255, 0.15)', - borderRadius: '10px', - backdropFilter: 'blur(10px)', - border: '2px solid rgba(0, 0, 0, 0.3)', - cursor: isAgentProcessing || !isConnected ? 'not-allowed' : 'pointer', - transition: 'all 0.3s ease', - opacity: isAgentProcessing || !isConnected ? 0.6 : 1, - }} - onMouseEnter={(e) => { - if (!isAgentProcessing && isConnected) { - e.currentTarget.style.background = 'rgba(200, 200, 200, 0.3)'; - e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.5)'; - e.currentTarget.style.transform = 'translateY(-2px)'; - e.currentTarget.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.2)'; - } - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)'; - e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.3)'; - e.currentTarget.style.transform = 'translateY(0)'; - e.currentTarget.style.boxShadow = 'none'; - }} - > - - - - Task - {!isAgentProcessing && isConnected && ( - - (click to run) - - )} - - - Find the price of a NVIDIA RTX 4090 GPU - - - - Model - - Qwen/Qwen3-VL-30B-A3B-Instruct - - - - - ); -}; diff --git a/cua2-front/src/components/mock/VNCStream.tsx b/cua2-front/src/components/mock/VNCStream.tsx deleted file mode 100644 index b69a820fdb5a9e6fccec2fcbcde8d240bd08a43e..0000000000000000000000000000000000000000 --- a/cua2-front/src/components/mock/VNCStream.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -interface VNCStreamProps { - vncUrl: string; -} - -export const VNCStream: React.FC = ({ vncUrl }) => { - return ( - - VNC Stream - - {vncUrl ? ( - - ) : ( - - - - - No VNC stream available - Stream will appear when agent starts - - )} - - - ); -}; diff --git a/cua2-front/src/components/mock/index.ts b/cua2-front/src/components/mock/index.ts deleted file mode 100644 index 0feefce1670f98ea2a8c7b6aaf79252116be22b1..0000000000000000000000000000000000000000 --- a/cua2-front/src/components/mock/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { ConnectionStatus } from './ConnectionStatus'; -export { ProcessingIndicator } from './ProcessingIndicator'; -export { TaskButton } from './TaskButton'; -export { Header } from './Header'; -export { VNCStream } from './VNCStream'; -export { Metadata } from './Metadata'; -export { StepCard } from './StepCard'; -export { StackSteps } from './StackSteps'; diff --git a/cua2-front/src/components/sandbox/SandboxViewer.tsx b/cua2-front/src/components/sandbox/SandboxViewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0a09b62081d482bf9ae5f20d7212783d0cee398d --- /dev/null +++ b/cua2-front/src/components/sandbox/SandboxViewer.tsx @@ -0,0 +1,367 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Box, Typography, CircularProgress, Button, keyframes } from '@mui/material'; +import MonitorIcon from '@mui/icons-material/Monitor'; +import ImageIcon from '@mui/icons-material/Image'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import { AgentTraceMetadata, AgentStep } from '@/types/agent'; +import { useAgentStore, selectError, selectFinalStep, selectSteps, selectTrace } from '@/stores/agentStore'; +import { CompletionView } from './CompletionView'; +import { useGifGenerator } from '@/hooks/useGifGenerator'; +import { useJsonExporter } from '@/hooks/useJsonExporter'; + +// Animation for live indicator +const livePulse = keyframes` + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.2); + } +`; + +interface SandboxViewerProps { + vncUrl: string; + isAgentProcessing?: boolean; + metadata?: AgentTraceMetadata; + traceStartTime?: Date; + selectedStep?: AgentStep | null; // The step to display in time-travel mode + isRunning?: boolean; // Is the agent currently running +} + +export const SandboxViewer: React.FC = ({ + vncUrl, + isAgentProcessing = false, + metadata, + traceStartTime, + selectedStep, + isRunning = false +}) => { + const navigate = useNavigate(); + const error = useAgentStore(selectError); + const finalStep = useAgentStore(selectFinalStep); + const steps = useAgentStore(selectSteps); + const trace = useAgentStore(selectTrace); + const resetAgent = useAgentStore((state) => state.resetAgent); + const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex); + + // Hook to generate GIF + const { isGenerating, error: gifError, generateAndDownloadGif } = useGifGenerator({ + steps: steps || [], + traceId: finalStep?.metadata.traceId || '', + }); + + // Hook to export JSON + const { downloadTraceAsJson } = useJsonExporter({ + trace, + steps: steps || [], + metadata: finalStep?.metadata || metadata, + }); + + // Extract final_answer from the last step, or fallback to last thought + const getFinalAnswer = (): string | null => { + console.log('🔍 getFinalAnswer - steps:', steps); + if (!steps || steps.length === 0) { + console.log('❌ No steps available'); + return null; + } + + // Try to find final_answer in any step (iterate backwards) + for (let i = steps.length - 1; i >= 0; i--) { + const step = steps[i]; + + if (step.actions && Array.isArray(step.actions)) { + const finalAnswerAction = step.actions.find( + (action) => action.function_name === 'final_answer' + ); + + if (finalAnswerAction) { + // Handle both named parameter and positional argument + const result = finalAnswerAction?.parameters?.answer || finalAnswerAction?.parameters?.arg_0 || null; + console.log('✅ Final answer found in step', i + 1, ':', result); + return result; + } + } + } + + console.log('🔍 No final_answer found, looking for last thought...'); + + // Fallback: find the last step with a thought (iterate backwards) + for (let i = steps.length - 1; i >= 0; i--) { + const step = steps[i]; + if (step.thought) { + console.log('📝 Using thought from step', i + 1, 'as fallback:', step.thought); + return step.thought; + } + } + + console.log('❌ No final answer or thought found in any step'); + return null; + }; + + const finalAnswer = getFinalAnswer(); + console.log('🎯 Final answer to display:', finalAnswer); + + // Determine if we should show success/fail status + const showStatus = !isRunning && !selectedStep && finalStep; + + // Handler to go back to home + const handleBackToHome = () => { + resetAgent(); + navigate('/'); + }; + + // Handler to go back to live mode + const handleGoLive = () => { + setSelectedStepIndex(null); + }; + + return ( + + {/* Live Badge or Go Live Button */} + {vncUrl && !showStatus && ( + <> + {!selectedStep ? ( + // Live Badge when in live mode + + theme.palette.mode === 'dark' + ? 'rgba(0, 0, 0, 0.7)' + : 'rgba(255, 255, 255, 0.9)', + backdropFilter: 'blur(8px)', + borderRadius: 0.75, + border: '1px solid', + borderColor: 'primary.main', + boxShadow: (theme) => + theme.palette.mode === 'dark' + ? '0 2px 8px rgba(0, 0, 0, 0.4)' + : '0 2px 8px rgba(0, 0, 0, 0.1)', + }} + > + + + Live + + + ) : ( + // Go Live Button when viewing a specific step + } + sx={{ + position: 'absolute', + top: 12, + right: 12, + zIndex: 10, + px: 2, + py: 1, + backgroundColor: (theme) => + theme.palette.mode === 'dark' + ? 'rgba(0, 0, 0, 0.7)' + : 'rgba(255, 255, 255, 0.9)', + backdropFilter: 'blur(8px)', + borderRadius: 0.75, + border: '1px solid', + borderColor: 'primary.main', + boxShadow: (theme) => + theme.palette.mode === 'dark' + ? '0 2px 8px rgba(0, 0, 0, 0.4)' + : '0 2px 8px rgba(0, 0, 0, 0.1)', + fontSize: '0.8rem', + fontWeight: 700, + textTransform: 'uppercase', + letterSpacing: '0.5px', + color: 'primary.main', + '&:hover': { + backgroundColor: (theme) => + theme.palette.mode === 'dark' + ? 'rgba(0, 0, 0, 0.85)' + : 'rgba(255, 255, 255, 1)', + borderColor: 'primary.dark', + }, + }} + > + Go Live + + )} + > + )} + + + {showStatus && finalStep ? ( + // Show success/fail status when agent has completed + + ) : selectedStep ? ( + // Time-travel mode: Show screenshot of selected step + + {selectedStep.image ? ( + + ) : ( + + + + No screenshot available + + + This step doesn't have a screenshot + + + )} + + ) : vncUrl ? ( + // Live mode: Show VNC stream + + ) : isAgentProcessing ? ( + // Loading state + + + + Connecting to E2B... + + + Setting up sandbox environment + + + ) : ( + // No stream available + + + + No stream available + + + Stream will appear when agent starts + + + )} + + + ); +}; diff --git a/cua2-front/src/components/sandbox/completionview/CompletionView.tsx b/cua2-front/src/components/sandbox/completionview/CompletionView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8d6f88a8967ed54175e2f5e3ff758a16c79f1189 --- /dev/null +++ b/cua2-front/src/components/sandbox/completionview/CompletionView.tsx @@ -0,0 +1,368 @@ +import React from 'react'; +import { Box, Typography, Button, Divider, Alert, Paper } from '@mui/material'; +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import AddIcon from '@mui/icons-material/Add'; +import SmartToyIcon from '@mui/icons-material/SmartToy'; +import AssignmentIcon from '@mui/icons-material/Assignment'; +import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import InputIcon from '@mui/icons-material/Input'; +import OutputIcon from '@mui/icons-material/Output'; +import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; +import { FinalStep, AgentTrace, AgentStep } from '@/types/agent'; +import { DownloadGifButton } from './DownloadGifButton'; +import { DownloadJsonButton } from './DownloadJsonButton'; + +interface CompletionViewProps { + finalStep: FinalStep; + trace?: AgentTrace; + steps?: AgentStep[]; + finalAnswer?: string | null; + isGenerating: boolean; + gifError: string | null; + onGenerateGif: () => void; + onDownloadJson: () => void; + onBackToHome: () => void; +} + +/** + * Component displaying the completion status (success or failure) of a task + */ +export const CompletionView: React.FC = ({ + finalStep, + trace, + steps, + finalAnswer, + isGenerating, + gifError, + onGenerateGif, + onDownloadJson, + onBackToHome, +}) => { + const isSuccess = finalStep.type === 'success'; + const statusColor = isSuccess ? 'success.main' : 'error.main'; + + // Format model name for display + const formatModelName = (modelId: string) => { + const parts = modelId.split('/'); + return parts.length > 1 ? parts[1] : modelId; + }; + + return ( + + {/* Status Header - Compact */} + + + + isSuccess + ? `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.3)' : 'rgba(102, 187, 106, 0.2)'}` + : `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.3)' : 'rgba(244, 67, 54, 0.2)'}`, + }} + > + {isSuccess ? ( + + ) : ( + + )} + + + {isSuccess ? 'Task Completed' : 'Task Failed'} + + + + + {/* Single Report Box - Task + Agent + Response + Metrics */} + theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)', + borderRadius: 1.5, + border: '1px solid', + borderColor: 'divider', + }} + > + {/* Task */} + {trace?.instruction && ( + + + + + + Task + + + {trace.instruction} + + + + + )} + + {/* Agent Response */} + {finalAnswer && ( + + + + + + Agent Response + + + {finalAnswer} + + + + + )} + + {/* Divider before metrics */} + + + {/* Metrics */} + + {/* Agent */} + {trace?.modelId && ( + <> + + + + {formatModelName(trace.modelId)} + + + + {/* Divider */} + + > + )} + + {/* Steps Count */} + + + + {finalStep.metadata.numberOfSteps} + + + {finalStep.metadata.numberOfSteps === 1 ? 'Step' : 'Steps'} + + + + {/* Divider */} + + + {/* Duration */} + + + + {finalStep.metadata.duration.toFixed(1)}s + + + + {/* Divider */} + + + {/* Input Tokens */} + + + + {finalStep.metadata.inputTokensUsed.toLocaleString()} + + + + {/* Divider */} + + + {/* Output Tokens */} + + + + {finalStep.metadata.outputTokensUsed.toLocaleString()} + + + + + + {/* GIF Error Alert */} + {gifError && ( + + {gifError} + + )} + + {/* Action Buttons */} + + {/* Download buttons */} + + + + + + {/* New Task button - larger and below */} + } + onClick={onBackToHome} + color="primary" + sx={{ + textTransform: 'none', + fontWeight: 700, + fontSize: '0.9rem', + px: 3, + py: 1, + boxShadow: 2, + minWidth: 200, + '&:hover': { + boxShadow: 4, + }, + }} + > + New Task + + + + ); +}; diff --git a/cua2-front/src/components/sandbox/completionview/DownloadGifButton.tsx b/cua2-front/src/components/sandbox/completionview/DownloadGifButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c4084f9e63657bfc92f47a36e5415064d1424207 --- /dev/null +++ b/cua2-front/src/components/sandbox/completionview/DownloadGifButton.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Button, CircularProgress, Tooltip } from '@mui/material'; +import GifIcon from '@mui/icons-material/Gif'; + +interface DownloadGifButtonProps { + isGenerating: boolean; + onClick: () => void; + disabled?: boolean; +} + +/** + * Button to download a GIF replay of the trace + */ +export const DownloadGifButton: React.FC = ({ + isGenerating, + onClick, + disabled = false, +}) => { + return ( + + + + ) : ( + + ) + } + sx={{ + textTransform: 'none', + fontSize: '0.75rem', + fontWeight: 600, + borderRadius: 1, + px: 1.5, + py: 0.5, + borderColor: 'divider', + color: 'text.primary', + '&:hover': { + borderColor: 'primary.main', + backgroundColor: 'action.hover', + }, + '&.Mui-disabled': { + borderColor: 'divider', + color: 'text.disabled', + }, + }} + > + {isGenerating ? 'Generating...' : 'Download GIF'} + + + + ); +}; diff --git a/cua2-front/src/components/sandbox/completionview/DownloadJsonButton.tsx b/cua2-front/src/components/sandbox/completionview/DownloadJsonButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..31bd1d997b4551804d2d0b49f2797e81eea7d530 --- /dev/null +++ b/cua2-front/src/components/sandbox/completionview/DownloadJsonButton.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Button, Tooltip } from '@mui/material'; +import DownloadIcon from '@mui/icons-material/Download'; + +interface DownloadJsonButtonProps { + onClick: () => void; + disabled?: boolean; +} + +/** + * Button to download trace as JSON + */ +export const DownloadJsonButton: React.FC = ({ + onClick, + disabled = false, +}) => { + return ( + + + } + sx={{ + textTransform: 'none', + fontSize: '0.75rem', + fontWeight: 600, + borderRadius: 1, + px: 1.5, + py: 0.5, + borderColor: 'divider', + color: 'text.primary', + '&:hover': { + borderColor: 'primary.main', + backgroundColor: 'action.hover', + }, + '&.Mui-disabled': { + borderColor: 'divider', + color: 'text.disabled', + }, + }} + > + Download JSON Trace + + + + ); +}; diff --git a/cua2-front/src/components/sandbox/completionview/index.ts b/cua2-front/src/components/sandbox/completionview/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..311aa137d7d52931d67271cb0bc3535cc5ae48a6 --- /dev/null +++ b/cua2-front/src/components/sandbox/completionview/index.ts @@ -0,0 +1,3 @@ +export { CompletionView } from './CompletionView'; +export { DownloadGifButton } from './DownloadGifButton'; +export { DownloadJsonButton } from './DownloadJsonButton'; diff --git a/cua2-front/src/components/sandbox/index.ts b/cua2-front/src/components/sandbox/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9174670f9b0d579272053a239221f7d5686a954 --- /dev/null +++ b/cua2-front/src/components/sandbox/index.ts @@ -0,0 +1,2 @@ +export { SandboxViewer } from './SandboxViewer'; +export { CompletionView, DownloadGifButton, DownloadJsonButton } from './completionview'; diff --git a/cua2-front/src/components/steps/ConnectionStepCard.tsx b/cua2-front/src/components/steps/ConnectionStepCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6b01895f7b176b5d3131e498918c2f07a6d690f6 --- /dev/null +++ b/cua2-front/src/components/steps/ConnectionStepCard.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Card, CardContent, Box, Typography, CircularProgress } from '@mui/material'; +import CableIcon from '@mui/icons-material/Cable'; +import { keyframes } from '@mui/system'; + +// Animation de pulsation pour le border +const borderPulse = keyframes` + 0%, 100% { + border-color: rgba(79, 134, 198, 0.4); + box-shadow: 0 2px 8px rgba(79, 134, 198, 0.15); + } + 50% { + border-color: rgba(79, 134, 198, 0.8); + box-shadow: 0 2px 12px rgba(79, 134, 198, 0.3); + } +`; + +// Animation de pulsation pour le fond +const backgroundPulse = keyframes` + 0%, 100% { + background-color: rgba(79, 134, 198, 0.03); + } + 50% { + background-color: rgba(79, 134, 198, 0.08); + } +`; + +interface ConnectionStepCardProps { + isConnecting: boolean; +} + +export const ConnectionStepCard: React.FC = ({ isConnecting }) => { + return ( + + + {/* Header avec spinner ou check */} + + + {isConnecting ? ( + + ) : ( + + )} + + + + + {isConnecting ? 'Connecting to E2B...' : 'Connected to E2B'} + + + {isConnecting ? 'Setting up sandbox environment' : 'Sandbox ready'} + + + + + + ); +}; diff --git a/cua2-front/src/components/steps/FinalStepCard.tsx b/cua2-front/src/components/steps/FinalStepCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..45558e22261f9fd4fa6a2b5b864bb115ee07c34c --- /dev/null +++ b/cua2-front/src/components/steps/FinalStepCard.tsx @@ -0,0 +1,70 @@ +import { FinalStep } from '@/types/agent'; +import React from 'react'; +import { Card, CardContent, Box, Typography } from '@mui/material'; +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import { useAgentStore } from '@/stores/agentStore'; + +interface FinalStepCardProps { + finalStep: FinalStep; + isActive?: boolean; +} + +export const FinalStepCard: React.FC = ({ finalStep, isActive = false }) => { + const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex); + + const isSuccess = finalStep.type === 'success'; + + const handleClick = () => { + // Clicking on final step goes to live mode (null) + setSelectedStepIndex(null); + }; + + return ( + `${isActive + ? isSuccess ? theme.palette.success.main : theme.palette.error.main + : theme.palette.divider} !important`, + borderRadius: 1.5, + transition: 'all 0.2s ease', + cursor: 'pointer', + boxShadow: isActive + ? (theme) => isSuccess + ? `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.3)' : 'rgba(102, 187, 106, 0.2)'}` + : `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.3)' : 'rgba(244, 67, 54, 0.2)'}` + : 'none', + '&:hover': { + borderColor: (theme) => `${isSuccess ? theme.palette.success.main : theme.palette.error.main} !important`, + boxShadow: (theme) => isSuccess + ? `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.2)' : 'rgba(102, 187, 106, 0.1)'}` + : `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.2)' : 'rgba(244, 67, 54, 0.1)'}`, + }, + }} + > + + {/* Header with icon */} + + {isSuccess ? ( + + ) : ( + + )} + + {isSuccess ? 'Task completed' : 'Task failed'} + + + + + ); +}; diff --git a/cua2-front/src/components/steps/StepCard.tsx b/cua2-front/src/components/steps/StepCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d81cdf38875d8032ce1e266119b580aad273f108 --- /dev/null +++ b/cua2-front/src/components/steps/StepCard.tsx @@ -0,0 +1,358 @@ +import { AgentStep } from '@/types/agent'; +import React, { useState } from 'react'; +import { Card, CardContent, Box, Typography, Divider, Chip, Paper, Accordion, AccordionSummary, AccordionDetails, IconButton, Tooltip } from '@mui/material'; +import ThoughtBubbleIcon from '@mui/icons-material/Psychology'; +import BoltIcon from '@mui/icons-material/Bolt'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import InputIcon from '@mui/icons-material/Input'; +import OutputIcon from '@mui/icons-material/Output'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ThumbUpIcon from '@mui/icons-material/ThumbUp'; +import ThumbDownIcon from '@mui/icons-material/ThumbDown'; +import { useAgentStore } from '@/stores/agentStore'; +import { updateStepEvaluation } from '@/services/api'; + +interface StepCardProps { + step: AgentStep; + index: number; + isLatest?: boolean; + isActive?: boolean; +} + +export const StepCard: React.FC = ({ step, index, isLatest = false, isActive = false }) => { + const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex); + const [thoughtExpanded, setThoughtExpanded] = useState(false); + const [evaluation, setEvaluation] = useState<'like' | 'dislike' | 'neutral'>(step.step_evaluation || 'neutral'); + const [isVoting, setIsVoting] = useState(false); + + const handleClick = () => { + setSelectedStepIndex(index); + }; + + const handleAccordionClick = (event: React.MouseEvent) => { + event.stopPropagation(); // Empêcher la propagation pour ne pas sélectionner la step + }; + + const handleVote = async (event: React.MouseEvent, vote: 'like' | 'dislike') => { + event.stopPropagation(); // Empêcher la propagation pour ne pas sélectionner la step + + if (isVoting) return; + + const newEvaluation = evaluation === vote ? 'neutral' : vote; + setIsVoting(true); + + try { + await updateStepEvaluation(step.traceId, step.stepId, newEvaluation); + setEvaluation(newEvaluation); + } catch (error) { + console.error('Failed to update step evaluation:', error); + } finally { + setIsVoting(false); + } + }; + + return ( + `${isActive ? theme.palette.primary.main : theme.palette.divider} !important`, + borderRadius: 1.5, + transition: 'all 0.2s ease', + cursor: 'pointer', + boxShadow: isActive ? (theme) => `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.3)' : 'rgba(79, 134, 198, 0.2)'}` : 'none', + '&:hover': { + borderColor: (theme) => `${theme.palette.primary.main} !important`, + boxShadow: (theme) => `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.2)' : 'rgba(79, 134, 198, 0.1)'}`, + }, + }} + > + + {/* Step header */} + + + {index + 1} + + + } + label={`${step.duration.toFixed(1)}s`} + size="small" + sx={{ + height: 'auto', + py: 0.25, + fontSize: '0.65rem', + fontWeight: 600, + backgroundColor: 'action.hover', + color: 'text.primary', + '& .MuiChip-icon': { marginLeft: 0.5, color: 'text.secondary' }, + }} + /> + } + label={step.inputTokensUsed.toLocaleString()} + size="small" + sx={{ + height: 'auto', + py: 0.25, + fontSize: '0.65rem', + fontWeight: 600, + backgroundColor: 'action.hover', + color: 'text.primary', + '& .MuiChip-icon': { marginLeft: 0.5, color: 'text.secondary' }, + }} + /> + } + label={step.outputTokensUsed.toLocaleString()} + size="small" + sx={{ + height: 'auto', + py: 0.25, + fontSize: '0.65rem', + fontWeight: 600, + backgroundColor: 'action.hover', + color: 'text.primary', + '& .MuiChip-icon': { marginLeft: 0.5, color: 'text.secondary' }, + }} + /> + + + + {/* Step image */} + {step.image && ( + isActive ? theme.palette.primary.main : theme.palette.divider, + backgroundColor: 'action.hover', + transition: 'border-color 0.2s ease', + }} + > + + + )} + + {/* Action */} + {step.actions && step.actions.length > 0 && ( + + + + + Action + + + + {/* Vote buttons */} + + + handleVote(e, 'like')} + disabled={isVoting} + sx={{ + padding: '2px', + color: evaluation === 'like' ? 'success.main' : 'action.disabled', + '&:hover': { + color: 'success.main', + backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.1)' : 'rgba(102, 187, 106, 0.08)', + }, + }} + > + + + + + handleVote(e, 'dislike')} + disabled={isVoting} + sx={{ + padding: '2px', + color: evaluation === 'dislike' ? 'error.main' : 'action.disabled', + '&:hover': { + color: 'error.main', + backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.08)', + }, + }} + > + + + + + + + {step.actions.map((action, actionIndex) => ( + + {/* + → + */} + + {action.description} + + + ))} + + + )} + + {/* Thought - Accordion */} + {step.thought && ( + setThoughtExpanded(expanded)} + onClick={handleAccordionClick} + elevation={0} + disableGutters + sx={{ + mb: 0.5, + backgroundColor: 'transparent', + border: 'none', + boxShadow: 'none', + '&:before': { display: 'none' }, + '&.MuiAccordion-root': { + backgroundColor: 'transparent', + boxShadow: 'none', + '&:before': { + display: 'none', + }, + }, + '& .MuiAccordionSummary-root': { + minHeight: 'auto', + p: 0, + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'transparent', + }, + '&.Mui-expanded': { + minHeight: 'auto', + }, + }, + '& .MuiAccordionSummary-content': { + margin: '0 !important', + }, + '& .MuiAccordionDetails-root': { + p: 0, + pt: 0.5, + pb: 0, + backgroundColor: 'transparent', + }, + }} + > + } + sx={{ + flexDirection: 'row', + border: 'none', + '& .MuiAccordionSummary-expandIconWrapper': { + transform: 'rotate(-90deg)', + transition: 'transform 0.2s', + '&.Mui-expanded': { + transform: 'rotate(0deg)', + }, + }, + }} + > + + + Thought + + + + + + {step.thought} + + + + )} + + {/* Error */} + {step.error && ( + + + Error: {step.error} + + + )} + + + ); +}; diff --git a/cua2-front/src/components/steps/StepsList.tsx b/cua2-front/src/components/steps/StepsList.tsx new file mode 100644 index 0000000000000000000000000000000000000000..54e2980d2eea15908ea0c25cee9c087e6cbbf23c --- /dev/null +++ b/cua2-front/src/components/steps/StepsList.tsx @@ -0,0 +1,388 @@ +import React, { useRef, useEffect } from 'react'; +import { AgentTrace } from '@/types/agent'; +import { Box, Typography, Stack, Paper } from '@mui/material'; +import { StepCard } from './StepCard'; +import { FinalStepCard } from './FinalStepCard'; +import { ThinkingStepCard } from './ThinkingStepCard'; +import { ConnectionStepCard } from './ConnectionStepCard'; +import ListAltIcon from '@mui/icons-material/ListAlt'; +import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; +import { useAgentStore, selectSelectedStepIndex, selectFinalStep, selectIsConnectingToE2B, selectIsAgentProcessing } from '@/stores/agentStore'; + +interface StepsListProps { + trace?: AgentTrace; +} + +export const StepsList: React.FC = ({ trace }) => { + const containerRef = useRef(null); + const selectedStepIndex = useAgentStore(selectSelectedStepIndex); + const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex); + const finalStep = useAgentStore(selectFinalStep); + const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B); + const isAgentProcessing = useAgentStore(selectIsAgentProcessing); + const isScrollingProgrammatically = useRef(false); + const [showThinkingCard, setShowThinkingCard] = React.useState(false); + const thinkingTimeoutRef = useRef(null); + const streamStartTimeRef = useRef(null); + const [showConnectionCard, setShowConnectionCard] = React.useState(false); + const hasConnectedRef = useRef(false); + + // Check if final step is active (when selectedStepIndex is null and finalStep exists and trace is not running) + const isFinalStepActive = selectedStepIndex === null && finalStep && !trace?.isRunning; + + // Determine the active step index + // If a specific step is selected, use that + // If the final step is active, no normal step should be active + // Otherwise, show the last step as active + const activeStepIndex = selectedStepIndex !== null + ? selectedStepIndex + : isFinalStepActive + ? null // When final step is active, no normal step is active + : (trace?.steps && trace.steps.length > 0 && trace?.isRunning) + ? trace.steps.length - 1 + : (trace?.steps && trace.steps.length > 0) + ? trace.steps.length - 1 + : null; + + // Manage ConnectionStepCard display: + // - Shows when isConnectingToE2B = true OR when we had a connection + // - Remains visible even when task is finished (if we have steps or finalStep) + useEffect(() => { + if (isConnectingToE2B || isAgentProcessing || (trace?.steps && trace.steps.length > 0) || finalStep) { + setShowConnectionCard(true); + hasConnectedRef.current = true; + } + }, [isConnectingToE2B, isAgentProcessing, trace?.steps, finalStep]); + + // Manage ThinkingCard display: + // - Appears 5 seconds AFTER stream starts (isAgentProcessing = true, NOT during isConnectingToE2B) + // - Remains visible during the entire agent processing + // - Hides only when agent stops OR a finalStep exists + useEffect(() => { + // Si le stream démarre vraiment (isAgentProcessing = true et PAS en train de se connecter) + // Et pas encore de startTime enregistré + if (isAgentProcessing && !isConnectingToE2B && !streamStartTimeRef.current) { + streamStartTimeRef.current = Date.now(); + } + + // Si l'agent s'arrête OU qu'on a un finalStep, reset et cacher + if (!isAgentProcessing || finalStep) { + streamStartTimeRef.current = null; + setShowThinkingCard(false); + if (thinkingTimeoutRef.current) { + clearTimeout(thinkingTimeoutRef.current); + thinkingTimeoutRef.current = null; + } + return; + } + + // If agent is running, not connecting, no finalStep: start 5 second timer + if (isAgentProcessing && !isConnectingToE2B && !finalStep && streamStartTimeRef.current) { + // Clean up any existing timeout + if (thinkingTimeoutRef.current) { + clearTimeout(thinkingTimeoutRef.current); + } + + // Calculer le temps écoulé depuis le début du stream + const elapsedTime = Date.now() - streamStartTimeRef.current; + const remainingTime = Math.max(0, 5000 - elapsedTime); + + thinkingTimeoutRef.current = setTimeout(() => { + setShowThinkingCard(true); + }, remainingTime); + } + + // Cleanup on unmount or when dependencies change + return () => { + if (thinkingTimeoutRef.current) { + clearTimeout(thinkingTimeoutRef.current); + thinkingTimeoutRef.current = null; + } + }; + }, [isAgentProcessing, isConnectingToE2B, finalStep]); + + // Auto-scroll to active step when it changes (timeline → steps) + useEffect(() => { + if (containerRef.current) { + isScrollingProgrammatically.current = true; + // Use setTimeout to ensure DOM has updated + setTimeout(() => { + if (containerRef.current) { + // Scroll to final step if it's active + if (isFinalStepActive) { + const finalStepElement = containerRef.current.querySelector(`[data-step-index="final"]`); + if (finalStepElement) { + finalStepElement.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + setTimeout(() => { + isScrollingProgrammatically.current = false; + }, 500); + } + } + // Otherwise scroll to active step + else if (activeStepIndex !== null && trace?.steps) { + const activeStepElement = containerRef.current.querySelector(`[data-step-index="${activeStepIndex}"]`); + if (activeStepElement) { + activeStepElement.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + // Reset flag after scroll animation + setTimeout(() => { + isScrollingProgrammatically.current = false; + }, 500); + } + } + } + }, 100); + } + }, [activeStepIndex, trace?.steps?.length, isFinalStepActive]); + + // Detect which step is visible when scrolling (steps → timeline) + useEffect(() => { + const container = containerRef.current; + if (!container || !trace?.steps || trace.steps.length === 0) return; + + const handleScroll = () => { + // Don't update if we're scrolling programmatically + if (isScrollingProgrammatically.current) return; + + const containerRect = container.getBoundingClientRect(); + const containerTop = containerRect.top; + const containerBottom = containerRect.bottom; + const containerCenter = containerRect.top + containerRect.height / 2; + + // Check scroll position + const isAtTop = container.scrollTop <= 5; // 5px tolerance + const isAtBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 5; // 5px tolerance + + let targetStepIndex: number | null = -1; + let targetDistance = Infinity; + let isFinalStepTarget = false; + + if (isAtTop) { + // At the top: find the highest visible step + let highestVisibleBottom = Infinity; + + trace.steps.forEach((_, index) => { + const stepElement = container.querySelector(`[data-step-index="${index}"]`); + if (stepElement) { + const stepRect = stepElement.getBoundingClientRect(); + const stepTop = stepRect.top; + const stepBottom = stepRect.bottom; + const isVisible = stepTop < containerBottom && stepBottom > containerTop; + + if (isVisible && stepTop < highestVisibleBottom) { + highestVisibleBottom = stepTop; + targetStepIndex = index; + isFinalStepTarget = false; + } + } + }); + } else if (isAtBottom) { + // At the bottom: find the lowest visible step + let lowestVisibleTop = -Infinity; + + trace.steps.forEach((_, index) => { + const stepElement = container.querySelector(`[data-step-index="${index}"]`); + if (stepElement) { + const stepRect = stepElement.getBoundingClientRect(); + const stepTop = stepRect.top; + const stepBottom = stepRect.bottom; + const isVisible = stepTop < containerBottom && stepBottom > containerTop; + + if (isVisible && stepTop > lowestVisibleTop) { + lowestVisibleTop = stepTop; + targetStepIndex = index; + isFinalStepTarget = false; + } + } + }); + + // Check if final step is the lowest visible + if (finalStep) { + const finalStepElement = container.querySelector(`[data-step-index="final"]`); + if (finalStepElement) { + const finalStepRect = finalStepElement.getBoundingClientRect(); + const finalStepTop = finalStepRect.top; + const finalStepBottom = finalStepRect.bottom; + const isVisible = finalStepTop < containerBottom && finalStepBottom > containerTop; + + if (isVisible && finalStepTop > lowestVisibleTop) { + targetStepIndex = null; + isFinalStepTarget = true; + } + } + } + } else { + // Not at bottom: find the step closest to center + trace.steps.forEach((_, index) => { + const stepElement = container.querySelector(`[data-step-index="${index}"]`); + if (stepElement) { + const stepRect = stepElement.getBoundingClientRect(); + const stepCenter = stepRect.top + stepRect.height / 2; + const distance = Math.abs(containerCenter - stepCenter); + + if (distance < targetDistance) { + targetDistance = distance; + targetStepIndex = index; + isFinalStepTarget = false; + } + } + }); + + // Check if final step is closest to center + if (finalStep) { + const finalStepElement = container.querySelector(`[data-step-index="final"]`); + if (finalStepElement) { + const finalStepRect = finalStepElement.getBoundingClientRect(); + const finalStepCenter = finalStepRect.top + finalStepRect.height / 2; + const distance = Math.abs(containerCenter - finalStepCenter); + + if (distance < targetDistance) { + targetStepIndex = null; + isFinalStepTarget = true; + } + } + } + } + + // Update the selected step if changed + if (isFinalStepTarget && selectedStepIndex !== null) { + setSelectedStepIndex(null); + } else if (!isFinalStepTarget && targetStepIndex !== -1 && targetStepIndex !== selectedStepIndex) { + setSelectedStepIndex(targetStepIndex); + } + }; + + // Throttle scroll events + let scrollTimeout: NodeJS.Timeout; + const throttledScroll = () => { + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(handleScroll, 150); + }; + + container.addEventListener('scroll', throttledScroll); + return () => { + container.removeEventListener('scroll', throttledScroll); + clearTimeout(scrollTimeout); + }; + }, [trace?.steps, selectedStepIndex, setSelectedStepIndex, finalStep]); + + return ( + + + + Steps + + {trace?.traceMetadata && trace.traceMetadata.numberOfSteps > 0 && ( + + + {trace.traceMetadata.numberOfSteps} + + + /{trace.traceMetadata.maxSteps} + + + )} + + + {(trace?.steps && trace.steps.length > 0) || finalStep || showThinkingCard || showConnectionCard ? ( + + {/* Show connection step card (first item) */} + {showConnectionCard && ( + + + + )} + + {/* Show all steps */} + {trace?.steps && trace.steps.map((step, index) => ( + + + + ))} + + {/* Show thinking indicator after steps (appears 5 seconds after stream start) */} + {showThinkingCard && ( + + + + )} + + {/* Show final step card if exists */} + {finalStep && ( + + + + )} + + ) : ( + + + + No steps yet + + + Steps will appear as the agent progresses + + + )} + + + ); +}; diff --git a/cua2-front/src/components/steps/ThinkingStepCard.tsx b/cua2-front/src/components/steps/ThinkingStepCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fe83af1339be3a5b4022ff79d63df1f3a0a06934 --- /dev/null +++ b/cua2-front/src/components/steps/ThinkingStepCard.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { Card, CardContent, Box, Typography, CircularProgress } from '@mui/material'; +import { keyframes } from '@mui/system'; + +// Animation de pulsation pour le border +const borderPulse = keyframes` + 0%, 100% { + border-color: rgba(79, 134, 198, 0.4); + box-shadow: 0 2px 8px rgba(79, 134, 198, 0.15); + } + 50% { + border-color: rgba(79, 134, 198, 0.8); + box-shadow: 0 2px 12px rgba(79, 134, 198, 0.3); + } +`; + +// Animation de pulsation pour le fond +const backgroundPulse = keyframes` + 0%, 100% { + background-color: rgba(79, 134, 198, 0.03); + } + 50% { + background-color: rgba(79, 134, 198, 0.08); + } +`; + +export const ThinkingStepCard: React.FC = () => { + + return ( + + + {/* Header avec spinner */} + + + {/* Spinner circulaire */} + + + + + + Agent + + + Thinking... + + + + + + ); +}; diff --git a/cua2-front/src/components/steps/index.ts b/cua2-front/src/components/steps/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9237daa6fe261d90ba3a2db02e65588a60bb1197 --- /dev/null +++ b/cua2-front/src/components/steps/index.ts @@ -0,0 +1,5 @@ +export { StepsList } from './StepsList'; +export { StepCard } from './StepCard'; +export { ThinkingStepCard } from './ThinkingStepCard'; +export { FinalStepCard } from './FinalStepCard'; +export { ConnectionStepCard } from './ConnectionStepCard'; diff --git a/cua2-front/src/components/timeline/Timeline.tsx b/cua2-front/src/components/timeline/Timeline.tsx new file mode 100644 index 0000000000000000000000000000000000000000..caee7c65a1cbf1e1161aeac6a2c4780bcdfa6d02 --- /dev/null +++ b/cua2-front/src/components/timeline/Timeline.tsx @@ -0,0 +1,413 @@ +import React, { useRef, useEffect } from 'react'; +import { Box, Typography, CircularProgress, Button } from '@mui/material'; +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import CableIcon from '@mui/icons-material/Cable'; +import { AgentTraceMetadata } from '@/types/agent'; +import { useAgentStore, selectSelectedStepIndex, selectFinalStep, selectIsConnectingToE2B, selectIsAgentProcessing } from '@/stores/agentStore'; + +interface TimelineProps { + metadata: AgentTraceMetadata; + isRunning: boolean; +} + +export const Timeline: React.FC = ({ metadata, isRunning }) => { + const timelineRef = useRef(null); + const selectedStepIndex = useAgentStore(selectSelectedStepIndex); + const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex); + const finalStep = useAgentStore(selectFinalStep); + const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B); + const isAgentProcessing = useAgentStore(selectIsAgentProcessing); + + // Show connection indicator if connecting or if we have started processing + const showConnectionIndicator = isConnectingToE2B || isAgentProcessing || (metadata.numberOfSteps > 0) || finalStep; + + // Generate array of steps with their status + // Show all steps up to maxSteps (200) + const totalStepsToShow = metadata.maxSteps; + + const steps = Array.from({ length: totalStepsToShow }, (_, index) => ({ + stepNumber: index + 1, + stepIndex: index, + isCompleted: index < metadata.numberOfSteps, + // Step is current if: we're at the right index AND running AND not connecting to E2B + isCurrent: (index === metadata.numberOfSteps && isRunning && !isConnectingToE2B) || + (index === 0 && metadata.numberOfSteps === 0 && isRunning && !isConnectingToE2B), + isSelected: selectedStepIndex === index, + })); + + // Handle step click + const handleStepClick = (stepIndex: number, isCompleted: boolean, isCurrent: boolean) => { + if (isCompleted) { + setSelectedStepIndex(stepIndex); + } else if (isCurrent) { + // Clicking on the current step (with animation) goes back to live mode + setSelectedStepIndex(null); + } + }; + + // Handle final step click (goes to live mode showing the final status) + const handleFinalStepClick = () => { + setSelectedStepIndex(null); + }; + + // Auto-scroll to current step while running + useEffect(() => { + if (timelineRef.current && isRunning) { + // Only auto-scroll while running, not when finished + const currentStepElement = timelineRef.current.querySelector(`[data-step="${metadata.numberOfSteps}"]`); + if (currentStepElement) { + currentStepElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); + } + } + }, [metadata.numberOfSteps, isRunning]); + + return ( + + + {/* Header with step count */} + + + Timeline + {selectedStepIndex !== null && ( + + - Viewing step {selectedStepIndex + 1} + + )} + + {selectedStepIndex !== null && ( + theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)', + borderColor: 'text.secondary', + }, + }} + > + Back to latest step + + )} + + + {/* Horizontal scrollable step indicators */} + theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', + zIndex: 0, + pointerEvents: 'none', + }, + }} + > + {/* Connection indicator (step 0) */} + {showConnectionIndicator && ( + + {/* Cercle blanc en arrière-plan pour cacher la ligne */} + + {/* Fond blanc/background pour cacher la ligne */} + + + {/* Connection icon */} + {isConnectingToE2B ? ( + + ) : ( + + )} + + + {/* Connection label */} + + {isConnectingToE2B ? 'Connecting' : 'Connected'} + + + )} + + {/* Render steps and insert final step at the right position */} + {steps.map((step, index) => ( + + handleStepClick(step.stepIndex, step.isCompleted, step.isCurrent)} + sx={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 0.75, + minWidth: 40, + flexShrink: 0, + position: 'relative', + zIndex: 1, + cursor: (step.isCompleted || step.isCurrent) ? 'pointer' : 'default', + '&:hover': (step.isCompleted || step.isCurrent) ? { + '& .step-dot': { + transform: 'scale(1.15)', + }, + } : {}, + }} + > + {/* Cercle blanc en arrière-plan pour cacher la ligne */} + + {/* Fond blanc/background pour cacher la ligne */} + + + {/* Step dot */} + {step.isCurrent ? ( + + ) : ( + theme.palette.mode === 'dark' ? 'grey.800' : 'grey.300', // Light grey for future steps + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 0.2s ease', + boxShadow: step.isCompleted || step.isSelected + ? step.isSelected + ? '0 0 8px rgba(255, 167, 38, 0.5)' + : '0 2px 4px rgba(0,0,0,0.1)' + : 'none', + position: 'relative', + zIndex: 1, + }} + /> + )} + + + {/* Step number - show for all steps */} + theme.palette.mode === 'dark' ? 'grey.700' : 'grey.400'), + whiteSpace: 'nowrap', + }} + > + {step.stepNumber} + + + + {/* Insert final step indicator right after the last completed step */} + {finalStep && step.stepNumber === metadata.numberOfSteps && ( + + {/* Cercle blanc en arrière-plan pour cacher la ligne */} + + {/* Fond blanc/background pour cacher la ligne */} + + + {/* Final step icon */} + + {finalStep.type === 'success' ? ( + + ) : ( + + )} + + + + {/* Final step label */} + + {finalStep.type === 'success' ? 'End' : 'Failed'} + + + )} + + ))} + + + + ); +}; diff --git a/cua2-front/src/components/timeline/index.ts b/cua2-front/src/components/timeline/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3667d487e7bab521ddb02b498d90d3491f1e9e5 --- /dev/null +++ b/cua2-front/src/components/timeline/index.ts @@ -0,0 +1 @@ +export { Timeline } from './Timeline'; diff --git a/cua2-front/src/config.ts b/cua2-front/src/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..e01159a8c43b2012e9e341d27955847d7a2ccc71 --- /dev/null +++ b/cua2-front/src/config.ts @@ -0,0 +1,11 @@ +// Application configuration +export const config = { + // WebSocket URL for backend connection + wsUrl: 'ws://localhost:8000/ws', + + // API Base URL + apiBaseUrl: 'http://localhost:8000/api/v1', + + // Default model (will be overridden by first available model from backend) + defaultModelId: 'Qwen/Qwen3-VL-8B-Instruct', +} as const; diff --git a/cua2-front/src/hooks/index.ts b/cua2-front/src/hooks/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f1fa061895e1906c8d06e481c9da2b11fd9571f --- /dev/null +++ b/cua2-front/src/hooks/index.ts @@ -0,0 +1,5 @@ +export { useAgentWebSocket } from './useAgentWebSocket'; +export { useWebSocket } from './useWebSocket'; +export { useSendTask } from './useSendTask'; +export { useGifGenerator } from './useGifGenerator'; +export { useJsonExporter } from './useJsonExporter'; diff --git a/cua2-front/src/hooks/useAgentWebSocket.ts b/cua2-front/src/hooks/useAgentWebSocket.ts new file mode 100644 index 0000000000000000000000000000000000000000..e64ee89ae6350d407904b295b5c00dbb752c58b9 --- /dev/null +++ b/cua2-front/src/hooks/useAgentWebSocket.ts @@ -0,0 +1,165 @@ +import { useCallback, useEffect } from 'react'; +import { useWebSocket } from './useWebSocket'; +import { useAgentStore } from '@/stores/agentStore'; +import { WebSocketEvent, AgentTrace, AgentStep } from '@/types/agent'; +import { ulid } from 'ulid'; + +interface UseAgentWebSocketOptions { + url: string; +} + +export const useAgentWebSocket = ({ url }: UseAgentWebSocketOptions) => { + const { + setTrace, + updateTraceWithStep, + completeTrace, + setIsAgentProcessing, + setIsConnectingToE2B, + setVncUrl, + setError, + setIsConnected, + selectedModelId, + resetAgent, + } = useAgentStore(); + + // Handle incoming WebSocket messages + const handleWebSocketMessage = useCallback( + (event: WebSocketEvent) => { + console.log('WebSocket event received:', event); + + switch (event.type) { + case 'agent_start': { + // Clear previous state (especially finalStep) + resetAgent(); + + setIsAgentProcessing(true); + setIsConnectingToE2B(true); // Start connecting to E2B + setError(undefined); // Clear any previous error + + // Ensure trace has proper metadata with default maxSteps if not provided + const traceWithMetadata = { + ...event.agentTrace, + traceMetadata: event.agentTrace.traceMetadata ? { + ...event.agentTrace.traceMetadata, + maxSteps: event.agentTrace.traceMetadata.maxSteps > 0 + ? event.agentTrace.traceMetadata.maxSteps + : 200, // Default if backend sends 0 + } : { + traceId: event.agentTrace.id, + inputTokensUsed: 0, + outputTokensUsed: 0, + duration: 0, + numberOfSteps: 0, + maxSteps: 200, + completed: false, + }, + }; + + setTrace(traceWithMetadata); + console.log('Agent start received:', traceWithMetadata); + break; + } + + case 'agent_progress': + // Add new step from agent trace run with image, generated text, actions, tokens and timestamp + setIsConnectingToE2B(false); // Connected! First step received + updateTraceWithStep(event.agentStep, event.traceMetadata); + console.log('Agent progress received:', event.agentStep); + break; + + case 'agent_complete': + setIsAgentProcessing(false); + setIsConnectingToE2B(false); + completeTrace(event.traceMetadata); + console.log('Agent complete received:', event.traceMetadata); + break; + + case 'agent_error': + setIsAgentProcessing(false); + setIsConnectingToE2B(false); + setError(event.error); + console.error('Agent error received:', event.error); + break; + + case 'vnc_url_set': + setIsConnectingToE2B(false); // Connected! VNC URL received + setVncUrl(event.vncUrl); + console.log('VNC URL set received:', event.vncUrl); + break; + + case 'vnc_url_unset': + setVncUrl(''); + console.log('VNC URL unset received'); + break; + + case 'heartbeat': + console.log('Heartbeat received:', event); + break; + } + }, + [setTrace, updateTraceWithStep, completeTrace, setIsAgentProcessing, setIsConnectingToE2B, setVncUrl, setError, resetAgent] + ); + + // Handle WebSocket errors + const handleWebSocketError = useCallback(() => { + // WebSocket Frontend Error handling + console.error('WebSocket connection error'); + }, []); + + // Initialize WebSocket connection + const { isConnected, connectionState, sendMessage, manualReconnect } = useWebSocket({ + url, + onMessage: handleWebSocketMessage, + onError: handleWebSocketError, + }); + + // Sync connection state to store + useEffect(() => { + setIsConnected(isConnected); + }, [isConnected, setIsConnected]); + + // Create a global sendNewTask function that can be called from anywhere + useEffect(() => { + // Store sendNewTask in window for global access + (window as Window & { __sendNewTask?: (instruction: string, modelId: string) => void }).__sendNewTask = (instruction: string, modelId: string) => { + // Reset agent state before starting a new task + resetAgent(); + + const traceId = ulid(); + const trace: AgentTrace = { + id: traceId, + instruction, + modelId: modelId, + timestamp: new Date(), + isRunning: true, + traceMetadata: { + traceId: traceId, + inputTokensUsed: 0, + outputTokensUsed: 0, + duration: 0, + numberOfSteps: 0, + maxSteps: 200, // Default max steps, will be updated by backend + completed: false, + }, + }; + + setTrace(trace); + setIsAgentProcessing(true); + setIsConnectingToE2B(true); // Start connecting when task is sent + + // Send message to Python backend via WebSocket + sendMessage({ + type: 'user_task', + trace: trace, + }); + + console.log('Task sent:', trace); + }; + }, [setTrace, setIsAgentProcessing, setIsConnectingToE2B, sendMessage, resetAgent]); + + return { + isConnected, + connectionState, + manualReconnect, + }; +}; diff --git a/cua2-front/src/hooks/useGifGenerator.ts b/cua2-front/src/hooks/useGifGenerator.ts new file mode 100644 index 0000000000000000000000000000000000000000..bedab97577e4b7dd0f9da994d575d53b9c8df330 --- /dev/null +++ b/cua2-front/src/hooks/useGifGenerator.ts @@ -0,0 +1,86 @@ +import { useState, useCallback } from 'react'; +import { generateGif, downloadGif, GifGenerationOptions } from '@/services/gifGenerator'; +import { AgentStep } from '@/types/agent'; + +interface UseGifGeneratorOptions { + steps: AgentStep[]; + traceId?: string; +} + +interface UseGifGeneratorReturn { + isGenerating: boolean; + error: string | null; + generateAndDownloadGif: () => Promise; +} + +/** + * Custom hook to generate and download a GIF from trace steps + */ +export const useGifGenerator = ({ + steps, + traceId, +}: UseGifGeneratorOptions): UseGifGeneratorReturn => { + const [isGenerating, setIsGenerating] = useState(false); + const [error, setError] = useState(null); + + const generateAndDownloadGif = useCallback(async () => { + if (!steps || steps.length === 0) { + setError('No steps available to generate GIF'); + return; + } + + setIsGenerating(true); + setError(null); + + try { + // Extract images from steps + const images = steps + .map((step) => step.image) + .filter((image): image is string => !!image); + + if (images.length === 0) { + setError('No images available in steps'); + setIsGenerating(false); + return; + } + + // Generate GIF with maximum dimensions of 400x200 + const options: GifGenerationOptions = { + images, + interval: 1.5, // 1.5 seconds per frame + gifWidth: 400, + gifHeight: 200, + quality: 10, // Medium quality for good size/quality compromise + }; + + const result = await generateGif(options); + + if (!result.success || !result.image) { + setError(result.error || 'Error generating GIF'); + setIsGenerating(false); + return; + } + + // Download the GIF + const filename = traceId + ? `trace-${traceId}-replay.gif` + : `trace-replay-${Date.now()}.gif`; + + downloadGif(result.image, filename); + setIsGenerating(false); + } catch (err) { + setError( + err instanceof Error + ? err.message + : 'Unexpected error generating GIF' + ); + setIsGenerating(false); + } + }, [steps, traceId]); + + return { + isGenerating, + error, + generateAndDownloadGif, + }; +}; diff --git a/cua2-front/src/hooks/useJsonExporter.ts b/cua2-front/src/hooks/useJsonExporter.ts new file mode 100644 index 0000000000000000000000000000000000000000..99384d51a0e968653cdc518f0e679603970c16fc --- /dev/null +++ b/cua2-front/src/hooks/useJsonExporter.ts @@ -0,0 +1,41 @@ +import { useCallback } from 'react'; +import { exportTraceToJson, downloadJson } from '@/services/jsonExporter'; +import { AgentTrace, AgentStep, AgentTraceMetadata } from '@/types/agent'; + +interface UseJsonExporterOptions { + trace?: AgentTrace; + steps: AgentStep[]; + metadata?: AgentTraceMetadata; +} + +interface UseJsonExporterReturn { + downloadTraceAsJson: () => void; +} + +/** + * Hook personnalisé pour exporter et télécharger une trace en JSON + */ +export const useJsonExporter = ({ + trace, + steps, + metadata, +}: UseJsonExporterOptions): UseJsonExporterReturn => { + const downloadTraceAsJson = useCallback(() => { + if (!trace) { + console.error('No trace available to export'); + return; + } + + try { + const jsonString = exportTraceToJson(trace, steps, metadata); + const filename = `trace-${trace.id}.json`; + downloadJson(jsonString, filename); + } catch (error) { + console.error('Error exporting trace to JSON:', error); + } + }, [trace, steps, metadata]); + + return { + downloadTraceAsJson, + }; +}; diff --git a/cua2-front/src/hooks/useSendTask.ts b/cua2-front/src/hooks/useSendTask.ts new file mode 100644 index 0000000000000000000000000000000000000000..3337c93fde215e09d04047a5e7f6883b13c20649 --- /dev/null +++ b/cua2-front/src/hooks/useSendTask.ts @@ -0,0 +1,14 @@ +import { useCallback } from 'react'; + +export const useSendTask = () => { + const sendTask = useCallback((instruction: string, modelId: string) => { + const sendNewTask = (window as Window & { __sendNewTask?: (instruction: string, modelId: string) => void }).__sendNewTask; + if (sendNewTask) { + sendNewTask(instruction, modelId); + } else { + console.error('WebSocket not initialized'); + } + }, []); + + return sendTask; +}; diff --git a/cua2-front/src/index.css b/cua2-front/src/index.css index ef496a59a7b6a8def4c475692b4574322bf43376..5a2863c0791133b04432d13cba9b8a0360ad9f79 100644 --- a/cua2-front/src/index.css +++ b/cua2-front/src/index.css @@ -1,8 +1,4 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} +/* Global styles - Material UI handles most of the styling */ html, body { margin: 0; @@ -10,11 +6,6 @@ html, body { height: 100%; width: 100%; overflow: hidden; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } #root { diff --git a/cua2-front/src/pages/Task.tsx b/cua2-front/src/pages/Task.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b4ceb6c3dab0ec6e3695b081173b9aaed0dc8f63 --- /dev/null +++ b/cua2-front/src/pages/Task.tsx @@ -0,0 +1,123 @@ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAgentStore, selectTrace, selectIsAgentProcessing, selectVncUrl, selectMetadata, selectSelectedStep } from '@/stores/agentStore'; +import { Header, SandboxViewer, StepsList, Timeline } from '@/components'; +import { Box } from '@mui/material'; + +const Task = () => { + const navigate = useNavigate(); + + // Get state from Zustand store + const trace = useAgentStore(selectTrace); + const isAgentProcessing = useAgentStore(selectIsAgentProcessing); + const vncUrl = useAgentStore(selectVncUrl); + const metadata = useAgentStore(selectMetadata); + const selectedStep = useAgentStore(selectSelectedStep); + const error = useAgentStore((state) => state.error); + + // Redirect to home if no trace is present + useEffect(() => { + if (!trace) { + console.log('No trace found, redirecting to home...'); + navigate('/', { replace: true }); + } + }, [trace, navigate]); + + // Handler for going back to home + const handleBackToHome = () => { + useAgentStore.getState().resetAgent(); + navigate('/'); + }; + + // Determine if we should show success/fail status (same logic as SandboxViewer) + const showStatus = !trace?.isRunning && !selectedStep && metadata && metadata.numberOfSteps > 0; + + // Don't render anything if no trace (will redirect) + if (!trace) { + return null; + } + + return ( + + {/* Header */} + + + {/* Main Content */} + + + {/* Left Side: OS Stream + Metadata */} + + {/* Sandbox Viewer */} + + + {/* Timeline - Always show, even with default values */} + 0 ? metadata : { + traceId: trace?.id || '', + inputTokensUsed: metadata?.inputTokensUsed || 0, + outputTokensUsed: metadata?.outputTokensUsed || 0, + duration: metadata?.duration || 0, + numberOfSteps: metadata?.numberOfSteps || 0, + maxSteps: 200, // Default max steps (will be updated by backend) + completed: metadata?.completed || false, + }} + isRunning={trace?.isRunning || false} + /> + + + {/* Right Side: Steps List */} + + + + + ); +}; + +export default Task; diff --git a/cua2-front/src/pages/Welcome.tsx b/cua2-front/src/pages/Welcome.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fdcd234afea1db0b8a36238100bbfc989edb4157 --- /dev/null +++ b/cua2-front/src/pages/Welcome.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSendTask } from '@/hooks/useSendTask'; +import { Box } from '@mui/material'; +import { WelcomeScreen } from '@/components'; +import { useAgentStore, selectIsConnected } from '@/stores/agentStore'; + +const Welcome = () => { + const navigate = useNavigate(); + const isConnected = useAgentStore(selectIsConnected); + const sendTask = useSendTask(); + + const handleSendNewTask = (instruction: string, modelId: string) => { + sendTask(instruction, modelId); + // Navigate to task page after starting task + navigate('/task'); + }; + + return ( + + + + ); +}; + +export default Welcome; diff --git a/cua2-front/src/services/api.ts b/cua2-front/src/services/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3303739492ec4533e2a88385d04e92770d2f84a --- /dev/null +++ b/cua2-front/src/services/api.ts @@ -0,0 +1,56 @@ +import { config } from '@/config'; + +/** + * Fetch available models from the backend + */ +export async function fetchAvailableModels(): Promise { + const response = await fetch(`${config.apiBaseUrl}/models`); + if (!response.ok) { + throw new Error('Failed to fetch models'); + } + const data = await response.json(); + return data.models; +} + +/** + * Generate a random instruction from the backend + */ +export async function generateRandomQuestion(modelId: string): Promise { + const response = await fetch(`${config.apiBaseUrl}/generate-instruction`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model_id: modelId, + }), + }); + if (!response.ok) { + throw new Error('Failed to generate instruction'); + } + const data = await response.json(); + return data.instruction; +} + +/** + * Update step evaluation (vote) + */ +export async function updateStepEvaluation( + traceId: string, + stepId: string, + evaluation: 'like' | 'dislike' | 'neutral' +): Promise { + const response = await fetch(`${config.apiBaseUrl}/traces/${traceId}/steps/${stepId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + step_evaluation: evaluation, + }), + }); + + if (!response.ok) { + throw new Error('Failed to update step evaluation'); + } +} diff --git a/cua2-front/src/services/gifGenerator.ts b/cua2-front/src/services/gifGenerator.ts new file mode 100644 index 0000000000000000000000000000000000000000..8eb4e77fb2a437f11bab7ac05f2e3d1b5cf4df29 --- /dev/null +++ b/cua2-front/src/services/gifGenerator.ts @@ -0,0 +1,168 @@ +import gifshot from 'gifshot'; + +export interface GifGenerationOptions { + images: string[]; + interval?: number; // Durée de chaque frame en secondes + gifWidth?: number; + gifHeight?: number; + quality?: number; +} + +export interface GifGenerationResult { + success: boolean; + image?: string; // Data URL du GIF + error?: string; +} + +/** + * Ajoute un compteur d'étapes sur une image + * @param imageSrc Source de l'image (base64 ou URL) + * @param stepNumber Numéro de l'étape + * @param totalSteps Nombre total d'étapes + * @param width Largeur de l'image + * @param height Hauteur de l'image + * @returns Promesse résolue avec l'image modifiée en base64 + */ +const addStepCounter = async ( + imageSrc: string, + stepNumber: number, + totalSteps: number, + width: number, + height: number +): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + + if (!ctx) { + reject(new Error('Cannot get canvas context')); + return; + } + + // Dessiner l'image + ctx.drawImage(img, 0, 0, width, height); + + // Configurer le style du compteur + const fontSize = Math.max(12, Math.floor(height * 0.08)); + const padding = Math.max(6, Math.floor(height * 0.03)); + const text = `${stepNumber}/${totalSteps}`; + + ctx.font = `bold ${fontSize}px Arial, sans-serif`; + const textMetrics = ctx.measureText(text); + const textWidth = textMetrics.width; + const textHeight = fontSize; + + // Position en bas à droite + const x = width - textWidth - padding * 2; + const y = height - padding * 2; + + // Dessiner un rectangle semi-transparent pour la lisibilité + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.fillRect( + x - padding, + y - textHeight - padding, + textWidth + padding * 2, + textHeight + padding * 2 + ); + + // Dessiner le texte en noir + ctx.fillStyle = '#000000'; + ctx.textBaseline = 'top'; + ctx.fillText(text, x, y - textHeight); + + // Convertir le canvas en base64 + resolve(canvas.toDataURL('image/png')); + }; + + img.onerror = () => { + reject(new Error('Failed to load image')); + }; + + img.src = imageSrc; + }); +}; + +/** + * Génère un GIF à partir d'une liste d'images (base64 ou URLs) + * @param options Options de génération du GIF + * @returns Promesse résolue avec le résultat de la génération + */ +export const generateGif = async ( + options: GifGenerationOptions +): Promise => { + const { + images, + interval = 1.5, // 1.5 secondes par frame par défaut + gifWidth = 400, + gifHeight = 200, + quality = 10, + } = options; + + if (!images || images.length === 0) { + return { + success: false, + error: 'Aucune image fournie pour générer le GIF', + }; + } + + try { + // Ajouter le compteur sur chaque image + const imagesWithCounter = await Promise.all( + images.map((img, index) => + addStepCounter(img, index + 1, images.length, gifWidth, gifHeight) + ) + ); + + return new Promise((resolve) => { + gifshot.createGIF( + { + images: imagesWithCounter, + interval, + gifWidth, + gifHeight, + numFrames: imagesWithCounter.length, + frameDuration: interval, + sampleInterval: quality, + }, + (obj: { error: boolean; errorMsg?: string; image?: string }) => { + if (obj.error) { + resolve({ + success: false, + error: obj.errorMsg || 'Erreur lors de la génération du GIF', + }); + } else { + resolve({ + success: true, + image: obj.image, + }); + } + } + ); + }); + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue', + }; + } +}; + +/** + * Télécharge un GIF (data URL) avec un nom de fichier + * @param dataUrl Data URL du GIF + * @param filename Nom du fichier à télécharger + */ +export const downloadGif = (dataUrl: string, filename: string = 'trace-replay.gif') => { + const link = document.createElement('a'); + link.href = dataUrl; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; diff --git a/cua2-front/src/services/index.ts b/cua2-front/src/services/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b1b0a5375ece7bb766cdb5f8f57144787ae9dcf4 --- /dev/null +++ b/cua2-front/src/services/index.ts @@ -0,0 +1,3 @@ +export { api } from './api'; +export * from './gifGenerator'; +export * from './jsonExporter'; diff --git a/cua2-front/src/services/jsonExporter.ts b/cua2-front/src/services/jsonExporter.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6b05d133b850724a198c2c8fb5206c7e82476b9 --- /dev/null +++ b/cua2-front/src/services/jsonExporter.ts @@ -0,0 +1,58 @@ +import { AgentTrace, AgentStep, AgentTraceMetadata } from '@/types/agent'; + +/** + * Export the complete trace as JSON + * @param trace The agent trace + * @param steps The trace steps + * @param metadata The final metadata + * @returns A JSON object containing the entire trace + */ +export const exportTraceToJson = ( + trace: AgentTrace, + steps: AgentStep[], + metadata?: AgentTraceMetadata +): string => { + const exportData = { + trace: { + id: trace.id, + timestamp: trace.timestamp, + instruction: trace.instruction, + modelId: trace.modelId, + isRunning: trace.isRunning, + }, + metadata: metadata || trace.traceMetadata, + steps: steps.map((step) => ({ + traceId: step.traceId, + stepId: step.stepId, + error: step.error, + thought: step.thought, + actions: step.actions, + duration: step.duration, + inputTokensUsed: step.inputTokensUsed, + outputTokensUsed: step.outputTokensUsed, + step_evaluation: step.step_evaluation, + // Ne pas inclure l'image base64 pour réduire la taille du JSON + hasImage: !!step.image, + })), + exportedAt: new Date().toISOString(), + }; + + return JSON.stringify(exportData, null, 2); +}; + +/** + * Télécharge un JSON avec un nom de fichier + * @param jsonString String JSON à télécharger + * @param filename Nom du fichier à télécharger + */ +export const downloadJson = (jsonString: string, filename: string = 'trace.json') => { + const blob = new Blob([jsonString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +}; diff --git a/cua2-front/src/stores/agentStore.ts b/cua2-front/src/stores/agentStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e14c180f9226d4714a347dc40d39f175c28f6c9 --- /dev/null +++ b/cua2-front/src/stores/agentStore.ts @@ -0,0 +1,251 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { AgentTrace, AgentStep, AgentTraceMetadata, FinalStep } from '@/types/agent'; + +interface AgentState { + // State + trace?: AgentTrace; + isAgentProcessing: boolean; + isConnectingToE2B: boolean; // New state for E2B connection + vncUrl: string; + selectedModelId: string; + availableModels: string[]; + isLoadingModels: boolean; + isConnected: boolean; + error?: string; + isDarkMode: boolean; + selectedStepIndex: number | null; // null = live mode, number = viewing specific step or 'final' + finalStep?: FinalStep; // Special step for success/failure + + // Actions + setTrace: (trace: AgentTrace | undefined) => void; + updateTraceWithStep: (step: AgentStep, metadata: AgentTraceMetadata) => void; + completeTrace: (metadata: AgentTraceMetadata) => void; + setIsAgentProcessing: (processing: boolean) => void; + setIsConnectingToE2B: (connecting: boolean) => void; + setVncUrl: (url: string) => void; + setSelectedModelId: (modelId: string) => void; + setAvailableModels: (models: string[]) => void; + setIsLoadingModels: (loading: boolean) => void; + setIsConnected: (connected: boolean) => void; + setError: (error: string | undefined) => void; + setSelectedStepIndex: (index: number | null) => void; + toggleDarkMode: () => void; + resetAgent: () => void; +} + +const initialState = { + trace: undefined, + isAgentProcessing: false, + isConnectingToE2B: false, + vncUrl: '', + selectedModelId: 'Qwen/Qwen3-VL-8B-Instruct', + availableModels: [], + isLoadingModels: false, + isConnected: false, + error: undefined, + isDarkMode: false, + selectedStepIndex: null, // null = live mode + finalStep: undefined, +}; + +export const useAgentStore = create()( + devtools( + (set) => ({ + ...initialState, + + // Set the complete trace + setTrace: (trace) => + set({ trace }, false, 'setTrace'), + + // Update trace with a new step + updateTraceWithStep: (step, metadata) => + set( + (state) => { + if (!state.trace) return state; + + const existingSteps = state.trace.steps || []; + const stepExists = existingSteps.some((s) => s.stepId === step.stepId); + + if (stepExists) return state; + + // Preserve existing maxSteps if new metadata has 0 + const updatedMetadata = { + ...metadata, + maxSteps: metadata.maxSteps > 0 + ? metadata.maxSteps + : (state.trace.traceMetadata?.maxSteps || 200), + }; + + return { + trace: { + ...state.trace, + steps: [...existingSteps, step], + traceMetadata: updatedMetadata, + isRunning: true, + }, + }; + }, + false, + 'updateTraceWithStep' + ), + + // Complete the trace + completeTrace: (metadata) => + set( + (state) => { + if (!state.trace) return state; + + // Preserve existing maxSteps if new metadata has 0 + const updatedMetadata = { + ...metadata, + maxSteps: metadata.maxSteps > 0 + ? metadata.maxSteps + : (state.trace.traceMetadata?.maxSteps || 200), + completed: true, + }; + + // Determine if the task succeeded or failed based on error state + const finalStep: FinalStep = { + type: state.error ? 'failure' : 'success', + message: state.error, + metadata: updatedMetadata, + }; + + return { + trace: { + ...state.trace, + isRunning: false, + traceMetadata: updatedMetadata, + }, + finalStep, + // Keep error in state for display + selectedStepIndex: null, // Reset to live mode on completion + }; + }, + false, + 'completeTrace' + ), + + // Set processing state + setIsAgentProcessing: (isAgentProcessing) => + set({ isAgentProcessing }, false, 'setIsAgentProcessing'), + + // Set E2B connection state + setIsConnectingToE2B: (isConnectingToE2B) => + set({ isConnectingToE2B }, false, 'setIsConnectingToE2B'), + + // Set VNC URL + setVncUrl: (vncUrl) => + set({ vncUrl }, false, 'setVncUrl'), + + // Set selected model ID + setSelectedModelId: (selectedModelId) => + set({ selectedModelId }, false, 'setSelectedModelId'), + + // Set available models + setAvailableModels: (availableModels) => + set({ availableModels }, false, 'setAvailableModels'), + + // Set loading models state + setIsLoadingModels: (isLoadingModels) => + set({ isLoadingModels }, false, 'setIsLoadingModels'), + + // Set connection status + setIsConnected: (isConnected) => + set({ isConnected }, false, 'setIsConnected'), + + // Set error + setError: (error) => + set( + (state) => { + // If there's an error and a trace, mark it as failed + if (error && state.trace) { + const metadata = state.trace.traceMetadata || { + traceId: state.trace.id, + inputTokensUsed: 0, + outputTokensUsed: 0, + duration: 0, + numberOfSteps: state.trace.steps?.length || 0, + maxSteps: 200, + completed: false, + }; + + // Ensure maxSteps is not 0 + const finalMetadata = { + ...metadata, + maxSteps: metadata.maxSteps > 0 ? metadata.maxSteps : 200, + }; + + const finalStep: FinalStep = { + type: 'failure', + message: error, + metadata: finalMetadata, + }; + + return { + error, + finalStep, + trace: { + ...state.trace, + isRunning: false, + }, + selectedStepIndex: null, // Reset to live mode on error + }; + } + return { error }; + }, + false, + 'setError' + ), + + // Set selected step index for time travel + setSelectedStepIndex: (selectedStepIndex) => + set({ selectedStepIndex }, false, 'setSelectedStepIndex'), + + // Toggle dark mode + toggleDarkMode: () => + set((state) => ({ isDarkMode: !state.isDarkMode }), false, 'toggleDarkMode'), + + // Reset agent state + resetAgent: () => + set((state) => ({ + ...initialState, + isDarkMode: state.isDarkMode, // Keep dark mode preference + isConnected: state.isConnected, // Keep connection status + selectedModelId: state.selectedModelId, // Keep selected model + availableModels: state.availableModels, // Keep available models + isLoadingModels: state.isLoadingModels // Keep loading state + }), false, 'resetAgent'), + }), + { name: 'AgentStore' } + ) +); + +// Selectors for better performance +export const selectTrace = (state: AgentState) => state.trace; +export const selectIsAgentProcessing = (state: AgentState) => state.isAgentProcessing; +export const selectIsConnectingToE2B = (state: AgentState) => state.isConnectingToE2B; +export const selectVncUrl = (state: AgentState) => state.vncUrl; +export const selectSelectedModelId = (state: AgentState) => state.selectedModelId; +export const selectAvailableModels = (state: AgentState) => state.availableModels; +export const selectIsLoadingModels = (state: AgentState) => state.isLoadingModels; +export const selectIsConnected = (state: AgentState) => state.isConnected; +export const selectSteps = (state: AgentState) => state.trace?.steps; +export const selectMetadata = (state: AgentState) => state.trace?.traceMetadata; +export const selectError = (state: AgentState) => state.error; +export const selectIsDarkMode = (state: AgentState) => state.isDarkMode; +export const selectSelectedStepIndex = (state: AgentState) => state.selectedStepIndex; +export const selectFinalStep = (state: AgentState) => state.finalStep; + +// Composite selector for selected step (avoids infinite loops) +export const selectSelectedStep = (state: AgentState) => { + const steps = state.trace?.steps; + const selectedIndex = state.selectedStepIndex; + + if (selectedIndex === null || !steps || selectedIndex >= steps.length) { + return null; + } + + return steps[selectedIndex]; +}; diff --git a/cua2-front/src/theme.ts b/cua2-front/src/theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..eddd3b1125da638792580f92fd94ac15a497b7cc --- /dev/null +++ b/cua2-front/src/theme.ts @@ -0,0 +1,397 @@ +import { createTheme, alpha, Theme } from "@mui/material/styles"; + +const getDesignTokens = (mode: 'light' | 'dark') => ({ + typography: { + fontFamily: [ + "-apple-system", + "BlinkMacSystemFont", + '"Segoe UI"', + "Roboto", + '"Helvetica Neue"', + "Arial", + "sans-serif", + ].join(","), + h1: { + fontFamily: '"Source Sans Pro", sans-serif', + }, + h2: { + fontFamily: '"Source Sans Pro", sans-serif', + }, + h3: { + fontFamily: '"Source Sans Pro", sans-serif', + }, + h4: { + fontFamily: '"Source Sans Pro", sans-serif', + }, + h5: { + fontFamily: '"Source Sans Pro", sans-serif', + }, + h6: { + fontFamily: '"Source Sans Pro", sans-serif', + }, + subtitle1: { + fontFamily: '"Source Sans Pro", sans-serif', + }, + subtitle2: { + fontFamily: '"Source Sans Pro", sans-serif', + }, + }, + palette: { + mode, + primary: { + main: "#4F86C6", + light: mode === "light" ? "#7BA7D7" : "#6B97D7", + dark: mode === "light" ? "#2B5C94" : "#3B6CA4", + 50: mode === "light" ? alpha("#4F86C6", 0.05) : alpha("#4F86C6", 0.15), + 100: mode === "light" ? alpha("#4F86C6", 0.1) : alpha("#4F86C6", 0.2), + 200: mode === "light" ? alpha("#4F86C6", 0.2) : alpha("#4F86C6", 0.3), + contrastText: "#fff", + }, + background: { + default: mode === "light" ? "#f8f9fa" : "#0a0a0a", + paper: mode === "light" ? "#fff" : "#1a1a1a", + subtle: mode === "light" ? "grey.100" : "grey.900", + hover: mode === "light" ? "action.hover" : alpha("#fff", 0.08), + tooltip: mode === "light" ? alpha("#212121", 0.9) : alpha("#fff", 0.9), + }, + text: { + primary: mode === "light" ? "rgba(0, 0, 0, 0.87)" : "#fff", + secondary: + mode === "light" ? "rgba(0, 0, 0, 0.6)" : "rgba(255, 255, 255, 0.7)", + disabled: + mode === "light" ? "rgba(0, 0, 0, 0.38)" : "rgba(255, 255, 255, 0.5)", + hint: + mode === "light" ? "rgba(0, 0, 0, 0.38)" : "rgba(255, 255, 255, 0.5)", + }, + divider: + mode === "light" ? "rgba(0, 0, 0, 0.15)" : "rgba(255, 255, 255, 0.18)", + action: { + active: + mode === "light" ? "rgba(0, 0, 0, 0.54)" : "rgba(255, 255, 255, 0.7)", + hover: + mode === "light" ? "rgba(0, 0, 0, 0.04)" : "rgba(255, 255, 255, 0.08)", + selected: + mode === "light" ? "rgba(0, 0, 0, 0.08)" : "rgba(255, 255, 255, 0.16)", + disabled: + mode === "light" ? "rgba(0, 0, 0, 0.26)" : "rgba(255, 255, 255, 0.3)", + disabledBackground: + mode === "light" ? "rgba(0, 0, 0, 0.12)" : "rgba(255, 255, 255, 0.12)", + }, + }, + shape: { + borderRadius: 6, + }, + components: { + MuiCssBaseline: { + styleOverrides: { + "html, body": { + backgroundColor: "background.default", + color: mode === "dark" ? "#fff" : "#000", + }, + body: { + "& *::-webkit-scrollbar": { + width: 8, + height: 8, + backgroundColor: "transparent", + }, + "& *::-webkit-scrollbar-thumb": { + borderRadius: 6, + backgroundColor: + mode === "light" ? alpha("#000", 0.2) : alpha("#fff", 0.1), + "&:hover": { + backgroundColor: + mode === "light" ? alpha("#000", 0.3) : alpha("#fff", 0.15), + }, + }, + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: 6, + textTransform: 'none', + fontWeight: 600, + }, + }, + }, + MuiPaper: { + defaultProps: { + elevation: 0, + }, + styleOverrides: { + root: { + backgroundImage: "none", + boxShadow: "none", + border: "1px solid", + borderColor: + mode === "light" + ? "rgba(0, 0, 0, 0.15)!important" + : "rgba(255, 255, 255, 0.18)!important", + }, + rounded: { + borderRadius: 10, + }, + }, + }, + + MuiTableCell: { + styleOverrides: { + root: { + borderColor: (theme: Theme) => + alpha( + theme.palette.divider, + theme.palette.mode === "dark" ? 0.1 : 0.2 + ), + }, + head: { + backgroundColor: mode === "light" ? "grey.50" : "grey.900", + color: "text.primary", + fontWeight: 600, + }, + }, + }, + MuiTableRow: { + styleOverrides: { + root: { + backgroundColor: "transparent", + }, + }, + }, + MuiTableContainer: { + styleOverrides: { + root: { + backgroundColor: "background.paper", + borderRadius: 6, + border: "none", + boxShadow: "none", + }, + }, + }, + MuiSlider: { + styleOverrides: { + root: { + "& .MuiSlider-valueLabel": { + backgroundColor: "background.paper", + color: "text.primary", + border: "1px solid", + borderColor: "divider", + boxShadow: + mode === "light" + ? "0px 2px 4px rgba(0, 0, 0, 0.1)" + : "0px 2px 4px rgba(0, 0, 0, 0.3)", + }, + }, + thumb: { + "&:hover": { + boxShadow: (theme: Theme) => + `0px 0px 0px 8px ${alpha( + theme.palette.primary.main, + mode === "light" ? 0.08 : 0.16 + )}`, + }, + "&.Mui-active": { + boxShadow: (theme: Theme) => + `0px 0px 0px 12px ${alpha( + theme.palette.primary.main, + mode === "light" ? 0.08 : 0.16 + )}`, + }, + }, + track: { + border: "none", + }, + rail: { + opacity: mode === "light" ? 0.38 : 0.3, + }, + mark: { + backgroundColor: mode === "light" ? "grey.400" : "grey.600", + }, + markLabel: { + color: "text.secondary", + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + "& .MuiOutlinedInput-root": { + borderRadius: 6, + }, + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + borderRadius: 6, + fontWeight: 600, + }, + outlinedInfo: { + borderWidth: 2, + fontWeight: 600, + bgcolor: "info.100", + borderColor: "info.400", + color: "info.700", + "& .MuiChip-label": { + px: 1.2, + }, + "&:hover": { + bgcolor: "info.200", + }, + }, + outlinedWarning: { + borderWidth: 2, + fontWeight: 600, + bgcolor: "warning.100", + borderColor: "warning.400", + color: "warning.700", + "& .MuiChip-label": { + px: 1.2, + }, + "&:hover": { + bgcolor: "warning.200", + }, + }, + outlinedSuccess: { + borderWidth: 2, + fontWeight: 600, + bgcolor: "success.100", + borderColor: "success.400", + color: "success.700", + "& .MuiChip-label": { + px: 1.2, + }, + "&:hover": { + bgcolor: "success.200", + }, + }, + outlinedError: { + borderWidth: 2, + fontWeight: 600, + bgcolor: "error.100", + borderColor: "error.400", + color: "error.700", + "& .MuiChip-label": { + px: 1.2, + }, + "&:hover": { + bgcolor: "error.200", + }, + }, + outlinedPrimary: { + borderWidth: 2, + fontWeight: 600, + bgcolor: "primary.100", + borderColor: "primary.400", + color: "primary.700", + "& .MuiChip-label": { + px: 1.2, + }, + "&:hover": { + bgcolor: "primary.200", + }, + }, + outlinedSecondary: { + borderWidth: 2, + fontWeight: 600, + bgcolor: "secondary.100", + borderColor: "secondary.400", + color: "secondary.700", + "& .MuiChip-label": { + px: 1.2, + }, + "&:hover": { + bgcolor: "secondary.200", + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + borderRadius: 6, + padding: "8px", + "&.MuiIconButton-sizeSmall": { + padding: "4px", + borderRadius: 4, + }, + }, + }, + }, + MuiTooltip: { + styleOverrides: { + tooltip: { + backgroundColor: + mode === "light" ? alpha("#212121", 0.9) : alpha("#424242", 0.9), + color: "#fff", + fontSize: "0.875rem", + padding: "8px 12px", + maxWidth: 400, + borderRadius: 6, + lineHeight: 1.4, + border: "1px solid", + borderColor: + mode === "light" ? alpha("#fff", 0.1) : alpha("#fff", 0.05), + boxShadow: + mode === "light" + ? "0 2px 8px rgba(0, 0, 0, 0.15)" + : "0 2px 8px rgba(0, 0, 0, 0.5)", + "& b": { + fontWeight: 600, + color: "inherit", + }, + "& a": { + color: mode === "light" ? "#90caf9" : "#64b5f6", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", + }, + }, + }, + arrow: { + color: + mode === "light" ? alpha("#212121", 0.9) : alpha("#424242", 0.9), + "&:before": { + border: "1px solid", + borderColor: + mode === "light" ? alpha("#fff", 0.1) : alpha("#fff", 0.05), + }, + }, + }, + defaultProps: { + arrow: true, + enterDelay: 400, + leaveDelay: 200, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + border: "none", + borderBottom: "none", + }, + }, + }, + }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 900, + lg: 1240, + xl: 1536, + }, + }, +}); + +const getTheme = (mode: 'light' | 'dark') => { + const tokens = getDesignTokens(mode); + return createTheme(tokens); +}; + +// Export light theme by default +export const theme = getTheme('light'); + +// Export function to get theme with mode +export default getTheme; diff --git a/cua2-front/src/types/agent.ts b/cua2-front/src/types/agent.ts index a3027c974efe3e8834323ccbd9f3a6e8f09af217..e695f055c5a0563037f44498ad6c30e92d173efd 100644 --- a/cua2-front/src/types/agent.ts +++ b/cua2-front/src/types/agent.ts @@ -1,4 +1,3 @@ - export interface AgentTrace { id: string; timestamp: Date; @@ -6,16 +5,22 @@ export interface AgentTrace { modelId: string; isRunning: boolean; steps?: AgentStep[]; - metadata?: AgentTraceMetadata; + traceMetadata?: AgentTraceMetadata; +} + +export interface AgentAction { + function_name: string; + parameters: Record; + description: string; } export interface AgentStep { traceId: string; stepId: string; - error: string; + error?: string | null; image: string; - thought: string; - actions: string[]; + thought?: string | null; + actions?: AgentAction[] | null; duration: number; inputTokensUsed: number; outputTokensUsed: number; @@ -28,6 +33,14 @@ export interface AgentTraceMetadata { outputTokensUsed: number; duration: number; numberOfSteps: number; + maxSteps: number; + completed: boolean; +} + +export interface FinalStep { + type: 'success' | 'failure'; + message?: string; + metadata: AgentTraceMetadata; } // #################### WebSocket Events Types - Server to Client ######################## @@ -82,28 +95,3 @@ export interface UserTaskMessage { type: 'user_task'; trace: AgentTrace; } - -// #################### API Routes Types ######################## - -export interface AvailableModelsResponse { - models: string[]; -} - -export interface UpdateStepRequest { - step_evaluation: 'like' | 'dislike' | 'neutral'; -} - -export interface UpdateStepResponse { - success: boolean; - message: string; -} - -export interface GenerateInstructionRequest { - model_id: string; - prompt?: string; -} - -export interface GenerateInstructionResponse { - instruction: string; - model_id: string; -} diff --git a/cua2-front/src/types/gifshot.d.ts b/cua2-front/src/types/gifshot.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..3da1fb9707a5f2bacdaa4e0e0851e04caa076249 --- /dev/null +++ b/cua2-front/src/types/gifshot.d.ts @@ -0,0 +1,33 @@ +declare module 'gifshot' { + export interface GifOptions { + images?: string[]; + video?: string | string[] | HTMLVideoElement | HTMLVideoElement[]; + gifWidth?: number; + gifHeight?: number; + interval?: number; + numFrames?: number; + frameDuration?: number; + sampleInterval?: number; + quality?: number; + numWorkers?: number; + progressCallback?: (progress: number) => void; + completeCallback?: () => void; + } + + export interface GifResult { + error: boolean; + errorCode?: string; + errorMsg?: string; + image?: string; + } + + export function createGIF( + options: GifOptions, + callback?: (result: GifResult) => void + ): void; + + export function takeSnapShot( + options: GifOptions, + callback: (result: GifResult) => void + ): void; +}
No steps yet
Steps will appear as agent progresses
{step.thought}
- Find the price of a NVIDIA RTX 4090 GPU -
- Qwen/Qwen3-VL-30B-A3B-Instruct -
No VNC stream available
Stream will appear when agent starts