tfrere HF Staff commited on
Commit
67b042f
·
1 Parent(s): 3f05344

Front update (#5)

Browse files

* Initial commit - sauvegarde du frontend actuel

* Update backend (cua2-core) from origin/main - frontend preserved

* first front commit

* update spinner in timeline

* update front

* update front

* Fix linting issues: ESLint TypeScript errors and codespell warnings

- Fix lexical declaration in case block (useAgentWebSocket)
- Replace 'any' types with proper types (Theme, unknown, Window interface)
- Translate French comments to English for codespell compliance
- Remove trailing whitespace and fix end of files

* Refactor: rename components for better clarity

- Rename StackSteps → StepsList (more descriptive)
- Rename TraceMetadata → Timeline (reflects actual purpose)
- Rename OSStream → SandboxViewer (clearer naming)
- Remove unused mock/ directory
- Update all imports and exports accordingly

* Refactor: reorganize component structure

- Rename stream/ → sandbox/ (more descriptive)
- Create completionview/ subdirectory
- Move CompletionView and its buttons into sandbox/completionview/
- Update all imports and exports accordingly

* Fix: backend sends actions as JSON objects

- Change actions_as_json from False to True in websocket_manager
- This ensures frontend receives actions as JSON objects instead of strings
- Fixes action display in StepCard component

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/workflows/pre-commit.yml +1 -1
  2. Makefile +1 -48
  3. cua2-core/src/cua2_core/app.py +0 -4
  4. cua2-core/src/cua2_core/websocket/websocket_manager.py +1 -1
  5. cua2-front/package-lock.json +954 -12
  6. cua2-front/package.json +9 -2
  7. cua2-front/src/App.tsx +28 -11
  8. cua2-front/src/components/ConnectionStatus.tsx +55 -0
  9. cua2-front/src/components/Header.tsx +409 -0
  10. cua2-front/src/components/ProcessingIndicator.tsx +31 -0
  11. cua2-front/src/components/WelcomeScreen.tsx +452 -0
  12. cua2-front/src/components/index.ts +14 -0
  13. cua2-front/src/components/mock/ConnectionStatus.tsx +0 -37
  14. cua2-front/src/components/mock/Header.tsx +0 -47
  15. cua2-front/src/components/mock/Metadata.tsx +0 -38
  16. cua2-front/src/components/mock/ProcessingIndicator.tsx +0 -34
  17. cua2-front/src/components/mock/StackSteps.tsx +0 -29
  18. cua2-front/src/components/mock/StepCard.tsx +0 -84
  19. cua2-front/src/components/mock/TaskButton.tsx +0 -76
  20. cua2-front/src/components/mock/VNCStream.tsx +0 -30
  21. cua2-front/src/components/mock/index.ts +0 -8
  22. cua2-front/src/components/sandbox/SandboxViewer.tsx +367 -0
  23. cua2-front/src/components/sandbox/completionview/CompletionView.tsx +368 -0
  24. cua2-front/src/components/sandbox/completionview/DownloadGifButton.tsx +64 -0
  25. cua2-front/src/components/sandbox/completionview/DownloadJsonButton.tsx +56 -0
  26. cua2-front/src/components/sandbox/completionview/index.ts +3 -0
  27. cua2-front/src/components/sandbox/index.ts +2 -0
  28. cua2-front/src/components/steps/ConnectionStepCard.tsx +110 -0
  29. cua2-front/src/components/steps/FinalStepCard.tsx +70 -0
  30. cua2-front/src/components/steps/StepCard.tsx +358 -0
  31. cua2-front/src/components/steps/StepsList.tsx +388 -0
  32. cua2-front/src/components/steps/ThinkingStepCard.tsx +98 -0
  33. cua2-front/src/components/steps/index.ts +5 -0
  34. cua2-front/src/components/timeline/Timeline.tsx +413 -0
  35. cua2-front/src/components/timeline/index.ts +1 -0
  36. cua2-front/src/config.ts +11 -0
  37. cua2-front/src/hooks/index.ts +5 -0
  38. cua2-front/src/hooks/useAgentWebSocket.ts +165 -0
  39. cua2-front/src/hooks/useGifGenerator.ts +86 -0
  40. cua2-front/src/hooks/useJsonExporter.ts +41 -0
  41. cua2-front/src/hooks/useSendTask.ts +14 -0
  42. cua2-front/src/index.css +1 -10
  43. cua2-front/src/pages/Task.tsx +123 -0
  44. cua2-front/src/pages/Welcome.tsx +35 -0
  45. cua2-front/src/services/api.ts +56 -0
  46. cua2-front/src/services/gifGenerator.ts +168 -0
  47. cua2-front/src/services/index.ts +3 -0
  48. cua2-front/src/services/jsonExporter.ts +58 -0
  49. cua2-front/src/stores/agentStore.ts +251 -0
  50. cua2-front/src/theme.ts +397 -0
.github/workflows/pre-commit.yml CHANGED
@@ -31,4 +31,4 @@ jobs:
31
 
32
  - name: Run pre-commit
33
  run: |
34
- make pre-commit
 
31
 
32
  - name: Run pre-commit
33
  run: |
34
+ uv run pre-commit run --all-files --show-diff-on-failure
Makefile CHANGED
@@ -1,4 +1,4 @@
1
- .PHONY: sync setup install dev-backend dev-frontend dev clean docker-build docker-run docker-stop docker-clean docker-logs
2
 
3
  # Sync all dependencies (Python + Node.js)
4
  sync:
@@ -23,14 +23,6 @@ dev-frontend:
23
 
24
  pre-commit:
25
  uv run pre-commit run --all-files --show-diff-on-failure
26
- make test
27
-
28
- # Run tests
29
- test:
30
- cd cua2-core && uv run pytest tests/ -v
31
-
32
- test-coverage:
33
- cd cua2-core && uv run pytest tests/ -v --cov=cua2_core --cov-report=html --cov-report=term
34
 
35
  clean:
36
  find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
@@ -38,42 +30,3 @@ clean:
38
  find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
39
  cd cua2-front && rm -rf node_modules dist 2>/dev/null || true
40
  @echo "✓ Cleaned!"
41
-
42
- # Docker commands
43
- docker-build:
44
- @echo "Building Docker image..."
45
- make docker-stop
46
- docker build -t cua2:latest .
47
- @echo "✓ Docker image built successfully!"
48
-
49
- docker-run:
50
- @echo "Starting CUA2 container..."
51
- @if [ -z "$$E2B_API_KEY" ]; then \
52
- echo "Error: E2B_API_KEY environment variable is not set"; \
53
- echo "Please set it with: export E2B_API_KEY=your-key"; \
54
- exit 1; \
55
- fi
56
- @if [ -z "$$HF_TOKEN" ]; then \
57
- echo "Error: HF_TOKEN environment variable is not set"; \
58
- echo "Please set it with: export HF_TOKEN=your-token"; \
59
- exit 1; \
60
- fi
61
- docker run -d --name cua2-app -p 7860:7860 \
62
- -e E2B_API_KEY="$$E2B_API_KEY" \
63
- -e HF_TOKEN="$$HF_TOKEN" \
64
- cua2:latest
65
- @echo "✓ Container started! Access at http://localhost:7860"
66
-
67
- docker-stop:
68
- @echo "Stopping CUA2 container..."
69
- docker stop cua2-app || true
70
- docker rm cua2-app || true
71
- @echo "✓ Container stopped!"
72
-
73
- docker-clean:
74
- @echo "Removing CUA2 Docker images..."
75
- docker rmi cua2:latest || true
76
- @echo "✓ Docker images removed!"
77
-
78
- docker-logs:
79
- docker logs -f cua2-app
 
1
+ .PHONY: sync setup install dev-backend dev-frontend dev clean
2
 
3
  # Sync all dependencies (Python + Node.js)
4
  sync:
 
23
 
24
  pre-commit:
25
  uv run pre-commit run --all-files --show-diff-on-failure
 
 
 
 
 
 
 
 
26
 
27
  clean:
28
  find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
 
30
  find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
31
  cd cua2-front && rm -rf node_modules dist 2>/dev/null || true
32
  @echo "✓ Cleaned!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cua2-core/src/cua2_core/app.py CHANGED
@@ -1,4 +1,3 @@
1
- import os
2
  from contextlib import asynccontextmanager
3
 
4
  from cua2_core.services.agent_service import AgentService
@@ -18,9 +17,6 @@ async def lifespan(app: FastAPI):
18
  # Startup: Initialize services
19
  print("Initializing services...")
20
 
21
- if not os.getenv("HF_TOKEN"):
22
- raise ValueError("HF_TOKEN is not set")
23
-
24
  websocket_manager = WebSocketManager()
25
 
26
  sandbox_service = SandboxService()
 
 
1
  from contextlib import asynccontextmanager
2
 
3
  from cua2_core.services.agent_service import AgentService
 
17
  # Startup: Initialize services
18
  print("Initializing services...")
19
 
 
 
 
20
  websocket_manager = WebSocketManager()
21
 
22
  sandbox_service = SandboxService()
cua2-core/src/cua2_core/websocket/websocket_manager.py CHANGED
@@ -52,7 +52,7 @@ class WebSocketManager:
52
  try:
53
  await websocket.send_text(
54
  json.dumps(
55
- message.model_dump(mode="json", context={"actions_as_json": False})
56
  )
57
  )
58
  except Exception as e:
 
52
  try:
53
  await websocket.send_text(
54
  json.dumps(
55
+ message.model_dump(mode="json", context={"actions_as_json": True})
56
  )
57
  )
58
  except Exception as e:
cua2-front/package-lock.json CHANGED
@@ -8,10 +8,17 @@
8
  "name": "cua2-front",
9
  "version": "0.0.0",
10
  "dependencies": {
 
 
 
 
 
 
11
  "react": "^18.3.1",
12
  "react-dom": "^18.3.1",
13
  "react-router-dom": "^6.30.1",
14
- "ulid": "^3.0.1"
 
15
  },
16
  "devDependencies": {
17
  "@eslint/js": "^9.38.0",
@@ -28,6 +35,291 @@
28
  "vite": "^5.4.19"
29
  }
30
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  "node_modules/@esbuild/aix-ppc64": {
32
  "version": "0.21.5",
33
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -628,6 +920,318 @@
628
  "url": "https://github.com/sponsors/nzakas"
629
  }
630
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  "node_modules/@nodelib/fs.scandir": {
632
  "version": "2.1.5",
633
  "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -666,6 +1270,16 @@
666
  "node": ">= 8"
667
  }
668
  },
 
 
 
 
 
 
 
 
 
 
669
  "node_modules/@remix-run/router": {
670
  "version": "1.23.0",
671
  "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
@@ -1240,18 +1854,22 @@
1240
  "undici-types": "~6.21.0"
1241
  }
1242
  },
 
 
 
 
 
 
1243
  "node_modules/@types/prop-types": {
1244
  "version": "15.7.15",
1245
  "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1246
  "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1247
- "dev": true,
1248
  "license": "MIT"
1249
  },
1250
  "node_modules/@types/react": {
1251
  "version": "18.3.26",
1252
  "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
1253
  "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
1254
- "dev": true,
1255
  "license": "MIT",
1256
  "dependencies": {
1257
  "@types/prop-types": "*",
@@ -1268,6 +1886,15 @@
1268
  "@types/react": "^18.0.0"
1269
  }
1270
  },
 
 
 
 
 
 
 
 
 
1271
  "node_modules/@typescript-eslint/eslint-plugin": {
1272
  "version": "8.46.1",
1273
  "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
@@ -1641,6 +2268,21 @@
1641
  "postcss": "^8.1.0"
1642
  }
1643
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1644
  "node_modules/balanced-match": {
1645
  "version": "1.0.2",
1646
  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1720,7 +2362,6 @@
1720
  "version": "3.1.0",
1721
  "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
1722
  "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
1723
- "dev": true,
1724
  "license": "MIT",
1725
  "engines": {
1726
  "node": ">=6"
@@ -1764,6 +2405,15 @@
1764
  "url": "https://github.com/chalk/chalk?sponsor=1"
1765
  }
1766
  },
 
 
 
 
 
 
 
 
 
1767
  "node_modules/color-convert": {
1768
  "version": "2.0.1",
1769
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1791,6 +2441,28 @@
1791
  "dev": true,
1792
  "license": "MIT"
1793
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1794
  "node_modules/cross-spawn": {
1795
  "version": "7.0.6",
1796
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1810,14 +2482,12 @@
1810
  "version": "3.1.3",
1811
  "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
1812
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
1813
- "dev": true,
1814
  "license": "MIT"
1815
  },
1816
  "node_modules/debug": {
1817
  "version": "4.4.3",
1818
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1819
  "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1820
- "dev": true,
1821
  "license": "MIT",
1822
  "dependencies": {
1823
  "ms": "^2.1.3"
@@ -1838,6 +2508,16 @@
1838
  "dev": true,
1839
  "license": "MIT"
1840
  },
 
 
 
 
 
 
 
 
 
 
1841
  "node_modules/electron-to-chromium": {
1842
  "version": "1.5.237",
1843
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
@@ -1845,6 +2525,15 @@
1845
  "dev": true,
1846
  "license": "ISC"
1847
  },
 
 
 
 
 
 
 
 
 
1848
  "node_modules/esbuild": {
1849
  "version": "0.21.5",
1850
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -1898,7 +2587,6 @@
1898
  "version": "4.0.0",
1899
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1900
  "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1901
- "dev": true,
1902
  "license": "MIT",
1903
  "engines": {
1904
  "node": ">=10"
@@ -2185,6 +2873,12 @@
2185
  "node": ">=8"
2186
  }
2187
  },
 
 
 
 
 
 
2188
  "node_modules/find-up": {
2189
  "version": "5.0.0",
2190
  "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2252,6 +2946,21 @@
2252
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
2253
  }
2254
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2255
  "node_modules/glob-parent": {
2256
  "version": "6.0.2",
2257
  "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2295,6 +3004,33 @@
2295
  "node": ">=8"
2296
  }
2297
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2298
  "node_modules/ignore": {
2299
  "version": "5.3.2",
2300
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2309,7 +3045,6 @@
2309
  "version": "3.3.1",
2310
  "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
2311
  "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
2312
- "dev": true,
2313
  "license": "MIT",
2314
  "dependencies": {
2315
  "parent-module": "^1.0.0",
@@ -2332,6 +3067,27 @@
2332
  "node": ">=0.8.19"
2333
  }
2334
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2335
  "node_modules/is-extglob": {
2336
  "version": "2.1.1",
2337
  "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2391,6 +3147,18 @@
2391
  "js-yaml": "bin/js-yaml.js"
2392
  }
2393
  },
 
 
 
 
 
 
 
 
 
 
 
 
2394
  "node_modules/json-buffer": {
2395
  "version": "3.0.1",
2396
  "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -2398,6 +3166,12 @@
2398
  "dev": true,
2399
  "license": "MIT"
2400
  },
 
 
 
 
 
 
2401
  "node_modules/json-schema-traverse": {
2402
  "version": "0.4.1",
2403
  "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -2436,6 +3210,12 @@
2436
  "node": ">= 0.8.0"
2437
  }
2438
  },
 
 
 
 
 
 
2439
  "node_modules/locate-path": {
2440
  "version": "6.0.0",
2441
  "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2512,7 +3292,6 @@
2512
  "version": "2.1.3",
2513
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2514
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2515
- "dev": true,
2516
  "license": "MIT"
2517
  },
2518
  "node_modules/nanoid": {
@@ -2558,6 +3337,15 @@
2558
  "node": ">=0.10.0"
2559
  }
2560
  },
 
 
 
 
 
 
 
 
 
2561
  "node_modules/optionator": {
2562
  "version": "0.9.4",
2563
  "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2612,7 +3400,6 @@
2612
  "version": "1.0.1",
2613
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
2614
  "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
2615
- "dev": true,
2616
  "license": "MIT",
2617
  "dependencies": {
2618
  "callsites": "^3.0.0"
@@ -2621,6 +3408,24 @@
2621
  "node": ">=6"
2622
  }
2623
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2624
  "node_modules/path-exists": {
2625
  "version": "4.0.0",
2626
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2641,11 +3446,25 @@
2641
  "node": ">=8"
2642
  }
2643
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2644
  "node_modules/picocolors": {
2645
  "version": "1.1.1",
2646
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2647
  "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2648
- "dev": true,
2649
  "license": "ISC"
2650
  },
2651
  "node_modules/picomatch": {
@@ -2707,6 +3526,23 @@
2707
  "node": ">= 0.8.0"
2708
  }
2709
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2710
  "node_modules/punycode": {
2711
  "version": "2.3.1",
2712
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2763,6 +3599,12 @@
2763
  "react": "^18.3.1"
2764
  }
2765
  },
 
 
 
 
 
 
2766
  "node_modules/react-router": {
2767
  "version": "6.30.1",
2768
  "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
@@ -2795,11 +3637,46 @@
2795
  "react-dom": ">=16.8"
2796
  }
2797
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2798
  "node_modules/resolve-from": {
2799
  "version": "4.0.0",
2800
  "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
2801
  "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
2802
- "dev": true,
2803
  "license": "MIT",
2804
  "engines": {
2805
  "node": ">=4"
@@ -2927,6 +3804,15 @@
2927
  "node": ">=8"
2928
  }
2929
  },
 
 
 
 
 
 
 
 
 
2930
  "node_modules/source-map-js": {
2931
  "version": "1.2.1",
2932
  "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2950,6 +3836,12 @@
2950
  "url": "https://github.com/sponsors/sindresorhus"
2951
  }
2952
  },
 
 
 
 
 
 
2953
  "node_modules/supports-color": {
2954
  "version": "7.2.0",
2955
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2963,6 +3855,18 @@
2963
  "node": ">=8"
2964
  }
2965
  },
 
 
 
 
 
 
 
 
 
 
 
 
2966
  "node_modules/to-regex-range": {
2967
  "version": "5.0.1",
2968
  "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3184,6 +4088,15 @@
3184
  "node": ">=0.10.0"
3185
  }
3186
  },
 
 
 
 
 
 
 
 
 
3187
  "node_modules/yocto-queue": {
3188
  "version": "0.1.0",
3189
  "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -3196,6 +4109,35 @@
3196
  "funding": {
3197
  "url": "https://github.com/sponsors/sindresorhus"
3198
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3199
  }
3200
  }
3201
  }
 
8
  "name": "cua2-front",
9
  "version": "0.0.0",
10
  "dependencies": {
11
+ "@emotion/react": "^11.14.0",
12
+ "@emotion/styled": "^11.14.1",
13
+ "@mui/icons-material": "^7.3.4",
14
+ "@mui/lab": "^7.0.1-beta.19",
15
+ "@mui/material": "^7.3.4",
16
+ "gifshot": "^0.4.5",
17
  "react": "^18.3.1",
18
  "react-dom": "^18.3.1",
19
  "react-router-dom": "^6.30.1",
20
+ "ulid": "^3.0.1",
21
+ "zustand": "^5.0.8"
22
  },
23
  "devDependencies": {
24
  "@eslint/js": "^9.38.0",
 
35
  "vite": "^5.4.19"
36
  }
37
  },
38
+ "node_modules/@babel/code-frame": {
39
+ "version": "7.27.1",
40
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
41
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "@babel/helper-validator-identifier": "^7.27.1",
45
+ "js-tokens": "^4.0.0",
46
+ "picocolors": "^1.1.1"
47
+ },
48
+ "engines": {
49
+ "node": ">=6.9.0"
50
+ }
51
+ },
52
+ "node_modules/@babel/generator": {
53
+ "version": "7.28.5",
54
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
55
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
56
+ "license": "MIT",
57
+ "dependencies": {
58
+ "@babel/parser": "^7.28.5",
59
+ "@babel/types": "^7.28.5",
60
+ "@jridgewell/gen-mapping": "^0.3.12",
61
+ "@jridgewell/trace-mapping": "^0.3.28",
62
+ "jsesc": "^3.0.2"
63
+ },
64
+ "engines": {
65
+ "node": ">=6.9.0"
66
+ }
67
+ },
68
+ "node_modules/@babel/helper-globals": {
69
+ "version": "7.28.0",
70
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
71
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
72
+ "license": "MIT",
73
+ "engines": {
74
+ "node": ">=6.9.0"
75
+ }
76
+ },
77
+ "node_modules/@babel/helper-module-imports": {
78
+ "version": "7.27.1",
79
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
80
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
81
+ "license": "MIT",
82
+ "dependencies": {
83
+ "@babel/traverse": "^7.27.1",
84
+ "@babel/types": "^7.27.1"
85
+ },
86
+ "engines": {
87
+ "node": ">=6.9.0"
88
+ }
89
+ },
90
+ "node_modules/@babel/helper-string-parser": {
91
+ "version": "7.27.1",
92
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
93
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
94
+ "license": "MIT",
95
+ "engines": {
96
+ "node": ">=6.9.0"
97
+ }
98
+ },
99
+ "node_modules/@babel/helper-validator-identifier": {
100
+ "version": "7.28.5",
101
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
102
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
103
+ "license": "MIT",
104
+ "engines": {
105
+ "node": ">=6.9.0"
106
+ }
107
+ },
108
+ "node_modules/@babel/parser": {
109
+ "version": "7.28.5",
110
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
111
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
112
+ "license": "MIT",
113
+ "dependencies": {
114
+ "@babel/types": "^7.28.5"
115
+ },
116
+ "bin": {
117
+ "parser": "bin/babel-parser.js"
118
+ },
119
+ "engines": {
120
+ "node": ">=6.0.0"
121
+ }
122
+ },
123
+ "node_modules/@babel/runtime": {
124
+ "version": "7.28.4",
125
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
126
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
127
+ "license": "MIT",
128
+ "engines": {
129
+ "node": ">=6.9.0"
130
+ }
131
+ },
132
+ "node_modules/@babel/template": {
133
+ "version": "7.27.2",
134
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
135
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
136
+ "license": "MIT",
137
+ "dependencies": {
138
+ "@babel/code-frame": "^7.27.1",
139
+ "@babel/parser": "^7.27.2",
140
+ "@babel/types": "^7.27.1"
141
+ },
142
+ "engines": {
143
+ "node": ">=6.9.0"
144
+ }
145
+ },
146
+ "node_modules/@babel/traverse": {
147
+ "version": "7.28.5",
148
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
149
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
150
+ "license": "MIT",
151
+ "dependencies": {
152
+ "@babel/code-frame": "^7.27.1",
153
+ "@babel/generator": "^7.28.5",
154
+ "@babel/helper-globals": "^7.28.0",
155
+ "@babel/parser": "^7.28.5",
156
+ "@babel/template": "^7.27.2",
157
+ "@babel/types": "^7.28.5",
158
+ "debug": "^4.3.1"
159
+ },
160
+ "engines": {
161
+ "node": ">=6.9.0"
162
+ }
163
+ },
164
+ "node_modules/@babel/types": {
165
+ "version": "7.28.5",
166
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
167
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
168
+ "license": "MIT",
169
+ "dependencies": {
170
+ "@babel/helper-string-parser": "^7.27.1",
171
+ "@babel/helper-validator-identifier": "^7.28.5"
172
+ },
173
+ "engines": {
174
+ "node": ">=6.9.0"
175
+ }
176
+ },
177
+ "node_modules/@emotion/babel-plugin": {
178
+ "version": "11.13.5",
179
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
180
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
181
+ "license": "MIT",
182
+ "dependencies": {
183
+ "@babel/helper-module-imports": "^7.16.7",
184
+ "@babel/runtime": "^7.18.3",
185
+ "@emotion/hash": "^0.9.2",
186
+ "@emotion/memoize": "^0.9.0",
187
+ "@emotion/serialize": "^1.3.3",
188
+ "babel-plugin-macros": "^3.1.0",
189
+ "convert-source-map": "^1.5.0",
190
+ "escape-string-regexp": "^4.0.0",
191
+ "find-root": "^1.1.0",
192
+ "source-map": "^0.5.7",
193
+ "stylis": "4.2.0"
194
+ }
195
+ },
196
+ "node_modules/@emotion/cache": {
197
+ "version": "11.14.0",
198
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
199
+ "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
200
+ "license": "MIT",
201
+ "dependencies": {
202
+ "@emotion/memoize": "^0.9.0",
203
+ "@emotion/sheet": "^1.4.0",
204
+ "@emotion/utils": "^1.4.2",
205
+ "@emotion/weak-memoize": "^0.4.0",
206
+ "stylis": "4.2.0"
207
+ }
208
+ },
209
+ "node_modules/@emotion/hash": {
210
+ "version": "0.9.2",
211
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
212
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
213
+ "license": "MIT"
214
+ },
215
+ "node_modules/@emotion/is-prop-valid": {
216
+ "version": "1.4.0",
217
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
218
+ "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
219
+ "license": "MIT",
220
+ "dependencies": {
221
+ "@emotion/memoize": "^0.9.0"
222
+ }
223
+ },
224
+ "node_modules/@emotion/memoize": {
225
+ "version": "0.9.0",
226
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
227
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
228
+ "license": "MIT"
229
+ },
230
+ "node_modules/@emotion/react": {
231
+ "version": "11.14.0",
232
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
233
+ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
234
+ "license": "MIT",
235
+ "dependencies": {
236
+ "@babel/runtime": "^7.18.3",
237
+ "@emotion/babel-plugin": "^11.13.5",
238
+ "@emotion/cache": "^11.14.0",
239
+ "@emotion/serialize": "^1.3.3",
240
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
241
+ "@emotion/utils": "^1.4.2",
242
+ "@emotion/weak-memoize": "^0.4.0",
243
+ "hoist-non-react-statics": "^3.3.1"
244
+ },
245
+ "peerDependencies": {
246
+ "react": ">=16.8.0"
247
+ },
248
+ "peerDependenciesMeta": {
249
+ "@types/react": {
250
+ "optional": true
251
+ }
252
+ }
253
+ },
254
+ "node_modules/@emotion/serialize": {
255
+ "version": "1.3.3",
256
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
257
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
258
+ "license": "MIT",
259
+ "dependencies": {
260
+ "@emotion/hash": "^0.9.2",
261
+ "@emotion/memoize": "^0.9.0",
262
+ "@emotion/unitless": "^0.10.0",
263
+ "@emotion/utils": "^1.4.2",
264
+ "csstype": "^3.0.2"
265
+ }
266
+ },
267
+ "node_modules/@emotion/sheet": {
268
+ "version": "1.4.0",
269
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
270
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
271
+ "license": "MIT"
272
+ },
273
+ "node_modules/@emotion/styled": {
274
+ "version": "11.14.1",
275
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
276
+ "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
277
+ "license": "MIT",
278
+ "dependencies": {
279
+ "@babel/runtime": "^7.18.3",
280
+ "@emotion/babel-plugin": "^11.13.5",
281
+ "@emotion/is-prop-valid": "^1.3.0",
282
+ "@emotion/serialize": "^1.3.3",
283
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
284
+ "@emotion/utils": "^1.4.2"
285
+ },
286
+ "peerDependencies": {
287
+ "@emotion/react": "^11.0.0-rc.0",
288
+ "react": ">=16.8.0"
289
+ },
290
+ "peerDependenciesMeta": {
291
+ "@types/react": {
292
+ "optional": true
293
+ }
294
+ }
295
+ },
296
+ "node_modules/@emotion/unitless": {
297
+ "version": "0.10.0",
298
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
299
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
300
+ "license": "MIT"
301
+ },
302
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
303
+ "version": "1.2.0",
304
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
305
+ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
306
+ "license": "MIT",
307
+ "peerDependencies": {
308
+ "react": ">=16.8.0"
309
+ }
310
+ },
311
+ "node_modules/@emotion/utils": {
312
+ "version": "1.4.2",
313
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
314
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
315
+ "license": "MIT"
316
+ },
317
+ "node_modules/@emotion/weak-memoize": {
318
+ "version": "0.4.0",
319
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
320
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
321
+ "license": "MIT"
322
+ },
323
  "node_modules/@esbuild/aix-ppc64": {
324
  "version": "0.21.5",
325
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
 
920
  "url": "https://github.com/sponsors/nzakas"
921
  }
922
  },
923
+ "node_modules/@jridgewell/gen-mapping": {
924
+ "version": "0.3.13",
925
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
926
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
927
+ "license": "MIT",
928
+ "dependencies": {
929
+ "@jridgewell/sourcemap-codec": "^1.5.0",
930
+ "@jridgewell/trace-mapping": "^0.3.24"
931
+ }
932
+ },
933
+ "node_modules/@jridgewell/resolve-uri": {
934
+ "version": "3.1.2",
935
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
936
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
937
+ "license": "MIT",
938
+ "engines": {
939
+ "node": ">=6.0.0"
940
+ }
941
+ },
942
+ "node_modules/@jridgewell/sourcemap-codec": {
943
+ "version": "1.5.5",
944
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
945
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
946
+ "license": "MIT"
947
+ },
948
+ "node_modules/@jridgewell/trace-mapping": {
949
+ "version": "0.3.31",
950
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
951
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
952
+ "license": "MIT",
953
+ "dependencies": {
954
+ "@jridgewell/resolve-uri": "^3.1.0",
955
+ "@jridgewell/sourcemap-codec": "^1.4.14"
956
+ }
957
+ },
958
+ "node_modules/@mui/core-downloads-tracker": {
959
+ "version": "7.3.5",
960
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz",
961
+ "integrity": "sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==",
962
+ "license": "MIT",
963
+ "funding": {
964
+ "type": "opencollective",
965
+ "url": "https://opencollective.com/mui-org"
966
+ }
967
+ },
968
+ "node_modules/@mui/icons-material": {
969
+ "version": "7.3.4",
970
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.4.tgz",
971
+ "integrity": "sha512-9n6Xcq7molXWYb680N2Qx+FRW8oT6j/LXF5PZFH3ph9X/Rct0B/BlLAsFI7iL9ySI6LVLuQIVtrLiPT82R7OZw==",
972
+ "license": "MIT",
973
+ "dependencies": {
974
+ "@babel/runtime": "^7.28.4"
975
+ },
976
+ "engines": {
977
+ "node": ">=14.0.0"
978
+ },
979
+ "funding": {
980
+ "type": "opencollective",
981
+ "url": "https://opencollective.com/mui-org"
982
+ },
983
+ "peerDependencies": {
984
+ "@mui/material": "^7.3.4",
985
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
986
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
987
+ },
988
+ "peerDependenciesMeta": {
989
+ "@types/react": {
990
+ "optional": true
991
+ }
992
+ }
993
+ },
994
+ "node_modules/@mui/lab": {
995
+ "version": "7.0.1-beta.19",
996
+ "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-7.0.1-beta.19.tgz",
997
+ "integrity": "sha512-Ekxd2mPnr5iKwrMXjN/y2xgpxPX8ithBBcDenjqNdBt/ZQumrmBl0ifVoqAHsL6lxN6DOgRsWTRc4eOdDiB+0Q==",
998
+ "license": "MIT",
999
+ "dependencies": {
1000
+ "@babel/runtime": "^7.28.4",
1001
+ "@mui/system": "^7.3.5",
1002
+ "@mui/types": "^7.4.8",
1003
+ "@mui/utils": "^7.3.5",
1004
+ "clsx": "^2.1.1",
1005
+ "prop-types": "^15.8.1"
1006
+ },
1007
+ "engines": {
1008
+ "node": ">=14.0.0"
1009
+ },
1010
+ "funding": {
1011
+ "type": "opencollective",
1012
+ "url": "https://opencollective.com/mui-org"
1013
+ },
1014
+ "peerDependencies": {
1015
+ "@emotion/react": "^11.5.0",
1016
+ "@emotion/styled": "^11.3.0",
1017
+ "@mui/material": "^7.3.5",
1018
+ "@mui/material-pigment-css": "^7.3.5",
1019
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
1020
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
1021
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
1022
+ },
1023
+ "peerDependenciesMeta": {
1024
+ "@emotion/react": {
1025
+ "optional": true
1026
+ },
1027
+ "@emotion/styled": {
1028
+ "optional": true
1029
+ },
1030
+ "@mui/material-pigment-css": {
1031
+ "optional": true
1032
+ },
1033
+ "@types/react": {
1034
+ "optional": true
1035
+ }
1036
+ }
1037
+ },
1038
+ "node_modules/@mui/material": {
1039
+ "version": "7.3.5",
1040
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
1041
+ "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
1042
+ "license": "MIT",
1043
+ "dependencies": {
1044
+ "@babel/runtime": "^7.28.4",
1045
+ "@mui/core-downloads-tracker": "^7.3.5",
1046
+ "@mui/system": "^7.3.5",
1047
+ "@mui/types": "^7.4.8",
1048
+ "@mui/utils": "^7.3.5",
1049
+ "@popperjs/core": "^2.11.8",
1050
+ "@types/react-transition-group": "^4.4.12",
1051
+ "clsx": "^2.1.1",
1052
+ "csstype": "^3.1.3",
1053
+ "prop-types": "^15.8.1",
1054
+ "react-is": "^19.2.0",
1055
+ "react-transition-group": "^4.4.5"
1056
+ },
1057
+ "engines": {
1058
+ "node": ">=14.0.0"
1059
+ },
1060
+ "funding": {
1061
+ "type": "opencollective",
1062
+ "url": "https://opencollective.com/mui-org"
1063
+ },
1064
+ "peerDependencies": {
1065
+ "@emotion/react": "^11.5.0",
1066
+ "@emotion/styled": "^11.3.0",
1067
+ "@mui/material-pigment-css": "^7.3.5",
1068
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
1069
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
1070
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
1071
+ },
1072
+ "peerDependenciesMeta": {
1073
+ "@emotion/react": {
1074
+ "optional": true
1075
+ },
1076
+ "@emotion/styled": {
1077
+ "optional": true
1078
+ },
1079
+ "@mui/material-pigment-css": {
1080
+ "optional": true
1081
+ },
1082
+ "@types/react": {
1083
+ "optional": true
1084
+ }
1085
+ }
1086
+ },
1087
+ "node_modules/@mui/private-theming": {
1088
+ "version": "7.3.5",
1089
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.5.tgz",
1090
+ "integrity": "sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==",
1091
+ "license": "MIT",
1092
+ "dependencies": {
1093
+ "@babel/runtime": "^7.28.4",
1094
+ "@mui/utils": "^7.3.5",
1095
+ "prop-types": "^15.8.1"
1096
+ },
1097
+ "engines": {
1098
+ "node": ">=14.0.0"
1099
+ },
1100
+ "funding": {
1101
+ "type": "opencollective",
1102
+ "url": "https://opencollective.com/mui-org"
1103
+ },
1104
+ "peerDependencies": {
1105
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
1106
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
1107
+ },
1108
+ "peerDependenciesMeta": {
1109
+ "@types/react": {
1110
+ "optional": true
1111
+ }
1112
+ }
1113
+ },
1114
+ "node_modules/@mui/styled-engine": {
1115
+ "version": "7.3.5",
1116
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.5.tgz",
1117
+ "integrity": "sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==",
1118
+ "license": "MIT",
1119
+ "dependencies": {
1120
+ "@babel/runtime": "^7.28.4",
1121
+ "@emotion/cache": "^11.14.0",
1122
+ "@emotion/serialize": "^1.3.3",
1123
+ "@emotion/sheet": "^1.4.0",
1124
+ "csstype": "^3.1.3",
1125
+ "prop-types": "^15.8.1"
1126
+ },
1127
+ "engines": {
1128
+ "node": ">=14.0.0"
1129
+ },
1130
+ "funding": {
1131
+ "type": "opencollective",
1132
+ "url": "https://opencollective.com/mui-org"
1133
+ },
1134
+ "peerDependencies": {
1135
+ "@emotion/react": "^11.4.1",
1136
+ "@emotion/styled": "^11.3.0",
1137
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
1138
+ },
1139
+ "peerDependenciesMeta": {
1140
+ "@emotion/react": {
1141
+ "optional": true
1142
+ },
1143
+ "@emotion/styled": {
1144
+ "optional": true
1145
+ }
1146
+ }
1147
+ },
1148
+ "node_modules/@mui/system": {
1149
+ "version": "7.3.5",
1150
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz",
1151
+ "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==",
1152
+ "license": "MIT",
1153
+ "dependencies": {
1154
+ "@babel/runtime": "^7.28.4",
1155
+ "@mui/private-theming": "^7.3.5",
1156
+ "@mui/styled-engine": "^7.3.5",
1157
+ "@mui/types": "^7.4.8",
1158
+ "@mui/utils": "^7.3.5",
1159
+ "clsx": "^2.1.1",
1160
+ "csstype": "^3.1.3",
1161
+ "prop-types": "^15.8.1"
1162
+ },
1163
+ "engines": {
1164
+ "node": ">=14.0.0"
1165
+ },
1166
+ "funding": {
1167
+ "type": "opencollective",
1168
+ "url": "https://opencollective.com/mui-org"
1169
+ },
1170
+ "peerDependencies": {
1171
+ "@emotion/react": "^11.5.0",
1172
+ "@emotion/styled": "^11.3.0",
1173
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
1174
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
1175
+ },
1176
+ "peerDependenciesMeta": {
1177
+ "@emotion/react": {
1178
+ "optional": true
1179
+ },
1180
+ "@emotion/styled": {
1181
+ "optional": true
1182
+ },
1183
+ "@types/react": {
1184
+ "optional": true
1185
+ }
1186
+ }
1187
+ },
1188
+ "node_modules/@mui/types": {
1189
+ "version": "7.4.8",
1190
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.8.tgz",
1191
+ "integrity": "sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==",
1192
+ "license": "MIT",
1193
+ "dependencies": {
1194
+ "@babel/runtime": "^7.28.4"
1195
+ },
1196
+ "peerDependencies": {
1197
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
1198
+ },
1199
+ "peerDependenciesMeta": {
1200
+ "@types/react": {
1201
+ "optional": true
1202
+ }
1203
+ }
1204
+ },
1205
+ "node_modules/@mui/utils": {
1206
+ "version": "7.3.5",
1207
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.5.tgz",
1208
+ "integrity": "sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==",
1209
+ "license": "MIT",
1210
+ "dependencies": {
1211
+ "@babel/runtime": "^7.28.4",
1212
+ "@mui/types": "^7.4.8",
1213
+ "@types/prop-types": "^15.7.15",
1214
+ "clsx": "^2.1.1",
1215
+ "prop-types": "^15.8.1",
1216
+ "react-is": "^19.2.0"
1217
+ },
1218
+ "engines": {
1219
+ "node": ">=14.0.0"
1220
+ },
1221
+ "funding": {
1222
+ "type": "opencollective",
1223
+ "url": "https://opencollective.com/mui-org"
1224
+ },
1225
+ "peerDependencies": {
1226
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
1227
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
1228
+ },
1229
+ "peerDependenciesMeta": {
1230
+ "@types/react": {
1231
+ "optional": true
1232
+ }
1233
+ }
1234
+ },
1235
  "node_modules/@nodelib/fs.scandir": {
1236
  "version": "2.1.5",
1237
  "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 
1270
  "node": ">= 8"
1271
  }
1272
  },
1273
+ "node_modules/@popperjs/core": {
1274
+ "version": "2.11.8",
1275
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
1276
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
1277
+ "license": "MIT",
1278
+ "funding": {
1279
+ "type": "opencollective",
1280
+ "url": "https://opencollective.com/popperjs"
1281
+ }
1282
+ },
1283
  "node_modules/@remix-run/router": {
1284
  "version": "1.23.0",
1285
  "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
 
1854
  "undici-types": "~6.21.0"
1855
  }
1856
  },
1857
+ "node_modules/@types/parse-json": {
1858
+ "version": "4.0.2",
1859
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
1860
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
1861
+ "license": "MIT"
1862
+ },
1863
  "node_modules/@types/prop-types": {
1864
  "version": "15.7.15",
1865
  "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1866
  "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
 
1867
  "license": "MIT"
1868
  },
1869
  "node_modules/@types/react": {
1870
  "version": "18.3.26",
1871
  "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
1872
  "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
 
1873
  "license": "MIT",
1874
  "dependencies": {
1875
  "@types/prop-types": "*",
 
1886
  "@types/react": "^18.0.0"
1887
  }
1888
  },
1889
+ "node_modules/@types/react-transition-group": {
1890
+ "version": "4.4.12",
1891
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
1892
+ "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
1893
+ "license": "MIT",
1894
+ "peerDependencies": {
1895
+ "@types/react": "*"
1896
+ }
1897
+ },
1898
  "node_modules/@typescript-eslint/eslint-plugin": {
1899
  "version": "8.46.1",
1900
  "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
 
2268
  "postcss": "^8.1.0"
2269
  }
2270
  },
2271
+ "node_modules/babel-plugin-macros": {
2272
+ "version": "3.1.0",
2273
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
2274
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
2275
+ "license": "MIT",
2276
+ "dependencies": {
2277
+ "@babel/runtime": "^7.12.5",
2278
+ "cosmiconfig": "^7.0.0",
2279
+ "resolve": "^1.19.0"
2280
+ },
2281
+ "engines": {
2282
+ "node": ">=10",
2283
+ "npm": ">=6"
2284
+ }
2285
+ },
2286
  "node_modules/balanced-match": {
2287
  "version": "1.0.2",
2288
  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
 
2362
  "version": "3.1.0",
2363
  "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
2364
  "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
 
2365
  "license": "MIT",
2366
  "engines": {
2367
  "node": ">=6"
 
2405
  "url": "https://github.com/chalk/chalk?sponsor=1"
2406
  }
2407
  },
2408
+ "node_modules/clsx": {
2409
+ "version": "2.1.1",
2410
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
2411
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
2412
+ "license": "MIT",
2413
+ "engines": {
2414
+ "node": ">=6"
2415
+ }
2416
+ },
2417
  "node_modules/color-convert": {
2418
  "version": "2.0.1",
2419
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
 
2441
  "dev": true,
2442
  "license": "MIT"
2443
  },
2444
+ "node_modules/convert-source-map": {
2445
+ "version": "1.9.0",
2446
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
2447
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
2448
+ "license": "MIT"
2449
+ },
2450
+ "node_modules/cosmiconfig": {
2451
+ "version": "7.1.0",
2452
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
2453
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
2454
+ "license": "MIT",
2455
+ "dependencies": {
2456
+ "@types/parse-json": "^4.0.0",
2457
+ "import-fresh": "^3.2.1",
2458
+ "parse-json": "^5.0.0",
2459
+ "path-type": "^4.0.0",
2460
+ "yaml": "^1.10.0"
2461
+ },
2462
+ "engines": {
2463
+ "node": ">=10"
2464
+ }
2465
+ },
2466
  "node_modules/cross-spawn": {
2467
  "version": "7.0.6",
2468
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 
2482
  "version": "3.1.3",
2483
  "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
2484
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
 
2485
  "license": "MIT"
2486
  },
2487
  "node_modules/debug": {
2488
  "version": "4.4.3",
2489
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
2490
  "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
 
2491
  "license": "MIT",
2492
  "dependencies": {
2493
  "ms": "^2.1.3"
 
2508
  "dev": true,
2509
  "license": "MIT"
2510
  },
2511
+ "node_modules/dom-helpers": {
2512
+ "version": "5.2.1",
2513
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
2514
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
2515
+ "license": "MIT",
2516
+ "dependencies": {
2517
+ "@babel/runtime": "^7.8.7",
2518
+ "csstype": "^3.0.2"
2519
+ }
2520
+ },
2521
  "node_modules/electron-to-chromium": {
2522
  "version": "1.5.237",
2523
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
 
2525
  "dev": true,
2526
  "license": "ISC"
2527
  },
2528
+ "node_modules/error-ex": {
2529
+ "version": "1.3.4",
2530
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
2531
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
2532
+ "license": "MIT",
2533
+ "dependencies": {
2534
+ "is-arrayish": "^0.2.1"
2535
+ }
2536
+ },
2537
  "node_modules/esbuild": {
2538
  "version": "0.21.5",
2539
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
 
2587
  "version": "4.0.0",
2588
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
2589
  "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
 
2590
  "license": "MIT",
2591
  "engines": {
2592
  "node": ">=10"
 
2873
  "node": ">=8"
2874
  }
2875
  },
2876
+ "node_modules/find-root": {
2877
+ "version": "1.1.0",
2878
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
2879
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
2880
+ "license": "MIT"
2881
+ },
2882
  "node_modules/find-up": {
2883
  "version": "5.0.0",
2884
  "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
 
2946
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
2947
  }
2948
  },
2949
+ "node_modules/function-bind": {
2950
+ "version": "1.1.2",
2951
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
2952
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
2953
+ "license": "MIT",
2954
+ "funding": {
2955
+ "url": "https://github.com/sponsors/ljharb"
2956
+ }
2957
+ },
2958
+ "node_modules/gifshot": {
2959
+ "version": "0.4.5",
2960
+ "resolved": "https://registry.npmjs.org/gifshot/-/gifshot-0.4.5.tgz",
2961
+ "integrity": "sha512-oaOTT7patjxFFv7ptR0R0NNhqy3ZAmcLUQCjM/sTsvsQaUAlB2fHirLajcNAKJ6ufoVhdP+ZkXYvmUycHP1FNg==",
2962
+ "license": "MIT"
2963
+ },
2964
  "node_modules/glob-parent": {
2965
  "version": "6.0.2",
2966
  "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
 
3004
  "node": ">=8"
3005
  }
3006
  },
3007
+ "node_modules/hasown": {
3008
+ "version": "2.0.2",
3009
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
3010
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
3011
+ "license": "MIT",
3012
+ "dependencies": {
3013
+ "function-bind": "^1.1.2"
3014
+ },
3015
+ "engines": {
3016
+ "node": ">= 0.4"
3017
+ }
3018
+ },
3019
+ "node_modules/hoist-non-react-statics": {
3020
+ "version": "3.3.2",
3021
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
3022
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
3023
+ "license": "BSD-3-Clause",
3024
+ "dependencies": {
3025
+ "react-is": "^16.7.0"
3026
+ }
3027
+ },
3028
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
3029
+ "version": "16.13.1",
3030
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
3031
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
3032
+ "license": "MIT"
3033
+ },
3034
  "node_modules/ignore": {
3035
  "version": "5.3.2",
3036
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
 
3045
  "version": "3.3.1",
3046
  "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
3047
  "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
 
3048
  "license": "MIT",
3049
  "dependencies": {
3050
  "parent-module": "^1.0.0",
 
3067
  "node": ">=0.8.19"
3068
  }
3069
  },
3070
+ "node_modules/is-arrayish": {
3071
+ "version": "0.2.1",
3072
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
3073
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
3074
+ "license": "MIT"
3075
+ },
3076
+ "node_modules/is-core-module": {
3077
+ "version": "2.16.1",
3078
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
3079
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
3080
+ "license": "MIT",
3081
+ "dependencies": {
3082
+ "hasown": "^2.0.2"
3083
+ },
3084
+ "engines": {
3085
+ "node": ">= 0.4"
3086
+ },
3087
+ "funding": {
3088
+ "url": "https://github.com/sponsors/ljharb"
3089
+ }
3090
+ },
3091
  "node_modules/is-extglob": {
3092
  "version": "2.1.1",
3093
  "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
 
3147
  "js-yaml": "bin/js-yaml.js"
3148
  }
3149
  },
3150
+ "node_modules/jsesc": {
3151
+ "version": "3.1.0",
3152
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
3153
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
3154
+ "license": "MIT",
3155
+ "bin": {
3156
+ "jsesc": "bin/jsesc"
3157
+ },
3158
+ "engines": {
3159
+ "node": ">=6"
3160
+ }
3161
+ },
3162
  "node_modules/json-buffer": {
3163
  "version": "3.0.1",
3164
  "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
 
3166
  "dev": true,
3167
  "license": "MIT"
3168
  },
3169
+ "node_modules/json-parse-even-better-errors": {
3170
+ "version": "2.3.1",
3171
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
3172
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
3173
+ "license": "MIT"
3174
+ },
3175
  "node_modules/json-schema-traverse": {
3176
  "version": "0.4.1",
3177
  "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
 
3210
  "node": ">= 0.8.0"
3211
  }
3212
  },
3213
+ "node_modules/lines-and-columns": {
3214
+ "version": "1.2.4",
3215
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
3216
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
3217
+ "license": "MIT"
3218
+ },
3219
  "node_modules/locate-path": {
3220
  "version": "6.0.0",
3221
  "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
 
3292
  "version": "2.1.3",
3293
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
3294
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
 
3295
  "license": "MIT"
3296
  },
3297
  "node_modules/nanoid": {
 
3337
  "node": ">=0.10.0"
3338
  }
3339
  },
3340
+ "node_modules/object-assign": {
3341
+ "version": "4.1.1",
3342
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
3343
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
3344
+ "license": "MIT",
3345
+ "engines": {
3346
+ "node": ">=0.10.0"
3347
+ }
3348
+ },
3349
  "node_modules/optionator": {
3350
  "version": "0.9.4",
3351
  "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
 
3400
  "version": "1.0.1",
3401
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
3402
  "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
 
3403
  "license": "MIT",
3404
  "dependencies": {
3405
  "callsites": "^3.0.0"
 
3408
  "node": ">=6"
3409
  }
3410
  },
3411
+ "node_modules/parse-json": {
3412
+ "version": "5.2.0",
3413
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
3414
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
3415
+ "license": "MIT",
3416
+ "dependencies": {
3417
+ "@babel/code-frame": "^7.0.0",
3418
+ "error-ex": "^1.3.1",
3419
+ "json-parse-even-better-errors": "^2.3.0",
3420
+ "lines-and-columns": "^1.1.6"
3421
+ },
3422
+ "engines": {
3423
+ "node": ">=8"
3424
+ },
3425
+ "funding": {
3426
+ "url": "https://github.com/sponsors/sindresorhus"
3427
+ }
3428
+ },
3429
  "node_modules/path-exists": {
3430
  "version": "4.0.0",
3431
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
 
3446
  "node": ">=8"
3447
  }
3448
  },
3449
+ "node_modules/path-parse": {
3450
+ "version": "1.0.7",
3451
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
3452
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
3453
+ "license": "MIT"
3454
+ },
3455
+ "node_modules/path-type": {
3456
+ "version": "4.0.0",
3457
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
3458
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
3459
+ "license": "MIT",
3460
+ "engines": {
3461
+ "node": ">=8"
3462
+ }
3463
+ },
3464
  "node_modules/picocolors": {
3465
  "version": "1.1.1",
3466
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
3467
  "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
 
3468
  "license": "ISC"
3469
  },
3470
  "node_modules/picomatch": {
 
3526
  "node": ">= 0.8.0"
3527
  }
3528
  },
3529
+ "node_modules/prop-types": {
3530
+ "version": "15.8.1",
3531
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
3532
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
3533
+ "license": "MIT",
3534
+ "dependencies": {
3535
+ "loose-envify": "^1.4.0",
3536
+ "object-assign": "^4.1.1",
3537
+ "react-is": "^16.13.1"
3538
+ }
3539
+ },
3540
+ "node_modules/prop-types/node_modules/react-is": {
3541
+ "version": "16.13.1",
3542
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
3543
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
3544
+ "license": "MIT"
3545
+ },
3546
  "node_modules/punycode": {
3547
  "version": "2.3.1",
3548
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
 
3599
  "react": "^18.3.1"
3600
  }
3601
  },
3602
+ "node_modules/react-is": {
3603
+ "version": "19.2.0",
3604
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
3605
+ "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
3606
+ "license": "MIT"
3607
+ },
3608
  "node_modules/react-router": {
3609
  "version": "6.30.1",
3610
  "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
 
3637
  "react-dom": ">=16.8"
3638
  }
3639
  },
3640
+ "node_modules/react-transition-group": {
3641
+ "version": "4.4.5",
3642
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
3643
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
3644
+ "license": "BSD-3-Clause",
3645
+ "dependencies": {
3646
+ "@babel/runtime": "^7.5.5",
3647
+ "dom-helpers": "^5.0.1",
3648
+ "loose-envify": "^1.4.0",
3649
+ "prop-types": "^15.6.2"
3650
+ },
3651
+ "peerDependencies": {
3652
+ "react": ">=16.6.0",
3653
+ "react-dom": ">=16.6.0"
3654
+ }
3655
+ },
3656
+ "node_modules/resolve": {
3657
+ "version": "1.22.11",
3658
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
3659
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
3660
+ "license": "MIT",
3661
+ "dependencies": {
3662
+ "is-core-module": "^2.16.1",
3663
+ "path-parse": "^1.0.7",
3664
+ "supports-preserve-symlinks-flag": "^1.0.0"
3665
+ },
3666
+ "bin": {
3667
+ "resolve": "bin/resolve"
3668
+ },
3669
+ "engines": {
3670
+ "node": ">= 0.4"
3671
+ },
3672
+ "funding": {
3673
+ "url": "https://github.com/sponsors/ljharb"
3674
+ }
3675
+ },
3676
  "node_modules/resolve-from": {
3677
  "version": "4.0.0",
3678
  "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
3679
  "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
 
3680
  "license": "MIT",
3681
  "engines": {
3682
  "node": ">=4"
 
3804
  "node": ">=8"
3805
  }
3806
  },
3807
+ "node_modules/source-map": {
3808
+ "version": "0.5.7",
3809
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
3810
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
3811
+ "license": "BSD-3-Clause",
3812
+ "engines": {
3813
+ "node": ">=0.10.0"
3814
+ }
3815
+ },
3816
  "node_modules/source-map-js": {
3817
  "version": "1.2.1",
3818
  "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
 
3836
  "url": "https://github.com/sponsors/sindresorhus"
3837
  }
3838
  },
3839
+ "node_modules/stylis": {
3840
+ "version": "4.2.0",
3841
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
3842
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
3843
+ "license": "MIT"
3844
+ },
3845
  "node_modules/supports-color": {
3846
  "version": "7.2.0",
3847
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
 
3855
  "node": ">=8"
3856
  }
3857
  },
3858
+ "node_modules/supports-preserve-symlinks-flag": {
3859
+ "version": "1.0.0",
3860
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
3861
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
3862
+ "license": "MIT",
3863
+ "engines": {
3864
+ "node": ">= 0.4"
3865
+ },
3866
+ "funding": {
3867
+ "url": "https://github.com/sponsors/ljharb"
3868
+ }
3869
+ },
3870
  "node_modules/to-regex-range": {
3871
  "version": "5.0.1",
3872
  "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
 
4088
  "node": ">=0.10.0"
4089
  }
4090
  },
4091
+ "node_modules/yaml": {
4092
+ "version": "1.10.2",
4093
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
4094
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
4095
+ "license": "ISC",
4096
+ "engines": {
4097
+ "node": ">= 6"
4098
+ }
4099
+ },
4100
  "node_modules/yocto-queue": {
4101
  "version": "0.1.0",
4102
  "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
 
4109
  "funding": {
4110
  "url": "https://github.com/sponsors/sindresorhus"
4111
  }
4112
+ },
4113
+ "node_modules/zustand": {
4114
+ "version": "5.0.8",
4115
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
4116
+ "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
4117
+ "license": "MIT",
4118
+ "engines": {
4119
+ "node": ">=12.20.0"
4120
+ },
4121
+ "peerDependencies": {
4122
+ "@types/react": ">=18.0.0",
4123
+ "immer": ">=9.0.6",
4124
+ "react": ">=18.0.0",
4125
+ "use-sync-external-store": ">=1.2.0"
4126
+ },
4127
+ "peerDependenciesMeta": {
4128
+ "@types/react": {
4129
+ "optional": true
4130
+ },
4131
+ "immer": {
4132
+ "optional": true
4133
+ },
4134
+ "react": {
4135
+ "optional": true
4136
+ },
4137
+ "use-sync-external-store": {
4138
+ "optional": true
4139
+ }
4140
+ }
4141
  }
4142
  }
4143
  }
cua2-front/package.json CHANGED
@@ -12,10 +12,17 @@
12
  "preview": "vite preview"
13
  },
14
  "dependencies": {
 
 
 
 
 
 
15
  "react": "^18.3.1",
16
- "react-router-dom": "^6.30.1",
17
  "react-dom": "^18.3.1",
18
- "ulid": "^3.0.1"
 
 
19
  },
20
  "devDependencies": {
21
  "@eslint/js": "^9.38.0",
 
12
  "preview": "vite preview"
13
  },
14
  "dependencies": {
15
+ "@emotion/react": "^11.14.0",
16
+ "@emotion/styled": "^11.14.1",
17
+ "@mui/icons-material": "^7.3.4",
18
+ "@mui/lab": "^7.0.1-beta.19",
19
+ "@mui/material": "^7.3.4",
20
+ "gifshot": "^0.4.5",
21
  "react": "^18.3.1",
 
22
  "react-dom": "^18.3.1",
23
+ "react-router-dom": "^6.30.1",
24
+ "ulid": "^3.0.1",
25
+ "zustand": "^5.0.8"
26
  },
27
  "devDependencies": {
28
  "@eslint/js": "^9.38.0",
cua2-front/src/App.tsx CHANGED
@@ -1,14 +1,31 @@
1
- import React from 'react';
2
  import { BrowserRouter, Routes, Route } from "react-router-dom";
3
- import Index from "./pages/Index";
4
-
5
- const App = () => (
6
- <BrowserRouter>
7
- <Routes>
8
- <Route path="/" element={<Index />} />
9
- {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
10
- </Routes>
11
- </BrowserRouter>
12
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  export default App;
 
1
+ import { useMemo } from 'react';
2
  import { BrowserRouter, Routes, Route } from "react-router-dom";
3
+ import { ThemeProvider, CssBaseline } from '@mui/material';
4
+ import getTheme from './theme';
5
+ import Welcome from "./pages/Welcome";
6
+ import Task from "./pages/Task";
7
+ import { useAgentStore, selectIsDarkMode } from './stores/agentStore';
8
+ import { useAgentWebSocket } from './hooks/useAgentWebSocket';
9
+ import { config } from './config';
10
+
11
+ const App = () => {
12
+ const isDarkMode = useAgentStore(selectIsDarkMode);
13
+ const theme = useMemo(() => getTheme(isDarkMode ? 'dark' : 'light'), [isDarkMode]);
14
+
15
+ // Initialize WebSocket connection at app level so it persists across route changes
16
+ useAgentWebSocket({ url: config.wsUrl });
17
+
18
+ return (
19
+ <ThemeProvider theme={theme}>
20
+ <CssBaseline />
21
+ <BrowserRouter>
22
+ <Routes>
23
+ <Route path="/" element={<Welcome />} />
24
+ <Route path="/task" element={<Task />} />
25
+ </Routes>
26
+ </BrowserRouter>
27
+ </ThemeProvider>
28
+ );
29
+ };
30
 
31
  export default App;
cua2-front/src/components/ConnectionStatus.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Box, Chip, keyframes } from '@mui/material';
3
+ import CircleIcon from '@mui/icons-material/Circle';
4
+
5
+ interface ConnectionStatusProps {
6
+ isConnected: boolean;
7
+ }
8
+
9
+ // Pulse animation for connected indicator
10
+ const pulse = keyframes`
11
+ 0%, 100% {
12
+ opacity: 1;
13
+ }
14
+ 50% {
15
+ opacity: 0.5;
16
+ }
17
+ `;
18
+
19
+ export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ isConnected }) => {
20
+ return (
21
+ <Chip
22
+ label={isConnected ? 'Backend Online' : 'Backend Offline'}
23
+ deleteIcon={
24
+ <CircleIcon
25
+ sx={{
26
+ fontSize: 6,
27
+ animation: isConnected ? `${pulse} 2s ease-in-out infinite` : 'none',
28
+ }}
29
+ />
30
+ }
31
+ onDelete={() => {}} // Required for deleteIcon to show
32
+ size="small"
33
+ sx={{
34
+ backgroundColor: 'action.hover',
35
+ border: '1px solid',
36
+ borderColor: 'divider',
37
+ color: 'text.primary',
38
+ fontSize: '0.7rem',
39
+ fontWeight: 500,
40
+ height: 'auto',
41
+ '& .MuiChip-label': {
42
+ px: 1,
43
+ py: 0.5,
44
+ },
45
+ '& .MuiChip-deleteIcon': {
46
+ color: isConnected ? '#10b981' : '#ef4444',
47
+ marginRight: 0.5,
48
+ '&:hover': {
49
+ color: isConnected ? '#10b981' : '#ef4444',
50
+ },
51
+ },
52
+ }}
53
+ />
54
+ );
55
+ };
cua2-front/src/components/Header.tsx ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { AppBar, Toolbar, Box, Typography, Chip, IconButton, CircularProgress, keyframes } from '@mui/material';
3
+ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
4
+ import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
5
+ import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
6
+ import CheckIcon from '@mui/icons-material/Check';
7
+ import CloseIcon from '@mui/icons-material/Close';
8
+ import AccessTimeIcon from '@mui/icons-material/AccessTime';
9
+ import InputIcon from '@mui/icons-material/Input';
10
+ import OutputIcon from '@mui/icons-material/Output';
11
+ import SmartToyIcon from '@mui/icons-material/SmartToy';
12
+ import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
13
+ import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
14
+ import { useAgentStore, selectTrace, selectError, selectIsDarkMode, selectMetadata, selectIsConnectingToE2B, selectFinalStep } from '@/stores/agentStore';
15
+
16
+ interface HeaderProps {
17
+ isAgentProcessing: boolean;
18
+ onBackToHome?: () => void;
19
+ }
20
+
21
+ // Animation for the running task border - smooth oscillation (primary)
22
+ const borderPulse = keyframes`
23
+ 0%, 100% {
24
+ border-color: rgba(79, 134, 198, 0.5);
25
+ box-shadow: 0 0 0 0 rgba(79, 134, 198, 0.3);
26
+ }
27
+ 50% {
28
+ border-color: rgba(79, 134, 198, 1);
29
+ box-shadow: 0 0 8px 2px rgba(79, 134, 198, 0.4);
30
+ }
31
+ `;
32
+
33
+ // Animation for the background glow (primary)
34
+ const backgroundPulse = keyframes`
35
+ 0%, 100% {
36
+ background-color: rgba(79, 134, 198, 0.08);
37
+ }
38
+ 50% {
39
+ background-color: rgba(79, 134, 198, 0.15);
40
+ }
41
+ `;
42
+
43
+ // Animation for token flash - smooth glow effect
44
+ const tokenFlash = keyframes`
45
+ 0% {
46
+ filter: brightness(1);
47
+ text-shadow: none;
48
+ }
49
+ 25% {
50
+ filter: brightness(1.4);
51
+ text-shadow: 0 0 8px rgba(79, 134, 198, 0.6);
52
+ }
53
+ 100% {
54
+ filter: brightness(1);
55
+ text-shadow: none;
56
+ }
57
+ `;
58
+
59
+ // Animation for token icon flash
60
+ const iconFlash = keyframes`
61
+ 0% {
62
+ filter: brightness(1);
63
+ transform: scale(1);
64
+ }
65
+ 25% {
66
+ filter: brightness(1.6);
67
+ transform: scale(1.15);
68
+ }
69
+ 100% {
70
+ filter: brightness(1);
71
+ transform: scale(1);
72
+ }
73
+ `;
74
+
75
+ export const Header: React.FC<HeaderProps> = ({ isAgentProcessing, onBackToHome }) => {
76
+ const trace = useAgentStore(selectTrace);
77
+ const error = useAgentStore(selectError);
78
+ const finalStep = useAgentStore(selectFinalStep);
79
+ const isDarkMode = useAgentStore(selectIsDarkMode);
80
+ const toggleDarkMode = useAgentStore((state) => state.toggleDarkMode);
81
+ const metadata = useAgentStore(selectMetadata);
82
+ const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B);
83
+ const [elapsedTime, setElapsedTime] = useState(0);
84
+ const [inputTokenFlash, setInputTokenFlash] = useState(false);
85
+ const [outputTokenFlash, setOutputTokenFlash] = useState(false);
86
+ const prevInputTokens = useRef(0);
87
+ const prevOutputTokens = useRef(0);
88
+
89
+ // Update elapsed time every 100ms when agent is processing
90
+ useEffect(() => {
91
+ if (isAgentProcessing && trace?.timestamp) {
92
+ const interval = setInterval(() => {
93
+ const now = new Date();
94
+ const startTime = new Date(trace.timestamp);
95
+ const elapsed = (now.getTime() - startTime.getTime()) / 1000;
96
+ setElapsedTime(elapsed);
97
+ }, 100);
98
+
99
+ return () => clearInterval(interval);
100
+ } else if (metadata && metadata.duration > 0) {
101
+ setElapsedTime(metadata.duration);
102
+ }
103
+ }, [isAgentProcessing, trace?.timestamp, metadata]);
104
+
105
+ // Detect token changes and trigger flash animation
106
+ useEffect(() => {
107
+ if (metadata) {
108
+ // Input tokens changed
109
+ if (metadata.inputTokensUsed > prevInputTokens.current && prevInputTokens.current > 0) {
110
+ setInputTokenFlash(true);
111
+ setTimeout(() => setInputTokenFlash(false), 800);
112
+ }
113
+ prevInputTokens.current = metadata.inputTokensUsed;
114
+
115
+ // Output tokens changed
116
+ if (metadata.outputTokensUsed > prevOutputTokens.current && prevOutputTokens.current > 0) {
117
+ setOutputTokenFlash(true);
118
+ setTimeout(() => setOutputTokenFlash(false), 800);
119
+ }
120
+ prevOutputTokens.current = metadata.outputTokensUsed;
121
+ }
122
+ }, [metadata?.inputTokensUsed, metadata?.outputTokensUsed]);
123
+
124
+ // Determine task status - Use finalStep as source of truth
125
+ const getTaskStatus = () => {
126
+ // If we have a final step, use its type
127
+ if (finalStep) {
128
+ if (finalStep.type === 'failure') {
129
+ return { label: 'Task failed', color: 'error', icon: <CloseIcon sx={{ fontSize: 16, color: 'error.main' }} /> };
130
+ }
131
+ return { label: 'Completed', color: 'success', icon: <CheckIcon sx={{ fontSize: 16, color: 'success.main' }} /> };
132
+ }
133
+ // Otherwise check running states
134
+ if (isConnectingToE2B) return { label: 'Connecting to E2B...', color: 'primary', icon: <CircularProgress size={16} thickness={5} sx={{ color: 'primary.main' }} /> };
135
+ if (isAgentProcessing || trace?.isRunning) return { label: 'Running', color: 'primary', icon: <CircularProgress size={16} thickness={5} sx={{ color: 'primary.main' }} /> };
136
+ return { label: 'Ready', color: 'default', icon: <CheckIcon sx={{ fontSize: 16, color: 'text.secondary' }} /> };
137
+ };
138
+
139
+ const taskStatus = getTaskStatus();
140
+
141
+ // Extract model name from modelId (e.g., "Qwen/Qwen3-VL-8B-Instruct" -> "Qwen3-VL-8B-Instruct")
142
+ const modelName = trace?.modelId?.split('/').pop() || 'Unknown Model';
143
+
144
+ return (
145
+ <AppBar
146
+ position="static"
147
+ elevation={0}
148
+ sx={{
149
+ backgroundColor: 'background.paper',
150
+ borderBottom: '1px solid',
151
+ borderColor: 'divider',
152
+ }}
153
+ >
154
+ <Toolbar disableGutters sx={{ px: 2, py: 2.5, flexDirection: 'column', alignItems: 'stretch', gap: 0 }}>
155
+ {/* First row: Back button + Task info + Connection Status */}
156
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', gap: 3 }}>
157
+ {/* Left side: Back button + Task info */}
158
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1, minWidth: 0 }}>
159
+ <IconButton
160
+ onClick={onBackToHome}
161
+ size="small"
162
+ sx={{
163
+ color: 'primary.main',
164
+ backgroundColor: 'primary.50',
165
+ border: '1px solid',
166
+ borderColor: 'primary.200',
167
+ cursor: 'pointer',
168
+ '&:hover': {
169
+ backgroundColor: 'primary.100',
170
+ borderColor: 'primary.main',
171
+ },
172
+ }}
173
+ >
174
+ <ArrowBackIcon fontSize="small" />
175
+ </IconButton>
176
+ <Typography
177
+ variant="body2"
178
+ sx={{
179
+ color: 'text.primary',
180
+ fontWeight: 700,
181
+ fontSize: '1rem',
182
+ overflow: 'hidden',
183
+ textOverflow: 'ellipsis',
184
+ whiteSpace: 'nowrap',
185
+ }}
186
+ >
187
+ {trace?.instruction || 'No task running'}
188
+ </Typography>
189
+ </Box>
190
+
191
+ {/* Right side: Dark Mode */}
192
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
193
+ <IconButton
194
+ onClick={toggleDarkMode}
195
+ size="small"
196
+ sx={{
197
+ color: 'primary.main',
198
+ backgroundColor: 'primary.50',
199
+ border: '1px solid',
200
+ borderColor: 'primary.200',
201
+ '&:hover': {
202
+ backgroundColor: 'primary.100',
203
+ borderColor: 'primary.main',
204
+ },
205
+ }}
206
+ >
207
+ {isDarkMode ? <LightModeOutlined fontSize="small" /> : <DarkModeOutlined fontSize="small" />}
208
+ </IconButton>
209
+ </Box>
210
+ </Box>
211
+
212
+ {/* Second row: Status + Model + Metadata - Only show when we have trace data */}
213
+ {trace && (
214
+ <Box
215
+ sx={{
216
+ display: 'flex',
217
+ alignItems: 'center',
218
+ gap: 1.5,
219
+ pl: 5.5,
220
+ pr: 1,
221
+ pt: .5,
222
+ mt: .5,
223
+ }}
224
+ >
225
+ {/* Status Badge - Compact */}
226
+ <Box
227
+ sx={{
228
+ display: 'flex',
229
+ alignItems: 'center',
230
+ gap: 0.5,
231
+ px: 1,
232
+ py: 0.25,
233
+ borderRadius: 1,
234
+ backgroundColor:
235
+ taskStatus.color === 'primary' ? 'primary.50' :
236
+ taskStatus.color === 'success' ? 'success.50' :
237
+ taskStatus.color === 'error' ? 'error.50' :
238
+ taskStatus.color === 'warning' ? 'warning.50' :
239
+ 'action.hover',
240
+ border: '1px solid',
241
+ borderColor:
242
+ taskStatus.color === 'primary' ? 'primary.main' :
243
+ taskStatus.color === 'success' ? 'success.main' :
244
+ taskStatus.color === 'error' ? 'error.main' :
245
+ taskStatus.color === 'warning' ? 'warning.main' :
246
+ 'divider',
247
+ }}
248
+ >
249
+ {taskStatus.icon}
250
+ <Typography
251
+ variant="caption"
252
+ sx={{
253
+ fontSize: '0.7rem',
254
+ fontWeight: 700,
255
+ color:
256
+ taskStatus.color === 'primary' ? 'primary.main' :
257
+ taskStatus.color === 'success' ? 'success.main' :
258
+ taskStatus.color === 'error' ? 'error.main' :
259
+ taskStatus.color === 'warning' ? 'warning.main' :
260
+ 'text.primary',
261
+ }}
262
+ >
263
+ {taskStatus.label}
264
+ </Typography>
265
+ </Box>
266
+
267
+ {/* Divider */}
268
+ <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
269
+
270
+ {/* Model */}
271
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
272
+ <SmartToyIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
273
+ <Typography
274
+ variant="caption"
275
+ sx={{
276
+ fontSize: '0.75rem',
277
+ fontWeight: 600,
278
+ color: 'text.primary',
279
+ }}
280
+ >
281
+ {modelName}
282
+ </Typography>
283
+ </Box>
284
+
285
+ {/* Steps Count */}
286
+ {metadata && (
287
+ <>
288
+ <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
289
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
290
+ <Typography
291
+ variant="caption"
292
+ sx={{
293
+ fontSize: '0.75rem',
294
+ fontWeight: 700,
295
+ color: 'text.primary',
296
+ mr: 0.5,
297
+ }}
298
+ >
299
+ {metadata.numberOfSteps}
300
+ </Typography>
301
+ <Typography
302
+ variant="caption"
303
+ sx={{
304
+ fontSize: '0.7rem',
305
+ fontWeight: 400,
306
+ color: 'text.secondary',
307
+ }}
308
+ >
309
+ {metadata.numberOfSteps === 1 ? 'Step' : 'Steps'}
310
+ </Typography>
311
+ </Box>
312
+ </>
313
+ )}
314
+
315
+ {/* Time */}
316
+ {(isAgentProcessing || metadata) && (
317
+ <>
318
+ <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
319
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
320
+ <AccessTimeIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
321
+ <Typography
322
+ variant="caption"
323
+ sx={{
324
+ fontSize: '0.75rem',
325
+ fontWeight: 700,
326
+ color: 'text.primary',
327
+ minWidth: '45px',
328
+ textAlign: 'left',
329
+ }}
330
+ >
331
+ {elapsedTime.toFixed(1)}s
332
+ </Typography>
333
+ </Box>
334
+ </>
335
+ )}
336
+
337
+ {/* Input Tokens */}
338
+ {metadata && metadata.inputTokensUsed > 0 && (
339
+ <>
340
+ <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
341
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
342
+ <InputIcon
343
+ sx={{
344
+ fontSize: '0.85rem',
345
+ color: 'primary.main',
346
+ transition: 'all 0.2s ease',
347
+ animation: inputTokenFlash ? `${iconFlash} 0.8s ease-out` : 'none',
348
+ }}
349
+ />
350
+ <Box
351
+ sx={{
352
+ transition: 'all 0.2s ease',
353
+ animation: inputTokenFlash ? `${tokenFlash} 0.8s ease-out` : 'none',
354
+ }}
355
+ >
356
+ <Typography
357
+ variant="caption"
358
+ sx={{
359
+ fontSize: '0.75rem',
360
+ fontWeight: 700,
361
+ color: 'text.primary',
362
+ }}
363
+ >
364
+ {metadata.inputTokensUsed.toLocaleString()}
365
+ </Typography>
366
+ </Box>
367
+ </Box>
368
+ </>
369
+ )}
370
+
371
+ {/* Output Tokens */}
372
+ {metadata && metadata.outputTokensUsed > 0 && (
373
+ <>
374
+ <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
375
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
376
+ <OutputIcon
377
+ sx={{
378
+ fontSize: '0.85rem',
379
+ color: 'primary.main',
380
+ transition: 'all 0.2s ease',
381
+ animation: outputTokenFlash ? `${iconFlash} 0.8s ease-out` : 'none',
382
+ }}
383
+ />
384
+ <Box
385
+ sx={{
386
+ transition: 'all 0.2s ease',
387
+ animation: outputTokenFlash ? `${tokenFlash} 0.8s ease-out` : 'none',
388
+ }}
389
+ >
390
+ <Typography
391
+ variant="caption"
392
+ sx={{
393
+ fontSize: '0.75rem',
394
+ fontWeight: 700,
395
+ color: 'text.primary',
396
+ }}
397
+ >
398
+ {metadata.outputTokensUsed.toLocaleString()}
399
+ </Typography>
400
+ </Box>
401
+ </Box>
402
+ </>
403
+ )}
404
+ </Box>
405
+ )}
406
+ </Toolbar>
407
+ </AppBar>
408
+ );
409
+ };
cua2-front/src/components/ProcessingIndicator.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Box, CircularProgress, Typography } from '@mui/material';
3
+
4
+ interface ProcessingIndicatorProps {
5
+ isAgentProcessing: boolean;
6
+ }
7
+
8
+ export const ProcessingIndicator: React.FC<ProcessingIndicatorProps> = ({ isAgentProcessing }) => {
9
+ if (!isAgentProcessing) return null;
10
+
11
+ return (
12
+ <Box
13
+ sx={{
14
+ display: 'flex',
15
+ alignItems: 'center',
16
+ gap: 2,
17
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
18
+ px: 2,
19
+ py: 1,
20
+ borderRadius: 2,
21
+ backdropFilter: 'blur(10px)',
22
+ border: '1px solid rgba(0, 0, 0, 0.1)',
23
+ }}
24
+ >
25
+ <CircularProgress size={20} thickness={4} />
26
+ <Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
27
+ Agent is running...
28
+ </Typography>
29
+ </Box>
30
+ );
31
+ };
cua2-front/src/components/WelcomeScreen.tsx ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Box, Typography, Button, Container, Paper, TextField, IconButton, Select, MenuItem, FormControl, InputLabel, CircularProgress } from '@mui/material';
3
+ import ShuffleIcon from '@mui/icons-material/Shuffle';
4
+ import SendIcon from '@mui/icons-material/Send';
5
+ import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
6
+ import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
7
+ import SmartToyIcon from '@mui/icons-material/SmartToy';
8
+ import { useAgentStore, selectSelectedModelId, selectIsDarkMode, selectAvailableModels, selectIsLoadingModels } from '@/stores/agentStore';
9
+ import { fetchAvailableModels, generateRandomQuestion } from '@/services/api';
10
+
11
+ interface WelcomeScreenProps {
12
+ onStartTask: (instruction: string, modelId: string) => void;
13
+ isConnected: boolean;
14
+ }
15
+
16
+ export const WelcomeScreen: React.FC<WelcomeScreenProps> = ({ onStartTask, isConnected }) => {
17
+ const [customTask, setCustomTask] = useState('');
18
+ const [isTyping, setIsTyping] = useState(false);
19
+ const [isGeneratingQuestion, setIsGeneratingQuestion] = useState(false);
20
+ const typingIntervalRef = useRef<NodeJS.Timeout | null>(null);
21
+
22
+ const isDarkMode = useAgentStore(selectIsDarkMode);
23
+ const toggleDarkMode = useAgentStore((state) => state.toggleDarkMode);
24
+ const selectedModelId = useAgentStore(selectSelectedModelId);
25
+ const setSelectedModelId = useAgentStore((state) => state.setSelectedModelId);
26
+ const availableModels = useAgentStore(selectAvailableModels);
27
+ const isLoadingModels = useAgentStore(selectIsLoadingModels);
28
+ const setAvailableModels = useAgentStore((state) => state.setAvailableModels);
29
+ const setIsLoadingModels = useAgentStore((state) => state.setIsLoadingModels);
30
+
31
+ // Load available models on mount
32
+ useEffect(() => {
33
+ const loadModels = async () => {
34
+ setIsLoadingModels(true);
35
+ try {
36
+ const models = await fetchAvailableModels();
37
+ setAvailableModels(models);
38
+
39
+ // Set first model as default if current selection is not in the list
40
+ if (models.length > 0 && !models.includes(selectedModelId)) {
41
+ setSelectedModelId(models[0]);
42
+ }
43
+ } catch (error) {
44
+ console.error('Failed to load models:', error);
45
+ // Fallback to empty array on error
46
+ setAvailableModels([]);
47
+ } finally {
48
+ setIsLoadingModels(false);
49
+ }
50
+ };
51
+
52
+ loadModels();
53
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
54
+
55
+ // Clean up typing interval on unmount
56
+ useEffect(() => {
57
+ return () => {
58
+ if (typingIntervalRef.current) {
59
+ clearInterval(typingIntervalRef.current);
60
+ }
61
+ };
62
+ }, []);
63
+
64
+ const handleWriteRandomTask = async () => {
65
+ // Clear any existing typing interval
66
+ if (typingIntervalRef.current) {
67
+ clearInterval(typingIntervalRef.current);
68
+ typingIntervalRef.current = null;
69
+ }
70
+
71
+ setIsGeneratingQuestion(true);
72
+ try {
73
+ const randomTask = await generateRandomQuestion(selectedModelId);
74
+
75
+ // Clear current text
76
+ setCustomTask('');
77
+ setIsTyping(true);
78
+
79
+ // Type effect
80
+ let currentIndex = 0;
81
+ typingIntervalRef.current = setInterval(() => {
82
+ if (currentIndex < randomTask.length) {
83
+ setCustomTask(randomTask.substring(0, currentIndex + 1));
84
+ currentIndex++;
85
+ } else {
86
+ if (typingIntervalRef.current) {
87
+ clearInterval(typingIntervalRef.current);
88
+ typingIntervalRef.current = null;
89
+ }
90
+ setIsTyping(false);
91
+ }
92
+ }, 30); // 30ms per character
93
+ } catch (error) {
94
+ console.error('Failed to generate question:', error);
95
+ setIsTyping(false);
96
+ } finally {
97
+ setIsGeneratingQuestion(false);
98
+ }
99
+ };
100
+
101
+ const handleCustomTask = () => {
102
+ if (customTask.trim() && !isTyping) {
103
+ onStartTask(customTask.trim(), selectedModelId);
104
+ }
105
+ };
106
+
107
+ return (
108
+ <>
109
+ {/* Dark Mode Toggle - Top Right (Absolute to viewport) */}
110
+ <Box sx={{ position: 'absolute', top: 24, right: 24, zIndex: 1000 }}>
111
+ <IconButton
112
+ onClick={toggleDarkMode}
113
+ size="medium"
114
+ sx={{
115
+ color: 'text.primary',
116
+ backgroundColor: 'background.paper',
117
+ border: '1px solid',
118
+ borderColor: 'divider',
119
+ '&:hover': {
120
+ backgroundColor: 'action.hover',
121
+ borderColor: 'primary.main',
122
+ },
123
+ }}
124
+ >
125
+ {isDarkMode ? <LightModeOutlined /> : <DarkModeOutlined />}
126
+ </IconButton>
127
+ </Box>
128
+
129
+ <Container
130
+ maxWidth="md"
131
+ sx={{
132
+ display: 'flex',
133
+ flexDirection: 'column',
134
+ alignItems: 'center',
135
+ justifyContent: 'center',
136
+ minHeight: '100vh',
137
+ textAlign: 'center',
138
+ py: 8,
139
+ }}
140
+ >
141
+ {/* Title */}
142
+ <Typography
143
+ variant="h2"
144
+ sx={{
145
+ fontWeight: 800,
146
+ mb: 1,
147
+ color: 'text.primary',
148
+ }}
149
+ >
150
+ CUA2 Agent
151
+ </Typography>
152
+
153
+ {/* Powered by smolagents */}
154
+ <Box
155
+ sx={{
156
+ display: 'flex',
157
+ alignItems: 'center',
158
+ gap: 1,
159
+ mb: 2,
160
+ }}
161
+ >
162
+ <Typography
163
+ variant="body2"
164
+ sx={{
165
+ color: 'text.secondary',
166
+ fontWeight: 500,
167
+ }}
168
+ >
169
+ Powered by
170
+ </Typography>
171
+ <Box
172
+ component="a"
173
+ href="https://github.com/huggingface/smolagents"
174
+ target="_blank"
175
+ rel="noopener noreferrer"
176
+ sx={{
177
+ display: 'flex',
178
+ alignItems: 'center',
179
+ gap: 0.75,
180
+ textDecoration: 'none',
181
+ transition: 'all 0.2s ease',
182
+ '&:hover': {
183
+ '& .smolagents-text': {
184
+ textDecoration: 'underline',
185
+ },
186
+ },
187
+ }}
188
+ >
189
+ {/* Hugging Face Official Logo */}
190
+ <Box
191
+ component="img"
192
+ src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
193
+ alt="Hugging Face"
194
+ sx={{
195
+ width: 24,
196
+ height: 24,
197
+ flexShrink: 0,
198
+ }}
199
+ />
200
+
201
+ <Typography
202
+ className="smolagents-text"
203
+ sx={{
204
+ color: 'primary.main',
205
+ fontWeight: 700,
206
+ fontSize: '1rem',
207
+ }}
208
+ >
209
+ smolagents
210
+ </Typography>
211
+
212
+ {/* GitHub stars badge */}
213
+ <Box
214
+ sx={{
215
+ display: 'flex',
216
+ alignItems: 'center',
217
+ gap: 0.5,
218
+ px: 1,
219
+ py: 0.25,
220
+ backgroundColor: (theme) =>
221
+ theme.palette.mode === 'dark'
222
+ ? 'rgba(144, 202, 249, 0.08)'
223
+ : 'rgba(25, 118, 210, 0.08)',
224
+ borderRadius: 1,
225
+ border: '1px solid',
226
+ borderColor: 'primary.main',
227
+ }}
228
+ >
229
+ <Box component="span" sx={{ fontSize: '0.75rem' }}>⭐</Box>
230
+ <Typography
231
+ variant="caption"
232
+ sx={{
233
+ fontWeight: 700,
234
+ color: 'primary.main',
235
+ fontSize: '0.75rem',
236
+ }}
237
+ >
238
+ 23.7k
239
+ </Typography>
240
+ </Box>
241
+ </Box>
242
+ </Box>
243
+
244
+ {/* Subtitle */}
245
+ <Typography
246
+ variant="h6"
247
+ sx={{
248
+ color: 'text.secondary',
249
+ fontWeight: 500,
250
+ mb: 1,
251
+ }}
252
+ >
253
+ AI-Powered Computer Use Automation
254
+ </Typography>
255
+
256
+ {/* Description */}
257
+ <Typography
258
+ variant="body1"
259
+ sx={{
260
+ color: 'text.secondary',
261
+ maxWidth: '650px',
262
+ mb: 6,
263
+ lineHeight: 1.7,
264
+ }}
265
+ >
266
+ Watch in real-time as AI agents write and execute Python code to complete tasks.
267
+ Built by Hugging Face, <strong>smolagents</strong> is LLM-agnostic and uses <strong>30% fewer steps</strong> than traditional agents.
268
+ </Typography>
269
+
270
+ {/* Task Input Section */}
271
+ <Paper
272
+ elevation={0}
273
+ sx={{
274
+ maxWidth: '700px',
275
+ width: '100%',
276
+ p: 2.5,
277
+ border: '2px solid',
278
+ borderColor: isConnected ? 'primary.main' : 'divider',
279
+ borderRadius: 2,
280
+ backgroundColor: 'background.paper',
281
+ transition: 'all 0.2s ease',
282
+ '&:hover': isConnected ? {
283
+ borderColor: 'primary.dark',
284
+ boxShadow: (theme) => `0 4px 16px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.3)' : 'rgba(79, 134, 198, 0.15)'}`,
285
+ } : {},
286
+ }}
287
+ >
288
+ {/* Input Field */}
289
+ <TextField
290
+ fullWidth
291
+ placeholder="Describe your task here..."
292
+ value={customTask}
293
+ onChange={(e) => setCustomTask(e.target.value)}
294
+ onKeyPress={(e) => {
295
+ if (e.key === 'Enter' && !e.shiftKey && isConnected && customTask.trim() && !isTyping) {
296
+ handleCustomTask();
297
+ }
298
+ }}
299
+ disabled={!isConnected || isTyping}
300
+ multiline
301
+ rows={3}
302
+ sx={{
303
+ mb: 2,
304
+ '& .MuiOutlinedInput-root': {
305
+ borderRadius: 1.5,
306
+ backgroundColor: 'action.hover',
307
+ color: 'text.primary',
308
+ '& fieldset': {
309
+ borderColor: 'divider',
310
+ },
311
+ '&:hover fieldset': {
312
+ borderColor: 'text.secondary',
313
+ },
314
+ '&.Mui-focused fieldset': {
315
+ borderColor: 'primary.main',
316
+ borderWidth: '2px',
317
+ },
318
+ },
319
+ '& .MuiInputBase-input': {
320
+ color: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
321
+ fontWeight: 500,
322
+ WebkitTextFillColor: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
323
+ },
324
+ '& .MuiInputBase-input.Mui-disabled': {
325
+ color: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
326
+ WebkitTextFillColor: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
327
+ },
328
+ '& .MuiInputBase-input::placeholder': {
329
+ color: 'text.secondary',
330
+ opacity: 0.7,
331
+ },
332
+ }}
333
+ />
334
+
335
+ {/* Model Selection + Buttons Row */}
336
+ <Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', justifyContent: 'space-between' }}>
337
+ {/* Model Select */}
338
+ <FormControl size="small" sx={{ minWidth: 240 }}>
339
+ <InputLabel id="model-select-label">Model</InputLabel>
340
+ <Select
341
+ labelId="model-select-label"
342
+ value={availableModels.length > 0 && availableModels.includes(selectedModelId) ? selectedModelId : ''}
343
+ label="Model"
344
+ onChange={(e) => setSelectedModelId(e.target.value)}
345
+ disabled={!isConnected || isTyping || isLoadingModels}
346
+ sx={{
347
+ borderRadius: 1.5,
348
+ '& .MuiOutlinedInput-notchedOutline': {
349
+ borderWidth: 2,
350
+ },
351
+ }}
352
+ >
353
+ {isLoadingModels ? (
354
+ <MenuItem disabled>
355
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
356
+ <CircularProgress size={16} />
357
+ <Typography variant="body2">Loading models...</Typography>
358
+ </Box>
359
+ </MenuItem>
360
+ ) : availableModels.length === 0 ? (
361
+ <MenuItem disabled>
362
+ <Typography variant="body2" sx={{ color: 'error.main' }}>
363
+ No models available
364
+ </Typography>
365
+ </MenuItem>
366
+ ) : (
367
+ availableModels.map((modelId) => (
368
+ <MenuItem key={modelId} value={modelId}>
369
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
370
+ <SmartToyIcon sx={{ fontSize: '0.9rem', color: 'primary.main' }} />
371
+ <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.875rem' }}>
372
+ {modelId.split('/').pop()}
373
+ </Typography>
374
+ </Box>
375
+ </MenuItem>
376
+ ))
377
+ )}
378
+ </Select>
379
+ </FormControl>
380
+
381
+ {/* Buttons on the right */}
382
+ <Box sx={{ display: 'flex', gap: 1.5 }}>
383
+ <Button
384
+ variant="outlined"
385
+ onClick={handleWriteRandomTask}
386
+ disabled={!isConnected || isTyping || isGeneratingQuestion}
387
+ startIcon={isGeneratingQuestion ? <CircularProgress size={16} /> : <ShuffleIcon />}
388
+ sx={{
389
+ borderRadius: 1.5,
390
+ textTransform: 'none',
391
+ fontWeight: 600,
392
+ borderWidth: 2,
393
+ px: 3,
394
+ '&:hover': {
395
+ borderWidth: 2,
396
+ },
397
+ }}
398
+ >
399
+ {isGeneratingQuestion ? 'Generating...' : isTyping ? 'Writing...' : 'Write random task'}
400
+ </Button>
401
+
402
+ <Button
403
+ variant="contained"
404
+ onClick={handleCustomTask}
405
+ disabled={!isConnected || !customTask.trim() || isTyping}
406
+ sx={{
407
+ borderRadius: 1.5,
408
+ textTransform: 'none',
409
+ fontWeight: 600,
410
+ px: 4,
411
+ background: 'linear-gradient(135deg, #4F86C6 0%, #2B5C94 100%)',
412
+ }}
413
+ endIcon={<SendIcon />}
414
+ >
415
+ Run Task
416
+ </Button>
417
+ </Box>
418
+ </Box>
419
+ </Paper>
420
+
421
+ {/* Connection status hint */}
422
+ {!isConnected && (
423
+ <Typography
424
+ variant="caption"
425
+ sx={{
426
+ mt: 2,
427
+ color: 'text.secondary',
428
+ display: 'flex',
429
+ alignItems: 'center',
430
+ gap: 1,
431
+ }}
432
+ >
433
+ <Box
434
+ sx={{
435
+ width: 8,
436
+ height: 8,
437
+ borderRadius: '50%',
438
+ backgroundColor: 'warning.main',
439
+ animation: 'pulse 2s ease-in-out infinite',
440
+ '@keyframes pulse': {
441
+ '0%, 100%': { opacity: 1 },
442
+ '50%': { opacity: 0.5 },
443
+ },
444
+ }}
445
+ />
446
+ Make sure the backend is running on port 8000
447
+ </Typography>
448
+ )}
449
+ </Container>
450
+ </>
451
+ );
452
+ };
cua2-front/src/components/index.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // General components
2
+ export { Header } from './Header';
3
+ export { ConnectionStatus } from './ConnectionStatus';
4
+ export { ProcessingIndicator } from './ProcessingIndicator';
5
+ export { WelcomeScreen } from './WelcomeScreen';
6
+
7
+ // Sandbox components
8
+ export { SandboxViewer, CompletionView, DownloadGifButton, DownloadJsonButton } from './sandbox';
9
+
10
+ // Timeline components
11
+ export { Timeline } from './timeline';
12
+
13
+ // Steps components
14
+ export { StepsList, StepCard, FinalStepCard, ThinkingStepCard, ConnectionStepCard } from './steps';
cua2-front/src/components/mock/ConnectionStatus.tsx DELETED
@@ -1,37 +0,0 @@
1
- import React from 'react';
2
-
3
- interface ConnectionStatusProps {
4
- isConnected: boolean;
5
- }
6
-
7
- export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ isConnected }) => {
8
- return (
9
- <div style={{
10
- display: 'flex',
11
- alignItems: 'center',
12
- gap: '8px',
13
- backgroundColor: 'rgba(255, 255, 255, 0.2)',
14
- padding: '8px 16px',
15
- borderRadius: '20px',
16
- backdropFilter: 'blur(10px)',
17
- border: '1px solid rgba(255, 255, 255, 0.3)'
18
- }}>
19
- <div style={{
20
- width: '8px',
21
- height: '8px',
22
- borderRadius: '50%',
23
- backgroundColor: isConnected ? '#10b981' : '#ef4444',
24
- boxShadow: isConnected ? '0 0 8px #10b981' : '0 0 8px #ef4444',
25
- animation: isConnected ? 'pulse 2s infinite' : 'none'
26
- }}></div>
27
- <div style={{ display: 'flex', flexDirection: 'column' }}>
28
- <span className="text-xs font-semibold text-white" style={{ lineHeight: '1.2' }}>
29
- {isConnected ? 'Connected' : 'Disconnected'}
30
- </span>
31
- <span className="text-xs text-white" style={{ opacity: 0.7, fontSize: '10px', lineHeight: '1.2' }}>
32
- WebSocket
33
- </span>
34
- </div>
35
- </div>
36
- );
37
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cua2-front/src/components/mock/Header.tsx DELETED
@@ -1,47 +0,0 @@
1
- import React from 'react';
2
- import { ConnectionStatus } from './ConnectionStatus';
3
- import { ProcessingIndicator } from './ProcessingIndicator';
4
- import { TaskButton } from './TaskButton';
5
-
6
- interface HeaderProps {
7
- isConnected: boolean;
8
- isAgentProcessing: boolean;
9
- onSendTask: (content: string, modelId: string) => void;
10
- }
11
-
12
- export const Header: React.FC<HeaderProps> = ({ isConnected, isAgentProcessing, onSendTask }) => {
13
- return (
14
- <>
15
- <div style={{
16
- flexShrink: 0,
17
- }}>
18
- <div style={{ maxWidth: '1400px', margin: '0 auto', padding: '20px 32px' }}>
19
- <div className="flex items-center justify-between">
20
- <div className="flex items-center gap-6">
21
- <ConnectionStatus isConnected={isConnected} />
22
- <h1 className="text-3xl font-bold text-white" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
23
- CUA2 Agent
24
- </h1>
25
- </div>
26
- <ProcessingIndicator isAgentProcessing={isAgentProcessing} />
27
- </div>
28
- <TaskButton
29
- isAgentProcessing={isAgentProcessing}
30
- isConnected={isConnected}
31
- onSendTask={onSendTask}
32
- />
33
- </div>
34
- </div>
35
-
36
- <style>{`
37
- @keyframes spin {
38
- to { transform: rotate(360deg); }
39
- }
40
- @keyframes pulse {
41
- 0%, 100% { opacity: 1; }
42
- 50% { opacity: 0.5; }
43
- }
44
- `}</style>
45
- </>
46
- );
47
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cua2-front/src/components/mock/Metadata.tsx DELETED
@@ -1,38 +0,0 @@
1
- import { AgentTrace } from '@/types/agent';
2
- import React from 'react';
3
-
4
- interface MetadataProps {
5
- trace?: AgentTrace;
6
- }
7
-
8
- export const Metadata: React.FC<MetadataProps> = ({ trace }) => {
9
- return (
10
- <div style={{ flexShrink: 0 }} className="bg-white rounded-lg shadow-md border border-gray-200 p-5">
11
- <h3 className="text-lg font-semibold text-gray-800 mb-4">Metadata</h3>
12
- {trace?.metadata ? (
13
- <div style={{ display: 'flex', gap: '12px' }}>
14
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '8px', padding: '12px', border: '1px solid #bfdbfe', boxShadow: '0 2px 4px rgba(59, 130, 246, 0.1)' }}>
15
- <span style={{ fontSize: '10px', fontWeight: 600, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Time</span>
16
- <span style={{ fontSize: '24px', fontWeight: 700, color: '#1e40af' }}>{trace.metadata.duration.toFixed(2)}s</span>
17
- </div>
18
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '8px', padding: '12px', border: '1px solid #bbf7d0', boxShadow: '0 2px 4px rgba(16, 185, 129, 0.1)' }}>
19
- <span style={{ fontSize: '10px', fontWeight: 600, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>In Tokens</span>
20
- <span style={{ fontSize: '24px', fontWeight: 700, color: '#166534' }}>{trace.metadata.inputTokensUsed.toLocaleString()}</span>
21
- </div>
22
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '8px', padding: '12px', border: '1px solid #e9d5ff', boxShadow: '0 2px 4px rgba(139, 92, 246, 0.1)' }}>
23
- <span style={{ fontSize: '10px', fontWeight: 600, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Out Tokens</span>
24
- <span style={{ fontSize: '24px', fontWeight: 700, color: '#6b21a8' }}>{trace.metadata.outputTokensUsed.toLocaleString()}</span>
25
- </div>
26
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#fff7ed', borderRadius: '8px', padding: '12px', border: '1px solid #fed7aa', boxShadow: '0 2px 4px rgba(249, 115, 22, 0.1)' }}>
27
- <span style={{ fontSize: '10px', fontWeight: 600, color: '#ea580c', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Steps</span>
28
- <span style={{ fontSize: '24px', fontWeight: 700, color: '#c2410c' }}>{trace.metadata.numberOfSteps}</span>
29
- </div>
30
- </div>
31
- ) : (
32
- <div className="text-gray-400 text-sm py-2">
33
- {trace ? 'Waiting for completion...' : 'No task started yet'}
34
- </div>
35
- )}
36
- </div>
37
- );
38
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cua2-front/src/components/mock/ProcessingIndicator.tsx DELETED
@@ -1,34 +0,0 @@
1
- import React from 'react';
2
-
3
- interface ProcessingIndicatorProps {
4
- isAgentProcessing: boolean;
5
- }
6
-
7
- export const ProcessingIndicator: React.FC<ProcessingIndicatorProps> = ({ isAgentProcessing }) => {
8
- if (!isAgentProcessing) return null;
9
-
10
- return (
11
- <div style={{
12
- display: 'flex',
13
- alignItems: 'center',
14
- gap: '10px',
15
- padding: '10px 20px',
16
- backgroundColor: 'rgba(251, 191, 36, 0.2)',
17
- borderRadius: '10px',
18
- border: '1px solid rgba(251, 191, 36, 0.4)'
19
- }}>
20
- <span style={{
21
- width: '16px',
22
- height: '16px',
23
- border: '2px solid #fbbf24',
24
- borderTopColor: 'transparent',
25
- borderRadius: '50%',
26
- animation: 'spin 1s linear infinite',
27
- display: 'inline-block'
28
- }}></span>
29
- <span style={{ fontSize: '14px', fontWeight: 600, color: '#fbbf24', letterSpacing: '0.5px' }}>
30
- PROCESSING...
31
- </span>
32
- </div>
33
- );
34
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cua2-front/src/components/mock/StackSteps.tsx DELETED
@@ -1,29 +0,0 @@
1
- import React from 'react';
2
- import { AgentTrace } from '@/types/agent';
3
- import { StepCard } from './StepCard';
4
-
5
- interface StackStepsProps {
6
- trace?: AgentTrace;
7
- }
8
-
9
- export const StackSteps: React.FC<StackStepsProps> = ({ trace }) => {
10
- return (
11
- <div style={{ width: '360px', flexShrink: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', marginLeft: '12px', marginTop: '20px', marginBottom: '20px', boxShadow: '0 2px 12px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb' }}>
12
- <h3 className="text-lg font-semibold text-gray-800 mb-4">Stack Steps</h3>
13
- <div style={{ flex: 1, overflowY: 'auto', minHeight: 0, padding: '16px' }}>
14
- {trace?.steps && trace.steps.length > 0 ? (
15
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
16
- {trace.steps.map((step, index) => (
17
- <StepCard key={step.stepId} step={step} index={index} />
18
- ))}
19
- </div>
20
- ) : (
21
- <div className="flex flex-col items-center justify-center h-full text-gray-400 p-6 text-center">
22
- <p className="font-medium">No steps yet</p>
23
- <p className="text-xs mt-1">Steps will appear as agent progresses</p>
24
- </div>
25
- )}
26
- </div>
27
- </div>
28
- );
29
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cua2-front/src/components/mock/StepCard.tsx DELETED
@@ -1,84 +0,0 @@
1
- import { AgentStep } from '@/types/agent';
2
- import React from 'react';
3
-
4
- interface StepCardProps {
5
- step: AgentStep;
6
- index: number;
7
- }
8
-
9
- export const StepCard: React.FC<StepCardProps> = ({ step, index }) => {
10
- return (
11
- <div
12
- key={step.stepId}
13
- style={{ backgroundColor: '#f9fafb', borderRadius: '8px', border: '1px solid #d1d5db', padding: '12px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)' }}
14
- className="hover:border-blue-400 transition-all"
15
- >
16
- {/* Step Header */}
17
- <div className="mb-6">
18
- <span className="text-xs font-bold text-blue-600 uppercase tracking-wide">Step {index + 1}</span>
19
- <hr style={{ margin: '12px 0', border: 'none', borderTop: '2px solid #d1d5db' }} />
20
- </div>
21
-
22
- {/* Step Image */}
23
- {step.image && (
24
- <div className="mb-6">
25
- <div className="rounded-md overflow-hidden border border-gray-300 bg-white" style={{ maxHeight: '140px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
26
- <img
27
- src={step.image}
28
- alt={`Step ${index + 1}`}
29
- style={{ width: '100%', height: 'auto', maxHeight: '140px', objectFit: 'contain' }}
30
- />
31
- </div>
32
- <hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
33
- </div>
34
- )}
35
-
36
- {/* Thought */}
37
- <div className="mb-6">
38
- <div className="bg-white rounded-md p-2.5 border border-gray-200">
39
- <h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
40
- <span>💭</span>
41
- <span>Thought</span>
42
- </h4>
43
- <p className="text-xs text-gray-600 leading-relaxed">{step.thought}</p>
44
- </div>
45
- <hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
46
- </div>
47
-
48
- {/* Actions */}
49
- <div className="mb-6">
50
- <div className="bg-white rounded-md p-2.5 border border-gray-200">
51
- <h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
52
- <span>⚡</span>
53
- <span>Actions</span>
54
- </h4>
55
- <ul className="space-y-1" style={{ listStyle: 'none', padding: 0, margin: 0 }}>
56
- {step.actions.map((action, actionIndex) => (
57
- <li key={actionIndex} className="text-xs text-gray-600 flex items-start leading-snug">
58
- <span className="mr-1.5 text-blue-500 flex-shrink-0">→</span>
59
- <span className="break-words">{action}</span>
60
- </li>
61
- ))}
62
- </ul>
63
- </div>
64
- <hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
65
- </div>
66
-
67
- {/* Step Metadata Footer */}
68
- <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
69
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bfdbfe' }}>
70
- <span style={{ fontSize: '9px', fontWeight: 500, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Time</span>
71
- <span style={{ fontSize: '12px', fontWeight: 700, color: '#1e40af' }}>{step.duration.toFixed(2)}s</span>
72
- </div>
73
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bbf7d0' }}>
74
- <span style={{ fontSize: '9px', fontWeight: 500, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.5px' }}>In Tokens</span>
75
- <span style={{ fontSize: '12px', fontWeight: 700, color: '#166534' }}>{step.inputTokensUsed}</span>
76
- </div>
77
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #e9d5ff' }}>
78
- <span style={{ fontSize: '9px', fontWeight: 500, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Out Tokens</span>
79
- <span style={{ fontSize: '12px', fontWeight: 700, color: '#6b21a8' }}>{step.outputTokensUsed}</span>
80
- </div>
81
- </div>
82
- </div>
83
- );
84
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cua2-front/src/components/mock/TaskButton.tsx DELETED
@@ -1,76 +0,0 @@
1
- import React from 'react';
2
-
3
- interface TaskButtonProps {
4
- isAgentProcessing: boolean;
5
- isConnected: boolean;
6
- onSendTask: (content: string, modelId: string) => void;
7
- }
8
-
9
- export const TaskButton: React.FC<TaskButtonProps> = ({ isAgentProcessing, isConnected, onSendTask }) => {
10
- return (
11
- <div
12
- onClick={() => {
13
- if (!isAgentProcessing && isConnected) {
14
- onSendTask(
15
- "Find the price of a NVIDIA RTX 4090 GPU",
16
- "Qwen/Qwen3-VL-30B-A3B-Instruct"
17
- );
18
- }
19
- }}
20
- style={{
21
- marginTop: '16px',
22
- padding: '14px 18px',
23
- background: isAgentProcessing || !isConnected
24
- ? 'rgba(255, 255, 255, 0.1)'
25
- : 'rgba(255, 255, 255, 0.15)',
26
- borderRadius: '10px',
27
- backdropFilter: 'blur(10px)',
28
- border: '2px solid rgba(0, 0, 0, 0.3)',
29
- cursor: isAgentProcessing || !isConnected ? 'not-allowed' : 'pointer',
30
- transition: 'all 0.3s ease',
31
- opacity: isAgentProcessing || !isConnected ? 0.6 : 1,
32
- }}
33
- onMouseEnter={(e) => {
34
- if (!isAgentProcessing && isConnected) {
35
- e.currentTarget.style.background = 'rgba(200, 200, 200, 0.3)';
36
- e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.5)';
37
- e.currentTarget.style.transform = 'translateY(-2px)';
38
- e.currentTarget.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.2)';
39
- }
40
- }}
41
- onMouseLeave={(e) => {
42
- e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
43
- e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.3)';
44
- e.currentTarget.style.transform = 'translateY(0)';
45
- e.currentTarget.style.boxShadow = 'none';
46
- }}
47
- >
48
- <div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>
49
- <div style={{ flex: 1 }}>
50
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
51
- <span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.7)', textTransform: 'uppercase', letterSpacing: '1px' }}>Task</span>
52
- {!isAgentProcessing && isConnected && (
53
- <span style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.5)', fontStyle: 'italic' }}>
54
- (click to run)
55
- </span>
56
- )}
57
- </div>
58
- <p style={{ fontSize: '15px', fontWeight: 500, color: '#1f2937' }}>
59
- Find the price of a NVIDIA RTX 4090 GPU
60
- </p>
61
- </div>
62
- <div style={{
63
- padding: '8px 16px',
64
- backgroundColor: 'rgba(0, 0, 0, 0.1)',
65
- borderRadius: '6px',
66
- border: '1px solid rgba(0, 0, 0, 0.2)'
67
- }}>
68
- <span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.6)', textTransform: 'uppercase', letterSpacing: '1px' }}>Model</span>
69
- <p style={{ fontSize: '12px', fontWeight: 600, color: '#1f2937', marginTop: '2px', whiteSpace: 'nowrap' }}>
70
- Qwen/Qwen3-VL-30B-A3B-Instruct
71
- </p>
72
- </div>
73
- </div>
74
- </div>
75
- );
76
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cua2-front/src/components/mock/VNCStream.tsx DELETED
@@ -1,30 +0,0 @@
1
- import React from 'react';
2
-
3
- interface VNCStreamProps {
4
- vncUrl: string;
5
- }
6
-
7
- export const VNCStream: React.FC<VNCStreamProps> = ({ vncUrl }) => {
8
- return (
9
- <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb', overflow: 'hidden', padding: '20px' }}>
10
- <h3 className="text-lg font-semibold text-gray-800 mb-4">VNC Stream</h3>
11
- <div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
12
- {vncUrl ? (
13
- <iframe
14
- src={vncUrl}
15
- style={{ width: '100%', height: '100%', border: 'none' }}
16
- title="VNC Stream"
17
- />
18
- ) : (
19
- <div className="text-gray-400 text-center p-8">
20
- <svg className="w-16 h-16 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
21
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
22
- </svg>
23
- <p className="font-medium">No VNC stream available</p>
24
- <p className="text-sm mt-1 text-gray-500">Stream will appear when agent starts</p>
25
- </div>
26
- )}
27
- </div>
28
- </div>
29
- );
30
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cua2-front/src/components/mock/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export { ConnectionStatus } from './ConnectionStatus';
2
- export { ProcessingIndicator } from './ProcessingIndicator';
3
- export { TaskButton } from './TaskButton';
4
- export { Header } from './Header';
5
- export { VNCStream } from './VNCStream';
6
- export { Metadata } from './Metadata';
7
- export { StepCard } from './StepCard';
8
- export { StackSteps } from './StackSteps';
 
 
 
 
 
 
 
 
 
cua2-front/src/components/sandbox/SandboxViewer.tsx ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { Box, Typography, CircularProgress, Button, keyframes } from '@mui/material';
4
+ import MonitorIcon from '@mui/icons-material/Monitor';
5
+ import ImageIcon from '@mui/icons-material/Image';
6
+ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
7
+ import { AgentTraceMetadata, AgentStep } from '@/types/agent';
8
+ import { useAgentStore, selectError, selectFinalStep, selectSteps, selectTrace } from '@/stores/agentStore';
9
+ import { CompletionView } from './CompletionView';
10
+ import { useGifGenerator } from '@/hooks/useGifGenerator';
11
+ import { useJsonExporter } from '@/hooks/useJsonExporter';
12
+
13
+ // Animation for live indicator
14
+ const livePulse = keyframes`
15
+ 0%, 100% {
16
+ opacity: 1;
17
+ transform: scale(1);
18
+ }
19
+ 50% {
20
+ opacity: 0.7;
21
+ transform: scale(1.2);
22
+ }
23
+ `;
24
+
25
+ interface SandboxViewerProps {
26
+ vncUrl: string;
27
+ isAgentProcessing?: boolean;
28
+ metadata?: AgentTraceMetadata;
29
+ traceStartTime?: Date;
30
+ selectedStep?: AgentStep | null; // The step to display in time-travel mode
31
+ isRunning?: boolean; // Is the agent currently running
32
+ }
33
+
34
+ export const SandboxViewer: React.FC<SandboxViewerProps> = ({
35
+ vncUrl,
36
+ isAgentProcessing = false,
37
+ metadata,
38
+ traceStartTime,
39
+ selectedStep,
40
+ isRunning = false
41
+ }) => {
42
+ const navigate = useNavigate();
43
+ const error = useAgentStore(selectError);
44
+ const finalStep = useAgentStore(selectFinalStep);
45
+ const steps = useAgentStore(selectSteps);
46
+ const trace = useAgentStore(selectTrace);
47
+ const resetAgent = useAgentStore((state) => state.resetAgent);
48
+ const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex);
49
+
50
+ // Hook to generate GIF
51
+ const { isGenerating, error: gifError, generateAndDownloadGif } = useGifGenerator({
52
+ steps: steps || [],
53
+ traceId: finalStep?.metadata.traceId || '',
54
+ });
55
+
56
+ // Hook to export JSON
57
+ const { downloadTraceAsJson } = useJsonExporter({
58
+ trace,
59
+ steps: steps || [],
60
+ metadata: finalStep?.metadata || metadata,
61
+ });
62
+
63
+ // Extract final_answer from the last step, or fallback to last thought
64
+ const getFinalAnswer = (): string | null => {
65
+ console.log('🔍 getFinalAnswer - steps:', steps);
66
+ if (!steps || steps.length === 0) {
67
+ console.log('❌ No steps available');
68
+ return null;
69
+ }
70
+
71
+ // Try to find final_answer in any step (iterate backwards)
72
+ for (let i = steps.length - 1; i >= 0; i--) {
73
+ const step = steps[i];
74
+
75
+ if (step.actions && Array.isArray(step.actions)) {
76
+ const finalAnswerAction = step.actions.find(
77
+ (action) => action.function_name === 'final_answer'
78
+ );
79
+
80
+ if (finalAnswerAction) {
81
+ // Handle both named parameter and positional argument
82
+ const result = finalAnswerAction?.parameters?.answer || finalAnswerAction?.parameters?.arg_0 || null;
83
+ console.log('✅ Final answer found in step', i + 1, ':', result);
84
+ return result;
85
+ }
86
+ }
87
+ }
88
+
89
+ console.log('🔍 No final_answer found, looking for last thought...');
90
+
91
+ // Fallback: find the last step with a thought (iterate backwards)
92
+ for (let i = steps.length - 1; i >= 0; i--) {
93
+ const step = steps[i];
94
+ if (step.thought) {
95
+ console.log('📝 Using thought from step', i + 1, 'as fallback:', step.thought);
96
+ return step.thought;
97
+ }
98
+ }
99
+
100
+ console.log('❌ No final answer or thought found in any step');
101
+ return null;
102
+ };
103
+
104
+ const finalAnswer = getFinalAnswer();
105
+ console.log('🎯 Final answer to display:', finalAnswer);
106
+
107
+ // Determine if we should show success/fail status
108
+ const showStatus = !isRunning && !selectedStep && finalStep;
109
+
110
+ // Handler to go back to home
111
+ const handleBackToHome = () => {
112
+ resetAgent();
113
+ navigate('/');
114
+ };
115
+
116
+ // Handler to go back to live mode
117
+ const handleGoLive = () => {
118
+ setSelectedStepIndex(null);
119
+ };
120
+
121
+ return (
122
+ <Box
123
+ sx={{
124
+ flex: '1 1 auto',
125
+ display: 'flex',
126
+ flexDirection: 'column',
127
+ position: 'relative',
128
+ border: '1px solid',
129
+ borderColor: showStatus
130
+ ? (finalStep?.type === 'failure' ? 'error.main' : 'success.main')
131
+ : ((vncUrl || isAgentProcessing) && !selectedStep && !showStatus ? 'primary.main' : 'divider'),
132
+ borderRadius: '12px',
133
+ backgroundColor: 'background.paper',
134
+ transition: 'border 0.3s ease',
135
+ overflow: 'hidden',
136
+ }}
137
+ >
138
+ {/* Live Badge or Go Live Button */}
139
+ {vncUrl && !showStatus && (
140
+ <>
141
+ {!selectedStep ? (
142
+ // Live Badge when in live mode
143
+ <Box
144
+ sx={{
145
+ position: 'absolute',
146
+ top: 12,
147
+ right: 12,
148
+ zIndex: 10,
149
+ display: 'flex',
150
+ alignItems: 'center',
151
+ gap: 1,
152
+ px: 2,
153
+ py: 1,
154
+ backgroundColor: (theme) =>
155
+ theme.palette.mode === 'dark'
156
+ ? 'rgba(0, 0, 0, 0.7)'
157
+ : 'rgba(255, 255, 255, 0.9)',
158
+ backdropFilter: 'blur(8px)',
159
+ borderRadius: 0.75,
160
+ border: '1px solid',
161
+ borderColor: 'primary.main',
162
+ boxShadow: (theme) =>
163
+ theme.palette.mode === 'dark'
164
+ ? '0 2px 8px rgba(0, 0, 0, 0.4)'
165
+ : '0 2px 8px rgba(0, 0, 0, 0.1)',
166
+ }}
167
+ >
168
+ <Box
169
+ sx={{
170
+ width: 10,
171
+ height: 10,
172
+ borderRadius: '50%',
173
+ backgroundColor: 'error.main',
174
+ animation: `${livePulse} 2s ease-in-out infinite`,
175
+ }}
176
+ />
177
+ <Typography
178
+ variant="caption"
179
+ sx={{
180
+ fontSize: '0.8rem',
181
+ fontWeight: 700,
182
+ color: 'text.primary',
183
+ textTransform: 'uppercase',
184
+ letterSpacing: '0.5px',
185
+ }}
186
+ >
187
+ Live
188
+ </Typography>
189
+ </Box>
190
+ ) : (
191
+ // Go Live Button when viewing a specific step
192
+ <Button
193
+ onClick={handleGoLive}
194
+ startIcon={<PlayArrowIcon sx={{ fontSize: 20 }} />}
195
+ sx={{
196
+ position: 'absolute',
197
+ top: 12,
198
+ right: 12,
199
+ zIndex: 10,
200
+ px: 2,
201
+ py: 1,
202
+ backgroundColor: (theme) =>
203
+ theme.palette.mode === 'dark'
204
+ ? 'rgba(0, 0, 0, 0.7)'
205
+ : 'rgba(255, 255, 255, 0.9)',
206
+ backdropFilter: 'blur(8px)',
207
+ borderRadius: 0.75,
208
+ border: '1px solid',
209
+ borderColor: 'primary.main',
210
+ boxShadow: (theme) =>
211
+ theme.palette.mode === 'dark'
212
+ ? '0 2px 8px rgba(0, 0, 0, 0.4)'
213
+ : '0 2px 8px rgba(0, 0, 0, 0.1)',
214
+ fontSize: '0.8rem',
215
+ fontWeight: 700,
216
+ textTransform: 'uppercase',
217
+ letterSpacing: '0.5px',
218
+ color: 'primary.main',
219
+ '&:hover': {
220
+ backgroundColor: (theme) =>
221
+ theme.palette.mode === 'dark'
222
+ ? 'rgba(0, 0, 0, 0.85)'
223
+ : 'rgba(255, 255, 255, 1)',
224
+ borderColor: 'primary.dark',
225
+ },
226
+ }}
227
+ >
228
+ Go Live
229
+ </Button>
230
+ )}
231
+ </>
232
+ )}
233
+
234
+ <Box
235
+ sx={{
236
+ flex: 1,
237
+ minHeight: 0,
238
+ display: 'flex',
239
+ alignItems: 'center',
240
+ justifyContent: 'center',
241
+ }}
242
+ >
243
+ {showStatus && finalStep ? (
244
+ // Show success/fail status when agent has completed
245
+ <CompletionView
246
+ finalStep={finalStep}
247
+ trace={trace}
248
+ steps={steps}
249
+ finalAnswer={finalAnswer}
250
+ isGenerating={isGenerating}
251
+ gifError={gifError}
252
+ onGenerateGif={generateAndDownloadGif}
253
+ onDownloadJson={downloadTraceAsJson}
254
+ onBackToHome={handleBackToHome}
255
+ />
256
+ ) : selectedStep ? (
257
+ // Time-travel mode: Show screenshot of selected step
258
+ <Box
259
+ sx={{
260
+ width: '100%',
261
+ height: '100%',
262
+ display: 'flex',
263
+ alignItems: 'center',
264
+ justifyContent: 'center',
265
+ overflow: 'auto',
266
+ backgroundColor: 'black',
267
+ position: 'relative',
268
+ }}
269
+ >
270
+ {selectedStep.image ? (
271
+ <img
272
+ src={selectedStep.image}
273
+ alt="Step screenshot"
274
+ style={{
275
+ maxWidth: '100%',
276
+ maxHeight: '100%',
277
+ objectFit: 'contain',
278
+ }}
279
+ />
280
+ ) : (
281
+ <Box
282
+ sx={{
283
+ textAlign: 'center',
284
+ p: 4,
285
+ color: 'text.secondary',
286
+ width: '100%',
287
+ height: '100%',
288
+ display: 'flex',
289
+ flexDirection: 'column',
290
+ alignItems: 'center',
291
+ justifyContent: 'center',
292
+ }}
293
+ >
294
+ <ImageIcon sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
295
+ <Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5, fontSize: '0.875rem', color: 'text.primary' }}>
296
+ No screenshot available
297
+ </Typography>
298
+ <Typography variant="caption" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
299
+ This step doesn't have a screenshot
300
+ </Typography>
301
+ </Box>
302
+ )}
303
+ </Box>
304
+ ) : vncUrl ? (
305
+ // Live mode: Show VNC stream
306
+ <iframe
307
+ src={vncUrl}
308
+ style={{ width: '100%', height: '100%', border: 'none' }}
309
+ title="OS Stream"
310
+ />
311
+ ) : isAgentProcessing ? (
312
+ // Loading state
313
+ <Box
314
+ sx={{
315
+ textAlign: 'center',
316
+ p: 4,
317
+ color: 'text.secondary',
318
+ width: '100%',
319
+ height: '100%',
320
+ display: 'flex',
321
+ flexDirection: 'column',
322
+ alignItems: 'center',
323
+ justifyContent: 'center',
324
+ }}
325
+ >
326
+ <CircularProgress
327
+ size={48}
328
+ sx={{
329
+ mb: 2,
330
+ color: 'primary.main'
331
+ }}
332
+ />
333
+ <Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5, fontSize: '0.875rem', color: 'text.primary' }}>
334
+ Connecting to E2B...
335
+ </Typography>
336
+ <Typography variant="caption" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
337
+ Setting up sandbox environment
338
+ </Typography>
339
+ </Box>
340
+ ) : (
341
+ // No stream available
342
+ <Box
343
+ sx={{
344
+ textAlign: 'center',
345
+ p: 4,
346
+ color: 'text.secondary',
347
+ width: '100%',
348
+ height: '100%',
349
+ display: 'flex',
350
+ flexDirection: 'column',
351
+ alignItems: 'center',
352
+ justifyContent: 'center',
353
+ }}
354
+ >
355
+ <MonitorIcon sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
356
+ <Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5, fontSize: '0.875rem' }}>
357
+ No stream available
358
+ </Typography>
359
+ <Typography variant="caption" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
360
+ Stream will appear when agent starts
361
+ </Typography>
362
+ </Box>
363
+ )}
364
+ </Box>
365
+ </Box>
366
+ );
367
+ };
cua2-front/src/components/sandbox/completionview/CompletionView.tsx ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Box, Typography, Button, Divider, Alert, Paper } from '@mui/material';
3
+ import CheckIcon from '@mui/icons-material/Check';
4
+ import CloseIcon from '@mui/icons-material/Close';
5
+ import AddIcon from '@mui/icons-material/Add';
6
+ import SmartToyIcon from '@mui/icons-material/SmartToy';
7
+ import AssignmentIcon from '@mui/icons-material/Assignment';
8
+ import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
9
+ import AccessTimeIcon from '@mui/icons-material/AccessTime';
10
+ import InputIcon from '@mui/icons-material/Input';
11
+ import OutputIcon from '@mui/icons-material/Output';
12
+ import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
13
+ import { FinalStep, AgentTrace, AgentStep } from '@/types/agent';
14
+ import { DownloadGifButton } from './DownloadGifButton';
15
+ import { DownloadJsonButton } from './DownloadJsonButton';
16
+
17
+ interface CompletionViewProps {
18
+ finalStep: FinalStep;
19
+ trace?: AgentTrace;
20
+ steps?: AgentStep[];
21
+ finalAnswer?: string | null;
22
+ isGenerating: boolean;
23
+ gifError: string | null;
24
+ onGenerateGif: () => void;
25
+ onDownloadJson: () => void;
26
+ onBackToHome: () => void;
27
+ }
28
+
29
+ /**
30
+ * Component displaying the completion status (success or failure) of a task
31
+ */
32
+ export const CompletionView: React.FC<CompletionViewProps> = ({
33
+ finalStep,
34
+ trace,
35
+ steps,
36
+ finalAnswer,
37
+ isGenerating,
38
+ gifError,
39
+ onGenerateGif,
40
+ onDownloadJson,
41
+ onBackToHome,
42
+ }) => {
43
+ const isSuccess = finalStep.type === 'success';
44
+ const statusColor = isSuccess ? 'success.main' : 'error.main';
45
+
46
+ // Format model name for display
47
+ const formatModelName = (modelId: string) => {
48
+ const parts = modelId.split('/');
49
+ return parts.length > 1 ? parts[1] : modelId;
50
+ };
51
+
52
+ return (
53
+ <Box
54
+ sx={{
55
+ width: '100%',
56
+ maxWidth: 600,
57
+ mx: 'auto',
58
+ p: 2,
59
+ display: 'flex',
60
+ flexDirection: 'column',
61
+ gap: 1.5,
62
+ }}
63
+ >
64
+ {/* Status Header - Compact */}
65
+ <Box sx={{ textAlign: 'center', mb: 0.5 }}>
66
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1.5, mb: 0.75 }}>
67
+ <Box
68
+ sx={{
69
+ width: 40,
70
+ height: 40,
71
+ borderRadius: '50%',
72
+ backgroundColor: statusColor,
73
+ display: 'flex',
74
+ alignItems: 'center',
75
+ justifyContent: 'center',
76
+ boxShadow: (theme) =>
77
+ isSuccess
78
+ ? `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.3)' : 'rgba(102, 187, 106, 0.2)'}`
79
+ : `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.3)' : 'rgba(244, 67, 54, 0.2)'}`,
80
+ }}
81
+ >
82
+ {isSuccess ? (
83
+ <CheckIcon sx={{ fontSize: 24, color: 'white' }} />
84
+ ) : (
85
+ <CloseIcon sx={{ fontSize: 24, color: 'white' }} />
86
+ )}
87
+ </Box>
88
+ <Typography
89
+ variant="h6"
90
+ sx={{
91
+ fontWeight: 700,
92
+ color: statusColor,
93
+ fontSize: '1.1rem',
94
+ letterSpacing: '-0.5px',
95
+ }}
96
+ >
97
+ {isSuccess ? 'Task Completed' : 'Task Failed'}
98
+ </Typography>
99
+ </Box>
100
+ </Box>
101
+
102
+ {/* Single Report Box - Task + Agent + Response + Metrics */}
103
+ <Paper
104
+ elevation={0}
105
+ sx={{
106
+ p: 2.5,
107
+ backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)',
108
+ borderRadius: 1.5,
109
+ border: '1px solid',
110
+ borderColor: 'divider',
111
+ }}
112
+ >
113
+ {/* Task */}
114
+ {trace?.instruction && (
115
+ <Box sx={{ mb: 2 }}>
116
+ <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5 }}>
117
+ <AssignmentIcon sx={{ fontSize: 18, color: 'text.secondary', mt: 0.25, flexShrink: 0 }} />
118
+ <Box sx={{ flex: 1, minWidth: 0 }}>
119
+ <Typography
120
+ variant="caption"
121
+ sx={{
122
+ fontWeight: 700,
123
+ color: 'text.secondary',
124
+ fontSize: '0.7rem',
125
+ textTransform: 'uppercase',
126
+ letterSpacing: '0.5px',
127
+ display: 'block',
128
+ mb: 0.5,
129
+ }}
130
+ >
131
+ Task
132
+ </Typography>
133
+ <Typography
134
+ variant="body2"
135
+ sx={{
136
+ color: 'text.primary',
137
+ fontWeight: 700,
138
+ lineHeight: 1.5,
139
+ fontSize: '0.85rem',
140
+ }}
141
+ >
142
+ {trace.instruction}
143
+ </Typography>
144
+ </Box>
145
+ </Box>
146
+ </Box>
147
+ )}
148
+
149
+ {/* Agent Response */}
150
+ {finalAnswer && (
151
+ <Box sx={{ mb: 2 }}>
152
+ <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5 }}>
153
+ <ChatBubbleOutlineIcon
154
+ sx={{
155
+ fontSize: 18,
156
+ color: 'text.secondary',
157
+ mt: 0.25,
158
+ flexShrink: 0
159
+ }}
160
+ />
161
+ <Box sx={{ flex: 1, minWidth: 0 }}>
162
+ <Typography
163
+ variant="caption"
164
+ sx={{
165
+ fontWeight: 700,
166
+ color: 'text.secondary',
167
+ fontSize: '0.7rem',
168
+ textTransform: 'uppercase',
169
+ letterSpacing: '0.5px',
170
+ display: 'block',
171
+ mb: 0.75,
172
+ }}
173
+ >
174
+ Agent Response
175
+ </Typography>
176
+ <Typography
177
+ variant="body2"
178
+ sx={{
179
+ color: 'text.primary',
180
+ lineHeight: 1.5,
181
+ fontSize: '0.85rem',
182
+ whiteSpace: 'pre-wrap',
183
+ wordBreak: 'break-word',
184
+ }}
185
+ >
186
+ {finalAnswer}
187
+ </Typography>
188
+ </Box>
189
+ </Box>
190
+ </Box>
191
+ )}
192
+
193
+ {/* Divider before metrics */}
194
+ <Divider sx={{ my: 2 }} />
195
+
196
+ {/* Metrics */}
197
+ <Box
198
+ sx={{
199
+ display: 'flex',
200
+ alignItems: 'center',
201
+ gap: 1.5,
202
+ flexWrap: 'wrap',
203
+ justifyContent: 'center',
204
+ }}
205
+ >
206
+ {/* Agent */}
207
+ {trace?.modelId && (
208
+ <>
209
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
210
+ <SmartToyIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
211
+ <Typography
212
+ variant="caption"
213
+ sx={{
214
+ color: 'text.primary',
215
+ fontFamily: 'monospace',
216
+ fontSize: '0.75rem',
217
+ fontWeight: 700,
218
+ }}
219
+ >
220
+ {formatModelName(trace.modelId)}
221
+ </Typography>
222
+ </Box>
223
+
224
+ {/* Divider */}
225
+ <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
226
+ </>
227
+ )}
228
+
229
+ {/* Steps Count */}
230
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
231
+ <FormatListNumberedIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
232
+ <Typography
233
+ variant="caption"
234
+ sx={{
235
+ fontSize: '0.75rem',
236
+ fontWeight: 700,
237
+ color: 'text.primary',
238
+ mr: 0.5,
239
+ }}
240
+ >
241
+ {finalStep.metadata.numberOfSteps}
242
+ </Typography>
243
+ <Typography
244
+ variant="caption"
245
+ sx={{
246
+ fontSize: '0.7rem',
247
+ fontWeight: 400,
248
+ color: 'text.secondary',
249
+ }}
250
+ >
251
+ {finalStep.metadata.numberOfSteps === 1 ? 'Step' : 'Steps'}
252
+ </Typography>
253
+ </Box>
254
+
255
+ {/* Divider */}
256
+ <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
257
+
258
+ {/* Duration */}
259
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
260
+ <AccessTimeIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
261
+ <Typography
262
+ variant="caption"
263
+ sx={{
264
+ fontSize: '0.75rem',
265
+ fontWeight: 700,
266
+ color: 'text.primary',
267
+ }}
268
+ >
269
+ {finalStep.metadata.duration.toFixed(1)}s
270
+ </Typography>
271
+ </Box>
272
+
273
+ {/* Divider */}
274
+ <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
275
+
276
+ {/* Input Tokens */}
277
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
278
+ <InputIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
279
+ <Typography
280
+ variant="caption"
281
+ sx={{
282
+ fontSize: '0.75rem',
283
+ fontWeight: 700,
284
+ color: 'text.primary',
285
+ }}
286
+ >
287
+ {finalStep.metadata.inputTokensUsed.toLocaleString()}
288
+ </Typography>
289
+ </Box>
290
+
291
+ {/* Divider */}
292
+ <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
293
+
294
+ {/* Output Tokens */}
295
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
296
+ <OutputIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
297
+ <Typography
298
+ variant="caption"
299
+ sx={{
300
+ fontSize: '0.75rem',
301
+ fontWeight: 700,
302
+ color: 'text.primary',
303
+ }}
304
+ >
305
+ {finalStep.metadata.outputTokensUsed.toLocaleString()}
306
+ </Typography>
307
+ </Box>
308
+ </Box>
309
+ </Paper>
310
+
311
+ {/* GIF Error Alert */}
312
+ {gifError && (
313
+ <Alert severity="error" sx={{ fontSize: '0.72rem', py: 0.5 }}>
314
+ {gifError}
315
+ </Alert>
316
+ )}
317
+
318
+ {/* Action Buttons */}
319
+ <Box
320
+ sx={{
321
+ display: 'flex',
322
+ flexDirection: 'column',
323
+ gap: 1.5,
324
+ alignItems: 'center',
325
+ }}
326
+ >
327
+ {/* Download buttons */}
328
+ <Box
329
+ sx={{
330
+ display: 'flex',
331
+ gap: 1,
332
+ justifyContent: 'center',
333
+ flexWrap: 'wrap',
334
+ }}
335
+ >
336
+ <DownloadGifButton
337
+ isGenerating={isGenerating}
338
+ onClick={onGenerateGif}
339
+ disabled={!steps || steps.length === 0}
340
+ />
341
+ <DownloadJsonButton onClick={onDownloadJson} disabled={!trace} />
342
+ </Box>
343
+
344
+ {/* New Task button - larger and below */}
345
+ <Button
346
+ variant="contained"
347
+ startIcon={<AddIcon sx={{ fontSize: 20 }} />}
348
+ onClick={onBackToHome}
349
+ color="primary"
350
+ sx={{
351
+ textTransform: 'none',
352
+ fontWeight: 700,
353
+ fontSize: '0.9rem',
354
+ px: 3,
355
+ py: 1,
356
+ boxShadow: 2,
357
+ minWidth: 200,
358
+ '&:hover': {
359
+ boxShadow: 4,
360
+ },
361
+ }}
362
+ >
363
+ New Task
364
+ </Button>
365
+ </Box>
366
+ </Box>
367
+ );
368
+ };
cua2-front/src/components/sandbox/completionview/DownloadGifButton.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Button, CircularProgress, Tooltip } from '@mui/material';
3
+ import GifIcon from '@mui/icons-material/Gif';
4
+
5
+ interface DownloadGifButtonProps {
6
+ isGenerating: boolean;
7
+ onClick: () => void;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Button to download a GIF replay of the trace
13
+ */
14
+ export const DownloadGifButton: React.FC<DownloadGifButtonProps> = ({
15
+ isGenerating,
16
+ onClick,
17
+ disabled = false,
18
+ }) => {
19
+ return (
20
+ <Tooltip
21
+ title={
22
+ disabled
23
+ ? "No steps available"
24
+ : "Download GIF replay"
25
+ }
26
+ >
27
+ <span>
28
+ <Button
29
+ variant="outlined"
30
+ size="small"
31
+ onClick={onClick}
32
+ disabled={disabled || isGenerating}
33
+ startIcon={
34
+ isGenerating ? (
35
+ <CircularProgress size={16} />
36
+ ) : (
37
+ <GifIcon sx={{ fontSize: '1.2rem' }} />
38
+ )
39
+ }
40
+ sx={{
41
+ textTransform: 'none',
42
+ fontSize: '0.75rem',
43
+ fontWeight: 600,
44
+ borderRadius: 1,
45
+ px: 1.5,
46
+ py: 0.5,
47
+ borderColor: 'divider',
48
+ color: 'text.primary',
49
+ '&:hover': {
50
+ borderColor: 'primary.main',
51
+ backgroundColor: 'action.hover',
52
+ },
53
+ '&.Mui-disabled': {
54
+ borderColor: 'divider',
55
+ color: 'text.disabled',
56
+ },
57
+ }}
58
+ >
59
+ {isGenerating ? 'Generating...' : 'Download GIF'}
60
+ </Button>
61
+ </span>
62
+ </Tooltip>
63
+ );
64
+ };
cua2-front/src/components/sandbox/completionview/DownloadJsonButton.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Button, Tooltip } from '@mui/material';
3
+ import DownloadIcon from '@mui/icons-material/Download';
4
+
5
+ interface DownloadJsonButtonProps {
6
+ onClick: () => void;
7
+ disabled?: boolean;
8
+ }
9
+
10
+ /**
11
+ * Button to download trace as JSON
12
+ */
13
+ export const DownloadJsonButton: React.FC<DownloadJsonButtonProps> = ({
14
+ onClick,
15
+ disabled = false,
16
+ }) => {
17
+ return (
18
+ <Tooltip
19
+ title={
20
+ disabled
21
+ ? "No trace available"
22
+ : "Download trace as JSON"
23
+ }
24
+ >
25
+ <span>
26
+ <Button
27
+ variant="outlined"
28
+ size="small"
29
+ onClick={onClick}
30
+ disabled={disabled}
31
+ startIcon={<DownloadIcon sx={{ fontSize: '1.2rem' }} />}
32
+ sx={{
33
+ textTransform: 'none',
34
+ fontSize: '0.75rem',
35
+ fontWeight: 600,
36
+ borderRadius: 1,
37
+ px: 1.5,
38
+ py: 0.5,
39
+ borderColor: 'divider',
40
+ color: 'text.primary',
41
+ '&:hover': {
42
+ borderColor: 'primary.main',
43
+ backgroundColor: 'action.hover',
44
+ },
45
+ '&.Mui-disabled': {
46
+ borderColor: 'divider',
47
+ color: 'text.disabled',
48
+ },
49
+ }}
50
+ >
51
+ Download JSON Trace
52
+ </Button>
53
+ </span>
54
+ </Tooltip>
55
+ );
56
+ };
cua2-front/src/components/sandbox/completionview/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export { CompletionView } from './CompletionView';
2
+ export { DownloadGifButton } from './DownloadGifButton';
3
+ export { DownloadJsonButton } from './DownloadJsonButton';
cua2-front/src/components/sandbox/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { SandboxViewer } from './SandboxViewer';
2
+ export { CompletionView, DownloadGifButton, DownloadJsonButton } from './completionview';
cua2-front/src/components/steps/ConnectionStepCard.tsx ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Card, CardContent, Box, Typography, CircularProgress } from '@mui/material';
3
+ import CableIcon from '@mui/icons-material/Cable';
4
+ import { keyframes } from '@mui/system';
5
+
6
+ // Animation de pulsation pour le border
7
+ const borderPulse = keyframes`
8
+ 0%, 100% {
9
+ border-color: rgba(79, 134, 198, 0.4);
10
+ box-shadow: 0 2px 8px rgba(79, 134, 198, 0.15);
11
+ }
12
+ 50% {
13
+ border-color: rgba(79, 134, 198, 0.8);
14
+ box-shadow: 0 2px 12px rgba(79, 134, 198, 0.3);
15
+ }
16
+ `;
17
+
18
+ // Animation de pulsation pour le fond
19
+ const backgroundPulse = keyframes`
20
+ 0%, 100% {
21
+ background-color: rgba(79, 134, 198, 0.03);
22
+ }
23
+ 50% {
24
+ background-color: rgba(79, 134, 198, 0.08);
25
+ }
26
+ `;
27
+
28
+ interface ConnectionStepCardProps {
29
+ isConnecting: boolean;
30
+ }
31
+
32
+ export const ConnectionStepCard: React.FC<ConnectionStepCardProps> = ({ isConnecting }) => {
33
+ return (
34
+ <Card
35
+ elevation={0}
36
+ sx={{
37
+ backgroundColor: 'background.paper',
38
+ border: '2px solid',
39
+ borderColor: isConnecting ? 'primary.main' : 'success.main',
40
+ borderRadius: 1.5,
41
+ animation: isConnecting ? `${borderPulse} 2s ease-in-out infinite` : 'none',
42
+ position: 'relative',
43
+ overflow: 'hidden',
44
+ '&::before': isConnecting ? {
45
+ content: '""',
46
+ position: 'absolute',
47
+ top: 0,
48
+ left: 0,
49
+ right: 0,
50
+ bottom: 0,
51
+ animation: `${backgroundPulse} 2s ease-in-out infinite`,
52
+ zIndex: 0,
53
+ } : {},
54
+ }}
55
+ >
56
+ <CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 }, position: 'relative', zIndex: 1 }}>
57
+ {/* Header avec spinner ou check */}
58
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
59
+ <Box
60
+ sx={{
61
+ display: 'flex',
62
+ alignItems: 'center',
63
+ justifyContent: 'center',
64
+ position: 'relative',
65
+ }}
66
+ >
67
+ {isConnecting ? (
68
+ <CircularProgress
69
+ size={32}
70
+ thickness={2.5}
71
+ sx={{
72
+ color: 'primary.main',
73
+ }}
74
+ />
75
+ ) : (
76
+ <CableIcon
77
+ sx={{
78
+ fontSize: 28,
79
+ color: 'success.main',
80
+ }}
81
+ />
82
+ )}
83
+ </Box>
84
+
85
+ <Box sx={{ flex: 1, minWidth: 0 }}>
86
+ <Typography
87
+ sx={{
88
+ fontSize: '0.85rem',
89
+ fontWeight: 700,
90
+ color: isConnecting ? 'primary.main' : 'success.main',
91
+ lineHeight: 1.3,
92
+ }}
93
+ >
94
+ {isConnecting ? 'Connecting to E2B...' : 'Connected to E2B'}
95
+ </Typography>
96
+ <Typography
97
+ sx={{
98
+ fontSize: '0.7rem',
99
+ color: 'text.secondary',
100
+ lineHeight: 1.2,
101
+ }}
102
+ >
103
+ {isConnecting ? 'Setting up sandbox environment' : 'Sandbox ready'}
104
+ </Typography>
105
+ </Box>
106
+ </Box>
107
+ </CardContent>
108
+ </Card>
109
+ );
110
+ };
cua2-front/src/components/steps/FinalStepCard.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FinalStep } from '@/types/agent';
2
+ import React from 'react';
3
+ import { Card, CardContent, Box, Typography } from '@mui/material';
4
+ import CheckIcon from '@mui/icons-material/Check';
5
+ import CloseIcon from '@mui/icons-material/Close';
6
+ import { useAgentStore } from '@/stores/agentStore';
7
+
8
+ interface FinalStepCardProps {
9
+ finalStep: FinalStep;
10
+ isActive?: boolean;
11
+ }
12
+
13
+ export const FinalStepCard: React.FC<FinalStepCardProps> = ({ finalStep, isActive = false }) => {
14
+ const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex);
15
+
16
+ const isSuccess = finalStep.type === 'success';
17
+
18
+ const handleClick = () => {
19
+ // Clicking on final step goes to live mode (null)
20
+ setSelectedStepIndex(null);
21
+ };
22
+
23
+ return (
24
+ <Card
25
+ elevation={0}
26
+ onClick={handleClick}
27
+ sx={{
28
+ backgroundColor: 'background.paper',
29
+ border: '1px solid',
30
+ borderColor: (theme) => `${isActive
31
+ ? isSuccess ? theme.palette.success.main : theme.palette.error.main
32
+ : theme.palette.divider} !important`,
33
+ borderRadius: 1.5,
34
+ transition: 'all 0.2s ease',
35
+ cursor: 'pointer',
36
+ boxShadow: isActive
37
+ ? (theme) => isSuccess
38
+ ? `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.3)' : 'rgba(102, 187, 106, 0.2)'}`
39
+ : `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.3)' : 'rgba(244, 67, 54, 0.2)'}`
40
+ : 'none',
41
+ '&:hover': {
42
+ borderColor: (theme) => `${isSuccess ? theme.palette.success.main : theme.palette.error.main} !important`,
43
+ boxShadow: (theme) => isSuccess
44
+ ? `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.2)' : 'rgba(102, 187, 106, 0.1)'}`
45
+ : `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.2)' : 'rgba(244, 67, 54, 0.1)'}`,
46
+ },
47
+ }}
48
+ >
49
+ <CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 } }}>
50
+ {/* Header with icon */}
51
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
52
+ {isSuccess ? (
53
+ <CheckIcon sx={{ fontSize: 20, color: 'success.main' }} />
54
+ ) : (
55
+ <CloseIcon sx={{ fontSize: 20, color: 'error.main' }} />
56
+ )}
57
+ <Typography
58
+ sx={{
59
+ fontSize: '0.85rem',
60
+ fontWeight: 700,
61
+ color: isSuccess ? 'success.main' : 'error.main',
62
+ }}
63
+ >
64
+ {isSuccess ? 'Task completed' : 'Task failed'}
65
+ </Typography>
66
+ </Box>
67
+ </CardContent>
68
+ </Card>
69
+ );
70
+ };
cua2-front/src/components/steps/StepCard.tsx ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentStep } from '@/types/agent';
2
+ import React, { useState } from 'react';
3
+ import { Card, CardContent, Box, Typography, Divider, Chip, Paper, Accordion, AccordionSummary, AccordionDetails, IconButton, Tooltip } from '@mui/material';
4
+ import ThoughtBubbleIcon from '@mui/icons-material/Psychology';
5
+ import BoltIcon from '@mui/icons-material/Bolt';
6
+ import AccessTimeIcon from '@mui/icons-material/AccessTime';
7
+ import InputIcon from '@mui/icons-material/Input';
8
+ import OutputIcon from '@mui/icons-material/Output';
9
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
10
+ import ThumbUpIcon from '@mui/icons-material/ThumbUp';
11
+ import ThumbDownIcon from '@mui/icons-material/ThumbDown';
12
+ import { useAgentStore } from '@/stores/agentStore';
13
+ import { updateStepEvaluation } from '@/services/api';
14
+
15
+ interface StepCardProps {
16
+ step: AgentStep;
17
+ index: number;
18
+ isLatest?: boolean;
19
+ isActive?: boolean;
20
+ }
21
+
22
+ export const StepCard: React.FC<StepCardProps> = ({ step, index, isLatest = false, isActive = false }) => {
23
+ const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex);
24
+ const [thoughtExpanded, setThoughtExpanded] = useState(false);
25
+ const [evaluation, setEvaluation] = useState<'like' | 'dislike' | 'neutral'>(step.step_evaluation || 'neutral');
26
+ const [isVoting, setIsVoting] = useState(false);
27
+
28
+ const handleClick = () => {
29
+ setSelectedStepIndex(index);
30
+ };
31
+
32
+ const handleAccordionClick = (event: React.MouseEvent) => {
33
+ event.stopPropagation(); // Empêcher la propagation pour ne pas sélectionner la step
34
+ };
35
+
36
+ const handleVote = async (event: React.MouseEvent, vote: 'like' | 'dislike') => {
37
+ event.stopPropagation(); // Empêcher la propagation pour ne pas sélectionner la step
38
+
39
+ if (isVoting) return;
40
+
41
+ const newEvaluation = evaluation === vote ? 'neutral' : vote;
42
+ setIsVoting(true);
43
+
44
+ try {
45
+ await updateStepEvaluation(step.traceId, step.stepId, newEvaluation);
46
+ setEvaluation(newEvaluation);
47
+ } catch (error) {
48
+ console.error('Failed to update step evaluation:', error);
49
+ } finally {
50
+ setIsVoting(false);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <Card
56
+ elevation={0}
57
+ onClick={handleClick}
58
+ sx={{
59
+ backgroundColor: 'background.paper',
60
+ border: '1px solid',
61
+ borderColor: (theme) => `${isActive ? theme.palette.primary.main : theme.palette.divider} !important`,
62
+ borderRadius: 1.5,
63
+ transition: 'all 0.2s ease',
64
+ cursor: 'pointer',
65
+ boxShadow: isActive ? (theme) => `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.3)' : 'rgba(79, 134, 198, 0.2)'}` : 'none',
66
+ '&:hover': {
67
+ borderColor: (theme) => `${theme.palette.primary.main} !important`,
68
+ boxShadow: (theme) => `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.2)' : 'rgba(79, 134, 198, 0.1)'}`,
69
+ },
70
+ }}
71
+ >
72
+ <CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 } }}>
73
+ {/* Step header */}
74
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
75
+ <Typography
76
+ sx={{
77
+ fontSize: '1.5rem',
78
+ fontWeight: 800,
79
+ color: isActive ? 'primary.main' : 'text.primary',
80
+ lineHeight: 1,
81
+ }}
82
+ >
83
+ {index + 1}
84
+ </Typography>
85
+ <Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
86
+ <Chip
87
+ icon={<AccessTimeIcon sx={{ fontSize: '0.7rem !important' }} />}
88
+ label={`${step.duration.toFixed(1)}s`}
89
+ size="small"
90
+ sx={{
91
+ height: 'auto',
92
+ py: 0.25,
93
+ fontSize: '0.65rem',
94
+ fontWeight: 600,
95
+ backgroundColor: 'action.hover',
96
+ color: 'text.primary',
97
+ '& .MuiChip-icon': { marginLeft: 0.5, color: 'text.secondary' },
98
+ }}
99
+ />
100
+ <Chip
101
+ icon={<InputIcon sx={{ fontSize: '0.7rem !important' }} />}
102
+ label={step.inputTokensUsed.toLocaleString()}
103
+ size="small"
104
+ sx={{
105
+ height: 'auto',
106
+ py: 0.25,
107
+ fontSize: '0.65rem',
108
+ fontWeight: 600,
109
+ backgroundColor: 'action.hover',
110
+ color: 'text.primary',
111
+ '& .MuiChip-icon': { marginLeft: 0.5, color: 'text.secondary' },
112
+ }}
113
+ />
114
+ <Chip
115
+ icon={<OutputIcon sx={{ fontSize: '0.7rem !important' }} />}
116
+ label={step.outputTokensUsed.toLocaleString()}
117
+ size="small"
118
+ sx={{
119
+ height: 'auto',
120
+ py: 0.25,
121
+ fontSize: '0.65rem',
122
+ fontWeight: 600,
123
+ backgroundColor: 'action.hover',
124
+ color: 'text.primary',
125
+ '& .MuiChip-icon': { marginLeft: 0.5, color: 'text.secondary' },
126
+ }}
127
+ />
128
+ </Box>
129
+ </Box>
130
+
131
+ {/* Step image */}
132
+ {step.image && (
133
+ <Box
134
+ sx={{
135
+ mb: 1.5,
136
+ borderRadius: 1,
137
+ overflow: 'hidden',
138
+ border: '1px solid',
139
+ borderColor: (theme) => isActive ? theme.palette.primary.main : theme.palette.divider,
140
+ backgroundColor: 'action.hover',
141
+ transition: 'border-color 0.2s ease',
142
+ }}
143
+ >
144
+ <img
145
+ src={step.image}
146
+ alt={`Step ${index + 1}`}
147
+ style={{ width: '100%', height: 'auto', display: 'block' }}
148
+ />
149
+ </Box>
150
+ )}
151
+
152
+ {/* Action */}
153
+ {step.actions && step.actions.length > 0 && (
154
+ <Box sx={{ mb: 1.5 }}>
155
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.75, justifyContent: 'space-between' }}>
156
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
157
+ <Typography
158
+ variant="caption"
159
+ sx={{
160
+ fontWeight: 700,
161
+ color: 'text.secondary',
162
+ fontSize: '0.65rem',
163
+ textTransform: 'uppercase',
164
+ letterSpacing: '0.5px',
165
+ }}
166
+ >
167
+ Action
168
+ </Typography>
169
+ </Box>
170
+
171
+ {/* Vote buttons */}
172
+ <Box sx={{ display: 'flex', gap: 0.5 }}>
173
+ <Tooltip title={evaluation === 'like' ? 'Remove like' : 'Like this step'}>
174
+ <IconButton
175
+ size="small"
176
+ onClick={(e) => handleVote(e, 'like')}
177
+ disabled={isVoting}
178
+ sx={{
179
+ padding: '2px',
180
+ color: evaluation === 'like' ? 'success.main' : 'action.disabled',
181
+ '&:hover': {
182
+ color: 'success.main',
183
+ backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.1)' : 'rgba(102, 187, 106, 0.08)',
184
+ },
185
+ }}
186
+ >
187
+ <ThumbUpIcon sx={{ fontSize: 14 }} />
188
+ </IconButton>
189
+ </Tooltip>
190
+ <Tooltip title={evaluation === 'dislike' ? 'Remove dislike' : 'Dislike this step'}>
191
+ <IconButton
192
+ size="small"
193
+ onClick={(e) => handleVote(e, 'dislike')}
194
+ disabled={isVoting}
195
+ sx={{
196
+ padding: '2px',
197
+ color: evaluation === 'dislike' ? 'error.main' : 'action.disabled',
198
+ '&:hover': {
199
+ color: 'error.main',
200
+ backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.08)',
201
+ },
202
+ }}
203
+ >
204
+ <ThumbDownIcon sx={{ fontSize: 14 }} />
205
+ </IconButton>
206
+ </Tooltip>
207
+ </Box>
208
+ </Box>
209
+ <Box component="ul" sx={{ listStyle: 'none', p: 0, m: 0}}>
210
+ {step.actions.map((action, actionIndex) => (
211
+ <Box
212
+ key={actionIndex}
213
+ component="li"
214
+ sx={{
215
+ display: 'flex',
216
+ alignItems: 'flex-start',
217
+ fontSize: '0.75rem',
218
+ color: 'text.primary',
219
+ lineHeight: 1.4,
220
+ mb: 0.5,
221
+ '&:last-child': { mb: 0 },
222
+ }}
223
+ >
224
+ {/* <Typography
225
+ component="span"
226
+ sx={{
227
+ mr: 0.5,
228
+ color: 'text.secondary',
229
+ fontWeight: 700,
230
+ flexShrink: 0,
231
+ fontSize: '0.75rem',
232
+ }}
233
+ >
234
+
235
+ </Typography> */}
236
+ <Typography
237
+ component="span"
238
+ sx={{
239
+ fontSize: '0.75rem',
240
+ fontWeight: 900,
241
+ wordBreak: 'break-word',
242
+ }}
243
+ >
244
+ {action.description}
245
+ </Typography>
246
+ </Box>
247
+ ))}
248
+ </Box>
249
+ </Box>
250
+ )}
251
+
252
+ {/* Thought - Accordion */}
253
+ {step.thought && (
254
+ <Accordion
255
+ expanded={thoughtExpanded}
256
+ onChange={(e, expanded) => setThoughtExpanded(expanded)}
257
+ onClick={handleAccordionClick}
258
+ elevation={0}
259
+ disableGutters
260
+ sx={{
261
+ mb: 0.5,
262
+ backgroundColor: 'transparent',
263
+ border: 'none',
264
+ boxShadow: 'none',
265
+ '&:before': { display: 'none' },
266
+ '&.MuiAccordion-root': {
267
+ backgroundColor: 'transparent',
268
+ boxShadow: 'none',
269
+ '&:before': {
270
+ display: 'none',
271
+ },
272
+ },
273
+ '& .MuiAccordionSummary-root': {
274
+ minHeight: 'auto',
275
+ p: 0,
276
+ backgroundColor: 'transparent',
277
+ '&:hover': {
278
+ backgroundColor: 'transparent',
279
+ },
280
+ '&.Mui-expanded': {
281
+ minHeight: 'auto',
282
+ },
283
+ },
284
+ '& .MuiAccordionSummary-content': {
285
+ margin: '0 !important',
286
+ },
287
+ '& .MuiAccordionDetails-root': {
288
+ p: 0,
289
+ pt: 0.5,
290
+ pb: 0,
291
+ backgroundColor: 'transparent',
292
+ },
293
+ }}
294
+ >
295
+ <AccordionSummary
296
+ expandIcon={<ExpandMoreIcon sx={{ fontSize: 16, color: 'text.secondary' }} />}
297
+ sx={{
298
+ flexDirection: 'row',
299
+ border: 'none',
300
+ '& .MuiAccordionSummary-expandIconWrapper': {
301
+ transform: 'rotate(-90deg)',
302
+ transition: 'transform 0.2s',
303
+ '&.Mui-expanded': {
304
+ transform: 'rotate(0deg)',
305
+ },
306
+ },
307
+ }}
308
+ >
309
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
310
+ <Typography
311
+ variant="caption"
312
+ sx={{
313
+ fontWeight: 700,
314
+ color: 'text.secondary',
315
+ fontSize: '0.65rem',
316
+ textTransform: 'uppercase',
317
+ letterSpacing: '0.5px',
318
+ }}
319
+ >
320
+ Thought
321
+ </Typography>
322
+ </Box>
323
+ </AccordionSummary>
324
+ <AccordionDetails>
325
+ <Typography
326
+ variant="body2"
327
+ sx={{
328
+ fontSize: '0.75rem',
329
+ color: 'text.primary',
330
+ lineHeight: 1.4,
331
+ pl: 2.5,
332
+ }}
333
+ >
334
+ {step.thought}
335
+ </Typography>
336
+ </AccordionDetails>
337
+ </Accordion>
338
+ )}
339
+
340
+ {/* Error */}
341
+ {step.error && (
342
+ <Box sx={{ mt: 1.5, p: 1, borderRadius: 1, backgroundColor: 'error.main', opacity: 0.1 }}>
343
+ <Typography
344
+ variant="caption"
345
+ sx={{
346
+ fontSize: '0.7rem',
347
+ color: 'error.main',
348
+ fontWeight: 600,
349
+ }}
350
+ >
351
+ Error: {step.error}
352
+ </Typography>
353
+ </Box>
354
+ )}
355
+ </CardContent>
356
+ </Card>
357
+ );
358
+ };
cua2-front/src/components/steps/StepsList.tsx ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useEffect } from 'react';
2
+ import { AgentTrace } from '@/types/agent';
3
+ import { Box, Typography, Stack, Paper } from '@mui/material';
4
+ import { StepCard } from './StepCard';
5
+ import { FinalStepCard } from './FinalStepCard';
6
+ import { ThinkingStepCard } from './ThinkingStepCard';
7
+ import { ConnectionStepCard } from './ConnectionStepCard';
8
+ import ListAltIcon from '@mui/icons-material/ListAlt';
9
+ import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
10
+ import { useAgentStore, selectSelectedStepIndex, selectFinalStep, selectIsConnectingToE2B, selectIsAgentProcessing } from '@/stores/agentStore';
11
+
12
+ interface StepsListProps {
13
+ trace?: AgentTrace;
14
+ }
15
+
16
+ export const StepsList: React.FC<StepsListProps> = ({ trace }) => {
17
+ const containerRef = useRef<HTMLDivElement>(null);
18
+ const selectedStepIndex = useAgentStore(selectSelectedStepIndex);
19
+ const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex);
20
+ const finalStep = useAgentStore(selectFinalStep);
21
+ const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B);
22
+ const isAgentProcessing = useAgentStore(selectIsAgentProcessing);
23
+ const isScrollingProgrammatically = useRef(false);
24
+ const [showThinkingCard, setShowThinkingCard] = React.useState(false);
25
+ const thinkingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
26
+ const streamStartTimeRef = useRef<number | null>(null);
27
+ const [showConnectionCard, setShowConnectionCard] = React.useState(false);
28
+ const hasConnectedRef = useRef(false);
29
+
30
+ // Check if final step is active (when selectedStepIndex is null and finalStep exists and trace is not running)
31
+ const isFinalStepActive = selectedStepIndex === null && finalStep && !trace?.isRunning;
32
+
33
+ // Determine the active step index
34
+ // If a specific step is selected, use that
35
+ // If the final step is active, no normal step should be active
36
+ // Otherwise, show the last step as active
37
+ const activeStepIndex = selectedStepIndex !== null
38
+ ? selectedStepIndex
39
+ : isFinalStepActive
40
+ ? null // When final step is active, no normal step is active
41
+ : (trace?.steps && trace.steps.length > 0 && trace?.isRunning)
42
+ ? trace.steps.length - 1
43
+ : (trace?.steps && trace.steps.length > 0)
44
+ ? trace.steps.length - 1
45
+ : null;
46
+
47
+ // Manage ConnectionStepCard display:
48
+ // - Shows when isConnectingToE2B = true OR when we had a connection
49
+ // - Remains visible even when task is finished (if we have steps or finalStep)
50
+ useEffect(() => {
51
+ if (isConnectingToE2B || isAgentProcessing || (trace?.steps && trace.steps.length > 0) || finalStep) {
52
+ setShowConnectionCard(true);
53
+ hasConnectedRef.current = true;
54
+ }
55
+ }, [isConnectingToE2B, isAgentProcessing, trace?.steps, finalStep]);
56
+
57
+ // Manage ThinkingCard display:
58
+ // - Appears 5 seconds AFTER stream starts (isAgentProcessing = true, NOT during isConnectingToE2B)
59
+ // - Remains visible during the entire agent processing
60
+ // - Hides only when agent stops OR a finalStep exists
61
+ useEffect(() => {
62
+ // Si le stream démarre vraiment (isAgentProcessing = true et PAS en train de se connecter)
63
+ // Et pas encore de startTime enregistré
64
+ if (isAgentProcessing && !isConnectingToE2B && !streamStartTimeRef.current) {
65
+ streamStartTimeRef.current = Date.now();
66
+ }
67
+
68
+ // Si l'agent s'arrête OU qu'on a un finalStep, reset et cacher
69
+ if (!isAgentProcessing || finalStep) {
70
+ streamStartTimeRef.current = null;
71
+ setShowThinkingCard(false);
72
+ if (thinkingTimeoutRef.current) {
73
+ clearTimeout(thinkingTimeoutRef.current);
74
+ thinkingTimeoutRef.current = null;
75
+ }
76
+ return;
77
+ }
78
+
79
+ // If agent is running, not connecting, no finalStep: start 5 second timer
80
+ if (isAgentProcessing && !isConnectingToE2B && !finalStep && streamStartTimeRef.current) {
81
+ // Clean up any existing timeout
82
+ if (thinkingTimeoutRef.current) {
83
+ clearTimeout(thinkingTimeoutRef.current);
84
+ }
85
+
86
+ // Calculer le temps écoulé depuis le début du stream
87
+ const elapsedTime = Date.now() - streamStartTimeRef.current;
88
+ const remainingTime = Math.max(0, 5000 - elapsedTime);
89
+
90
+ thinkingTimeoutRef.current = setTimeout(() => {
91
+ setShowThinkingCard(true);
92
+ }, remainingTime);
93
+ }
94
+
95
+ // Cleanup on unmount or when dependencies change
96
+ return () => {
97
+ if (thinkingTimeoutRef.current) {
98
+ clearTimeout(thinkingTimeoutRef.current);
99
+ thinkingTimeoutRef.current = null;
100
+ }
101
+ };
102
+ }, [isAgentProcessing, isConnectingToE2B, finalStep]);
103
+
104
+ // Auto-scroll to active step when it changes (timeline → steps)
105
+ useEffect(() => {
106
+ if (containerRef.current) {
107
+ isScrollingProgrammatically.current = true;
108
+ // Use setTimeout to ensure DOM has updated
109
+ setTimeout(() => {
110
+ if (containerRef.current) {
111
+ // Scroll to final step if it's active
112
+ if (isFinalStepActive) {
113
+ const finalStepElement = containerRef.current.querySelector(`[data-step-index="final"]`);
114
+ if (finalStepElement) {
115
+ finalStepElement.scrollIntoView({
116
+ behavior: 'smooth',
117
+ block: 'center'
118
+ });
119
+ setTimeout(() => {
120
+ isScrollingProgrammatically.current = false;
121
+ }, 500);
122
+ }
123
+ }
124
+ // Otherwise scroll to active step
125
+ else if (activeStepIndex !== null && trace?.steps) {
126
+ const activeStepElement = containerRef.current.querySelector(`[data-step-index="${activeStepIndex}"]`);
127
+ if (activeStepElement) {
128
+ activeStepElement.scrollIntoView({
129
+ behavior: 'smooth',
130
+ block: 'center'
131
+ });
132
+ // Reset flag after scroll animation
133
+ setTimeout(() => {
134
+ isScrollingProgrammatically.current = false;
135
+ }, 500);
136
+ }
137
+ }
138
+ }
139
+ }, 100);
140
+ }
141
+ }, [activeStepIndex, trace?.steps?.length, isFinalStepActive]);
142
+
143
+ // Detect which step is visible when scrolling (steps → timeline)
144
+ useEffect(() => {
145
+ const container = containerRef.current;
146
+ if (!container || !trace?.steps || trace.steps.length === 0) return;
147
+
148
+ const handleScroll = () => {
149
+ // Don't update if we're scrolling programmatically
150
+ if (isScrollingProgrammatically.current) return;
151
+
152
+ const containerRect = container.getBoundingClientRect();
153
+ const containerTop = containerRect.top;
154
+ const containerBottom = containerRect.bottom;
155
+ const containerCenter = containerRect.top + containerRect.height / 2;
156
+
157
+ // Check scroll position
158
+ const isAtTop = container.scrollTop <= 5; // 5px tolerance
159
+ const isAtBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 5; // 5px tolerance
160
+
161
+ let targetStepIndex: number | null = -1;
162
+ let targetDistance = Infinity;
163
+ let isFinalStepTarget = false;
164
+
165
+ if (isAtTop) {
166
+ // At the top: find the highest visible step
167
+ let highestVisibleBottom = Infinity;
168
+
169
+ trace.steps.forEach((_, index) => {
170
+ const stepElement = container.querySelector(`[data-step-index="${index}"]`);
171
+ if (stepElement) {
172
+ const stepRect = stepElement.getBoundingClientRect();
173
+ const stepTop = stepRect.top;
174
+ const stepBottom = stepRect.bottom;
175
+ const isVisible = stepTop < containerBottom && stepBottom > containerTop;
176
+
177
+ if (isVisible && stepTop < highestVisibleBottom) {
178
+ highestVisibleBottom = stepTop;
179
+ targetStepIndex = index;
180
+ isFinalStepTarget = false;
181
+ }
182
+ }
183
+ });
184
+ } else if (isAtBottom) {
185
+ // At the bottom: find the lowest visible step
186
+ let lowestVisibleTop = -Infinity;
187
+
188
+ trace.steps.forEach((_, index) => {
189
+ const stepElement = container.querySelector(`[data-step-index="${index}"]`);
190
+ if (stepElement) {
191
+ const stepRect = stepElement.getBoundingClientRect();
192
+ const stepTop = stepRect.top;
193
+ const stepBottom = stepRect.bottom;
194
+ const isVisible = stepTop < containerBottom && stepBottom > containerTop;
195
+
196
+ if (isVisible && stepTop > lowestVisibleTop) {
197
+ lowestVisibleTop = stepTop;
198
+ targetStepIndex = index;
199
+ isFinalStepTarget = false;
200
+ }
201
+ }
202
+ });
203
+
204
+ // Check if final step is the lowest visible
205
+ if (finalStep) {
206
+ const finalStepElement = container.querySelector(`[data-step-index="final"]`);
207
+ if (finalStepElement) {
208
+ const finalStepRect = finalStepElement.getBoundingClientRect();
209
+ const finalStepTop = finalStepRect.top;
210
+ const finalStepBottom = finalStepRect.bottom;
211
+ const isVisible = finalStepTop < containerBottom && finalStepBottom > containerTop;
212
+
213
+ if (isVisible && finalStepTop > lowestVisibleTop) {
214
+ targetStepIndex = null;
215
+ isFinalStepTarget = true;
216
+ }
217
+ }
218
+ }
219
+ } else {
220
+ // Not at bottom: find the step closest to center
221
+ trace.steps.forEach((_, index) => {
222
+ const stepElement = container.querySelector(`[data-step-index="${index}"]`);
223
+ if (stepElement) {
224
+ const stepRect = stepElement.getBoundingClientRect();
225
+ const stepCenter = stepRect.top + stepRect.height / 2;
226
+ const distance = Math.abs(containerCenter - stepCenter);
227
+
228
+ if (distance < targetDistance) {
229
+ targetDistance = distance;
230
+ targetStepIndex = index;
231
+ isFinalStepTarget = false;
232
+ }
233
+ }
234
+ });
235
+
236
+ // Check if final step is closest to center
237
+ if (finalStep) {
238
+ const finalStepElement = container.querySelector(`[data-step-index="final"]`);
239
+ if (finalStepElement) {
240
+ const finalStepRect = finalStepElement.getBoundingClientRect();
241
+ const finalStepCenter = finalStepRect.top + finalStepRect.height / 2;
242
+ const distance = Math.abs(containerCenter - finalStepCenter);
243
+
244
+ if (distance < targetDistance) {
245
+ targetStepIndex = null;
246
+ isFinalStepTarget = true;
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ // Update the selected step if changed
253
+ if (isFinalStepTarget && selectedStepIndex !== null) {
254
+ setSelectedStepIndex(null);
255
+ } else if (!isFinalStepTarget && targetStepIndex !== -1 && targetStepIndex !== selectedStepIndex) {
256
+ setSelectedStepIndex(targetStepIndex);
257
+ }
258
+ };
259
+
260
+ // Throttle scroll events
261
+ let scrollTimeout: NodeJS.Timeout;
262
+ const throttledScroll = () => {
263
+ clearTimeout(scrollTimeout);
264
+ scrollTimeout = setTimeout(handleScroll, 150);
265
+ };
266
+
267
+ container.addEventListener('scroll', throttledScroll);
268
+ return () => {
269
+ container.removeEventListener('scroll', throttledScroll);
270
+ clearTimeout(scrollTimeout);
271
+ };
272
+ }, [trace?.steps, selectedStepIndex, setSelectedStepIndex, finalStep]);
273
+
274
+ return (
275
+ <Paper
276
+ elevation={0}
277
+ sx={{
278
+ width: { xs: '100%', md: 320 },
279
+ flexShrink: 0,
280
+ display: 'flex',
281
+ flexDirection: 'column',
282
+ ml: { xs: 0, md: 1.5 },
283
+ mt: { xs: 3, md: 0 },
284
+ overflow: 'hidden',
285
+ }}
286
+ >
287
+ <Box sx={{ px: 2, py: 1.5, borderBottom: '1px solid', borderColor: 'divider', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
288
+ <Typography variant="h6" sx={{ fontSize: '0.9rem', fontWeight: 700, color: 'text.primary' }}>
289
+ Steps
290
+ </Typography>
291
+ {trace?.traceMetadata && trace.traceMetadata.numberOfSteps > 0 && (
292
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0 }}>
293
+ <Typography
294
+ variant="caption"
295
+ sx={{
296
+ fontSize: '0.75rem',
297
+ fontWeight: 700,
298
+ color: 'text.primary',
299
+ }}
300
+ >
301
+ {trace.traceMetadata.numberOfSteps}
302
+ </Typography>
303
+ <Typography
304
+ variant="caption"
305
+ sx={{
306
+ fontSize: '0.75rem',
307
+ fontWeight: 700,
308
+ color: 'text.disabled',
309
+ }}
310
+ >
311
+ /{trace.traceMetadata.maxSteps}
312
+ </Typography>
313
+ </Box>
314
+ )}
315
+ </Box>
316
+ <Box
317
+ ref={containerRef}
318
+ sx={{
319
+ flex: 1,
320
+ overflowY: 'auto',
321
+ minHeight: 0,
322
+ p: 2,
323
+ }}
324
+ >
325
+ {(trace?.steps && trace.steps.length > 0) || finalStep || showThinkingCard || showConnectionCard ? (
326
+ <Stack spacing={2.5}>
327
+ {/* Show connection step card (first item) */}
328
+ {showConnectionCard && (
329
+ <Box data-step-index="connection">
330
+ <ConnectionStepCard isConnecting={isConnectingToE2B} />
331
+ </Box>
332
+ )}
333
+
334
+ {/* Show all steps */}
335
+ {trace?.steps && trace.steps.map((step, index) => (
336
+ <Box key={step.stepId} data-step-index={index}>
337
+ <StepCard
338
+ step={step}
339
+ index={index}
340
+ isLatest={index === trace.steps!.length - 1}
341
+ isActive={index === activeStepIndex}
342
+ />
343
+ </Box>
344
+ ))}
345
+
346
+ {/* Show thinking indicator after steps (appears 5 seconds after stream start) */}
347
+ {showThinkingCard && (
348
+ <Box data-step-index="thinking">
349
+ <ThinkingStepCard />
350
+ </Box>
351
+ )}
352
+
353
+ {/* Show final step card if exists */}
354
+ {finalStep && (
355
+ <Box data-step-index="final">
356
+ <FinalStepCard
357
+ finalStep={finalStep}
358
+ isActive={isFinalStepActive}
359
+ />
360
+ </Box>
361
+ )}
362
+ </Stack>
363
+ ) : (
364
+ <Box
365
+ sx={{
366
+ display: 'flex',
367
+ flexDirection: 'column',
368
+ alignItems: 'center',
369
+ justifyContent: 'center',
370
+ height: '100%',
371
+ color: 'text.secondary',
372
+ p: 3,
373
+ textAlign: 'center',
374
+ }}
375
+ >
376
+ <ListAltIcon sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
377
+ <Typography variant="body1" sx={{ fontWeight: 600, mb: 0.5 }}>
378
+ No steps yet
379
+ </Typography>
380
+ <Typography variant="caption" sx={{ fontSize: '0.75rem' }}>
381
+ Steps will appear as the agent progresses
382
+ </Typography>
383
+ </Box>
384
+ )}
385
+ </Box>
386
+ </Paper>
387
+ );
388
+ };
cua2-front/src/components/steps/ThinkingStepCard.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Card, CardContent, Box, Typography, CircularProgress } from '@mui/material';
3
+ import { keyframes } from '@mui/system';
4
+
5
+ // Animation de pulsation pour le border
6
+ const borderPulse = keyframes`
7
+ 0%, 100% {
8
+ border-color: rgba(79, 134, 198, 0.4);
9
+ box-shadow: 0 2px 8px rgba(79, 134, 198, 0.15);
10
+ }
11
+ 50% {
12
+ border-color: rgba(79, 134, 198, 0.8);
13
+ box-shadow: 0 2px 12px rgba(79, 134, 198, 0.3);
14
+ }
15
+ `;
16
+
17
+ // Animation de pulsation pour le fond
18
+ const backgroundPulse = keyframes`
19
+ 0%, 100% {
20
+ background-color: rgba(79, 134, 198, 0.03);
21
+ }
22
+ 50% {
23
+ background-color: rgba(79, 134, 198, 0.08);
24
+ }
25
+ `;
26
+
27
+ export const ThinkingStepCard: React.FC = () => {
28
+
29
+ return (
30
+ <Card
31
+ elevation={0}
32
+ sx={{
33
+ backgroundColor: 'background.paper',
34
+ border: '2px solid',
35
+ borderColor: 'primary.main',
36
+ borderRadius: 1.5,
37
+ animation: `${borderPulse} 2s ease-in-out infinite`,
38
+ position: 'relative',
39
+ overflow: 'hidden',
40
+ '&::before': {
41
+ content: '""',
42
+ position: 'absolute',
43
+ top: 0,
44
+ left: 0,
45
+ right: 0,
46
+ bottom: 0,
47
+ animation: `${backgroundPulse} 2s ease-in-out infinite`,
48
+ zIndex: 0,
49
+ },
50
+ }}
51
+ >
52
+ <CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 }, position: 'relative', zIndex: 1 }}>
53
+ {/* Header avec spinner */}
54
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
55
+ <Box
56
+ sx={{
57
+ display: 'flex',
58
+ alignItems: 'center',
59
+ justifyContent: 'center',
60
+ }}
61
+ >
62
+ {/* Spinner circulaire */}
63
+ <CircularProgress
64
+ size={32}
65
+ thickness={3.5}
66
+ sx={{
67
+ color: 'primary.main',
68
+ }}
69
+ />
70
+ </Box>
71
+
72
+ <Box sx={{ flex: 1, minWidth: 0 }}>
73
+ <Typography
74
+ sx={{
75
+ fontSize: '0.85rem',
76
+ fontWeight: 700,
77
+ color: 'primary.main',
78
+ lineHeight: 1.3,
79
+ }}
80
+ >
81
+ Agent
82
+ </Typography>
83
+ <Typography
84
+ sx={{
85
+ fontSize: '0.7rem',
86
+ color: 'text.secondary',
87
+ lineHeight: 1.2,
88
+ fontStyle: 'italic',
89
+ }}
90
+ >
91
+ Thinking...
92
+ </Typography>
93
+ </Box>
94
+ </Box>
95
+ </CardContent>
96
+ </Card>
97
+ );
98
+ };
cua2-front/src/components/steps/index.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export { StepsList } from './StepsList';
2
+ export { StepCard } from './StepCard';
3
+ export { ThinkingStepCard } from './ThinkingStepCard';
4
+ export { FinalStepCard } from './FinalStepCard';
5
+ export { ConnectionStepCard } from './ConnectionStepCard';
cua2-front/src/components/timeline/Timeline.tsx ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useEffect } from 'react';
2
+ import { Box, Typography, CircularProgress, Button } from '@mui/material';
3
+ import CheckIcon from '@mui/icons-material/Check';
4
+ import CloseIcon from '@mui/icons-material/Close';
5
+ import CableIcon from '@mui/icons-material/Cable';
6
+ import { AgentTraceMetadata } from '@/types/agent';
7
+ import { useAgentStore, selectSelectedStepIndex, selectFinalStep, selectIsConnectingToE2B, selectIsAgentProcessing } from '@/stores/agentStore';
8
+
9
+ interface TimelineProps {
10
+ metadata: AgentTraceMetadata;
11
+ isRunning: boolean;
12
+ }
13
+
14
+ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
15
+ const timelineRef = useRef<HTMLDivElement>(null);
16
+ const selectedStepIndex = useAgentStore(selectSelectedStepIndex);
17
+ const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex);
18
+ const finalStep = useAgentStore(selectFinalStep);
19
+ const isConnectingToE2B = useAgentStore(selectIsConnectingToE2B);
20
+ const isAgentProcessing = useAgentStore(selectIsAgentProcessing);
21
+
22
+ // Show connection indicator if connecting or if we have started processing
23
+ const showConnectionIndicator = isConnectingToE2B || isAgentProcessing || (metadata.numberOfSteps > 0) || finalStep;
24
+
25
+ // Generate array of steps with their status
26
+ // Show all steps up to maxSteps (200)
27
+ const totalStepsToShow = metadata.maxSteps;
28
+
29
+ const steps = Array.from({ length: totalStepsToShow }, (_, index) => ({
30
+ stepNumber: index + 1,
31
+ stepIndex: index,
32
+ isCompleted: index < metadata.numberOfSteps,
33
+ // Step is current if: we're at the right index AND running AND not connecting to E2B
34
+ isCurrent: (index === metadata.numberOfSteps && isRunning && !isConnectingToE2B) ||
35
+ (index === 0 && metadata.numberOfSteps === 0 && isRunning && !isConnectingToE2B),
36
+ isSelected: selectedStepIndex === index,
37
+ }));
38
+
39
+ // Handle step click
40
+ const handleStepClick = (stepIndex: number, isCompleted: boolean, isCurrent: boolean) => {
41
+ if (isCompleted) {
42
+ setSelectedStepIndex(stepIndex);
43
+ } else if (isCurrent) {
44
+ // Clicking on the current step (with animation) goes back to live mode
45
+ setSelectedStepIndex(null);
46
+ }
47
+ };
48
+
49
+ // Handle final step click (goes to live mode showing the final status)
50
+ const handleFinalStepClick = () => {
51
+ setSelectedStepIndex(null);
52
+ };
53
+
54
+ // Auto-scroll to current step while running
55
+ useEffect(() => {
56
+ if (timelineRef.current && isRunning) {
57
+ // Only auto-scroll while running, not when finished
58
+ const currentStepElement = timelineRef.current.querySelector(`[data-step="${metadata.numberOfSteps}"]`);
59
+ if (currentStepElement) {
60
+ currentStepElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
61
+ }
62
+ }
63
+ }, [metadata.numberOfSteps, isRunning]);
64
+
65
+ return (
66
+ <Box
67
+ sx={{
68
+ p: 2,
69
+ border: '1px solid',
70
+ borderColor: 'divider',
71
+ borderRadius: '12px',
72
+ backgroundColor: 'background.paper',
73
+ flexShrink: 0,
74
+ }}
75
+ >
76
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
77
+ {/* Header with step count */}
78
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
79
+ <Typography variant="h6" sx={{ fontSize: '0.9rem', fontWeight: 700, color: 'text.primary' }}>
80
+ Timeline
81
+ {selectedStepIndex !== null && (
82
+ <Typography component="span" sx={{ ml: 1, color: 'text.secondary', fontWeight: 500, fontSize: '0.65rem' }}>
83
+ - Viewing step {selectedStepIndex + 1}
84
+ </Typography>
85
+ )}
86
+ </Typography>
87
+ {selectedStepIndex !== null && (
88
+ <Button
89
+ size="small"
90
+ variant="outlined"
91
+ onClick={handleFinalStepClick}
92
+ sx={{
93
+ textTransform: 'none',
94
+ fontSize: '0.7rem',
95
+ fontWeight: 600,
96
+ px: 1.5,
97
+ py: 0.25,
98
+ minWidth: 'auto',
99
+ color: 'text.secondary',
100
+ borderColor: 'divider',
101
+ '&:hover': {
102
+ backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
103
+ borderColor: 'text.secondary',
104
+ },
105
+ }}
106
+ >
107
+ Back to latest step
108
+ </Button>
109
+ )}
110
+ </Box>
111
+
112
+ {/* Horizontal scrollable step indicators */}
113
+ <Box
114
+ ref={timelineRef}
115
+ sx={{
116
+ display: 'flex',
117
+ alignItems: 'center',
118
+ overflowX: 'auto',
119
+ overflowY: 'hidden',
120
+ gap: 1.5,
121
+ py: 1.5,
122
+ height: 60,
123
+ position: 'relative',
124
+ // Hide scrollbar completely
125
+ scrollbarWidth: 'none', // Firefox
126
+ '&::-webkit-scrollbar': {
127
+ display: 'none', // Chrome, Safari, Edge
128
+ },
129
+ // Horizontal line crossing through circles
130
+ '&::before': {
131
+ content: '""',
132
+ position: 'absolute',
133
+ left: "15px",
134
+ // Calculer la largeur pour couvrir tous les steps (200 steps * (40px minWidth + 12px gap))
135
+ width: `calc(${metadata.maxSteps} * (40px + 12px))`,
136
+ top: '17.5px',
137
+ transform: 'translateY(-50%)',
138
+ height: '2px',
139
+ backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
140
+ zIndex: 0,
141
+ pointerEvents: 'none',
142
+ },
143
+ }}
144
+ >
145
+ {/* Connection indicator (step 0) */}
146
+ {showConnectionIndicator && (
147
+ <Box
148
+ data-step="connection"
149
+ sx={{
150
+ display: 'flex',
151
+ flexDirection: 'column',
152
+ alignItems: 'center',
153
+ gap: 0.75,
154
+ minWidth: 50,
155
+ flexShrink: 0,
156
+ position: 'relative',
157
+ zIndex: 1,
158
+ }}
159
+ >
160
+ {/* Cercle blanc en arrière-plan pour cacher la ligne */}
161
+ <Box
162
+ sx={{
163
+ position: 'relative',
164
+ display: 'flex',
165
+ alignItems: 'center',
166
+ justifyContent: 'center',
167
+ }}
168
+ >
169
+ {/* Fond blanc/background pour cacher la ligne */}
170
+ <Box
171
+ sx={{
172
+ position: 'absolute',
173
+ width: 32,
174
+ height: 32,
175
+ borderRadius: '50%',
176
+ backgroundColor: 'background.paper',
177
+ zIndex: 0,
178
+ }}
179
+ />
180
+
181
+ {/* Connection icon */}
182
+ {isConnectingToE2B ? (
183
+ <CircularProgress
184
+ size={20}
185
+ thickness={5}
186
+ sx={{
187
+ color: 'primary.main',
188
+ position: 'relative',
189
+ zIndex: 1,
190
+ }}
191
+ />
192
+ ) : (
193
+ <CableIcon
194
+ sx={{
195
+ fontSize: 20,
196
+ color: 'success.main',
197
+ position: 'relative',
198
+ zIndex: 1,
199
+ }}
200
+ />
201
+ )}
202
+ </Box>
203
+
204
+ {/* Connection label */}
205
+ <Typography
206
+ variant="caption"
207
+ sx={{
208
+ fontSize: '0.7rem',
209
+ fontWeight: 700,
210
+ color: isConnectingToE2B ? 'primary.main' : 'success.main',
211
+ whiteSpace: 'nowrap',
212
+ }}
213
+ >
214
+ {isConnectingToE2B ? 'Connecting' : 'Connected'}
215
+ </Typography>
216
+ </Box>
217
+ )}
218
+
219
+ {/* Render steps and insert final step at the right position */}
220
+ {steps.map((step, index) => (
221
+ <React.Fragment key={step.stepNumber}>
222
+ <Box
223
+ data-step={step.stepNumber}
224
+ onClick={() => handleStepClick(step.stepIndex, step.isCompleted, step.isCurrent)}
225
+ sx={{
226
+ display: 'flex',
227
+ flexDirection: 'column',
228
+ alignItems: 'center',
229
+ gap: 0.75,
230
+ minWidth: 40,
231
+ flexShrink: 0,
232
+ position: 'relative',
233
+ zIndex: 1,
234
+ cursor: (step.isCompleted || step.isCurrent) ? 'pointer' : 'default',
235
+ '&:hover': (step.isCompleted || step.isCurrent) ? {
236
+ '& .step-dot': {
237
+ transform: 'scale(1.15)',
238
+ },
239
+ } : {},
240
+ }}
241
+ >
242
+ {/* Cercle blanc en arrière-plan pour cacher la ligne */}
243
+ <Box
244
+ sx={{
245
+ position: 'relative',
246
+ display: 'flex',
247
+ alignItems: 'center',
248
+ justifyContent: 'center',
249
+ }}
250
+ >
251
+ {/* Fond blanc/background pour cacher la ligne */}
252
+ <Box
253
+ sx={{
254
+ position: 'absolute',
255
+ width: step.isCurrent || step.isSelected ? 28 : step.isCompleted ? 22 : 20,
256
+ height: step.isCurrent || step.isSelected ? 28 : step.isCompleted ? 22 : 20,
257
+ borderRadius: '50%',
258
+ backgroundColor: 'background.paper',
259
+ zIndex: 0,
260
+ }}
261
+ />
262
+
263
+ {/* Step dot */}
264
+ {step.isCurrent ? (
265
+ <CircularProgress
266
+ size={20}
267
+ thickness={5}
268
+ sx={{
269
+ color: 'primary.main',
270
+ position: 'relative',
271
+ zIndex: 1,
272
+ }}
273
+ />
274
+ ) : (
275
+ <Box
276
+ className="step-dot"
277
+ sx={{
278
+ width: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
279
+ height: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
280
+ borderRadius: '50%',
281
+ // Always keep steps in primary color (blue)
282
+ backgroundColor: step.isCompleted
283
+ ? 'primary.main' // Blue for completed steps
284
+ : (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.300', // Light grey for future steps
285
+ display: 'flex',
286
+ alignItems: 'center',
287
+ justifyContent: 'center',
288
+ transition: 'all 0.2s ease',
289
+ boxShadow: step.isCompleted || step.isSelected
290
+ ? step.isSelected
291
+ ? '0 0 8px rgba(255, 167, 38, 0.5)'
292
+ : '0 2px 4px rgba(0,0,0,0.1)'
293
+ : 'none',
294
+ position: 'relative',
295
+ zIndex: 1,
296
+ }}
297
+ />
298
+ )}
299
+ </Box>
300
+
301
+ {/* Step number - show for all steps */}
302
+ <Typography
303
+ variant="caption"
304
+ sx={{
305
+ fontSize: '0.7rem',
306
+ fontWeight: step.isCompleted || step.isCurrent || step.isSelected ? 700 : 400,
307
+ color: step.isCurrent
308
+ ? 'primary.main'
309
+ : (step.isCompleted || step.isSelected
310
+ ? 'text.primary'
311
+ : (theme) => theme.palette.mode === 'dark' ? 'grey.700' : 'grey.400'),
312
+ whiteSpace: 'nowrap',
313
+ }}
314
+ >
315
+ {step.stepNumber}
316
+ </Typography>
317
+ </Box>
318
+
319
+ {/* Insert final step indicator right after the last completed step */}
320
+ {finalStep && step.stepNumber === metadata.numberOfSteps && (
321
+ <Box
322
+ data-step="final"
323
+ onClick={handleFinalStepClick}
324
+ sx={{
325
+ display: 'flex',
326
+ flexDirection: 'column',
327
+ alignItems: 'center',
328
+ gap: 0.75,
329
+ minWidth: 50,
330
+ flexShrink: 0,
331
+ position: 'relative',
332
+ zIndex: 1,
333
+ cursor: 'pointer',
334
+ '&:hover': {
335
+ '& .final-step-icon': {
336
+ transform: 'scale(1.15)',
337
+ },
338
+ },
339
+ }}
340
+ >
341
+ {/* Cercle blanc en arrière-plan pour cacher la ligne */}
342
+ <Box
343
+ sx={{
344
+ position: 'relative',
345
+ display: 'flex',
346
+ alignItems: 'center',
347
+ justifyContent: 'center',
348
+ }}
349
+ >
350
+ {/* Fond blanc/background pour cacher la ligne */}
351
+ <Box
352
+ sx={{
353
+ position: 'absolute',
354
+ width: selectedStepIndex === null ? 32 : 28,
355
+ height: selectedStepIndex === null ? 32 : 28,
356
+ borderRadius: '50%',
357
+ backgroundColor: 'background.paper',
358
+ zIndex: 0,
359
+ }}
360
+ />
361
+
362
+ {/* Final step icon */}
363
+ <Box
364
+ className="final-step-icon"
365
+ sx={{
366
+ width: selectedStepIndex === null ? 24 : 20,
367
+ height: selectedStepIndex === null ? 24 : 20,
368
+ borderRadius: '50%',
369
+ backgroundColor: finalStep.type === 'success' ? 'success.main' : 'error.main',
370
+ display: 'flex',
371
+ alignItems: 'center',
372
+ justifyContent: 'center',
373
+ transition: 'all 0.2s ease',
374
+ boxShadow: selectedStepIndex === null
375
+ ? finalStep.type === 'success'
376
+ ? '0 2px 8px rgba(102, 187, 106, 0.4)'
377
+ : '0 2px 8px rgba(244, 67, 54, 0.4)'
378
+ : '0 2px 4px rgba(0,0,0,0.1)',
379
+ position: 'relative',
380
+ zIndex: 1,
381
+ }}
382
+ >
383
+ {finalStep.type === 'success' ? (
384
+ <CheckIcon sx={{ fontSize: 14, color: 'white' }} />
385
+ ) : (
386
+ <CloseIcon sx={{ fontSize: 14, color: 'white' }} />
387
+ )}
388
+ </Box>
389
+ </Box>
390
+
391
+ {/* Final step label */}
392
+ <Typography
393
+ variant="caption"
394
+ sx={{
395
+ fontSize: '0.7rem',
396
+ fontWeight: selectedStepIndex === null ? 700 : 500,
397
+ color: finalStep.type === 'success'
398
+ ? (selectedStepIndex === null ? 'text.primary' : 'text.secondary')
399
+ : 'error.main',
400
+ whiteSpace: 'nowrap',
401
+ }}
402
+ >
403
+ {finalStep.type === 'success' ? 'End' : 'Failed'}
404
+ </Typography>
405
+ </Box>
406
+ )}
407
+ </React.Fragment>
408
+ ))}
409
+ </Box>
410
+ </Box>
411
+ </Box>
412
+ );
413
+ };
cua2-front/src/components/timeline/index.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export { Timeline } from './Timeline';
cua2-front/src/config.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Application configuration
2
+ export const config = {
3
+ // WebSocket URL for backend connection
4
+ wsUrl: 'ws://localhost:8000/ws',
5
+
6
+ // API Base URL
7
+ apiBaseUrl: 'http://localhost:8000/api/v1',
8
+
9
+ // Default model (will be overridden by first available model from backend)
10
+ defaultModelId: 'Qwen/Qwen3-VL-8B-Instruct',
11
+ } as const;
cua2-front/src/hooks/index.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export { useAgentWebSocket } from './useAgentWebSocket';
2
+ export { useWebSocket } from './useWebSocket';
3
+ export { useSendTask } from './useSendTask';
4
+ export { useGifGenerator } from './useGifGenerator';
5
+ export { useJsonExporter } from './useJsonExporter';
cua2-front/src/hooks/useAgentWebSocket.ts ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect } from 'react';
2
+ import { useWebSocket } from './useWebSocket';
3
+ import { useAgentStore } from '@/stores/agentStore';
4
+ import { WebSocketEvent, AgentTrace, AgentStep } from '@/types/agent';
5
+ import { ulid } from 'ulid';
6
+
7
+ interface UseAgentWebSocketOptions {
8
+ url: string;
9
+ }
10
+
11
+ export const useAgentWebSocket = ({ url }: UseAgentWebSocketOptions) => {
12
+ const {
13
+ setTrace,
14
+ updateTraceWithStep,
15
+ completeTrace,
16
+ setIsAgentProcessing,
17
+ setIsConnectingToE2B,
18
+ setVncUrl,
19
+ setError,
20
+ setIsConnected,
21
+ selectedModelId,
22
+ resetAgent,
23
+ } = useAgentStore();
24
+
25
+ // Handle incoming WebSocket messages
26
+ const handleWebSocketMessage = useCallback(
27
+ (event: WebSocketEvent) => {
28
+ console.log('WebSocket event received:', event);
29
+
30
+ switch (event.type) {
31
+ case 'agent_start': {
32
+ // Clear previous state (especially finalStep)
33
+ resetAgent();
34
+
35
+ setIsAgentProcessing(true);
36
+ setIsConnectingToE2B(true); // Start connecting to E2B
37
+ setError(undefined); // Clear any previous error
38
+
39
+ // Ensure trace has proper metadata with default maxSteps if not provided
40
+ const traceWithMetadata = {
41
+ ...event.agentTrace,
42
+ traceMetadata: event.agentTrace.traceMetadata ? {
43
+ ...event.agentTrace.traceMetadata,
44
+ maxSteps: event.agentTrace.traceMetadata.maxSteps > 0
45
+ ? event.agentTrace.traceMetadata.maxSteps
46
+ : 200, // Default if backend sends 0
47
+ } : {
48
+ traceId: event.agentTrace.id,
49
+ inputTokensUsed: 0,
50
+ outputTokensUsed: 0,
51
+ duration: 0,
52
+ numberOfSteps: 0,
53
+ maxSteps: 200,
54
+ completed: false,
55
+ },
56
+ };
57
+
58
+ setTrace(traceWithMetadata);
59
+ console.log('Agent start received:', traceWithMetadata);
60
+ break;
61
+ }
62
+
63
+ case 'agent_progress':
64
+ // Add new step from agent trace run with image, generated text, actions, tokens and timestamp
65
+ setIsConnectingToE2B(false); // Connected! First step received
66
+ updateTraceWithStep(event.agentStep, event.traceMetadata);
67
+ console.log('Agent progress received:', event.agentStep);
68
+ break;
69
+
70
+ case 'agent_complete':
71
+ setIsAgentProcessing(false);
72
+ setIsConnectingToE2B(false);
73
+ completeTrace(event.traceMetadata);
74
+ console.log('Agent complete received:', event.traceMetadata);
75
+ break;
76
+
77
+ case 'agent_error':
78
+ setIsAgentProcessing(false);
79
+ setIsConnectingToE2B(false);
80
+ setError(event.error);
81
+ console.error('Agent error received:', event.error);
82
+ break;
83
+
84
+ case 'vnc_url_set':
85
+ setIsConnectingToE2B(false); // Connected! VNC URL received
86
+ setVncUrl(event.vncUrl);
87
+ console.log('VNC URL set received:', event.vncUrl);
88
+ break;
89
+
90
+ case 'vnc_url_unset':
91
+ setVncUrl('');
92
+ console.log('VNC URL unset received');
93
+ break;
94
+
95
+ case 'heartbeat':
96
+ console.log('Heartbeat received:', event);
97
+ break;
98
+ }
99
+ },
100
+ [setTrace, updateTraceWithStep, completeTrace, setIsAgentProcessing, setIsConnectingToE2B, setVncUrl, setError, resetAgent]
101
+ );
102
+
103
+ // Handle WebSocket errors
104
+ const handleWebSocketError = useCallback(() => {
105
+ // WebSocket Frontend Error handling
106
+ console.error('WebSocket connection error');
107
+ }, []);
108
+
109
+ // Initialize WebSocket connection
110
+ const { isConnected, connectionState, sendMessage, manualReconnect } = useWebSocket({
111
+ url,
112
+ onMessage: handleWebSocketMessage,
113
+ onError: handleWebSocketError,
114
+ });
115
+
116
+ // Sync connection state to store
117
+ useEffect(() => {
118
+ setIsConnected(isConnected);
119
+ }, [isConnected, setIsConnected]);
120
+
121
+ // Create a global sendNewTask function that can be called from anywhere
122
+ useEffect(() => {
123
+ // Store sendNewTask in window for global access
124
+ (window as Window & { __sendNewTask?: (instruction: string, modelId: string) => void }).__sendNewTask = (instruction: string, modelId: string) => {
125
+ // Reset agent state before starting a new task
126
+ resetAgent();
127
+
128
+ const traceId = ulid();
129
+ const trace: AgentTrace = {
130
+ id: traceId,
131
+ instruction,
132
+ modelId: modelId,
133
+ timestamp: new Date(),
134
+ isRunning: true,
135
+ traceMetadata: {
136
+ traceId: traceId,
137
+ inputTokensUsed: 0,
138
+ outputTokensUsed: 0,
139
+ duration: 0,
140
+ numberOfSteps: 0,
141
+ maxSteps: 200, // Default max steps, will be updated by backend
142
+ completed: false,
143
+ },
144
+ };
145
+
146
+ setTrace(trace);
147
+ setIsAgentProcessing(true);
148
+ setIsConnectingToE2B(true); // Start connecting when task is sent
149
+
150
+ // Send message to Python backend via WebSocket
151
+ sendMessage({
152
+ type: 'user_task',
153
+ trace: trace,
154
+ });
155
+
156
+ console.log('Task sent:', trace);
157
+ };
158
+ }, [setTrace, setIsAgentProcessing, setIsConnectingToE2B, sendMessage, resetAgent]);
159
+
160
+ return {
161
+ isConnected,
162
+ connectionState,
163
+ manualReconnect,
164
+ };
165
+ };
cua2-front/src/hooks/useGifGenerator.ts ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react';
2
+ import { generateGif, downloadGif, GifGenerationOptions } from '@/services/gifGenerator';
3
+ import { AgentStep } from '@/types/agent';
4
+
5
+ interface UseGifGeneratorOptions {
6
+ steps: AgentStep[];
7
+ traceId?: string;
8
+ }
9
+
10
+ interface UseGifGeneratorReturn {
11
+ isGenerating: boolean;
12
+ error: string | null;
13
+ generateAndDownloadGif: () => Promise<void>;
14
+ }
15
+
16
+ /**
17
+ * Custom hook to generate and download a GIF from trace steps
18
+ */
19
+ export const useGifGenerator = ({
20
+ steps,
21
+ traceId,
22
+ }: UseGifGeneratorOptions): UseGifGeneratorReturn => {
23
+ const [isGenerating, setIsGenerating] = useState(false);
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ const generateAndDownloadGif = useCallback(async () => {
27
+ if (!steps || steps.length === 0) {
28
+ setError('No steps available to generate GIF');
29
+ return;
30
+ }
31
+
32
+ setIsGenerating(true);
33
+ setError(null);
34
+
35
+ try {
36
+ // Extract images from steps
37
+ const images = steps
38
+ .map((step) => step.image)
39
+ .filter((image): image is string => !!image);
40
+
41
+ if (images.length === 0) {
42
+ setError('No images available in steps');
43
+ setIsGenerating(false);
44
+ return;
45
+ }
46
+
47
+ // Generate GIF with maximum dimensions of 400x200
48
+ const options: GifGenerationOptions = {
49
+ images,
50
+ interval: 1.5, // 1.5 seconds per frame
51
+ gifWidth: 400,
52
+ gifHeight: 200,
53
+ quality: 10, // Medium quality for good size/quality compromise
54
+ };
55
+
56
+ const result = await generateGif(options);
57
+
58
+ if (!result.success || !result.image) {
59
+ setError(result.error || 'Error generating GIF');
60
+ setIsGenerating(false);
61
+ return;
62
+ }
63
+
64
+ // Download the GIF
65
+ const filename = traceId
66
+ ? `trace-${traceId}-replay.gif`
67
+ : `trace-replay-${Date.now()}.gif`;
68
+
69
+ downloadGif(result.image, filename);
70
+ setIsGenerating(false);
71
+ } catch (err) {
72
+ setError(
73
+ err instanceof Error
74
+ ? err.message
75
+ : 'Unexpected error generating GIF'
76
+ );
77
+ setIsGenerating(false);
78
+ }
79
+ }, [steps, traceId]);
80
+
81
+ return {
82
+ isGenerating,
83
+ error,
84
+ generateAndDownloadGif,
85
+ };
86
+ };
cua2-front/src/hooks/useJsonExporter.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback } from 'react';
2
+ import { exportTraceToJson, downloadJson } from '@/services/jsonExporter';
3
+ import { AgentTrace, AgentStep, AgentTraceMetadata } from '@/types/agent';
4
+
5
+ interface UseJsonExporterOptions {
6
+ trace?: AgentTrace;
7
+ steps: AgentStep[];
8
+ metadata?: AgentTraceMetadata;
9
+ }
10
+
11
+ interface UseJsonExporterReturn {
12
+ downloadTraceAsJson: () => void;
13
+ }
14
+
15
+ /**
16
+ * Hook personnalisé pour exporter et télécharger une trace en JSON
17
+ */
18
+ export const useJsonExporter = ({
19
+ trace,
20
+ steps,
21
+ metadata,
22
+ }: UseJsonExporterOptions): UseJsonExporterReturn => {
23
+ const downloadTraceAsJson = useCallback(() => {
24
+ if (!trace) {
25
+ console.error('No trace available to export');
26
+ return;
27
+ }
28
+
29
+ try {
30
+ const jsonString = exportTraceToJson(trace, steps, metadata);
31
+ const filename = `trace-${trace.id}.json`;
32
+ downloadJson(jsonString, filename);
33
+ } catch (error) {
34
+ console.error('Error exporting trace to JSON:', error);
35
+ }
36
+ }, [trace, steps, metadata]);
37
+
38
+ return {
39
+ downloadTraceAsJson,
40
+ };
41
+ };
cua2-front/src/hooks/useSendTask.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback } from 'react';
2
+
3
+ export const useSendTask = () => {
4
+ const sendTask = useCallback((instruction: string, modelId: string) => {
5
+ const sendNewTask = (window as Window & { __sendNewTask?: (instruction: string, modelId: string) => void }).__sendNewTask;
6
+ if (sendNewTask) {
7
+ sendNewTask(instruction, modelId);
8
+ } else {
9
+ console.error('WebSocket not initialized');
10
+ }
11
+ }, []);
12
+
13
+ return sendTask;
14
+ };
cua2-front/src/index.css CHANGED
@@ -1,8 +1,4 @@
1
- * {
2
- margin: 0;
3
- padding: 0;
4
- box-sizing: border-box;
5
- }
6
 
7
  html, body {
8
  margin: 0;
@@ -10,11 +6,6 @@ html, body {
10
  height: 100%;
11
  width: 100%;
12
  overflow: hidden;
13
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
14
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
15
- sans-serif;
16
- -webkit-font-smoothing: antialiased;
17
- -moz-osx-font-smoothing: grayscale;
18
  }
19
 
20
  #root {
 
1
+ /* Global styles - Material UI handles most of the styling */
 
 
 
 
2
 
3
  html, body {
4
  margin: 0;
 
6
  height: 100%;
7
  width: 100%;
8
  overflow: hidden;
 
 
 
 
 
9
  }
10
 
11
  #root {
cua2-front/src/pages/Task.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useAgentStore, selectTrace, selectIsAgentProcessing, selectVncUrl, selectMetadata, selectSelectedStep } from '@/stores/agentStore';
4
+ import { Header, SandboxViewer, StepsList, Timeline } from '@/components';
5
+ import { Box } from '@mui/material';
6
+
7
+ const Task = () => {
8
+ const navigate = useNavigate();
9
+
10
+ // Get state from Zustand store
11
+ const trace = useAgentStore(selectTrace);
12
+ const isAgentProcessing = useAgentStore(selectIsAgentProcessing);
13
+ const vncUrl = useAgentStore(selectVncUrl);
14
+ const metadata = useAgentStore(selectMetadata);
15
+ const selectedStep = useAgentStore(selectSelectedStep);
16
+ const error = useAgentStore((state) => state.error);
17
+
18
+ // Redirect to home if no trace is present
19
+ useEffect(() => {
20
+ if (!trace) {
21
+ console.log('No trace found, redirecting to home...');
22
+ navigate('/', { replace: true });
23
+ }
24
+ }, [trace, navigate]);
25
+
26
+ // Handler for going back to home
27
+ const handleBackToHome = () => {
28
+ useAgentStore.getState().resetAgent();
29
+ navigate('/');
30
+ };
31
+
32
+ // Determine if we should show success/fail status (same logic as SandboxViewer)
33
+ const showStatus = !trace?.isRunning && !selectedStep && metadata && metadata.numberOfSteps > 0;
34
+
35
+ // Don't render anything if no trace (will redirect)
36
+ if (!trace) {
37
+ return null;
38
+ }
39
+
40
+ return (
41
+ <Box
42
+ sx={{
43
+ height: '100vh',
44
+ width: '100%',
45
+ display: 'flex',
46
+ flexDirection: 'column',
47
+ backgroundColor: 'background.default',
48
+ }}
49
+ >
50
+ {/* Header */}
51
+ <Header
52
+ isAgentProcessing={isAgentProcessing}
53
+ onBackToHome={handleBackToHome}
54
+ />
55
+
56
+ {/* Main Content */}
57
+ <Box
58
+ sx={{
59
+ flex: 1,
60
+ display: 'flex',
61
+ justifyContent: 'center',
62
+ alignItems: 'stretch',
63
+ minHeight: 0,
64
+ p: 0,
65
+ overflowY: 'auto',
66
+ overflowX: 'hidden',
67
+ }}
68
+ >
69
+ <Box
70
+ sx={{
71
+ width: '100%',
72
+ display: 'flex',
73
+ flexDirection: { xs: 'column', md: 'row' },
74
+ p: { xs: 2, md: 4 },
75
+ pb: { xs: 2, md: 3 },
76
+ }}
77
+ >
78
+ {/* Left Side: OS Stream + Metadata */}
79
+ <Box
80
+ sx={{
81
+ flex: 1,
82
+ display: 'flex',
83
+ flexDirection: 'column',
84
+ minWidth: 0,
85
+ pr: { xs: 0, md: 1.5 },
86
+ gap: { xs: 2, md: 3 },
87
+ overflow: 'visible',
88
+ }}
89
+ >
90
+ {/* Sandbox Viewer */}
91
+ <SandboxViewer
92
+ vncUrl={vncUrl}
93
+ isAgentProcessing={isAgentProcessing}
94
+ metadata={metadata}
95
+ traceStartTime={trace?.timestamp}
96
+ selectedStep={selectedStep}
97
+ isRunning={trace?.isRunning || false}
98
+ />
99
+
100
+ {/* Timeline - Always show, even with default values */}
101
+ <Timeline
102
+ metadata={metadata && metadata.maxSteps > 0 ? metadata : {
103
+ traceId: trace?.id || '',
104
+ inputTokensUsed: metadata?.inputTokensUsed || 0,
105
+ outputTokensUsed: metadata?.outputTokensUsed || 0,
106
+ duration: metadata?.duration || 0,
107
+ numberOfSteps: metadata?.numberOfSteps || 0,
108
+ maxSteps: 200, // Default max steps (will be updated by backend)
109
+ completed: metadata?.completed || false,
110
+ }}
111
+ isRunning={trace?.isRunning || false}
112
+ />
113
+ </Box>
114
+
115
+ {/* Right Side: Steps List */}
116
+ <StepsList trace={trace} />
117
+ </Box>
118
+ </Box>
119
+ </Box>
120
+ );
121
+ };
122
+
123
+ export default Task;
cua2-front/src/pages/Welcome.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useSendTask } from '@/hooks/useSendTask';
4
+ import { Box } from '@mui/material';
5
+ import { WelcomeScreen } from '@/components';
6
+ import { useAgentStore, selectIsConnected } from '@/stores/agentStore';
7
+
8
+ const Welcome = () => {
9
+ const navigate = useNavigate();
10
+ const isConnected = useAgentStore(selectIsConnected);
11
+ const sendTask = useSendTask();
12
+
13
+ const handleSendNewTask = (instruction: string, modelId: string) => {
14
+ sendTask(instruction, modelId);
15
+ // Navigate to task page after starting task
16
+ navigate('/task');
17
+ };
18
+
19
+ return (
20
+ <Box
21
+ sx={{
22
+ height: '100vh',
23
+ width: '100%',
24
+ display: 'flex',
25
+ flexDirection: 'column',
26
+ backgroundColor: 'background.default',
27
+ position: 'relative',
28
+ }}
29
+ >
30
+ <WelcomeScreen onStartTask={handleSendNewTask} isConnected={isConnected} />
31
+ </Box>
32
+ );
33
+ };
34
+
35
+ export default Welcome;
cua2-front/src/services/api.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { config } from '@/config';
2
+
3
+ /**
4
+ * Fetch available models from the backend
5
+ */
6
+ export async function fetchAvailableModels(): Promise<string[]> {
7
+ const response = await fetch(`${config.apiBaseUrl}/models`);
8
+ if (!response.ok) {
9
+ throw new Error('Failed to fetch models');
10
+ }
11
+ const data = await response.json();
12
+ return data.models;
13
+ }
14
+
15
+ /**
16
+ * Generate a random instruction from the backend
17
+ */
18
+ export async function generateRandomQuestion(modelId: string): Promise<string> {
19
+ const response = await fetch(`${config.apiBaseUrl}/generate-instruction`, {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ },
24
+ body: JSON.stringify({
25
+ model_id: modelId,
26
+ }),
27
+ });
28
+ if (!response.ok) {
29
+ throw new Error('Failed to generate instruction');
30
+ }
31
+ const data = await response.json();
32
+ return data.instruction;
33
+ }
34
+
35
+ /**
36
+ * Update step evaluation (vote)
37
+ */
38
+ export async function updateStepEvaluation(
39
+ traceId: string,
40
+ stepId: string,
41
+ evaluation: 'like' | 'dislike' | 'neutral'
42
+ ): Promise<void> {
43
+ const response = await fetch(`${config.apiBaseUrl}/traces/${traceId}/steps/${stepId}`, {
44
+ method: 'PATCH',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ },
48
+ body: JSON.stringify({
49
+ step_evaluation: evaluation,
50
+ }),
51
+ });
52
+
53
+ if (!response.ok) {
54
+ throw new Error('Failed to update step evaluation');
55
+ }
56
+ }
cua2-front/src/services/gifGenerator.ts ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gifshot from 'gifshot';
2
+
3
+ export interface GifGenerationOptions {
4
+ images: string[];
5
+ interval?: number; // Durée de chaque frame en secondes
6
+ gifWidth?: number;
7
+ gifHeight?: number;
8
+ quality?: number;
9
+ }
10
+
11
+ export interface GifGenerationResult {
12
+ success: boolean;
13
+ image?: string; // Data URL du GIF
14
+ error?: string;
15
+ }
16
+
17
+ /**
18
+ * Ajoute un compteur d'étapes sur une image
19
+ * @param imageSrc Source de l'image (base64 ou URL)
20
+ * @param stepNumber Numéro de l'étape
21
+ * @param totalSteps Nombre total d'étapes
22
+ * @param width Largeur de l'image
23
+ * @param height Hauteur de l'image
24
+ * @returns Promesse résolue avec l'image modifiée en base64
25
+ */
26
+ const addStepCounter = async (
27
+ imageSrc: string,
28
+ stepNumber: number,
29
+ totalSteps: number,
30
+ width: number,
31
+ height: number
32
+ ): Promise<string> => {
33
+ return new Promise((resolve, reject) => {
34
+ const img = new Image();
35
+ img.crossOrigin = 'anonymous';
36
+
37
+ img.onload = () => {
38
+ const canvas = document.createElement('canvas');
39
+ canvas.width = width;
40
+ canvas.height = height;
41
+ const ctx = canvas.getContext('2d');
42
+
43
+ if (!ctx) {
44
+ reject(new Error('Cannot get canvas context'));
45
+ return;
46
+ }
47
+
48
+ // Dessiner l'image
49
+ ctx.drawImage(img, 0, 0, width, height);
50
+
51
+ // Configurer le style du compteur
52
+ const fontSize = Math.max(12, Math.floor(height * 0.08));
53
+ const padding = Math.max(6, Math.floor(height * 0.03));
54
+ const text = `${stepNumber}/${totalSteps}`;
55
+
56
+ ctx.font = `bold ${fontSize}px Arial, sans-serif`;
57
+ const textMetrics = ctx.measureText(text);
58
+ const textWidth = textMetrics.width;
59
+ const textHeight = fontSize;
60
+
61
+ // Position en bas à droite
62
+ const x = width - textWidth - padding * 2;
63
+ const y = height - padding * 2;
64
+
65
+ // Dessiner un rectangle semi-transparent pour la lisibilité
66
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
67
+ ctx.fillRect(
68
+ x - padding,
69
+ y - textHeight - padding,
70
+ textWidth + padding * 2,
71
+ textHeight + padding * 2
72
+ );
73
+
74
+ // Dessiner le texte en noir
75
+ ctx.fillStyle = '#000000';
76
+ ctx.textBaseline = 'top';
77
+ ctx.fillText(text, x, y - textHeight);
78
+
79
+ // Convertir le canvas en base64
80
+ resolve(canvas.toDataURL('image/png'));
81
+ };
82
+
83
+ img.onerror = () => {
84
+ reject(new Error('Failed to load image'));
85
+ };
86
+
87
+ img.src = imageSrc;
88
+ });
89
+ };
90
+
91
+ /**
92
+ * Génère un GIF à partir d'une liste d'images (base64 ou URLs)
93
+ * @param options Options de génération du GIF
94
+ * @returns Promesse résolue avec le résultat de la génération
95
+ */
96
+ export const generateGif = async (
97
+ options: GifGenerationOptions
98
+ ): Promise<GifGenerationResult> => {
99
+ const {
100
+ images,
101
+ interval = 1.5, // 1.5 secondes par frame par défaut
102
+ gifWidth = 400,
103
+ gifHeight = 200,
104
+ quality = 10,
105
+ } = options;
106
+
107
+ if (!images || images.length === 0) {
108
+ return {
109
+ success: false,
110
+ error: 'Aucune image fournie pour générer le GIF',
111
+ };
112
+ }
113
+
114
+ try {
115
+ // Ajouter le compteur sur chaque image
116
+ const imagesWithCounter = await Promise.all(
117
+ images.map((img, index) =>
118
+ addStepCounter(img, index + 1, images.length, gifWidth, gifHeight)
119
+ )
120
+ );
121
+
122
+ return new Promise((resolve) => {
123
+ gifshot.createGIF(
124
+ {
125
+ images: imagesWithCounter,
126
+ interval,
127
+ gifWidth,
128
+ gifHeight,
129
+ numFrames: imagesWithCounter.length,
130
+ frameDuration: interval,
131
+ sampleInterval: quality,
132
+ },
133
+ (obj: { error: boolean; errorMsg?: string; image?: string }) => {
134
+ if (obj.error) {
135
+ resolve({
136
+ success: false,
137
+ error: obj.errorMsg || 'Erreur lors de la génération du GIF',
138
+ });
139
+ } else {
140
+ resolve({
141
+ success: true,
142
+ image: obj.image,
143
+ });
144
+ }
145
+ }
146
+ );
147
+ });
148
+ } catch (error) {
149
+ return {
150
+ success: false,
151
+ error: error instanceof Error ? error.message : 'Erreur inconnue',
152
+ };
153
+ }
154
+ };
155
+
156
+ /**
157
+ * Télécharge un GIF (data URL) avec un nom de fichier
158
+ * @param dataUrl Data URL du GIF
159
+ * @param filename Nom du fichier à télécharger
160
+ */
161
+ export const downloadGif = (dataUrl: string, filename: string = 'trace-replay.gif') => {
162
+ const link = document.createElement('a');
163
+ link.href = dataUrl;
164
+ link.download = filename;
165
+ document.body.appendChild(link);
166
+ link.click();
167
+ document.body.removeChild(link);
168
+ };
cua2-front/src/services/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export { api } from './api';
2
+ export * from './gifGenerator';
3
+ export * from './jsonExporter';
cua2-front/src/services/jsonExporter.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentTrace, AgentStep, AgentTraceMetadata } from '@/types/agent';
2
+
3
+ /**
4
+ * Export the complete trace as JSON
5
+ * @param trace The agent trace
6
+ * @param steps The trace steps
7
+ * @param metadata The final metadata
8
+ * @returns A JSON object containing the entire trace
9
+ */
10
+ export const exportTraceToJson = (
11
+ trace: AgentTrace,
12
+ steps: AgentStep[],
13
+ metadata?: AgentTraceMetadata
14
+ ): string => {
15
+ const exportData = {
16
+ trace: {
17
+ id: trace.id,
18
+ timestamp: trace.timestamp,
19
+ instruction: trace.instruction,
20
+ modelId: trace.modelId,
21
+ isRunning: trace.isRunning,
22
+ },
23
+ metadata: metadata || trace.traceMetadata,
24
+ steps: steps.map((step) => ({
25
+ traceId: step.traceId,
26
+ stepId: step.stepId,
27
+ error: step.error,
28
+ thought: step.thought,
29
+ actions: step.actions,
30
+ duration: step.duration,
31
+ inputTokensUsed: step.inputTokensUsed,
32
+ outputTokensUsed: step.outputTokensUsed,
33
+ step_evaluation: step.step_evaluation,
34
+ // Ne pas inclure l'image base64 pour réduire la taille du JSON
35
+ hasImage: !!step.image,
36
+ })),
37
+ exportedAt: new Date().toISOString(),
38
+ };
39
+
40
+ return JSON.stringify(exportData, null, 2);
41
+ };
42
+
43
+ /**
44
+ * Télécharge un JSON avec un nom de fichier
45
+ * @param jsonString String JSON à télécharger
46
+ * @param filename Nom du fichier à télécharger
47
+ */
48
+ export const downloadJson = (jsonString: string, filename: string = 'trace.json') => {
49
+ const blob = new Blob([jsonString], { type: 'application/json' });
50
+ const url = URL.createObjectURL(blob);
51
+ const link = document.createElement('a');
52
+ link.href = url;
53
+ link.download = filename;
54
+ document.body.appendChild(link);
55
+ link.click();
56
+ document.body.removeChild(link);
57
+ URL.revokeObjectURL(url);
58
+ };
cua2-front/src/stores/agentStore.ts ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { create } from 'zustand';
2
+ import { devtools } from 'zustand/middleware';
3
+ import { AgentTrace, AgentStep, AgentTraceMetadata, FinalStep } from '@/types/agent';
4
+
5
+ interface AgentState {
6
+ // State
7
+ trace?: AgentTrace;
8
+ isAgentProcessing: boolean;
9
+ isConnectingToE2B: boolean; // New state for E2B connection
10
+ vncUrl: string;
11
+ selectedModelId: string;
12
+ availableModels: string[];
13
+ isLoadingModels: boolean;
14
+ isConnected: boolean;
15
+ error?: string;
16
+ isDarkMode: boolean;
17
+ selectedStepIndex: number | null; // null = live mode, number = viewing specific step or 'final'
18
+ finalStep?: FinalStep; // Special step for success/failure
19
+
20
+ // Actions
21
+ setTrace: (trace: AgentTrace | undefined) => void;
22
+ updateTraceWithStep: (step: AgentStep, metadata: AgentTraceMetadata) => void;
23
+ completeTrace: (metadata: AgentTraceMetadata) => void;
24
+ setIsAgentProcessing: (processing: boolean) => void;
25
+ setIsConnectingToE2B: (connecting: boolean) => void;
26
+ setVncUrl: (url: string) => void;
27
+ setSelectedModelId: (modelId: string) => void;
28
+ setAvailableModels: (models: string[]) => void;
29
+ setIsLoadingModels: (loading: boolean) => void;
30
+ setIsConnected: (connected: boolean) => void;
31
+ setError: (error: string | undefined) => void;
32
+ setSelectedStepIndex: (index: number | null) => void;
33
+ toggleDarkMode: () => void;
34
+ resetAgent: () => void;
35
+ }
36
+
37
+ const initialState = {
38
+ trace: undefined,
39
+ isAgentProcessing: false,
40
+ isConnectingToE2B: false,
41
+ vncUrl: '',
42
+ selectedModelId: 'Qwen/Qwen3-VL-8B-Instruct',
43
+ availableModels: [],
44
+ isLoadingModels: false,
45
+ isConnected: false,
46
+ error: undefined,
47
+ isDarkMode: false,
48
+ selectedStepIndex: null, // null = live mode
49
+ finalStep: undefined,
50
+ };
51
+
52
+ export const useAgentStore = create<AgentState>()(
53
+ devtools(
54
+ (set) => ({
55
+ ...initialState,
56
+
57
+ // Set the complete trace
58
+ setTrace: (trace) =>
59
+ set({ trace }, false, 'setTrace'),
60
+
61
+ // Update trace with a new step
62
+ updateTraceWithStep: (step, metadata) =>
63
+ set(
64
+ (state) => {
65
+ if (!state.trace) return state;
66
+
67
+ const existingSteps = state.trace.steps || [];
68
+ const stepExists = existingSteps.some((s) => s.stepId === step.stepId);
69
+
70
+ if (stepExists) return state;
71
+
72
+ // Preserve existing maxSteps if new metadata has 0
73
+ const updatedMetadata = {
74
+ ...metadata,
75
+ maxSteps: metadata.maxSteps > 0
76
+ ? metadata.maxSteps
77
+ : (state.trace.traceMetadata?.maxSteps || 200),
78
+ };
79
+
80
+ return {
81
+ trace: {
82
+ ...state.trace,
83
+ steps: [...existingSteps, step],
84
+ traceMetadata: updatedMetadata,
85
+ isRunning: true,
86
+ },
87
+ };
88
+ },
89
+ false,
90
+ 'updateTraceWithStep'
91
+ ),
92
+
93
+ // Complete the trace
94
+ completeTrace: (metadata) =>
95
+ set(
96
+ (state) => {
97
+ if (!state.trace) return state;
98
+
99
+ // Preserve existing maxSteps if new metadata has 0
100
+ const updatedMetadata = {
101
+ ...metadata,
102
+ maxSteps: metadata.maxSteps > 0
103
+ ? metadata.maxSteps
104
+ : (state.trace.traceMetadata?.maxSteps || 200),
105
+ completed: true,
106
+ };
107
+
108
+ // Determine if the task succeeded or failed based on error state
109
+ const finalStep: FinalStep = {
110
+ type: state.error ? 'failure' : 'success',
111
+ message: state.error,
112
+ metadata: updatedMetadata,
113
+ };
114
+
115
+ return {
116
+ trace: {
117
+ ...state.trace,
118
+ isRunning: false,
119
+ traceMetadata: updatedMetadata,
120
+ },
121
+ finalStep,
122
+ // Keep error in state for display
123
+ selectedStepIndex: null, // Reset to live mode on completion
124
+ };
125
+ },
126
+ false,
127
+ 'completeTrace'
128
+ ),
129
+
130
+ // Set processing state
131
+ setIsAgentProcessing: (isAgentProcessing) =>
132
+ set({ isAgentProcessing }, false, 'setIsAgentProcessing'),
133
+
134
+ // Set E2B connection state
135
+ setIsConnectingToE2B: (isConnectingToE2B) =>
136
+ set({ isConnectingToE2B }, false, 'setIsConnectingToE2B'),
137
+
138
+ // Set VNC URL
139
+ setVncUrl: (vncUrl) =>
140
+ set({ vncUrl }, false, 'setVncUrl'),
141
+
142
+ // Set selected model ID
143
+ setSelectedModelId: (selectedModelId) =>
144
+ set({ selectedModelId }, false, 'setSelectedModelId'),
145
+
146
+ // Set available models
147
+ setAvailableModels: (availableModels) =>
148
+ set({ availableModels }, false, 'setAvailableModels'),
149
+
150
+ // Set loading models state
151
+ setIsLoadingModels: (isLoadingModels) =>
152
+ set({ isLoadingModels }, false, 'setIsLoadingModels'),
153
+
154
+ // Set connection status
155
+ setIsConnected: (isConnected) =>
156
+ set({ isConnected }, false, 'setIsConnected'),
157
+
158
+ // Set error
159
+ setError: (error) =>
160
+ set(
161
+ (state) => {
162
+ // If there's an error and a trace, mark it as failed
163
+ if (error && state.trace) {
164
+ const metadata = state.trace.traceMetadata || {
165
+ traceId: state.trace.id,
166
+ inputTokensUsed: 0,
167
+ outputTokensUsed: 0,
168
+ duration: 0,
169
+ numberOfSteps: state.trace.steps?.length || 0,
170
+ maxSteps: 200,
171
+ completed: false,
172
+ };
173
+
174
+ // Ensure maxSteps is not 0
175
+ const finalMetadata = {
176
+ ...metadata,
177
+ maxSteps: metadata.maxSteps > 0 ? metadata.maxSteps : 200,
178
+ };
179
+
180
+ const finalStep: FinalStep = {
181
+ type: 'failure',
182
+ message: error,
183
+ metadata: finalMetadata,
184
+ };
185
+
186
+ return {
187
+ error,
188
+ finalStep,
189
+ trace: {
190
+ ...state.trace,
191
+ isRunning: false,
192
+ },
193
+ selectedStepIndex: null, // Reset to live mode on error
194
+ };
195
+ }
196
+ return { error };
197
+ },
198
+ false,
199
+ 'setError'
200
+ ),
201
+
202
+ // Set selected step index for time travel
203
+ setSelectedStepIndex: (selectedStepIndex) =>
204
+ set({ selectedStepIndex }, false, 'setSelectedStepIndex'),
205
+
206
+ // Toggle dark mode
207
+ toggleDarkMode: () =>
208
+ set((state) => ({ isDarkMode: !state.isDarkMode }), false, 'toggleDarkMode'),
209
+
210
+ // Reset agent state
211
+ resetAgent: () =>
212
+ set((state) => ({
213
+ ...initialState,
214
+ isDarkMode: state.isDarkMode, // Keep dark mode preference
215
+ isConnected: state.isConnected, // Keep connection status
216
+ selectedModelId: state.selectedModelId, // Keep selected model
217
+ availableModels: state.availableModels, // Keep available models
218
+ isLoadingModels: state.isLoadingModels // Keep loading state
219
+ }), false, 'resetAgent'),
220
+ }),
221
+ { name: 'AgentStore' }
222
+ )
223
+ );
224
+
225
+ // Selectors for better performance
226
+ export const selectTrace = (state: AgentState) => state.trace;
227
+ export const selectIsAgentProcessing = (state: AgentState) => state.isAgentProcessing;
228
+ export const selectIsConnectingToE2B = (state: AgentState) => state.isConnectingToE2B;
229
+ export const selectVncUrl = (state: AgentState) => state.vncUrl;
230
+ export const selectSelectedModelId = (state: AgentState) => state.selectedModelId;
231
+ export const selectAvailableModels = (state: AgentState) => state.availableModels;
232
+ export const selectIsLoadingModels = (state: AgentState) => state.isLoadingModels;
233
+ export const selectIsConnected = (state: AgentState) => state.isConnected;
234
+ export const selectSteps = (state: AgentState) => state.trace?.steps;
235
+ export const selectMetadata = (state: AgentState) => state.trace?.traceMetadata;
236
+ export const selectError = (state: AgentState) => state.error;
237
+ export const selectIsDarkMode = (state: AgentState) => state.isDarkMode;
238
+ export const selectSelectedStepIndex = (state: AgentState) => state.selectedStepIndex;
239
+ export const selectFinalStep = (state: AgentState) => state.finalStep;
240
+
241
+ // Composite selector for selected step (avoids infinite loops)
242
+ export const selectSelectedStep = (state: AgentState) => {
243
+ const steps = state.trace?.steps;
244
+ const selectedIndex = state.selectedStepIndex;
245
+
246
+ if (selectedIndex === null || !steps || selectedIndex >= steps.length) {
247
+ return null;
248
+ }
249
+
250
+ return steps[selectedIndex];
251
+ };
cua2-front/src/theme.ts ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createTheme, alpha, Theme } from "@mui/material/styles";
2
+
3
+ const getDesignTokens = (mode: 'light' | 'dark') => ({
4
+ typography: {
5
+ fontFamily: [
6
+ "-apple-system",
7
+ "BlinkMacSystemFont",
8
+ '"Segoe UI"',
9
+ "Roboto",
10
+ '"Helvetica Neue"',
11
+ "Arial",
12
+ "sans-serif",
13
+ ].join(","),
14
+ h1: {
15
+ fontFamily: '"Source Sans Pro", sans-serif',
16
+ },
17
+ h2: {
18
+ fontFamily: '"Source Sans Pro", sans-serif',
19
+ },
20
+ h3: {
21
+ fontFamily: '"Source Sans Pro", sans-serif',
22
+ },
23
+ h4: {
24
+ fontFamily: '"Source Sans Pro", sans-serif',
25
+ },
26
+ h5: {
27
+ fontFamily: '"Source Sans Pro", sans-serif',
28
+ },
29
+ h6: {
30
+ fontFamily: '"Source Sans Pro", sans-serif',
31
+ },
32
+ subtitle1: {
33
+ fontFamily: '"Source Sans Pro", sans-serif',
34
+ },
35
+ subtitle2: {
36
+ fontFamily: '"Source Sans Pro", sans-serif',
37
+ },
38
+ },
39
+ palette: {
40
+ mode,
41
+ primary: {
42
+ main: "#4F86C6",
43
+ light: mode === "light" ? "#7BA7D7" : "#6B97D7",
44
+ dark: mode === "light" ? "#2B5C94" : "#3B6CA4",
45
+ 50: mode === "light" ? alpha("#4F86C6", 0.05) : alpha("#4F86C6", 0.15),
46
+ 100: mode === "light" ? alpha("#4F86C6", 0.1) : alpha("#4F86C6", 0.2),
47
+ 200: mode === "light" ? alpha("#4F86C6", 0.2) : alpha("#4F86C6", 0.3),
48
+ contrastText: "#fff",
49
+ },
50
+ background: {
51
+ default: mode === "light" ? "#f8f9fa" : "#0a0a0a",
52
+ paper: mode === "light" ? "#fff" : "#1a1a1a",
53
+ subtle: mode === "light" ? "grey.100" : "grey.900",
54
+ hover: mode === "light" ? "action.hover" : alpha("#fff", 0.08),
55
+ tooltip: mode === "light" ? alpha("#212121", 0.9) : alpha("#fff", 0.9),
56
+ },
57
+ text: {
58
+ primary: mode === "light" ? "rgba(0, 0, 0, 0.87)" : "#fff",
59
+ secondary:
60
+ mode === "light" ? "rgba(0, 0, 0, 0.6)" : "rgba(255, 255, 255, 0.7)",
61
+ disabled:
62
+ mode === "light" ? "rgba(0, 0, 0, 0.38)" : "rgba(255, 255, 255, 0.5)",
63
+ hint:
64
+ mode === "light" ? "rgba(0, 0, 0, 0.38)" : "rgba(255, 255, 255, 0.5)",
65
+ },
66
+ divider:
67
+ mode === "light" ? "rgba(0, 0, 0, 0.15)" : "rgba(255, 255, 255, 0.18)",
68
+ action: {
69
+ active:
70
+ mode === "light" ? "rgba(0, 0, 0, 0.54)" : "rgba(255, 255, 255, 0.7)",
71
+ hover:
72
+ mode === "light" ? "rgba(0, 0, 0, 0.04)" : "rgba(255, 255, 255, 0.08)",
73
+ selected:
74
+ mode === "light" ? "rgba(0, 0, 0, 0.08)" : "rgba(255, 255, 255, 0.16)",
75
+ disabled:
76
+ mode === "light" ? "rgba(0, 0, 0, 0.26)" : "rgba(255, 255, 255, 0.3)",
77
+ disabledBackground:
78
+ mode === "light" ? "rgba(0, 0, 0, 0.12)" : "rgba(255, 255, 255, 0.12)",
79
+ },
80
+ },
81
+ shape: {
82
+ borderRadius: 6,
83
+ },
84
+ components: {
85
+ MuiCssBaseline: {
86
+ styleOverrides: {
87
+ "html, body": {
88
+ backgroundColor: "background.default",
89
+ color: mode === "dark" ? "#fff" : "#000",
90
+ },
91
+ body: {
92
+ "& *::-webkit-scrollbar": {
93
+ width: 8,
94
+ height: 8,
95
+ backgroundColor: "transparent",
96
+ },
97
+ "& *::-webkit-scrollbar-thumb": {
98
+ borderRadius: 6,
99
+ backgroundColor:
100
+ mode === "light" ? alpha("#000", 0.2) : alpha("#fff", 0.1),
101
+ "&:hover": {
102
+ backgroundColor:
103
+ mode === "light" ? alpha("#000", 0.3) : alpha("#fff", 0.15),
104
+ },
105
+ },
106
+ },
107
+ },
108
+ },
109
+ MuiButton: {
110
+ styleOverrides: {
111
+ root: {
112
+ borderRadius: 6,
113
+ textTransform: 'none',
114
+ fontWeight: 600,
115
+ },
116
+ },
117
+ },
118
+ MuiPaper: {
119
+ defaultProps: {
120
+ elevation: 0,
121
+ },
122
+ styleOverrides: {
123
+ root: {
124
+ backgroundImage: "none",
125
+ boxShadow: "none",
126
+ border: "1px solid",
127
+ borderColor:
128
+ mode === "light"
129
+ ? "rgba(0, 0, 0, 0.15)!important"
130
+ : "rgba(255, 255, 255, 0.18)!important",
131
+ },
132
+ rounded: {
133
+ borderRadius: 10,
134
+ },
135
+ },
136
+ },
137
+
138
+ MuiTableCell: {
139
+ styleOverrides: {
140
+ root: {
141
+ borderColor: (theme: Theme) =>
142
+ alpha(
143
+ theme.palette.divider,
144
+ theme.palette.mode === "dark" ? 0.1 : 0.2
145
+ ),
146
+ },
147
+ head: {
148
+ backgroundColor: mode === "light" ? "grey.50" : "grey.900",
149
+ color: "text.primary",
150
+ fontWeight: 600,
151
+ },
152
+ },
153
+ },
154
+ MuiTableRow: {
155
+ styleOverrides: {
156
+ root: {
157
+ backgroundColor: "transparent",
158
+ },
159
+ },
160
+ },
161
+ MuiTableContainer: {
162
+ styleOverrides: {
163
+ root: {
164
+ backgroundColor: "background.paper",
165
+ borderRadius: 6,
166
+ border: "none",
167
+ boxShadow: "none",
168
+ },
169
+ },
170
+ },
171
+ MuiSlider: {
172
+ styleOverrides: {
173
+ root: {
174
+ "& .MuiSlider-valueLabel": {
175
+ backgroundColor: "background.paper",
176
+ color: "text.primary",
177
+ border: "1px solid",
178
+ borderColor: "divider",
179
+ boxShadow:
180
+ mode === "light"
181
+ ? "0px 2px 4px rgba(0, 0, 0, 0.1)"
182
+ : "0px 2px 4px rgba(0, 0, 0, 0.3)",
183
+ },
184
+ },
185
+ thumb: {
186
+ "&:hover": {
187
+ boxShadow: (theme: Theme) =>
188
+ `0px 0px 0px 8px ${alpha(
189
+ theme.palette.primary.main,
190
+ mode === "light" ? 0.08 : 0.16
191
+ )}`,
192
+ },
193
+ "&.Mui-active": {
194
+ boxShadow: (theme: Theme) =>
195
+ `0px 0px 0px 12px ${alpha(
196
+ theme.palette.primary.main,
197
+ mode === "light" ? 0.08 : 0.16
198
+ )}`,
199
+ },
200
+ },
201
+ track: {
202
+ border: "none",
203
+ },
204
+ rail: {
205
+ opacity: mode === "light" ? 0.38 : 0.3,
206
+ },
207
+ mark: {
208
+ backgroundColor: mode === "light" ? "grey.400" : "grey.600",
209
+ },
210
+ markLabel: {
211
+ color: "text.secondary",
212
+ },
213
+ },
214
+ },
215
+ MuiTextField: {
216
+ styleOverrides: {
217
+ root: {
218
+ "& .MuiOutlinedInput-root": {
219
+ borderRadius: 6,
220
+ },
221
+ },
222
+ },
223
+ },
224
+ MuiChip: {
225
+ styleOverrides: {
226
+ root: {
227
+ borderRadius: 6,
228
+ fontWeight: 600,
229
+ },
230
+ outlinedInfo: {
231
+ borderWidth: 2,
232
+ fontWeight: 600,
233
+ bgcolor: "info.100",
234
+ borderColor: "info.400",
235
+ color: "info.700",
236
+ "& .MuiChip-label": {
237
+ px: 1.2,
238
+ },
239
+ "&:hover": {
240
+ bgcolor: "info.200",
241
+ },
242
+ },
243
+ outlinedWarning: {
244
+ borderWidth: 2,
245
+ fontWeight: 600,
246
+ bgcolor: "warning.100",
247
+ borderColor: "warning.400",
248
+ color: "warning.700",
249
+ "& .MuiChip-label": {
250
+ px: 1.2,
251
+ },
252
+ "&:hover": {
253
+ bgcolor: "warning.200",
254
+ },
255
+ },
256
+ outlinedSuccess: {
257
+ borderWidth: 2,
258
+ fontWeight: 600,
259
+ bgcolor: "success.100",
260
+ borderColor: "success.400",
261
+ color: "success.700",
262
+ "& .MuiChip-label": {
263
+ px: 1.2,
264
+ },
265
+ "&:hover": {
266
+ bgcolor: "success.200",
267
+ },
268
+ },
269
+ outlinedError: {
270
+ borderWidth: 2,
271
+ fontWeight: 600,
272
+ bgcolor: "error.100",
273
+ borderColor: "error.400",
274
+ color: "error.700",
275
+ "& .MuiChip-label": {
276
+ px: 1.2,
277
+ },
278
+ "&:hover": {
279
+ bgcolor: "error.200",
280
+ },
281
+ },
282
+ outlinedPrimary: {
283
+ borderWidth: 2,
284
+ fontWeight: 600,
285
+ bgcolor: "primary.100",
286
+ borderColor: "primary.400",
287
+ color: "primary.700",
288
+ "& .MuiChip-label": {
289
+ px: 1.2,
290
+ },
291
+ "&:hover": {
292
+ bgcolor: "primary.200",
293
+ },
294
+ },
295
+ outlinedSecondary: {
296
+ borderWidth: 2,
297
+ fontWeight: 600,
298
+ bgcolor: "secondary.100",
299
+ borderColor: "secondary.400",
300
+ color: "secondary.700",
301
+ "& .MuiChip-label": {
302
+ px: 1.2,
303
+ },
304
+ "&:hover": {
305
+ bgcolor: "secondary.200",
306
+ },
307
+ },
308
+ },
309
+ },
310
+ MuiIconButton: {
311
+ styleOverrides: {
312
+ root: {
313
+ borderRadius: 6,
314
+ padding: "8px",
315
+ "&.MuiIconButton-sizeSmall": {
316
+ padding: "4px",
317
+ borderRadius: 4,
318
+ },
319
+ },
320
+ },
321
+ },
322
+ MuiTooltip: {
323
+ styleOverrides: {
324
+ tooltip: {
325
+ backgroundColor:
326
+ mode === "light" ? alpha("#212121", 0.9) : alpha("#424242", 0.9),
327
+ color: "#fff",
328
+ fontSize: "0.875rem",
329
+ padding: "8px 12px",
330
+ maxWidth: 400,
331
+ borderRadius: 6,
332
+ lineHeight: 1.4,
333
+ border: "1px solid",
334
+ borderColor:
335
+ mode === "light" ? alpha("#fff", 0.1) : alpha("#fff", 0.05),
336
+ boxShadow:
337
+ mode === "light"
338
+ ? "0 2px 8px rgba(0, 0, 0, 0.15)"
339
+ : "0 2px 8px rgba(0, 0, 0, 0.5)",
340
+ "& b": {
341
+ fontWeight: 600,
342
+ color: "inherit",
343
+ },
344
+ "& a": {
345
+ color: mode === "light" ? "#90caf9" : "#64b5f6",
346
+ textDecoration: "none",
347
+ "&:hover": {
348
+ textDecoration: "underline",
349
+ },
350
+ },
351
+ },
352
+ arrow: {
353
+ color:
354
+ mode === "light" ? alpha("#212121", 0.9) : alpha("#424242", 0.9),
355
+ "&:before": {
356
+ border: "1px solid",
357
+ borderColor:
358
+ mode === "light" ? alpha("#fff", 0.1) : alpha("#fff", 0.05),
359
+ },
360
+ },
361
+ },
362
+ defaultProps: {
363
+ arrow: true,
364
+ enterDelay: 400,
365
+ leaveDelay: 200,
366
+ },
367
+ },
368
+ MuiAppBar: {
369
+ styleOverrides: {
370
+ root: {
371
+ border: "none",
372
+ borderBottom: "none",
373
+ },
374
+ },
375
+ },
376
+ },
377
+ breakpoints: {
378
+ values: {
379
+ xs: 0,
380
+ sm: 600,
381
+ md: 900,
382
+ lg: 1240,
383
+ xl: 1536,
384
+ },
385
+ },
386
+ });
387
+
388
+ const getTheme = (mode: 'light' | 'dark') => {
389
+ const tokens = getDesignTokens(mode);
390
+ return createTheme(tokens);
391
+ };
392
+
393
+ // Export light theme by default
394
+ export const theme = getTheme('light');
395
+
396
+ // Export function to get theme with mode
397
+ export default getTheme;