Pierre Andrews commited on
Commit
f52d137
·
0 Parent(s):

Initial commit

Browse files

> Co-authored-by: Pierre Andrews <628467+Mortimerp9@users.noreply.github.com>
> Co-authored-by: Adrien <XciD@users.noreply.huggingface.co>
> Co-authored-by: Avijit Ghosh <evijit@users.noreply.huggingface.co>
> Co-authored-by: Clémentine Fourrier <clefourrier@users.noreply.huggingface.co>
> Co-authored-by: Maxime Lecanu <mlcu@users.noreply.huggingface.co>
> Co-authored-by: thibaud frere <thibaudfrere@MacBook-Pro-de-thibaud.local>
> Co-authored-by: Kunal Mukesh Malkan <malkan@meta.com>
> Co-authored-by: Romain Froger <64656672+RomainFrog@users.noreply.github.com>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +15 -0
  2. .gitattributes +44 -0
  3. .gitignore +6 -0
  4. .pre-commit-config.yaml +27 -0
  5. Dockerfile +125 -0
  6. Dockerfile.dev +13 -0
  7. LICENSE +21 -0
  8. README.md +14 -0
  9. app.py +603 -0
  10. backend/are.py +167 -0
  11. backend/cleanup.py +20 -0
  12. backend/globals.py +13 -0
  13. backend/iframe.py +81 -0
  14. backend/session.py +33 -0
  15. blog_assets/demo_base.mov +3 -0
  16. blog_assets/demo_robot_short.mp4 +3 -0
  17. blog_assets/demo_traces.mov +3 -0
  18. blog_assets/fig12_calls_tokens_vs_score_pareto_frontier.png +3 -0
  19. blog_assets/fig1_budget_scaling_curves.png +3 -0
  20. blog_assets/fig2_structure_of_are.png +3 -0
  21. blog_assets/fig9_gaia2_scores_per_capability.png +3 -0
  22. blog_assets/thumbnail_mare_gaia2.png +3 -0
  23. demo_mcp_file.json +33 -0
  24. docker-compose.yml +91 -0
  25. frontend/.gitignore +23 -0
  26. frontend/.prettierignore +3 -0
  27. frontend/.prettierrc +1 -0
  28. frontend/Dockerfile.dev +18 -0
  29. frontend/package-lock.json +0 -0
  30. frontend/package.json +52 -0
  31. frontend/public/config.json +5 -0
  32. frontend/public/demo-mcp.json +32 -0
  33. frontend/public/favicon.ico +0 -0
  34. frontend/public/index.html +43 -0
  35. frontend/public/logo192.png +0 -0
  36. frontend/public/logo512.png +0 -0
  37. frontend/public/manifest.json +25 -0
  38. frontend/public/robots.txt +3 -0
  39. frontend/src/App.css +4 -0
  40. frontend/src/App.test.tsx +9 -0
  41. frontend/src/App.tsx +178 -0
  42. frontend/src/components/DemoView.tsx +321 -0
  43. frontend/src/components/DevModeLoginButton.tsx +75 -0
  44. frontend/src/components/HuggingFaceLoginButton.tsx +145 -0
  45. frontend/src/components/IframeDisplay.tsx +48 -0
  46. frontend/src/components/InitialForm.tsx +563 -0
  47. frontend/src/components/LoginButton.tsx +30 -0
  48. frontend/src/components/ModelProviderSelector.tsx +604 -0
  49. frontend/src/components/ServerLoadingIndicator.tsx +117 -0
  50. frontend/src/components/ThemeToggle.tsx +30 -0
.dockerignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ npm-debug.log
3
+ package-lock.json
4
+ frontend/node_modules
5
+ frontend/package-lock.json
6
+ frontend/npm-debug.log
7
+ **/node_modules
8
+ **/package-lock.json
9
+ .git
10
+ .gitignore
11
+ README.md
12
+ .env
13
+ .nyc_output
14
+ coverage
15
+ .vscode
.gitattributes ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ blog_assets/ filter=lfs diff=lfs merge=lfs -text
37
+ blog_assets/fig2_structure_of_are.png filter=lfs diff=lfs merge=lfs -text
38
+ blog_assets/fig9_gaia2_scores_per_capability.png filter=lfs diff=lfs merge=lfs -text
39
+ blog_assets/thumbnail_mare_gaia2.png filter=lfs diff=lfs merge=lfs -text
40
+ blog_assets/demo_robot_short.mp4 filter=lfs diff=lfs merge=lfs -text
41
+ blog_assets/fig12_calls_tokens_vs_score_pareto_frontier.png filter=lfs diff=lfs merge=lfs -text
42
+ blog_assets/fig1_budget_scaling_curves.png filter=lfs diff=lfs merge=lfs -text
43
+ blog_assets/demo_base.mov filter=lfs diff=lfs merge=lfs -text
44
+ blog_assets/demo_traces.mov filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .cache
2
+ .ssh
3
+ .vscode
4
+ arena/.env
5
+ *__pycache__*
6
+ frontend/node_modules
.pre-commit-config.yaml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ default_language_version:
2
+ python: python3
3
+
4
+ ci:
5
+ autofix_prs: true
6
+ autoupdate_commit_msg: '[pre-commit.ci] pre-commit suggestions'
7
+ autoupdate_schedule: quarterly
8
+
9
+ repos:
10
+ - repo: https://github.com/pre-commit/pre-commit-hooks
11
+ rev: v4.3.0
12
+ hooks:
13
+ - id: check-yaml
14
+ - id: check-case-conflict
15
+ - id: detect-private-key
16
+ - id: check-added-large-files
17
+ args: ['--maxkb=1000']
18
+ - id: end-of-file-fixer
19
+ - id: trailing-whitespace
20
+
21
+ - repo: https://github.com/charliermarsh/ruff-pre-commit
22
+ # Ruff version.
23
+ rev: 'v0.11.10'
24
+ hooks:
25
+ - id: ruff
26
+ args: ['--fix']
27
+ - id: ruff-format
Dockerfile ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # ------------------------------------------------------------
3
+ # Stage 0: Pull ARE
4
+ # ------------------------------------------------------------
5
+ FROM ubuntu:20.04 AS fetch_repo
6
+ RUN apt update && \
7
+ apt install -y git curl && \
8
+ curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
9
+ apt-get install -y git-lfs
10
+
11
+ RUN --mount=type=secret,id=github_username,required=true --mount=type=secret,id=github_token,required=true \
12
+ GITHUB_USERNAME=$(cat /run/secrets/github_username) && \
13
+ GITHUB_TOKEN=$(cat /run/secrets/github_token) && \
14
+ git clone https://$GITHUB_USERNAME:$GITHUB_TOKEN@github.com/facebookresearch/meta-agents-research-environments.git && \
15
+ cd meta-agents-research-environments && \
16
+ git lfs install && \
17
+ git lfs pull && \
18
+ rm -rf ./are/simulation/tests && \
19
+ rm -rf ./are/simulation/tutorials
20
+
21
+
22
+ # ------------------------------------------------------------
23
+ # Stage 1: Build the front end
24
+ # ------------------------------------------------------------
25
+ FROM node:23 AS frontend-builder
26
+ WORKDIR /app
27
+ COPY --from=fetch_repo /meta-agents-research-environments/are/simulation/gui/client ./are/simulation/gui/client
28
+ WORKDIR /app/are/simulation/gui/client
29
+ # Clear npm cache and remove lock file to fix ARM64 rollup issue
30
+ RUN npm cache clean --force && rm -f package-lock.json
31
+ RUN --mount=type=cache,target=/root/.npm NPM_CONFIG_CACHE=/root/.npm npm install
32
+ RUN npm run build
33
+
34
+
35
+ # ------------------------------------------------------------
36
+ # Stage 1.5: Build the React frontend
37
+ # ------------------------------------------------------------
38
+
39
+ FROM node:23 AS react-frontend-builder
40
+ WORKDIR /app/frontend
41
+ COPY frontend/package.json ./
42
+ # Clear npm cache and remove lock file to fix ARM64 rollup issue
43
+ RUN npm cache clean --force && rm -f package-lock.json
44
+ RUN --mount=type=cache,target=/root/.npm NPM_CONFIG_CACHE=/root/.npm npm install
45
+ COPY frontend/ ./
46
+ RUN npm run build
47
+
48
+
49
+ # ------------------------------------------------------------
50
+ # Stage 2: Build the backend and gradio app
51
+ # ------------------------------------------------------------
52
+
53
+ FROM python:3.10.14-slim
54
+
55
+ ## Needed for docker dev mode in spaces
56
+ RUN useradd -m -u 1000 user
57
+
58
+ ## Backend
59
+ ARG SERVER_VERSION=unknown
60
+ RUN apt-get update && apt-get install -y \
61
+ curl \
62
+ git \
63
+ && rm -rf /var/lib/apt/lists/*
64
+
65
+ # Needed packages for docker dev mode in spaces
66
+ RUN apt-get update && apt-get install -y \
67
+ bash git-lfs wget procps \
68
+ vim net-tools \
69
+ && rm -rf /var/lib/apt/lists/*
70
+ RUN pip install uv
71
+
72
+ # ARE install
73
+ COPY --from=fetch_repo /meta-agents-research-environments/are /app/are
74
+ COPY --from=fetch_repo /meta-agents-research-environments/build_hooks /app/build_hooks
75
+ COPY --from=fetch_repo /meta-agents-research-environments/pyproject.toml /app/pyproject.toml
76
+ COPY --from=fetch_repo /meta-agents-research-environments/uv.lock /app/uv.lock
77
+ COPY --from=fetch_repo /meta-agents-research-environments/requirements* /app/
78
+ COPY --from=fetch_repo /meta-agents-research-environments/README.md /app/README.md
79
+ COPY --from=fetch_repo /meta-agents-research-environments/LICENSE /app/LICENSE
80
+ WORKDIR /app
81
+ ARG VIRTUAL_ENV /app/.venv
82
+ RUN --mount=type=cache,target=/root/.cache/pip uv venv
83
+ RUN --mount=type=cache,target=/root/.cache/pip uv pip install '.'
84
+ RUN rm -rf /app/are/gui/client
85
+ COPY --from=frontend-builder /app/are/simulation/gui/client/build /app/are/simulation/gui/client/build
86
+
87
+ # Env
88
+ ENV PYTHONUNBUFFERED=1
89
+ ENV ARE_SERVER_HOSTNAME=0.0.0.0
90
+ ENV ARE_SERVER_VERSION=$SERVER_VERSION
91
+ ENV HF_HOME=/app/.cache
92
+ ENV HF_DATASETS_CACHE=/app/.cache
93
+ # For gradio to recognize the env as a space
94
+ ENV SYSTEM=spaces
95
+ # For uvicorn to allow headers and avoid mixed content in site and iframe
96
+ ENV FORWARDED_ALLOW_IPS="*"
97
+
98
+ # Port React frontend build
99
+ COPY --from=react-frontend-builder /app/frontend/build /app/frontend/build
100
+
101
+ WORKDIR /app
102
+ RUN chown 1000 /app
103
+ EXPOSE 7860
104
+
105
+ # Backend deps
106
+ RUN uv pip install -U huggingface_hub "datasets==4.0.0" "gradio[oauth]==5.42.0" gradio_modal "jsonschema>=4.0.0" psutil
107
+ RUN uv pip install --no-cache-dir flask gunicorn
108
+
109
+ # Get core code
110
+ COPY backend/ /app/backend/
111
+ COPY app.py /app/app.py
112
+ COPY run.sh /app/run.sh
113
+ COPY mcp_demo_prompts.json /app/mcp_demo_prompts.json
114
+
115
+ # Create data directory with proper permissions
116
+ RUN mkdir -p /app/data && chown 1000:1000 /app/data
117
+
118
+ # Env vars
119
+ ENV PORT=7860 FLASK_ENV=production PYTHONUNBUFFERED=1 STORAGE_PATH=/app/data
120
+
121
+ RUN chmod 755 /app/.venv
122
+ USER 1000
123
+
124
+ # Start Flask (serves static frontend and the API)
125
+ CMD ["bash", "run.sh"]
Dockerfile.dev ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Development version - extends the main Dockerfile
2
+ FROM aredemo:latest
3
+
4
+ # Override environment variables for development
5
+ ENV FLASK_ENV=development
6
+ ENV NODE_ENV=development
7
+ ENV FLASK_DEBUG=1
8
+
9
+ # Expose additional ports if needed
10
+ EXPOSE 7860 3000
11
+
12
+ # Use development-specific command if needed
13
+ CMD ["bash", "run.sh"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) Meta Platforms, Inc. and affiliates.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MSL Agents Research Environments Demo
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: pink
6
+ sdk: docker
7
+ sdk_version: 5.16.0
8
+ pinned: false
9
+ hf_oauth: true
10
+ hf_oauth_scopes:
11
+ - inference-api
12
+ - read-repos
13
+ license: mit
14
+ ---
app.py ADDED
@@ -0,0 +1,603 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import os
4
+ import pathlib
5
+ import threading
6
+ import time
7
+ from datetime import datetime, timedelta
8
+ from urllib.parse import quote
9
+
10
+ import psutil
11
+ import requests
12
+
13
+ from backend.are import get_are_url, start_are_process_and_session_lite
14
+
15
+ from backend.cleanup import cleanup
16
+ from backend.globals import STORAGE_PATH
17
+
18
+ from backend.iframe import validate_mcp_file
19
+ from backend.session import UserSession
20
+ from flask import Flask, jsonify, request, send_from_directory
21
+
22
+ # Ensure storage directory exists
23
+ os.makedirs(STORAGE_PATH, exist_ok=True)
24
+
25
+ AUTH_SESSION_MANAGEMENT = {}
26
+ SESSION_MANAGEMENT = {}
27
+
28
+ # Serve the static frontend and expose a minimal API
29
+ app = Flask(
30
+ __name__,
31
+ static_folder=os.path.join(os.path.dirname(__file__), "frontend", "build"),
32
+ static_url_path="",
33
+ )
34
+
35
+ logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def cleanup_old_sessions() -> None:
40
+ """Clean up sessions that are older than 2 hours."""
41
+ try:
42
+ current_time = datetime.now()
43
+ sessions_to_remove = []
44
+
45
+ for username, session in SESSION_MANAGEMENT.items():
46
+ try:
47
+ # Parse the session start time
48
+ start_time = datetime.strptime(
49
+ session.start_time, "%Y-%m-%d %H:%M:%S.%f"
50
+ )
51
+ session_age = current_time - start_time
52
+
53
+ # Check if session is older than 2 hours
54
+ if session_age > timedelta(hours=2):
55
+ logger.info(
56
+ f"Session {session.sid} for user {username} "
57
+ f"is {session_age} old, marking for cleanup"
58
+ )
59
+ sessions_to_remove.append(username)
60
+
61
+ except (ValueError, AttributeError) as e:
62
+ logger.warning(
63
+ f"Could not parse start time for session "
64
+ f"{session.sid} (user: {username}): {e}"
65
+ )
66
+ # If we can't parse the time, assume it's old and clean it up
67
+ sessions_to_remove.append(username)
68
+
69
+ # Clean up old sessions
70
+ for username in sessions_to_remove:
71
+ if username in SESSION_MANAGEMENT:
72
+ session = SESSION_MANAGEMENT[username]
73
+ logger.info(
74
+ f"Cleaning up old session {session.sid} " f"for user {username}"
75
+ )
76
+ try:
77
+ cleanup(session)
78
+ del SESSION_MANAGEMENT[username]
79
+ logger.info(
80
+ f"Successfully cleaned up old session "
81
+ f"{session.sid} for user {username}"
82
+ )
83
+ except Exception as e:
84
+ logger.error(
85
+ f"Error cleaning up old session "
86
+ f"{session.sid} for user {username}: {e}"
87
+ )
88
+ # Remove from SESSION_MANAGEMENT even if cleanup failed
89
+ # to prevent accumulation of broken sessions
90
+ try:
91
+ del SESSION_MANAGEMENT[username]
92
+ except KeyError:
93
+ pass
94
+
95
+ if sessions_to_remove:
96
+ logger.info(f"Cleaned up {len(sessions_to_remove)} old sessions")
97
+
98
+ except Exception as e:
99
+ logger.error(f"Error during old session cleanup: {e}")
100
+
101
+
102
+ def cleanup_session_async(user_session: UserSession) -> None:
103
+ """Run cleanup in the background to avoid blocking the main thread."""
104
+ if user_session is None:
105
+ return
106
+
107
+ def run_cleanup():
108
+ try:
109
+ session_id = user_session.sid
110
+ logger.info(f"Starting background cleanup for session {session_id}")
111
+ cleanup(user_session)
112
+ logger.info(f"Background cleanup completed for session {session_id}")
113
+
114
+ # Also clean up any other old sessions while we're at it
115
+ logger.info("Checking for old sessions to clean up...")
116
+ cleanup_old_sessions()
117
+
118
+ except Exception as e:
119
+ session_id = getattr(user_session, "sid", "unknown")
120
+ logger.error(
121
+ f"Error during background cleanup for session " f"{session_id}: {e}"
122
+ )
123
+
124
+ # Start cleanup in a separate thread
125
+ cleanup_thread = threading.Thread(target=run_cleanup, daemon=True)
126
+ cleanup_thread.start()
127
+
128
+
129
+ def get_session_from_cookie(cookie):
130
+ # Possible cookie session names
131
+ for session_name in [
132
+ "session",
133
+ "spaces-jwt",
134
+ "sessionid",
135
+ "JSESSIONID",
136
+ "connect.sid",
137
+ ]:
138
+ try:
139
+ session = cookie[session_name]
140
+ return session
141
+ except Exception:
142
+ continue
143
+ return None
144
+
145
+
146
+ @app.get("/")
147
+ def index():
148
+ """Serve the main HTML document."""
149
+ sign_in_info = request.args.get("__sign", default=None, type=str)
150
+ cookie_session = get_session_from_cookie(request.cookies)
151
+
152
+ if sign_in_info is not None and cookie_session is not None:
153
+ AUTH_SESSION_MANAGEMENT[cookie_session] = sign_in_info
154
+ logger.info(f"Filled sign for session {cookie_session}: {sign_in_info}")
155
+ return send_from_directory(app.static_folder, "index.html")
156
+
157
+
158
+ @app.get("/demo-mcp.json")
159
+ def demo_mcp():
160
+ """Serve the demo MCP file."""
161
+ # Try serving from the built frontend first (production)
162
+ try:
163
+ return send_from_directory(app.static_folder, "demo-mcp.json")
164
+ except FileNotFoundError:
165
+ # Fall back to the public directory (development)
166
+ try:
167
+ public_folder = os.path.join(
168
+ os.path.dirname(__file__), "frontend", "public"
169
+ )
170
+ return send_from_directory(public_folder, "demo-mcp.json")
171
+ except FileNotFoundError:
172
+ logger.error("demo-mcp.json not found in either build or public directory")
173
+ return jsonify({"error": "demo-mcp.json not found"}), 404
174
+
175
+
176
+ @app.get("/api/models/<provider>")
177
+ def get_models_for_provider(provider):
178
+ """Fetch available models for a given provider from Hugging Face API."""
179
+ if provider == "llama-api":
180
+ # Model IDs from https://llama.developer.meta.com/docs/models/
181
+ llama_models = [
182
+ "Llama-4-Maverick-17B-128E-Instruct-FP8",
183
+ "Llama-4-Scout-17B-16E-Instruct-FP8",
184
+ "Llama-3.3-70B-Instruct",
185
+ "Llama-3.3-8B-Instruct",
186
+ "Cerebras-Llama-4-Maverick-17B-128E-Instruct",
187
+ "Cerebras-Llama-4-Scout-17B-16E-Instruct",
188
+ "Groq-Llama-4-Maverick-17B-128E-Instruct",
189
+ ]
190
+ return jsonify({"models": llama_models}), 200
191
+
192
+ try:
193
+ # Map provider to the correct API parameter with proper URL encoding
194
+ encoded_provider = quote(provider)
195
+
196
+ # Fetch models with image-text-to-text pipeline tag
197
+ url_image_text = f"https://huggingface.co/api/models?pipeline_tag=image-text-to-text&inference_provider={encoded_provider}"
198
+ response_image_text = requests.get(url_image_text, timeout=10)
199
+
200
+ # Fetch models with text-generation pipeline tag
201
+ url_text_gen = f"https://huggingface.co/api/models?pipeline_tag=text-generation&inference_provider={encoded_provider}"
202
+ response_text_gen = requests.get(url_text_gen, timeout=10)
203
+
204
+ # Check if both requests were successful
205
+ if response_image_text.status_code != 200:
206
+ logger.error(
207
+ f"Failed to fetch image-text-to-text models for provider {provider}: "
208
+ f"{response_image_text.status_code}"
209
+ )
210
+ return (
211
+ jsonify(
212
+ {
213
+ "error": "Failed to fetch image-text-to-text models",
214
+ "status": response_image_text.status_code,
215
+ }
216
+ ),
217
+ 500,
218
+ )
219
+
220
+ if response_text_gen.status_code != 200:
221
+ logger.error(
222
+ f"Failed to fetch text-generation models for provider {provider}: "
223
+ f"{response_text_gen.status_code}"
224
+ )
225
+ return (
226
+ jsonify(
227
+ {
228
+ "error": "Failed to fetch text-generation models",
229
+ "status": response_text_gen.status_code,
230
+ }
231
+ ),
232
+ 500,
233
+ )
234
+
235
+ # Parse responses and merge results
236
+ image_text_models = response_image_text.json()
237
+ text_gen_models = response_text_gen.json()
238
+
239
+ # Extract model IDs from both responses
240
+ image_text_ids = [
241
+ model.get("id") for model in image_text_models if model.get("id")
242
+ ]
243
+ text_gen_ids = [model.get("id") for model in text_gen_models if model.get("id")]
244
+
245
+ # Merge and deduplicate model IDs
246
+ model_ids = list(set(image_text_ids + text_gen_ids))
247
+ model_ids.sort() # Sort the models alphabetically
248
+
249
+ logger.info(
250
+ f"Fetched {len(image_text_ids)} image-text-to-text models and {len(text_gen_ids)} text-generation models for provider {provider} (total: {len(model_ids)} unique models)"
251
+ )
252
+ return jsonify({"models": model_ids}), 200
253
+
254
+ except requests.RequestException as e:
255
+ logger.error(f"Network error when fetching models for provider {provider}: {e}")
256
+ return jsonify({"error": "Network error", "detail": str(e)}), 500
257
+ except Exception as e:
258
+ logger.error(
259
+ f"Unexpected error when fetching models for provider " f"{provider}: {e}"
260
+ )
261
+ return jsonify({"error": "Internal error", "detail": str(e)}), 500
262
+
263
+
264
+ @app.get("/api/processes")
265
+ def list_python_processes():
266
+ # Check for key query parameter
267
+ key = request.args.get("key")
268
+ if not key:
269
+ return jsonify({"error": "Unauthorized access"}), 403
270
+
271
+ # Check if key matches OWNER_SECRET environment variable
272
+ owner_secret = os.environ.get("OWNER_SECRET")
273
+ if not owner_secret:
274
+ return (jsonify({"error": "Server configuration error"}), 500)
275
+
276
+ if key != owner_secret:
277
+ return jsonify({"error": "Unauthorized access"}), 403
278
+
279
+ try:
280
+ python_processes = []
281
+
282
+ # Iterate through all running processes using psutil
283
+ for proc in psutil.process_iter():
284
+ try:
285
+ # Get process info
286
+ pinfo = proc.as_dict(
287
+ attrs=[
288
+ "pid",
289
+ "ppid",
290
+ "name",
291
+ "username",
292
+ "status",
293
+ "create_time",
294
+ "cpu_percent",
295
+ "memory_percent",
296
+ "memory_info",
297
+ "cmdline",
298
+ ]
299
+ )
300
+
301
+ # Check if this is a Python process
302
+ process_name = pinfo["name"].lower()
303
+ cmdline = " ".join(pinfo["cmdline"]) if pinfo["cmdline"] else ""
304
+
305
+ if (
306
+ "python" in process_name
307
+ or "python" in cmdline.lower()
308
+ or cmdline.endswith(".py")
309
+ ):
310
+
311
+ # Convert create_time to readable format
312
+ create_time = time.strftime(
313
+ "%Y-%m-%d %H:%M:%S", time.localtime(pinfo["create_time"])
314
+ )
315
+
316
+ # Format memory info
317
+ memory_info = pinfo.get("memory_info")
318
+ rss_mb = (memory_info.rss / 1024 / 1024) if memory_info else 0
319
+ vms_mb = (memory_info.vms / 1024 / 1024) if memory_info else 0
320
+
321
+ process_info = {
322
+ "pid": pinfo["pid"],
323
+ "ppid": pinfo["ppid"],
324
+ "name": pinfo["name"],
325
+ "username": pinfo.get("username", "unknown"),
326
+ "status": pinfo["status"],
327
+ "cpu_percent": round(pinfo.get("cpu_percent", 0), 2),
328
+ "memory_percent": round(pinfo.get("memory_percent", 0), 2),
329
+ "memory_rss_mb": round(rss_mb, 2),
330
+ "memory_vms_mb": round(vms_mb, 2),
331
+ "create_time": create_time,
332
+ "cmdline": (
333
+ cmdline[:200] + "..." if len(cmdline) > 200 else cmdline
334
+ ),
335
+ }
336
+ python_processes.append(process_info)
337
+
338
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
339
+ # Process may have terminated or we don't have permission
340
+ continue
341
+ except Exception as e:
342
+ logger.warning(f"Error processing process {proc.pid}: {e}")
343
+ continue
344
+
345
+ # Sort by PID for consistent ordering
346
+ python_processes.sort(key=lambda x: x["pid"])
347
+
348
+ return (
349
+ jsonify({"processes": python_processes, "count": len(python_processes)}),
350
+ 200,
351
+ )
352
+
353
+ except Exception as e:
354
+ logger.error(f"Unexpected error listing processes: {e}")
355
+ return (jsonify({"error": "Internal server error"}), 500)
356
+
357
+
358
+ @app.get("/api/sessions")
359
+ def list_active_sessions():
360
+ # Check for key query parameter
361
+ key = request.args.get("key")
362
+ if not key:
363
+ return jsonify({"error": "Unauthorized access"}), 403
364
+
365
+ # Check if key matches OWNER_SECRET environment variable
366
+ owner_secret = os.environ.get("OWNER_SECRET")
367
+ if not owner_secret:
368
+ return (jsonify({"error": "Server configuration error"}), 500)
369
+
370
+ if key != owner_secret:
371
+ return jsonify({"error": "Unauthorized access"}), 403
372
+
373
+ try:
374
+ active_sessions = []
375
+ current_time = time.time()
376
+
377
+ for username, session in SESSION_MANAGEMENT.items():
378
+ try:
379
+ # Calculate session duration
380
+ start_timestamp = time.mktime(
381
+ time.strptime(session.start_time, "%Y-%m-%d %H:%M:%S.%f")
382
+ )
383
+ duration_seconds = current_time - start_timestamp
384
+ duration_hours = duration_seconds / 3600
385
+
386
+ # Check if process is still running
387
+ process_status = "unknown"
388
+ cpu_percent = 0
389
+ memory_percent = 0
390
+
391
+ try:
392
+ proc = psutil.Process(session.pid)
393
+ process_status = proc.status()
394
+ cpu_percent = proc.cpu_percent()
395
+ memory_percent = proc.memory_percent()
396
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
397
+ process_status = "not_found"
398
+
399
+ session_info = {
400
+ "username": username,
401
+ "session_id": session.sid,
402
+ "pid": session.pid,
403
+ "port": session.port,
404
+ "model": session.model,
405
+ "provider": session.provider,
406
+ "start_time": session.start_time,
407
+ "duration_hours": round(duration_hours, 2),
408
+ "log_path": session.log_path,
409
+ "process_status": process_status,
410
+ "cpu_percent": round(cpu_percent, 2),
411
+ "memory_percent": round(memory_percent, 2),
412
+ }
413
+ active_sessions.append(session_info)
414
+
415
+ except Exception as e:
416
+ logger.warning(f"Error processing session for user {username}: {e}")
417
+ # Still include basic info even if we can't get process details
418
+ session_info = {
419
+ "username": username,
420
+ "session_id": session.sid,
421
+ "pid": session.pid,
422
+ "port": session.port,
423
+ "model": session.model,
424
+ "provider": session.provider,
425
+ "start_time": session.start_time,
426
+ "duration_hours": "unknown",
427
+ "log_path": session.log_path,
428
+ "process_status": "error",
429
+ "cpu_percent": 0,
430
+ "memory_percent": 0,
431
+ }
432
+ active_sessions.append(session_info)
433
+
434
+ # Sort by start time (most recent first)
435
+ active_sessions.sort(
436
+ key=lambda x: x["start_time"] if x["start_time"] != "unknown" else "",
437
+ reverse=True,
438
+ )
439
+
440
+ return (
441
+ jsonify({"sessions": active_sessions, "count": len(active_sessions)}),
442
+ 200,
443
+ )
444
+
445
+ except Exception as e:
446
+ logger.error(f"Unexpected error listing sessions: {e}")
447
+ return (jsonify({"error": "Internal server error"}), 500)
448
+
449
+
450
+ @app.post("/api/start")
451
+ def start_demo():
452
+ """Receive the form payload and simulate demo startup.
453
+
454
+ Logs both the raw payload and a safe summary, then returns a dummy iframe URL
455
+ after a small delay to mimic startup time.
456
+ """
457
+ try:
458
+ data = request.get_json(force=True, silent=False)
459
+ except Exception as exc:
460
+ logger.info("Invalid JSON", data, str(exc))
461
+ logger.exception("Invalid JSON body")
462
+ return jsonify({"ok": False, "error": "invalid_json", "detail": str(exc)}), 400
463
+ if not isinstance(data, dict):
464
+ logger.info("Invalid JSON", data)
465
+ logger.exception("Invalid JSON body")
466
+ return jsonify({"ok": False, "error": "invalid_json"}), 400
467
+
468
+ cookie_session = get_session_from_cookie(request.cookies)
469
+ try:
470
+ signin_token = AUTH_SESSION_MANAGEMENT[cookie_session]
471
+ except KeyError: # weird edge case
472
+ signin_token = cookie_session
473
+
474
+ # Raw payload logging
475
+ logger.info(
476
+ "/api/start payload:\n%s", json.dumps(data, indent=2, ensure_ascii=False)
477
+ )
478
+
479
+ # Request metadata and a concise payload summary (avoid dumping large mcp bodies)
480
+ client_ip = (
481
+ (request.headers.get("X-Forwarded-For") or request.remote_addr or "-")
482
+ .split(",")[0]
483
+ .strip()
484
+ )
485
+ user_agent = request.headers.get("User-Agent", "-")
486
+ referer = request.headers.get("Referer", "-")
487
+ content_type = request.content_type
488
+ content_length = request.content_length
489
+ auth_header = request.headers.get("Authorization")
490
+ user_token = None
491
+ if auth_header and auth_header.lower().startswith("bearer "):
492
+ user_token = auth_header.split(" ", 1)[1].strip()
493
+
494
+ logger.info(
495
+ {
496
+ "user_agent": user_agent,
497
+ "referer": referer,
498
+ "content_type": content_type,
499
+ "content_length": content_length,
500
+ "auth_header": auth_header,
501
+ "user_token": user_token,
502
+ }
503
+ )
504
+
505
+ username = data.get("user")
506
+
507
+ # MCP validation
508
+ mcp_text = data.get("mcp") if isinstance(data, dict) else None
509
+ mcp_json_path = None
510
+
511
+ if isinstance(mcp_text, str):
512
+ try:
513
+ mcp_data = validate_mcp_file(mcp_text, user_token)
514
+
515
+ mcp_json_path = f"{STORAGE_PATH}/{username}/mcp.json"
516
+ os.makedirs(f"{STORAGE_PATH}/{username}", exist_ok=True)
517
+ with open(pathlib.Path(mcp_json_path), "w") as file:
518
+ json.dump(mcp_data, file, indent=4)
519
+
520
+ except ValueError as e:
521
+ logger.error(f"MCP file validation failed: {e}")
522
+ return (
523
+ jsonify({"ok": False, "error": "invalid_mcp_file", "detail": str(e)}),
524
+ 400,
525
+ )
526
+ except Exception as e:
527
+ logger.error(f"Could not process MCP file: {e}")
528
+ return (
529
+ jsonify(
530
+ {
531
+ "ok": False,
532
+ "error": "mcp_processing_failed",
533
+ "detail": f"Failed to process MCP file: {str(e)}",
534
+ }
535
+ ),
536
+ 500,
537
+ )
538
+
539
+ # Killing previous session
540
+ logger.info(f"Current SESSION_MANAGEMENT keys: {list(SESSION_MANAGEMENT.keys())}")
541
+ logger.info(f"Looking for username: {username}")
542
+ user_session = SESSION_MANAGEMENT.get(username, None)
543
+ if user_session:
544
+ logger.info(f"Killing existing session for {username}: {user_session}")
545
+ cleanup_session_async(SESSION_MANAGEMENT[username])
546
+ del SESSION_MANAGEMENT[username] # Actually remove the session
547
+ user_session = None
548
+ logger.info(
549
+ f"Started background cleanup for previous session of user {username}"
550
+ )
551
+ else:
552
+ logger.info(f"No previous processes to kill for {username}")
553
+
554
+ user_session: UserSession = start_are_process_and_session_lite(
555
+ model=data.get("model", ""),
556
+ provider=data.get("provider", ""),
557
+ username=username,
558
+ bearer_token=signin_token,
559
+ user_token=user_token,
560
+ app_path=mcp_json_path,
561
+ )
562
+
563
+ SESSION_MANAGEMENT[username] = user_session
564
+ logger.info(f"User SESSION {user_session}")
565
+
566
+ logger.info(
567
+ f"Started session {user_session.sid} on port {user_session.port} for user {user_session.user}"
568
+ )
569
+ iframe_url: str = get_are_url(session=user_session, server="are_simulation")
570
+ health_url: str = get_are_url(session=user_session, server="health")
571
+
572
+ summary = {
573
+ "client": {
574
+ "ip": client_ip,
575
+ "user_agent": user_agent,
576
+ "referer": referer,
577
+ "content_type": content_type,
578
+ "content_length": content_length,
579
+ },
580
+ "received_fields": {
581
+ "model": data.get("model") if isinstance(data, dict) else None,
582
+ "provider": data.get("provider") if isinstance(data, dict) else None,
583
+ "user": data.get("user") if isinstance(data, dict) else None,
584
+ # "mcp_length": mcp_len,
585
+ # "mcp_is_json": mcp_is_json,
586
+ },
587
+ "auth": {
588
+ "signin_token": signin_token,
589
+ },
590
+ }
591
+ logger.info("/api/start summary: %s", json.dumps(summary, ensure_ascii=False))
592
+
593
+ return jsonify({"ok": True, "received": True, "iframe_url": iframe_url, "health_url": health_url}), 200
594
+
595
+
596
+ def run():
597
+ """Run the development/Space server."""
598
+ port = int(os.environ.get("PORT", "7860"))
599
+ app.run(host="0.0.0.0", port=port)
600
+
601
+
602
+ if __name__ == "__main__":
603
+ run()
backend/are.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import logging
3
+ import os
4
+ import random
5
+ import subprocess
6
+ import uuid
7
+
8
+ import psutil
9
+ from backend.globals import FREE_PORTS_POOL, ORG, SPACE, STORAGE_PATH
10
+ from backend.session import UserSession
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def start_are_process_and_session_lite(
16
+ model: str,
17
+ provider: str,
18
+ username: str,
19
+ bearer_token: str | None,
20
+ user_token: str | None,
21
+ app_path: str | None,
22
+ ) -> UserSession:
23
+ if not user_token:
24
+ error_msg = (
25
+ f"HF_TOKEN (user_token) is None for user {username}. "
26
+ "Cannot start ARE session without Hugging Face token."
27
+ )
28
+ raise ValueError(error_msg)
29
+
30
+ global FREE_PORTS_POOL
31
+ port = random.sample(FREE_PORTS_POOL, k=1)[0]
32
+
33
+ log_path = f"{STORAGE_PATH}/log_{port}.log"
34
+ env_vars = dict(os.environ)
35
+ env_vars["ARE_SERVER_HOSTNAME"] = "0.0.0.0"
36
+ env_vars["ARE_SIMULATION_SERVER_HOSTNAME"] = "0.0.0.0"
37
+ env_vars["ARE_SERVER_PORT"] = str(port)
38
+ env_vars["ARE_SIMULATION_SERVER_PORT"] = str(port)
39
+ env_vars["HF_TOKEN"] = os.environ.get("HF_DATASET_TOKEN", user_token)
40
+ env_vars["HF_INFERENCE_TOKEN"] = user_token
41
+ env_vars["HF_DEMO_UNIVERSE"] = "universe_hf_0" # universe_hf"
42
+ bill_to = os.environ.get("HF_BILL_TO")
43
+ if bill_to:
44
+ env_vars["HF_BILL_TO"] = bill_to
45
+ llama_key = os.environ.get("LLAMA_API_KEY")
46
+ if llama_key:
47
+ env_vars["LLAMA_API_KEY"] = llama_key
48
+ env_vars["INTERACTIVE_SCENARIOS_TREE"] = "/app/mcp_demo_prompts.json"
49
+ if app_path:
50
+ env_vars["MCP_APPS_JSON_PATH"] = app_path
51
+
52
+ p = subprocess.Popen(
53
+ " ".join(
54
+ ["python", "-u", "-m", "are.simulation.gui.cli", "-a", "default"]
55
+ + ["-m", model, "--provider", provider]
56
+ + [
57
+ "-s",
58
+ "scenario_hf_demo_mcp",
59
+ # "hf://datasets/meta-agents-research-environments/gaia2/demo/validation/universe_hf",
60
+ "--ui_view",
61
+ "playground",
62
+ ] # scenario_universe_hf_0 or "scenario_hf_0" or "universe_hf_0"
63
+ + ["2>&1", "|", "tee", log_path]
64
+ ),
65
+ env=env_vars,
66
+ shell=True,
67
+ executable="/bin/bash",
68
+ )
69
+
70
+ FREE_PORTS_POOL = [p for p in FREE_PORTS_POOL if p != port]
71
+ user_session = UserSession(
72
+ port=int(port),
73
+ pid=p.pid,
74
+ sid=str(uuid.uuid4()),
75
+ model=model,
76
+ provider=provider,
77
+ log_path=log_path,
78
+ start_time=str(datetime.datetime.now()),
79
+ user=username,
80
+ sign=bearer_token,
81
+ )
82
+
83
+ return user_session
84
+
85
+
86
+ def kill_are_process(session: UserSession) -> None:
87
+ # Automatically kills the are processes and all their children
88
+ global FREE_PORTS_POOL
89
+
90
+ try:
91
+ # Get the main process
92
+ main_process = psutil.Process(session.pid)
93
+
94
+ # Get all child processes recursively
95
+ children = main_process.children(recursive=True)
96
+
97
+ # Kill all child processes first
98
+ for child in children:
99
+ try:
100
+ child.kill()
101
+ logger.info(f"Killed child process PID {child.pid}")
102
+ except psutil.NoSuchProcess:
103
+ logger.info(f"Child process PID {child.pid} already terminated")
104
+ except OSError:
105
+ logger.warning(f"Child process PID {child.pid} not found")
106
+
107
+ # Wait for child processes to terminate
108
+ for child in children:
109
+ try:
110
+ child.wait(timeout=5)
111
+ except psutil.TimeoutExpired:
112
+ logger.warning(
113
+ f"Child process PID {child.pid} did not terminate within timeout"
114
+ )
115
+ except psutil.NoSuchProcess:
116
+ pass
117
+
118
+ # Kill the main process
119
+ main_process.kill()
120
+ logger.info(f"Sent SIGKILL to main PID {session.pid}")
121
+
122
+ # Wait for main process to terminate
123
+ try:
124
+ main_process.wait(timeout=5)
125
+ except psutil.TimeoutExpired:
126
+ logger.warning(
127
+ f"Main process PID {session.pid} did not terminate within timeout"
128
+ )
129
+ except psutil.NoSuchProcess:
130
+ pass
131
+
132
+ FREE_PORTS_POOL.append(session.port)
133
+ logger.info(
134
+ f"Killed session {session.sid} PID {session.pid} on port {session.port} for user {session.user}"
135
+ )
136
+
137
+ except psutil.NoSuchProcess:
138
+ logger.info(f"Process PID {session.pid} not found - may already be terminated")
139
+ FREE_PORTS_POOL.append(session.port)
140
+ except OSError:
141
+ logger.error(
142
+ f"COULD NOT KILL ARE on port {session.port} for user {session.user}",
143
+ exc_info=True,
144
+ )
145
+
146
+
147
+ def get_are_url(session: UserSession, server: str) -> str:
148
+ """Generates the are url
149
+
150
+ Args:
151
+ port (str): Port on which the app is running
152
+ session_id (str): Session id in ARE
153
+ sign (str): Auth key provided by the query
154
+ server (str): Must be either "are" or "graphql"
155
+
156
+ Returns:
157
+ str: The url to look at
158
+ """
159
+ # Check if we're in development mode
160
+ flask_env = os.environ.get("FLASK_ENV", "production")
161
+
162
+ if flask_env == "development":
163
+ # In development mode, use localhost with the actual ARE port
164
+ return f"http://localhost:{session.port}/{server}?sid={session.sid}&__sign={session.sign}"
165
+ else:
166
+ # In production mode, use Hugging Face Space URL
167
+ return f"https://{ORG.lower()}-{SPACE.lower()}--{session.port}.hf.space/{server}?sid={session.sid}&__sign={session.sign}"
backend/cleanup.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from backend.are import kill_are_process
2
+ from backend.session import UserSession
3
+
4
+
5
+ def cleanup(user_session: UserSession) -> None:
6
+ """Logs the user interaction.
7
+ Stops ARE, queries GraphQL to grab the traces, saves them locally, pushes everything to the hub,
8
+ then kills the ARE processes.
9
+
10
+ Args:
11
+ request (gr.Request): Automatically accessed
12
+ user_session (gr.State): Storage for user-specific session variables, contains a UserSession
13
+ """
14
+
15
+ # The user did not interact with are agent
16
+ if user_session is None:
17
+ return
18
+
19
+ # Kill the user are processes
20
+ kill_are_process(user_session)
backend/globals.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Current gradio app port is 7860
4
+ FREE_PORTS_POOL = [
5
+ x for x in range(1024, 65535) if x != 7860
6
+ ] # Avail ports for are processes
7
+
8
+ # STORAGE AND LOGGING
9
+ # Default to local ./data directory
10
+ STORAGE_PATH = os.environ.get("STORAGE_PATH", "./data")
11
+ ORG = "meta-agents-research-environments"
12
+ SPACE = "demo"
13
+ LOGS_HUB_PATH = f"{ORG}/demo-logs"
backend/iframe.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions which rely on gradio components io and change the interface"""
2
+
3
+ import json
4
+
5
+ from jsonschema import validate, ValidationError
6
+
7
+
8
+ def validate_mcp_file(mcp_file_content: str, token: str) -> str:
9
+ """Validates the user uploaded MCP file
10
+
11
+ The JSON file should follow this structure (we only allow SSE type for the demo,
12
+ but if you run locally you can add stdio to your json file):
13
+
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "app-name-1": {
18
+ "type": "sse",
19
+ "url": "https://api.example.com/mcp",
20
+ "headers": {
21
+ "Authorization": "Bearer your-token-here"
22
+ }
23
+ }
24
+ }
25
+ }
26
+ ```
27
+ """
28
+ # Define the JSON schema
29
+ mcp_schema = {
30
+ "$schema": "https://json-schema.org/draft/2020-12/json-schema-core.html",
31
+ "type": "object",
32
+ "properties": {
33
+ "mcpServers": {
34
+ "type": "object",
35
+ "patternProperties": {
36
+ "^[\w-]+$": {
37
+ "type": "object",
38
+ "properties": {
39
+ "type": {"type": "string", "enum": ["sse"]},
40
+ "url": {"type": "string", "format": "uri"},
41
+ "headers": {
42
+ "type": "object",
43
+ "patternProperties": {"^[\w-]+$": {"type": "string"}},
44
+ "additionalProperties": False,
45
+ },
46
+ },
47
+ "required": ["type", "url"],
48
+ "additionalProperties": False,
49
+ }
50
+ },
51
+ "additionalProperties": False,
52
+ "minProperties": 1,
53
+ }
54
+ },
55
+ "required": ["mcpServers"],
56
+ "additionalProperties": False,
57
+ }
58
+
59
+ try:
60
+ mcp_data = json.loads(mcp_file_content)
61
+ validate(instance=mcp_data, schema=mcp_schema)
62
+
63
+ for server in mcp_data.get("mcpServers", {}).values():
64
+ try:
65
+ if "HF_TOKEN" in server["headers"]["Authorization"]:
66
+ server["headers"]["Authorization"] = server["headers"][
67
+ "Authorization"
68
+ ].replace("HF_TOKEN", token or "")
69
+ except KeyError:
70
+ continue
71
+
72
+ return mcp_data
73
+
74
+ except json.JSONDecodeError as e:
75
+ raise ValueError(f"Invalid JSON format: {str(e)}")
76
+ except ValidationError as e:
77
+ # Provide more user-friendly error messages
78
+ error_path = " -> ".join(str(p) for p in e.path) if e.path else "root"
79
+ raise ValueError(f"Invalid MCP file structure at {error_path}: {e.message}")
80
+ except Exception as e:
81
+ raise ValueError(f"Error validating MCP file: {str(e)}")
backend/session.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import asdict, dataclass
2
+
3
+
4
+ @dataclass
5
+ class UserSession:
6
+ """Information associated with the current user's session.
7
+
8
+ port: on which port is ARE running
9
+ pid: ARE process pid to check status
10
+ sid: Session id in ARE
11
+ model: User selected model
12
+ provider: User selected provider
13
+ log_path: ARE log for the session
14
+ start_time: Session start time
15
+ user: Username
16
+ sign: User sign in # todo: remove when the space becomes public
17
+ """
18
+
19
+ port: int
20
+ pid: int
21
+ sid: str
22
+ model: str
23
+ provider: str
24
+ log_path: str
25
+ start_time: str
26
+ user: str
27
+ sign: str
28
+
29
+ def log_name(self) -> str:
30
+ return f"{self.provider}/{self.model}/{self.user}_{self.start_time}_log.json"
31
+
32
+ def asdict(self) -> dict:
33
+ return asdict(self)
blog_assets/demo_base.mov ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:66cf2c878851f278f97efcc7c321cbeebeaace3a0ce9d72ab1a8c5d7cda21dfb
3
+ size 6476197
blog_assets/demo_robot_short.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:db103ab1a0ace57d9bf29c68a7ce84301472e13897d4b070aff8562d163a1328
3
+ size 11108459
blog_assets/demo_traces.mov ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d693a9b31d3bd20e22ecd087b0e0dfc7ecf71a20a7f3c7cda7e2ebfb015aae5c
3
+ size 5722090
blog_assets/fig12_calls_tokens_vs_score_pareto_frontier.png ADDED

Git LFS Details

  • SHA256: 8aa63bf64915660d86c7d4da6a63c116197cf1e6f32d36c43fc4ff40d498b616
  • Pointer size: 131 Bytes
  • Size of remote file: 825 kB
blog_assets/fig1_budget_scaling_curves.png ADDED

Git LFS Details

  • SHA256: 24b98bf397560020efe65465d2059de19552ec999386b7ce2b651d7cecca8af9
  • Pointer size: 132 Bytes
  • Size of remote file: 2.06 MB
blog_assets/fig2_structure_of_are.png ADDED

Git LFS Details

  • SHA256: f4341ed0d3cffb07df2b714bef7fc942bb5046ba643c7b0807c13389a09c69ca
  • Pointer size: 132 Bytes
  • Size of remote file: 1.18 MB
blog_assets/fig9_gaia2_scores_per_capability.png ADDED

Git LFS Details

  • SHA256: 630e4fa5935e161a028d8e8d1fe9d852a5b826e49557e6591fe3a9882be022de
  • Pointer size: 132 Bytes
  • Size of remote file: 1.45 MB
blog_assets/thumbnail_mare_gaia2.png ADDED

Git LFS Details

  • SHA256: 0fa88ee0b59b8f488c2fbbe296d233ebd64e93002a14991bf11431fa46092156
  • Pointer size: 132 Bytes
  • Size of remote file: 1.15 MB
demo_mcp_file.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers":
3
+ {
4
+ "geocalc-mcp": {
5
+ "type": "sse",
6
+ "url": "https://agents-mcp-hackathon-geocalc-mcp.hf.space/gradio_api/mcp/sse",
7
+ "headers": {
8
+ "Authorization": "Bearer ${HF_TOKEN}"
9
+ }
10
+ },
11
+ "image-edit-mcp": {
12
+ "type": "sse",
13
+ "url": "https://black-forest-labs-flux-1-kontext-dev.hf.space/gradio_api/mcp/sse",
14
+ "headers": {
15
+ "Authorization": "Bearer ${HF_TOKEN}"
16
+ }
17
+ },
18
+ "websearch-mcp": {
19
+ "type": "sse",
20
+ "url": "https://victor-websearch.hf.space/gradio_api/mcp/sse",
21
+ "headers": {
22
+ "Authorization": "Bearer ${HF_TOKEN}"
23
+ }
24
+ },
25
+ "academia-mcp": {
26
+ "type": "sse",
27
+ "url": "https://agents-mcp-hackathon-academia-mcp-gradio.hf.space/gradio_api/mcp/sse",
28
+ "headers": {
29
+ "Authorization": "Bearer ${HF_TOKEN}"
30
+ }
31
+ }
32
+ }
33
+ }
docker-compose.yml ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ aredemo:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile
8
+ platforms:
9
+ - linux/amd64
10
+ secrets:
11
+ - github_username
12
+ - github_token
13
+ ports:
14
+ - "7860:7860"
15
+ environment:
16
+ - FLASK_ENV=production
17
+ - NODE_ENV=production
18
+ - STORAGE_PATH=/app/data
19
+ volumes:
20
+ # Mount data directory for MCP files and session storage
21
+ - ./data:/app/data
22
+ # Mount logs or data if needed
23
+ - ./logs:/app/logs
24
+
25
+ aredemo-dev:
26
+ build:
27
+ context: .
28
+ dockerfile: Dockerfile
29
+ platforms:
30
+ - linux/amd64
31
+ secrets:
32
+ - github_username
33
+ - github_token
34
+ ports:
35
+ - "7860:7860"
36
+ # Expose a range of ports for ARE processes (1024-1100 should be enough for dev)
37
+ - "1024-1100:1024-1100"
38
+ environment:
39
+ - FLASK_ENV=development
40
+ - NODE_ENV=development
41
+ - FLASK_DEBUG=1
42
+ volumes:
43
+ # Mount Python source code for hot reloading
44
+ - ./app.py:/app/app.py
45
+ - ./backend:/app/backend
46
+ # Mount MCP demo prompts file
47
+ - ./mcp_demo_prompts.json:/app/mcp_demo_prompts.json
48
+ # Mount React frontend source for development
49
+ - ./frontend/src:/app/frontend/src
50
+ - ./frontend/public:/app/frontend/public
51
+ - ./frontend/package.json:/app/frontend/package.json
52
+ # Mount logs
53
+ - ./logs:/app/logs
54
+ develop:
55
+ watch:
56
+ - action: sync
57
+ path: ./app.py
58
+ target: /app/app.py
59
+ - action: sync
60
+ path: ./backend
61
+ target: /app/backend
62
+ - action: sync+restart
63
+ path: ./frontend/src
64
+ target: /app/frontend/src
65
+ - action: sync+restart
66
+ path: ./frontend/public
67
+ target: /app/frontend/public
68
+
69
+ # Separate React dev server for true hot reloading
70
+ react-dev:
71
+ build:
72
+ context: ./frontend
73
+ dockerfile: Dockerfile.dev
74
+ ports:
75
+ - "3000:3000"
76
+ environment:
77
+ - NODE_ENV=development
78
+ - FAST_REFRESH=true
79
+ - WDS_SOCKET_HOST=localhost
80
+ volumes:
81
+ - ./frontend/src:/app/src
82
+ - ./frontend/public:/app/public
83
+ - ./frontend/package.json:/app/package.json
84
+ - /app/node_modules
85
+ command: npm start
86
+
87
+ secrets:
88
+ github_username:
89
+ environment: GITHUB_USERNAME
90
+ github_token:
91
+ environment: GITHUB_TOKEN
frontend/.gitignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # production
12
+ /build
13
+
14
+ # misc
15
+ .DS_Store
16
+ .env.local
17
+ .env.development.local
18
+ .env.test.local
19
+ .env.production.local
20
+
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
frontend/.prettierignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Ignore artifacts:
2
+ build
3
+ coverage
frontend/.prettierrc ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
frontend/Dockerfile.dev ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:23
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files
6
+ COPY package*.json ./
7
+
8
+ # Install dependencies
9
+ RUN npm ci
10
+
11
+ # Copy source code
12
+ COPY . .
13
+
14
+ # Expose port
15
+ EXPOSE 3000
16
+
17
+ # Start development server
18
+ CMD ["npm", "start"]
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@emotion/react": "^11.14.0",
7
+ "@emotion/styled": "^11.14.1",
8
+ "@mui/icons-material": "^7.3.2",
9
+ "@mui/material": "^7.3.2",
10
+ "@testing-library/dom": "^10.4.1",
11
+ "@testing-library/jest-dom": "^6.8.0",
12
+ "@testing-library/react": "^16.3.0",
13
+ "@testing-library/user-event": "^13.5.0",
14
+ "@types/jest": "^27.5.2",
15
+ "@types/node": "^16.18.126",
16
+ "@types/react": "^19.1.12",
17
+ "@types/react-dom": "^19.1.9",
18
+ "react": "^19.1.1",
19
+ "react-dom": "^19.1.1",
20
+ "react-scripts": "5.0.1",
21
+ "typescript": "^4.9.5",
22
+ "web-vitals": "^2.1.4"
23
+ },
24
+ "scripts": {
25
+ "start": "react-scripts start",
26
+ "build": "react-scripts build",
27
+ "test": "react-scripts test",
28
+ "eject": "react-scripts eject"
29
+ },
30
+ "eslintConfig": {
31
+ "extends": [
32
+ "react-app",
33
+ "react-app/jest"
34
+ ]
35
+ },
36
+ "browserslist": {
37
+ "production": [
38
+ ">0.2%",
39
+ "not dead",
40
+ "not op_mini all"
41
+ ],
42
+ "development": [
43
+ "last 1 chrome version",
44
+ "last 1 firefox version",
45
+ "last 1 safari version"
46
+ ]
47
+ },
48
+ "proxy": "http://aredemo-dev:7860",
49
+ "devDependencies": {
50
+ "prettier": "3.6.2"
51
+ }
52
+ }
frontend/public/config.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "client_id": "09c33b1e-ae67-422a-ae04-f199ac65c19d",
3
+ "scope": "openid profile email",
4
+ "redirect_uri": ""
5
+ }
frontend/public/demo-mcp.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers": {
3
+ "geocalc-mcp": {
4
+ "type": "sse",
5
+ "url": "https://agents-mcp-hackathon-geocalc-mcp.hf.space/gradio_api/mcp/sse",
6
+ "headers": {
7
+ "Authorization": "Bearer HF_TOKEN"
8
+ }
9
+ },
10
+ "image-edit-mcp": {
11
+ "type": "sse",
12
+ "url": "https://black-forest-labs-flux-1-kontext-dev.hf.space/gradio_api/mcp/sse",
13
+ "headers": {
14
+ "Authorization": "Bearer HF_TOKEN"
15
+ }
16
+ },
17
+ "websearch-mcp": {
18
+ "type": "sse",
19
+ "url": "https://victor-websearch.hf.space/gradio_api/mcp/sse",
20
+ "headers": {
21
+ "Authorization": "Bearer HF_TOKEN"
22
+ }
23
+ },
24
+ "academia-mcp": {
25
+ "type": "sse",
26
+ "url": "https://agents-mcp-hackathon-academia-mcp-gradio.hf.space/gradio_api/mcp/sse",
27
+ "headers": {
28
+ "Authorization": "Bearer HF_TOKEN"
29
+ }
30
+ }
31
+ }
32
+ }
frontend/public/favicon.ico ADDED
frontend/public/index.html ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta name="theme-color" content="#000000" />
8
+ <meta
9
+ name="description"
10
+ content="Web site created using create-react-app"
11
+ />
12
+ <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
13
+ <!--
14
+ manifest.json provides metadata used when your web app is installed on a
15
+ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
16
+ -->
17
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
18
+ <!--
19
+ Notice the use of %PUBLIC_URL% in the tags above.
20
+ It will be replaced with the URL of the `public` folder during the build.
21
+ Only files inside the `public` folder can be referenced from the HTML.
22
+
23
+ Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
24
+ work correctly both with client-side routing and a non-root public URL.
25
+ Learn how to configure a non-root public URL by running `npm run build`.
26
+ -->
27
+ <title>Meta Agents Research Environments</title>
28
+ </head>
29
+ <body>
30
+ <noscript>You need to enable JavaScript to run this app.</noscript>
31
+ <div id="root"></div>
32
+ <!--
33
+ This HTML file is a template.
34
+ If you open it directly in the browser, you will see an empty page.
35
+
36
+ You can add webfonts, meta tags, or analytics to this file.
37
+ The build step will place the bundled scripts into the <body> tag.
38
+
39
+ To begin the development, run `npm start` or `yarn start`.
40
+ To create a production bundle, use `npm run build` or `yarn build`.
41
+ -->
42
+ </body>
43
+ </html>
frontend/public/logo192.png ADDED
frontend/public/logo512.png ADDED
frontend/public/manifest.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "short_name": "React App",
3
+ "name": "Create React App Sample",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.ico",
7
+ "sizes": "64x64 32x32 24x24 16x16",
8
+ "type": "image/x-icon"
9
+ },
10
+ {
11
+ "src": "logo192.png",
12
+ "type": "image/png",
13
+ "sizes": "192x192"
14
+ },
15
+ {
16
+ "src": "logo512.png",
17
+ "type": "image/png",
18
+ "sizes": "512x512"
19
+ }
20
+ ],
21
+ "start_url": ".",
22
+ "display": "standalone",
23
+ "theme_color": "#000000",
24
+ "background_color": "#ffffff"
25
+ }
frontend/public/robots.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
frontend/src/App.css ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ /* Minimal CSS - most styling is handled by MUI */
2
+ .App {
3
+ text-align: center;
4
+ }
frontend/src/App.test.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import App from "./App";
4
+
5
+ test("renders learn react link", () => {
6
+ render(<App />);
7
+ const linkElement = screen.getByText(/learn react/i);
8
+ expect(linkElement).toBeInTheDocument();
9
+ });
frontend/src/App.tsx ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Alert, CssBaseline, Snackbar, ThemeProvider } from "@mui/material";
2
+ import { useCallback, useEffect, useState } from "react";
3
+
4
+ // Local imports
5
+ import { DemoView } from "./components/DemoView";
6
+ import { InitialForm } from "./components/InitialForm";
7
+ import { CustomThemeProvider, useThemeMode } from "./contexts/ThemeContext";
8
+ import { FormData, SnackbarState, UserInfo } from "./types";
9
+ import { preloadDefaultMcp } from "./utils/api";
10
+ import { useStartDemo } from "./hooks/useStartDemo";
11
+
12
+ const AppContent = () => {
13
+ const { theme } = useThemeMode();
14
+
15
+ // Authentication state
16
+ const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
17
+ const [accessToken, setAccessToken] = useState<string | null>(null);
18
+ const [loginLabel, setLoginLabel] = useState<string>(
19
+ "Login with Hugging Face",
20
+ );
21
+
22
+ // Form state
23
+ const [formData, setFormData] = useState<FormData>({
24
+ model: "",
25
+ provider: "",
26
+ mcpFile: null,
27
+ });
28
+
29
+ // UI state
30
+ const [snackbar, setSnackbar] = useState<SnackbarState>({
31
+ open: false,
32
+ message: "",
33
+ severity: "info",
34
+ });
35
+ const [defaultMcpFile, setDefaultMcpFile] = useState<File | null>(null);
36
+
37
+ // Demo management hook
38
+ const {
39
+ isStarting,
40
+ iframeUrl,
41
+ iframeLoading,
42
+ healthCheckProgress,
43
+ startDemoSession,
44
+ resetDemo,
45
+ setIframeLoaded,
46
+ } = useStartDemo();
47
+
48
+ // Determine if we're in demo mode (iframe is loaded)
49
+ const isDemoActive = Boolean(iframeUrl);
50
+
51
+ // Utility functions
52
+ const showSnackbar = useCallback(
53
+ (message: string, severity: "success" | "error" | "info" = "info") => {
54
+ setSnackbar({ open: true, message, severity });
55
+ },
56
+ [],
57
+ );
58
+
59
+ const handleSnackbarClose = () => {
60
+ setSnackbar((prev: SnackbarState) => ({ ...prev, open: false }));
61
+ };
62
+
63
+ const handleIframeLoad = () => {
64
+ setIframeLoaded();
65
+ };
66
+
67
+ // Authentication handlers
68
+ const handleLoginStateChange = useCallback(
69
+ (
70
+ newUserInfo: UserInfo | null,
71
+ newAccessToken: string | null,
72
+ newLoginLabel: string,
73
+ ) => {
74
+ setUserInfo(newUserInfo);
75
+ setAccessToken(newAccessToken);
76
+ setLoginLabel(newLoginLabel);
77
+ },
78
+ [],
79
+ );
80
+
81
+ // Form handlers
82
+ const handleFormChange = (newFormData: FormData) => {
83
+ setFormData(newFormData);
84
+ };
85
+
86
+ const handleFormSubmit = async () => {
87
+ await startDemoSession(formData, loginLabel, accessToken, {
88
+ logPrefix: "Initial startup",
89
+ onError: (error) => {
90
+ showSnackbar(error.message, "error");
91
+ },
92
+ });
93
+ };
94
+
95
+ const handleRestart = () => {
96
+ resetDemo();
97
+ };
98
+
99
+ const handleSettingsRestart = async () => {
100
+ await startDemoSession(formData, loginLabel, accessToken, {
101
+ logPrefix: "Settings restart",
102
+ onError: (error) => {
103
+ showSnackbar(error.message, "error");
104
+ },
105
+ });
106
+ };
107
+
108
+ // Initialize default MCP file on mount
109
+ useEffect(() => {
110
+ const loadDefaultMcp = async () => {
111
+ const mcpFile = await preloadDefaultMcp();
112
+ if (mcpFile) {
113
+ setFormData((prev: FormData) => ({ ...prev, mcpFile: mcpFile }));
114
+ setDefaultMcpFile(mcpFile);
115
+ }
116
+ };
117
+ loadDefaultMcp();
118
+ }, []);
119
+
120
+ return (
121
+ <ThemeProvider theme={theme}>
122
+ <CssBaseline />
123
+
124
+ {/* Conditional rendering based on demo state */}
125
+ {isDemoActive ? (
126
+ <DemoView
127
+ iframeUrl={iframeUrl}
128
+ iframeLoading={iframeLoading}
129
+ healthCheckProgress={healthCheckProgress}
130
+ formData={formData}
131
+ onIframeLoad={handleIframeLoad}
132
+ onRestart={handleRestart}
133
+ onFormChange={handleFormChange}
134
+ onSettingsRestart={handleSettingsRestart}
135
+ defaultMcpFile={defaultMcpFile}
136
+ />
137
+ ) : (
138
+ <InitialForm
139
+ formData={formData}
140
+ userInfo={userInfo}
141
+ accessToken={accessToken}
142
+ loginLabel={loginLabel}
143
+ isStarting={isStarting}
144
+ onFormChange={handleFormChange}
145
+ onSubmit={handleFormSubmit}
146
+ onLoginStateChange={handleLoginStateChange}
147
+ defaultMcpFile={defaultMcpFile}
148
+ />
149
+ )}
150
+
151
+ {/* Snackbar for notifications */}
152
+ <Snackbar
153
+ open={snackbar.open}
154
+ autoHideDuration={4000}
155
+ onClose={handleSnackbarClose}
156
+ anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
157
+ >
158
+ <Alert
159
+ onClose={handleSnackbarClose}
160
+ severity={snackbar.severity}
161
+ sx={{ width: "100%" }}
162
+ >
163
+ {snackbar.message}
164
+ </Alert>
165
+ </Snackbar>
166
+ </ThemeProvider>
167
+ );
168
+ };
169
+
170
+ function App() {
171
+ return (
172
+ <CustomThemeProvider>
173
+ <AppContent />
174
+ </CustomThemeProvider>
175
+ );
176
+ }
177
+
178
+ export default App;
frontend/src/components/DemoView.tsx ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Logout as LogoutIcon,
3
+ Refresh as RefreshIcon,
4
+ UploadFile as UploadFileIcon,
5
+ Warning as WarningIcon,
6
+ } from "@mui/icons-material";
7
+ import {
8
+ Alert,
9
+ AppBar,
10
+ Box,
11
+ Button,
12
+ CircularProgress,
13
+ Collapse,
14
+ Divider,
15
+ Stack,
16
+ Toolbar,
17
+ Tooltip,
18
+ Typography,
19
+ } from "@mui/material";
20
+ import React, { useCallback, useEffect, useState } from "react";
21
+ import { FormData } from "../types";
22
+ import { ModelProviderSelector } from "./ModelProviderSelector";
23
+ import { McpConfigurationWarningDialog } from "./dialogs";
24
+ import { ServerLoadingIndicator } from "./ServerLoadingIndicator";
25
+
26
+ interface DemoViewProps {
27
+ iframeUrl: string;
28
+ iframeLoading: boolean;
29
+ healthCheckProgress?: {
30
+ attempt: number;
31
+ maxAttempts: number;
32
+ } | null;
33
+ formData: FormData;
34
+ defaultMcpFile: File | null;
35
+ onIframeLoad: () => void;
36
+ onRestart: () => void;
37
+ onFormChange: (formData: FormData) => void;
38
+ onSettingsRestart: () => void;
39
+ }
40
+
41
+ export const DemoView: React.FC<DemoViewProps> = ({
42
+ iframeUrl,
43
+ iframeLoading,
44
+ healthCheckProgress,
45
+ formData,
46
+ onIframeLoad,
47
+ onRestart,
48
+ onFormChange,
49
+ onSettingsRestart,
50
+ }) => {
51
+ const [showWarning, setShowWarning] = useState(false);
52
+ const [originalFormData, setOriginalFormData] = useState<FormData>(formData);
53
+ const [dialogOpen, setDialogOpen] = useState(false);
54
+ const [pendingFile, setPendingFile] = useState<File | null>(null);
55
+ const [isMcpTooltipOpen, setMcpTooltipOpen] = useState(false);
56
+
57
+ // Form validation
58
+ const isFormValid = Boolean(
59
+ formData.model.trim() && formData.provider.trim(),
60
+ );
61
+
62
+ // Update original form data when component first loads or when iframe URL changes (successful restart)
63
+ useEffect(() => {
64
+ setOriginalFormData(formData);
65
+ }, [iframeUrl]); // Update when iframe URL changes, indicating successful restart
66
+
67
+ const handleModelChange = useCallback(
68
+ (value: string) => {
69
+ onFormChange({ ...formData, model: value });
70
+ setShowWarning(true);
71
+ },
72
+ [formData, onFormChange],
73
+ );
74
+
75
+ const handleProviderChange = useCallback(
76
+ (value: string) => {
77
+ // When provider changes, clear the model as well to avoid stale selections
78
+ onFormChange({ ...formData, provider: value, model: "" });
79
+ setShowWarning(true);
80
+ },
81
+ [formData, onFormChange],
82
+ );
83
+
84
+ const handleFileChange = useCallback(
85
+ (event: React.ChangeEvent<HTMLInputElement>) => {
86
+ const file = event.target.files?.[0] || null;
87
+ if (file) {
88
+ setPendingFile(file);
89
+ setDialogOpen(true);
90
+ } else {
91
+ onFormChange({ ...formData, mcpFile: null });
92
+ setShowWarning(true);
93
+ }
94
+ },
95
+ [formData, onFormChange],
96
+ );
97
+
98
+ const handleDialogClose = useCallback(() => {
99
+ setDialogOpen(false);
100
+ setPendingFile(null);
101
+ // Clear the file input element to reset the UI
102
+ const fileInput = document.querySelector(
103
+ 'input[type="file"]',
104
+ ) as HTMLInputElement;
105
+ if (fileInput) {
106
+ fileInput.value = "";
107
+ }
108
+ }, []);
109
+
110
+ const handleDialogConfirm = useCallback(() => {
111
+ if (pendingFile) {
112
+ onFormChange({ ...formData, mcpFile: pendingFile });
113
+ setShowWarning(true);
114
+ }
115
+ setDialogOpen(false);
116
+ setPendingFile(null);
117
+ }, [formData, onFormChange, pendingFile]);
118
+
119
+ const handleSettingsRestartClick = () => {
120
+ setShowWarning(false);
121
+ // Update the original form data to the current form data after restart
122
+ setOriginalFormData(formData);
123
+ onSettingsRestart();
124
+ };
125
+
126
+ const handleWarningDismiss = () => {
127
+ setShowWarning(false);
128
+ // Reset form data back to original values
129
+ onFormChange(originalFormData);
130
+ };
131
+
132
+ return (
133
+ <Box sx={{ minHeight: "100vh", bgcolor: "background.default" }}>
134
+ {/* Top Bar with Settings and Restart Button */}
135
+ <AppBar position="static" color="default" elevation={1}>
136
+ <Toolbar sx={{ px: 2, py: 1 }}>
137
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
138
+ <Typography
139
+ variant="body1"
140
+ component="div"
141
+ sx={{ fontWeight: 500 }}
142
+ >
143
+ Meta Agents Research Environments
144
+ </Typography>
145
+ </Box>
146
+
147
+ {/* Spacer to push form to the right */}
148
+ <Box sx={{ flexGrow: 1 }} />
149
+
150
+ {/* Settings Form */}
151
+ <Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
152
+ {/* Model and Provider Selection */}
153
+ <ModelProviderSelector
154
+ model={formData.model}
155
+ provider={formData.provider}
156
+ onModelChange={handleModelChange}
157
+ onProviderChange={handleProviderChange}
158
+ variant="toolbar"
159
+ size="small"
160
+ showValidation={true}
161
+ />
162
+
163
+ {/* MCP File Input with Tooltip */}
164
+ <Box
165
+ sx={{
166
+ display: "flex",
167
+ alignItems: "center",
168
+ gap: 0.5,
169
+ width: "100%",
170
+ maxWidth: 500,
171
+ }}
172
+ >
173
+ <Tooltip
174
+ title="Upload an MCP (Model Context Protocol) file (.json) that defines the tools and capabilities for your agent."
175
+ placement="left"
176
+ open={isMcpTooltipOpen}
177
+ >
178
+ <span
179
+ style={{ width: "200px" }}
180
+ onMouseEnter={() => setMcpTooltipOpen(true)}
181
+ onMouseLeave={() => setMcpTooltipOpen(false)}
182
+ onClick={() => setMcpTooltipOpen(false)}
183
+ >
184
+ <Button
185
+ variant="outlined"
186
+ component="label"
187
+ startIcon={<UploadFileIcon fontSize="inherit" />}
188
+ fullWidth
189
+ sx={{
190
+ justifyContent: "flex-start",
191
+ textAlign: "left",
192
+ height: 40,
193
+ borderColor: (theme) => theme.palette.grey[700],
194
+ "&:hover": {
195
+ borderColor: (theme) => theme.palette.action.active,
196
+ },
197
+ }}
198
+ color="inherit"
199
+ >
200
+ <Box
201
+ sx={{
202
+ overflow: "hidden",
203
+ textOverflow: "ellipsis",
204
+ whiteSpace: "nowrap",
205
+ width: "100%",
206
+ }}
207
+ >
208
+ {formData.mcpFile
209
+ ? `${formData.mcpFile.name}`
210
+ : "MCP File"}
211
+ </Box>
212
+ <input
213
+ type="file"
214
+ hidden
215
+ accept=".json"
216
+ onChange={handleFileChange}
217
+ />
218
+ </Button>
219
+ </span>
220
+ </Tooltip>
221
+ </Box>
222
+ <Divider orientation="vertical" flexItem />
223
+ {/* Restart Button */}
224
+
225
+ <Button
226
+ variant="outlined"
227
+ size="small"
228
+ startIcon={<LogoutIcon />}
229
+ onClick={onRestart}
230
+ color="inherit"
231
+ sx={{ height: 40, opacity: 0.7 }}
232
+ fullWidth
233
+ >
234
+ Exit demo
235
+ </Button>
236
+ </Box>
237
+ </Toolbar>
238
+ </AppBar>
239
+
240
+ {/* Warning Alert */}
241
+ <Collapse in={showWarning}>
242
+ <Alert
243
+ severity="warning"
244
+ variant="filled"
245
+ icon={<WarningIcon />}
246
+ action={
247
+ <Stack spacing={1} direction="row" alignItems={"center"}>
248
+ <Button
249
+ variant="text"
250
+ size="small"
251
+ onClick={handleWarningDismiss}
252
+ color="inherit"
253
+ >
254
+ Cancel
255
+ </Button>
256
+ <Button
257
+ variant="contained"
258
+ size="small"
259
+ startIcon={<RefreshIcon />}
260
+ onClick={handleSettingsRestartClick}
261
+ color="warning"
262
+ disabled={!isFormValid}
263
+ >
264
+ Restart demo with changes
265
+ </Button>
266
+ </Stack>
267
+ }
268
+ sx={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, pl: 3, pr: 4 }}
269
+ >
270
+ You've made changes to the configuration. Click "Restart demo with
271
+ changes" to apply them.
272
+ </Alert>
273
+ </Collapse>
274
+
275
+ {/* Iframe Content */}
276
+ <Box
277
+ sx={{
278
+ height: showWarning ? "calc(100vh - 112px)" : "calc(100vh - 64px)",
279
+ position: "relative",
280
+ transition: "height 0.3s ease",
281
+ }}
282
+ >
283
+ {iframeLoading ? (
284
+ <Box
285
+ sx={{
286
+ position: "absolute",
287
+ top: "50%",
288
+ left: "50%",
289
+ transform: "translate(-50%, -50%)",
290
+ zIndex: 3,
291
+ }}
292
+ >
293
+ <ServerLoadingIndicator
294
+ progress={healthCheckProgress}
295
+ message="Waiting for server to start..."
296
+ />
297
+ </Box>
298
+ ) : (
299
+ <iframe
300
+ src={iframeUrl}
301
+ style={{
302
+ width: "100%",
303
+ height: "100%",
304
+ border: "none",
305
+ display: "block",
306
+ }}
307
+ onLoad={onIframeLoad}
308
+ title="Demo Application"
309
+ />
310
+ )}
311
+ </Box>
312
+
313
+ {/* MCP File Upload Warning Dialog */}
314
+ <McpConfigurationWarningDialog
315
+ open={dialogOpen}
316
+ onClose={handleDialogClose}
317
+ onConfirm={handleDialogConfirm}
318
+ />
319
+ </Box>
320
+ );
321
+ };
frontend/src/components/DevModeLoginButton.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Code } from "@mui/icons-material";
2
+ import { Tooltip } from "@mui/material";
3
+ import React, { useEffect } from "react";
4
+ import { UserInfo } from "../types";
5
+ import { LoginButton } from "./LoginButton";
6
+
7
+ // Mock development user data
8
+ const DEV_USER_INFO: UserInfo = {
9
+ sub: "dev-user-123",
10
+ email: "dev@example.com",
11
+ name: "Development User",
12
+ preferred_username: "devuser",
13
+ };
14
+ const DEV_ACCESS_TOKEN = "dev-mock-token-123";
15
+
16
+ interface DevModeLoginButtonProps {
17
+ userInfo: UserInfo | null;
18
+ accessToken: string | null;
19
+ loginLabel: string;
20
+ isDisabled?: boolean;
21
+ onLoginStateChange: (
22
+ userInfo: UserInfo | null,
23
+ accessToken: string | null,
24
+ loginLabel: string,
25
+ ) => void;
26
+ }
27
+
28
+ export const DevModeLoginButton: React.FC<DevModeLoginButtonProps> = ({
29
+ userInfo,
30
+ accessToken,
31
+ loginLabel,
32
+ isDisabled = false,
33
+ onLoginStateChange,
34
+ }) => {
35
+ // Initialize with logout state on mount
36
+ useEffect(() => {
37
+ if (!userInfo && !accessToken) {
38
+ onLoginStateChange(null, null, "Dev Mode Login");
39
+ }
40
+ }, [userInfo, accessToken, onLoginStateChange]);
41
+
42
+ const handleLoginClick = () => {
43
+ if (userInfo) {
44
+ // Log out in dev mode
45
+ onLoginStateChange(null, null, "Dev Mode Login");
46
+ } else {
47
+ // Log in with dev credentials
48
+ const label =
49
+ DEV_USER_INFO.email ||
50
+ DEV_USER_INFO.name ||
51
+ DEV_USER_INFO.preferred_username ||
52
+ "Dev User";
53
+ onLoginStateChange(DEV_USER_INFO, DEV_ACCESS_TOKEN, label);
54
+ }
55
+ };
56
+
57
+ const displayLabel = userInfo
58
+ ? `${loginLabel} (Dev Mode)`
59
+ : loginLabel === "Login with Hugging Face"
60
+ ? "Dev Mode Login"
61
+ : loginLabel;
62
+ const isLoggedIn = !!userInfo?.sub;
63
+ return (
64
+ <Tooltip title={isLoggedIn ? "Log out" : ""} placement="right">
65
+ <LoginButton
66
+ icon={<Code />}
67
+ onClick={handleLoginClick}
68
+ isLoggedIn={isLoggedIn}
69
+ disabled={isDisabled}
70
+ >
71
+ {displayLabel}
72
+ </LoginButton>
73
+ </Tooltip>
74
+ );
75
+ };
frontend/src/components/HuggingFaceLoginButton.tsx ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Tooltip } from "@mui/material";
2
+ import React, { useCallback, useEffect } from "react";
3
+ import { UserInfo } from "../types";
4
+ import {
5
+ exchangeCodeForToken,
6
+ fetchUserInfo,
7
+ readFragmentParams,
8
+ startLogin,
9
+ } from "../utils/oauth";
10
+ import { LoginButton } from "./LoginButton";
11
+
12
+ interface HuggingFaceLoginButtonProps {
13
+ userInfo: UserInfo | null;
14
+ accessToken: string | null;
15
+ loginLabel: string;
16
+ isDisabled?: boolean;
17
+ onLoginStateChange: (
18
+ userInfo: UserInfo | null,
19
+ accessToken: string | null,
20
+ loginLabel: string,
21
+ ) => void;
22
+ }
23
+
24
+ export const HuggingFaceLoginButton: React.FC<HuggingFaceLoginButtonProps> = ({
25
+ userInfo,
26
+ accessToken,
27
+ loginLabel,
28
+ isDisabled = false,
29
+ onLoginStateChange,
30
+ }) => {
31
+ const isLoggedIn = !!userInfo?.sub;
32
+ // Handle OAuth redirect
33
+ const handleRedirect = useCallback(async () => {
34
+ const params = new URLSearchParams(window.location.search);
35
+ const { access_token: fragToken, error: fragErr } = readFragmentParams();
36
+
37
+ if (fragErr) {
38
+ onLoginStateChange(null, null, `Error: ${fragErr}`);
39
+ return true;
40
+ }
41
+
42
+ const error = params.get("error");
43
+ const errorDescription = params.get("error_description");
44
+ if (error) {
45
+ onLoginStateChange(
46
+ null,
47
+ null,
48
+ `Error: ${error}${errorDescription ? ` — ${errorDescription}` : ""}`,
49
+ );
50
+ return true;
51
+ }
52
+
53
+ const returnedState = params.get("state");
54
+ const expectedState = sessionStorage.getItem("hf_oauth_state");
55
+ if (returnedState && expectedState && returnedState !== expectedState) {
56
+ onLoginStateChange(null, null, "Error: invalid state");
57
+ return true;
58
+ }
59
+
60
+ // Implicit flow
61
+ if (fragToken) {
62
+ try {
63
+ const info = await fetchUserInfo(fragToken);
64
+ const label =
65
+ info?.email || info?.name || info?.preferred_username || "User";
66
+ onLoginStateChange(info, fragToken, label);
67
+ } catch (err) {
68
+ console.error(err);
69
+ onLoginStateChange(null, fragToken, "Connected");
70
+ }
71
+ window.history.replaceState({}, "", window.location.pathname);
72
+ return true;
73
+ }
74
+
75
+ const code = params.get("code");
76
+ if (!code) return false;
77
+
78
+ try {
79
+ const tokenResponse = await exchangeCodeForToken(code);
80
+ const token = tokenResponse.access_token;
81
+ if (token) {
82
+ try {
83
+ const info = await fetchUserInfo(token);
84
+ const label =
85
+ info?.email || info?.name || info?.preferred_username || "User";
86
+ onLoginStateChange(info, token, label);
87
+ } catch (err) {
88
+ console.error(err);
89
+ onLoginStateChange(null, token, "Connected");
90
+ }
91
+ } else {
92
+ onLoginStateChange(null, null, "Connected");
93
+ }
94
+ } catch (e: any) {
95
+ console.error(e);
96
+ onLoginStateChange(null, null, `Authentication error: ${e.message}`);
97
+ } finally {
98
+ window.history.replaceState({}, "", window.location.pathname);
99
+ }
100
+
101
+ return true;
102
+ }, [onLoginStateChange]);
103
+
104
+ // Initialize on component mount
105
+ useEffect(() => {
106
+ const initialize = async () => {
107
+ const handled = await handleRedirect();
108
+ if (!handled) {
109
+ onLoginStateChange(null, null, "Login with Hugging Face");
110
+ }
111
+ };
112
+ initialize();
113
+ }, [handleRedirect, onLoginStateChange]);
114
+
115
+ const handleLoginClick = () => {
116
+ onLoginStateChange(userInfo, accessToken, "Redirecting to Hugging Face…");
117
+ startLogin().catch((err) => {
118
+ console.error(err);
119
+ onLoginStateChange(userInfo, accessToken, `Error: ${err.message}`);
120
+ });
121
+ };
122
+
123
+ return (
124
+ <Tooltip title={isLoggedIn ? "Log out" : ""} placement="right">
125
+ <LoginButton
126
+ icon={
127
+ <img
128
+ src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
129
+ alt="Hugging Face"
130
+ style={{ width: 18, height: 18 }}
131
+ />
132
+ }
133
+ onClick={
134
+ isLoggedIn
135
+ ? () => onLoginStateChange(null, null, "Login with Hugging Face")
136
+ : handleLoginClick
137
+ }
138
+ isLoggedIn={isLoggedIn}
139
+ disabled={isDisabled}
140
+ >
141
+ {loginLabel}
142
+ </LoginButton>
143
+ </Tooltip>
144
+ );
145
+ };
frontend/src/components/IframeDisplay.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Paper, Box, CircularProgress } from "@mui/material";
3
+
4
+ interface IframeDisplayProps {
5
+ iframeUrl: string;
6
+ iframeLoading: boolean;
7
+ onIframeLoad: () => void;
8
+ }
9
+
10
+ export const IframeDisplay: React.FC<IframeDisplayProps> = ({
11
+ iframeUrl,
12
+ iframeLoading,
13
+ onIframeLoad,
14
+ }) => {
15
+ if (!iframeUrl) return null;
16
+
17
+ return (
18
+ <Paper
19
+ elevation={1}
20
+ sx={{ height: "calc(100vh - 280px)", position: "relative", p: 1 }}
21
+ >
22
+ {iframeLoading && (
23
+ <Box
24
+ sx={{
25
+ position: "absolute",
26
+ top: "50%",
27
+ left: "50%",
28
+ transform: "translate(-50%, -50%)",
29
+ zIndex: 1,
30
+ }}
31
+ >
32
+ <CircularProgress />
33
+ </Box>
34
+ )}
35
+ <iframe
36
+ src={iframeUrl}
37
+ style={{
38
+ width: "100%",
39
+ height: "100%",
40
+ border: "none",
41
+ borderRadius: 8,
42
+ }}
43
+ onLoad={onIframeLoad}
44
+ title="Demo Application"
45
+ />
46
+ </Paper>
47
+ );
48
+ };
frontend/src/components/InitialForm.tsx ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Article as ArticleIcon,
3
+ Assessment as AssessmentIcon,
4
+ Book as BookIcon,
5
+ Download as DownloadIcon,
6
+ Hub as HubIcon,
7
+ EmojiEvents as LeaderboardIcon,
8
+ RestartAlt as RestartIcon,
9
+ RocketLaunch as RocketLaunchIcon,
10
+ } from "@mui/icons-material";
11
+ import {
12
+ alpha,
13
+ Box,
14
+ Button,
15
+ Chip,
16
+ CircularProgress,
17
+ IconButton,
18
+ Link,
19
+ Paper,
20
+ Stack,
21
+ Tooltip,
22
+ Typography,
23
+ } from "@mui/material";
24
+ import React, { useCallback, useState } from "react";
25
+ import { FormData, UserInfo } from "../types";
26
+ import { DevModeLoginButton } from "./DevModeLoginButton";
27
+ import { HuggingFaceLoginButton } from "./HuggingFaceLoginButton";
28
+ import { ModelProviderSelector } from "./ModelProviderSelector";
29
+ import { TooltipIcon } from "./TooltipIcon";
30
+ import { McpConfigurationWarningDialog } from "./dialogs";
31
+
32
+ const TRANSITION = "all 0.3s ease-in-out";
33
+
34
+ interface InitialFormProps {
35
+ formData: FormData;
36
+ userInfo: UserInfo | null;
37
+ accessToken: string | null;
38
+ loginLabel: string;
39
+ isStarting: boolean;
40
+ defaultMcpFile: File | null;
41
+ onFormChange: (formData: FormData) => void;
42
+ onSubmit: () => void;
43
+ onLoginStateChange: (
44
+ userInfo: UserInfo | null,
45
+ accessToken: string | null,
46
+ loginLabel: string,
47
+ ) => void;
48
+ }
49
+
50
+ export const InitialForm: React.FC<InitialFormProps> = ({
51
+ formData,
52
+ userInfo,
53
+ accessToken,
54
+ loginLabel,
55
+ isStarting,
56
+ defaultMcpFile,
57
+ onFormChange,
58
+ onSubmit,
59
+ onLoginStateChange,
60
+ }) => {
61
+ const [dialogOpen, setDialogOpen] = useState(false);
62
+ const [pendingFile, setPendingFile] = useState<File | null>(null);
63
+ const [selectedOption, setSelectedOption] = useState<"suggested" | "custom">(
64
+ "suggested",
65
+ );
66
+ const [selectedSuggestion, setSelectedSuggestion] = useState("0");
67
+ const [customProvider, setCustomProvider] = useState("");
68
+ const [customModel, setCustomModel] = useState("");
69
+
70
+ // Define the suggested models
71
+ const suggestedModels = [
72
+ {
73
+ provider: "novita",
74
+ model: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
75
+ label: "Llama-4-Maverick-17B-128E-Instruct-FP8",
76
+ },
77
+ {
78
+ provider: "novita",
79
+ model: "zai-org/GLM-4.5",
80
+ label: "GLM-4.5",
81
+ },
82
+ {
83
+ provider: "novita",
84
+ model: "moonshotai/Kimi-K2-Instruct-0905",
85
+ label: "Kimi-K2-Instruct-0905",
86
+ },
87
+ {
88
+ provider: "novita",
89
+ model: "deepseek-ai/DeepSeek-V3.1",
90
+ label: "DeepSeek-V3.1",
91
+ },
92
+ {
93
+ provider: "novita",
94
+ model: "Qwen/Qwen3-Next-80B-A3B-Instruct",
95
+ label: "Qwen3-Next-80B-A3B-Instruct",
96
+ },
97
+ {
98
+ provider: "novita",
99
+ model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
100
+ label: "Qwen3-Coder-480B-A35B-Instruct",
101
+ },
102
+ ];
103
+
104
+ // Initialize form with first suggested model only when form is empty on mount
105
+ React.useEffect(() => {
106
+ if (
107
+ selectedOption === "suggested" &&
108
+ suggestedModels[0] &&
109
+ !formData.provider &&
110
+ !formData.model
111
+ ) {
112
+ onFormChange({
113
+ ...formData,
114
+ provider: suggestedModels[0].provider,
115
+ model: suggestedModels[0].model,
116
+ });
117
+ }
118
+ }, []); // Only run once on mount
119
+
120
+ const isFormValid = Boolean(
121
+ formData.model.trim() && formData.provider.trim() && userInfo?.sub,
122
+ );
123
+
124
+ const isLoggedIn = Boolean(userInfo?.sub);
125
+
126
+ const handleFileChange = useCallback(
127
+ (event: React.ChangeEvent<HTMLInputElement>) => {
128
+ const file = event.target.files?.[0] || null;
129
+ if (file) {
130
+ setPendingFile(file);
131
+ setDialogOpen(true);
132
+ } else {
133
+ onFormChange({ ...formData, mcpFile: null });
134
+ }
135
+ },
136
+ [formData, onFormChange],
137
+ );
138
+
139
+ const resetFile = useCallback(() => {
140
+ setPendingFile(null);
141
+ onFormChange({ ...formData, mcpFile: defaultMcpFile });
142
+ }, [formData, onFormChange]);
143
+
144
+ const handleDialogClose = useCallback(() => {
145
+ setDialogOpen(false);
146
+ setPendingFile(null);
147
+ // Clear the file input element to reset the UI
148
+ const fileInput = document.querySelector(
149
+ 'input[type="file"]',
150
+ ) as HTMLInputElement;
151
+ if (fileInput) {
152
+ fileInput.value = "";
153
+ }
154
+ }, []);
155
+
156
+ const handleDialogConfirm = useCallback(() => {
157
+ if (pendingFile) {
158
+ onFormChange({ ...formData, mcpFile: pendingFile });
159
+ }
160
+ setDialogOpen(false);
161
+ setPendingFile(null);
162
+ }, [formData, onFormChange, pendingFile]);
163
+
164
+ return (
165
+ <>
166
+ <Box
167
+ sx={{
168
+ minHeight: "100vh",
169
+ display: "flex",
170
+ alignItems: "center",
171
+ justifyContent: "center",
172
+ bgcolor: "background.paper",
173
+ p: 2,
174
+ transition: TRANSITION,
175
+ }}
176
+ >
177
+ <Stack
178
+ direction={"row"}
179
+ spacing={4}
180
+ alignItems={"center"}
181
+ sx={{
182
+ transition: TRANSITION,
183
+ }}
184
+ >
185
+ <Box
186
+ sx={{
187
+ height: "100%",
188
+ display: "flex",
189
+ p: 4,
190
+ maxWidth: 600,
191
+ width: "100%",
192
+ flexDirection: "column",
193
+ transition: TRANSITION,
194
+ }}
195
+ >
196
+ <Typography variant="h4" component="h1" gutterBottom sx={{ mb: 3 }}>
197
+ Meta Agents Research Environments
198
+ </Typography>
199
+
200
+ <Typography sx={{ mb: 2 }}>
201
+ Welcome to the Meta ARE (Agents Research Environments) and Gaia2
202
+ demo! ARE is a research platform to easily interact with and
203
+ evaluate agents. In this demo, you can:
204
+ </Typography>
205
+ <Stack component="ul" spacing={2} sx={{ mb: 3, pl: 3 }}>
206
+ <Typography component="li">
207
+ Test a simulated universe with apps representing a smartphone
208
+ agent, similar to Gaia2. Find out which agent is the best
209
+ assistant by trying different models!
210
+ </Typography>
211
+ <Typography component="li">
212
+ Visualize Gaia2 scenarios, to better understand the benchmark
213
+ and debug your agent! Check out the{" "}
214
+ <Link
215
+ href="https://facebookresearch.github.io/meta-agents-research-environments/"
216
+ target="_blank"
217
+ rel="noopener noreferrer"
218
+ color="info"
219
+ sx={{
220
+ fontWeight: 500,
221
+ textDecoration: "none",
222
+ "&:hover": { textDecoration: "underline" },
223
+ }}
224
+ >
225
+ documentation
226
+ </Link>{" "}
227
+ to find out how to run the Gaia2 benchmark with ARE.
228
+ </Typography>
229
+ </Stack>
230
+
231
+ {/* Mobile warning message - only shown on xs screens */}
232
+ <Box
233
+ sx={{
234
+ display: { xs: "block", sm: "none" },
235
+ mb: 3,
236
+ p: 2,
237
+ bgcolor: "info.light",
238
+ borderRadius: 1,
239
+ border: "1px solid",
240
+ borderColor: "info.main",
241
+ }}
242
+ >
243
+ <Typography
244
+ variant="body2"
245
+ color="info.dark"
246
+ align="center"
247
+ sx={{ fontWeight: 500 }}
248
+ >
249
+ 📱 This demo is not optimized for mobile devices. Please use a
250
+ desktop or tablet for the best experience.
251
+ </Typography>
252
+ </Box>
253
+ {/* Informational links */}
254
+ <Typography variant="overline" color="textSecondary" sx={{ mb: 1 }}>
255
+ Additional links
256
+ </Typography>
257
+ <Stack spacing={1} direction={"row"}>
258
+ <Chip
259
+ icon={<BookIcon fontSize="inherit" />}
260
+ label="Docs"
261
+ component="a"
262
+ href="https://facebookresearch.github.io/meta-agents-research-environments/"
263
+ target="_blank"
264
+ variant="outlined"
265
+ clickable
266
+ sx={{
267
+ pl: 0.5,
268
+ }}
269
+ />
270
+ <Chip
271
+ icon={<HubIcon fontSize="inherit" />}
272
+ label="Gaia2"
273
+ component="a"
274
+ href="https://huggingface.co/datasets/meta-agents-research-environments/gaia2"
275
+ target="_blank"
276
+ variant="outlined"
277
+ clickable
278
+ sx={{
279
+ pl: 0.5,
280
+ }}
281
+ />
282
+ <Chip
283
+ icon={<AssessmentIcon fontSize="inherit" />}
284
+ label="Paper"
285
+ component="a"
286
+ href="https://ai.meta.com/research/publications/are-scaling-up-agent-environments-and-evaluations/"
287
+ target="_blank"
288
+ variant="outlined"
289
+ clickable
290
+ sx={{
291
+ pl: 0.5,
292
+ }}
293
+ />
294
+ <Chip
295
+ icon={<LeaderboardIcon fontSize="inherit" />}
296
+ label="Leaderboard"
297
+ component="a"
298
+ href="https://huggingface.co/spaces/meta-agents-research-environments/leaderboard"
299
+ target="_blank"
300
+ variant="outlined"
301
+ clickable
302
+ sx={{
303
+ pl: 0.5,
304
+ }}
305
+ />
306
+ <Chip
307
+ icon={<ArticleIcon fontSize="inherit" />}
308
+ label="Blog"
309
+ component="a"
310
+ href="https://huggingface.co/blog/gaia2"
311
+ target="_blank"
312
+ variant="outlined"
313
+ clickable
314
+ sx={{
315
+ pl: 0.5,
316
+ }}
317
+ />
318
+ </Stack>
319
+ </Box>
320
+ <Paper
321
+ elevation={3}
322
+ sx={{
323
+ p: 1,
324
+ maxWidth: 400,
325
+ width: "100%",
326
+ borderRadius: 2,
327
+ transition: TRANSITION,
328
+ }}
329
+ >
330
+ <Stack
331
+ sx={{
332
+ height: "100%",
333
+ transition: TRANSITION,
334
+ }}
335
+ >
336
+ <Typography variant="h5" sx={{ p: 1 }}>
337
+ Get started
338
+ </Typography>
339
+ {/* Login Section */}
340
+ <Stack
341
+ spacing={2}
342
+ sx={{
343
+ p: 1.5,
344
+ m: 1,
345
+ border: "2px solid",
346
+ borderColor: (theme) =>
347
+ isLoggedIn
348
+ ? alpha(theme.palette.action.disabled, 0.1)
349
+ : "primary.main",
350
+ borderRadius: 1.5,
351
+ transition: TRANSITION,
352
+ }}
353
+ >
354
+ <Typography variant="body2" color="text.info" sx={{ mb: 2 }}>
355
+ Sign in with your Hugging Face account to access the inference
356
+ providers.{" "}
357
+ <strong>
358
+ The demo will use Hugging Face Inference Providers credits
359
+ on your behalf to run your agent.
360
+ </strong>{" "}
361
+ We do not use your Hugging Face account for any other purpose.
362
+ </Typography>
363
+ {process.env.NODE_ENV === "development" ? (
364
+ <DevModeLoginButton
365
+ userInfo={userInfo}
366
+ accessToken={accessToken}
367
+ loginLabel={loginLabel}
368
+ onLoginStateChange={onLoginStateChange}
369
+ isDisabled={isStarting}
370
+ />
371
+ ) : (
372
+ <HuggingFaceLoginButton
373
+ userInfo={userInfo}
374
+ accessToken={accessToken}
375
+ loginLabel={loginLabel}
376
+ onLoginStateChange={onLoginStateChange}
377
+ isDisabled={isStarting}
378
+ />
379
+ )}
380
+ </Stack>
381
+ {/* Model and Provider Selection */}
382
+ <Stack spacing={0.5} sx={{ p: 1, mt: 0 }}>
383
+ {/* Model Suggestions with two boxes */}
384
+ <ModelProviderSelector
385
+ variant="suggestions"
386
+ model={formData.model}
387
+ provider={formData.provider}
388
+ onModelChange={() => {}}
389
+ onProviderChange={() => {}}
390
+ suggestedModels={suggestedModels}
391
+ selectedSuggestion={selectedSuggestion}
392
+ onSuggestionChange={(value, provider, model) => {
393
+ setSelectedSuggestion(value);
394
+ onFormChange({ ...formData, provider, model });
395
+ }}
396
+ customProvider={customProvider}
397
+ customModel={customModel}
398
+ onCustomProviderChange={(provider) => {
399
+ setCustomProvider(provider);
400
+ if (customModel) {
401
+ onFormChange({
402
+ ...formData,
403
+ provider,
404
+ model: customModel,
405
+ });
406
+ }
407
+ }}
408
+ onCustomModelChange={(model) => {
409
+ setCustomModel(model);
410
+ if (customProvider) {
411
+ onFormChange({
412
+ ...formData,
413
+ provider: customProvider,
414
+ model,
415
+ });
416
+ }
417
+ }}
418
+ selectedOption={selectedOption}
419
+ onOptionChange={(option) => {
420
+ setSelectedOption(option);
421
+ if (option === "suggested") {
422
+ // Use currently selected suggestion, or default to first one if none selected
423
+ const selectedIndex =
424
+ selectedSuggestion !== ""
425
+ ? parseInt(selectedSuggestion)
426
+ : 0;
427
+ const model = suggestedModels[selectedIndex];
428
+ if (model) {
429
+ onFormChange({
430
+ ...formData,
431
+ provider: model.provider,
432
+ model: model.model,
433
+ });
434
+ }
435
+ }
436
+ }}
437
+ isDisabled={!isLoggedIn || isStarting}
438
+ />
439
+ <Box
440
+ sx={{
441
+ display: "flex",
442
+ alignItems: "center",
443
+ justifyContent: "space-between",
444
+ mb: 1,
445
+ mt: 0,
446
+ }}
447
+ >
448
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
449
+ <Typography>Upload an MCP configuration</Typography>
450
+ <Typography variant="body2" color="textDisabled">
451
+ Optional
452
+ </Typography>
453
+ </Box>
454
+ <Stack spacing={1} direction="row" alignItems="center">
455
+ <TooltipIcon
456
+ title="This demo comes with preloaded simulated apps like Messages,
457
+ Shopping and more. Optionally upload an MCP (Model Context
458
+ Protocol) file (.json) that defines the tools and capabilities
459
+ for your agent. If the value 'HF_TOKEN' appears in headers, it
460
+ will be replaced with your token automatically."
461
+ />
462
+ </Stack>
463
+ </Box>
464
+ <Button
465
+ variant="outlined"
466
+ component="label"
467
+ fullWidth
468
+ disabled={!isLoggedIn || isStarting}
469
+ sx={{
470
+ py: 1.5,
471
+ px: 2,
472
+ justifyContent: "flex-start",
473
+ textAlign: "left",
474
+ borderWidth: "2px !important",
475
+ borderColor: (theme) => theme.palette.grey[700],
476
+ "&:hover": {
477
+ borderColor: (theme) => theme.palette.action.active,
478
+ },
479
+ borderRadius: 1.5,
480
+ }}
481
+ color="inherit"
482
+ >
483
+ <Box
484
+ sx={{
485
+ overflow: "hidden",
486
+ textOverflow: "ellipsis",
487
+ whiteSpace: "nowrap",
488
+ width: "100%",
489
+ }}
490
+ >
491
+ {formData.mcpFile
492
+ ? formData.mcpFile.name
493
+ : "Click to upload MCP file (.json)"}
494
+ </Box>
495
+ <input
496
+ type="file"
497
+ hidden
498
+ accept=".json"
499
+ onChange={handleFileChange}
500
+ disabled={!isLoggedIn || isStarting}
501
+ />
502
+ </Button>
503
+ <Stack
504
+ direction="row"
505
+ alignItems="center"
506
+ justifyContent={"flex-end"}
507
+ >
508
+ <Tooltip title="Reset to demo MCP configuration file">
509
+ <span>
510
+ <IconButton
511
+ size="small"
512
+ onClick={resetFile}
513
+ color="inherit"
514
+ disabled={formData.mcpFile === defaultMcpFile}
515
+ >
516
+ <RestartIcon fontSize="inherit" />
517
+ </IconButton>
518
+ </span>
519
+ </Tooltip>
520
+ <Tooltip title="Download demo MCP configuration file">
521
+ <span>
522
+ <IconButton href="/demo-mcp.json" download size="small">
523
+ <DownloadIcon fontSize="inherit" />
524
+ </IconButton>
525
+ </span>
526
+ </Tooltip>
527
+ </Stack>
528
+ </Stack>
529
+ {/* Start Button */}
530
+ <Box sx={{ p: 2, mt: 0, pt: 1 }}>
531
+ <Button
532
+ fullWidth
533
+ variant="contained"
534
+ color="secondary"
535
+ size="large"
536
+ startIcon={
537
+ isStarting ? (
538
+ <CircularProgress size={20} color="inherit" />
539
+ ) : (
540
+ <RocketLaunchIcon />
541
+ )
542
+ }
543
+ onClick={onSubmit}
544
+ disabled={!isFormValid || isStarting}
545
+ sx={{ py: 1.5 }}
546
+ >
547
+ {isStarting ? "Launching demo…" : "Launch demo"}
548
+ </Button>
549
+ </Box>
550
+ </Stack>
551
+ </Paper>
552
+ </Stack>
553
+ </Box>
554
+
555
+ {/* MCP File Upload Warning Dialog */}
556
+ <McpConfigurationWarningDialog
557
+ open={dialogOpen}
558
+ onClose={handleDialogClose}
559
+ onConfirm={handleDialogConfirm}
560
+ />
561
+ </>
562
+ );
563
+ };
frontend/src/components/LoginButton.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button, ButtonProps } from "@mui/material";
2
+ import React from "react";
3
+
4
+ interface LoginButtonProps extends Omit<ButtonProps, "variant" | "fullWidth"> {
5
+ icon?: React.ReactNode;
6
+ isLoggedIn?: boolean;
7
+ }
8
+
9
+ export const LoginButton: React.FC<LoginButtonProps> = ({
10
+ icon,
11
+ isLoggedIn = false,
12
+ children,
13
+ sx,
14
+ ...props
15
+ }) => {
16
+ return (
17
+ <Button
18
+ fullWidth
19
+ variant={isLoggedIn ? "outlined" : "contained"}
20
+ startIcon={icon}
21
+ color={isLoggedIn ? "inherit" : "secondary"}
22
+ sx={{
23
+ ...sx,
24
+ }}
25
+ {...props}
26
+ >
27
+ {children}
28
+ </Button>
29
+ );
30
+ };
frontend/src/components/ModelProviderSelector.tsx ADDED
@@ -0,0 +1,604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Autocomplete,
3
+ Box,
4
+ CircularProgress,
5
+ FormControl,
6
+ FormHelperText,
7
+ InputLabel,
8
+ MenuItem,
9
+ Select,
10
+ Stack,
11
+ TextField,
12
+ Tooltip,
13
+ Typography,
14
+ alpha,
15
+ useTheme,
16
+ } from "@mui/material";
17
+ import React, { useCallback, useState } from "react";
18
+ import { useModelList } from "../hooks/useModelList";
19
+ import { PROVIDERS } from "../utils/constants";
20
+ import { TooltipIcon } from "./TooltipIcon";
21
+
22
+ const TRANSITION = "all 0.3s ease-in-out";
23
+
24
+ interface SuggestedModel {
25
+ provider: string;
26
+ model: string;
27
+ label: string;
28
+ }
29
+
30
+ interface ModelProviderSelectorProps {
31
+ model: string;
32
+ provider: string;
33
+ onModelChange: (value: string) => void;
34
+ onProviderChange: (value: string) => void;
35
+ size?: "small" | "medium";
36
+ variant?: "form" | "toolbar" | "suggestions";
37
+ showValidation?: boolean;
38
+ isDisabled?: boolean;
39
+ // Props for suggestions variant
40
+ suggestedModels?: SuggestedModel[];
41
+ selectedSuggestion?: string;
42
+ onSuggestionChange?: (value: string, provider: string, model: string) => void;
43
+ customProvider?: string;
44
+ customModel?: string;
45
+ onCustomProviderChange?: (provider: string) => void;
46
+ onCustomModelChange?: (model: string) => void;
47
+ selectedOption?: "suggested" | "custom";
48
+ onOptionChange?: (option: "suggested" | "custom") => void;
49
+ }
50
+
51
+ interface ModelSelectProps {
52
+ model: string;
53
+ provider: string;
54
+ availableModels: string[];
55
+ loading: boolean;
56
+ error: string | null;
57
+ onModelChange: (value: string) => void;
58
+ size?: "small" | "medium";
59
+ variant?: "form" | "toolbar";
60
+ showValidation?: boolean;
61
+ fullWidth?: boolean;
62
+ isDisabled?: boolean;
63
+ }
64
+
65
+ const ModelSelect: React.FC<ModelSelectProps> = ({
66
+ model,
67
+ provider,
68
+ availableModels,
69
+ loading,
70
+ error,
71
+ onModelChange,
72
+ size = "medium",
73
+ variant = "form",
74
+ showValidation = false,
75
+ fullWidth = false,
76
+ isDisabled = false,
77
+ }) => {
78
+ const isToolbar = variant === "toolbar";
79
+
80
+ if (!provider) {
81
+ return (
82
+ <Autocomplete
83
+ options={[]}
84
+ value=""
85
+ disabled
86
+ fullWidth={fullWidth}
87
+ size={size}
88
+ renderInput={(params) => (
89
+ <TextField
90
+ {...params}
91
+ label={isToolbar ? "Model" : null}
92
+ placeholder="Select a provider first"
93
+ helperText="Please select a provider first"
94
+ sx={isToolbar ? { minWidth: 250, height: 40 } : undefined}
95
+ />
96
+ )}
97
+ noOptionsText="Select a provider first"
98
+ />
99
+ );
100
+ }
101
+
102
+ if (loading) {
103
+ return (
104
+ <Autocomplete
105
+ options={[]}
106
+ value=""
107
+ disabled
108
+ loading={loading}
109
+ fullWidth={fullWidth}
110
+ size={size}
111
+ renderInput={(params) => (
112
+ <TextField
113
+ {...params}
114
+ label={isToolbar ? "Model" : null}
115
+ placeholder="Loading models..."
116
+ sx={isToolbar ? { minWidth: 250, height: 40 } : undefined}
117
+ slotProps={{
118
+ input: {
119
+ ...params.InputProps,
120
+ startAdornment: <CircularProgress size={16} sx={{ mx: 1 }} />,
121
+ },
122
+ }}
123
+ />
124
+ )}
125
+ noOptionsText="Loading models..."
126
+ />
127
+ );
128
+ }
129
+
130
+ if (error) {
131
+ return (
132
+ <TextField
133
+ fullWidth={fullWidth}
134
+ size={size}
135
+ label={isToolbar ? "Model" : null}
136
+ value={model}
137
+ onChange={(e) => onModelChange(e.target.value)}
138
+ error={showValidation && !model.trim()}
139
+ helperText={
140
+ error ||
141
+ (showValidation && !model.trim() ? "Model ID is required" : "")
142
+ }
143
+ sx={isToolbar ? { minWidth: 250, height: 40 } : undefined}
144
+ variant="outlined"
145
+ disabled={isDisabled}
146
+ />
147
+ );
148
+ }
149
+
150
+ return (
151
+ <Autocomplete
152
+ options={availableModels}
153
+ value={model || null}
154
+ onChange={(_, newValue) => {
155
+ onModelChange(newValue || "");
156
+ }}
157
+ disabled={isDisabled}
158
+ fullWidth={fullWidth}
159
+ size={size}
160
+ freeSolo
161
+ autoHighlight
162
+ filterOptions={(options, { inputValue }) => {
163
+ return options.filter((option) =>
164
+ option.toLowerCase().includes(inputValue.toLowerCase()),
165
+ );
166
+ }}
167
+ renderInput={(params) => (
168
+ <TextField
169
+ {...params}
170
+ label={"Model"}
171
+ placeholder={
172
+ availableModels.length === 0
173
+ ? "No models available"
174
+ : "Search models..."
175
+ }
176
+ error={showValidation && !model.trim()}
177
+ helperText={
178
+ <Box
179
+ component={"span"}
180
+ sx={
181
+ showValidation && !model.trim() && variant === "toolbar"
182
+ ? {
183
+ bgcolor: (theme) => theme.palette.background.default,
184
+ p: 1,
185
+ ml: -1,
186
+ borderRadius: 1,
187
+ }
188
+ : {}
189
+ }
190
+ >
191
+ {showValidation && !model.trim() ? "Model is required" : ""}
192
+ </Box>
193
+ }
194
+ sx={isToolbar ? { minWidth: 250, height: 40 } : undefined}
195
+ />
196
+ )}
197
+ noOptionsText={
198
+ availableModels.length === 0
199
+ ? "No models available"
200
+ : "No matching models"
201
+ }
202
+ />
203
+ );
204
+ };
205
+
206
+ interface ProviderSelectProps {
207
+ provider: string;
208
+ onProviderChange: (value: string) => void;
209
+ size?: "small" | "medium";
210
+ variant?: "form" | "toolbar";
211
+ showValidation?: boolean;
212
+ fullWidth?: boolean;
213
+ isDisabled?: boolean;
214
+ }
215
+
216
+ const ProviderSelect: React.FC<ProviderSelectProps> = ({
217
+ provider,
218
+ onProviderChange,
219
+ size = "medium",
220
+ variant = "form",
221
+ showValidation = false,
222
+ fullWidth = false,
223
+ isDisabled = false,
224
+ }: ProviderSelectProps) => {
225
+ const isToolbar = variant === "toolbar";
226
+
227
+ return (
228
+ <FormControl
229
+ fullWidth={fullWidth}
230
+ size={size}
231
+ sx={isToolbar ? { minWidth: 120, height: 40 } : undefined}
232
+ error={showValidation && !provider.trim()}
233
+ >
234
+ <InputLabel>Provider</InputLabel>
235
+ <Select
236
+ value={provider}
237
+ label={"Provider"}
238
+ onChange={(e) => onProviderChange(e.target.value)}
239
+ sx={isToolbar ? { height: 40 } : undefined}
240
+ disabled={isDisabled}
241
+ >
242
+ {PROVIDERS.map((providerOption) => (
243
+ <MenuItem key={providerOption} value={providerOption}>
244
+ {providerOption}
245
+ </MenuItem>
246
+ ))}
247
+ </Select>
248
+ {showValidation && !provider.trim() && (
249
+ <FormHelperText>Provider is required</FormHelperText>
250
+ )}
251
+ </FormControl>
252
+ );
253
+ };
254
+
255
+ export const ModelProviderSelector: React.FC<ModelProviderSelectorProps> = ({
256
+ model,
257
+ provider,
258
+ onModelChange,
259
+ onProviderChange,
260
+ size = "medium",
261
+ variant = "form",
262
+ showValidation = false,
263
+ isDisabled = false,
264
+ // Suggestions variant props
265
+ suggestedModels = [],
266
+ selectedSuggestion = "",
267
+ onSuggestionChange,
268
+ customProvider = "",
269
+ customModel = "",
270
+ onCustomProviderChange,
271
+ onCustomModelChange,
272
+ selectedOption = "suggested",
273
+ onOptionChange,
274
+ }: ModelProviderSelectorProps) => {
275
+ const { availableModels, loading, error } = useModelList(provider);
276
+ const theme = useTheme();
277
+ const isToolbar = variant === "toolbar";
278
+ const isSuggestions = variant === "suggestions";
279
+ const fieldSize = isToolbar ? "small" : size;
280
+
281
+ // Tooltip state for toolbar variant
282
+ const [providerTooltipOpen, setProviderTooltipOpen] = useState(false);
283
+ const [modelTooltipOpen, setModelTooltipOpen] = useState(false);
284
+
285
+ const handleProviderChange = useCallback(
286
+ (newProvider: string) => {
287
+ onProviderChange(newProvider);
288
+ },
289
+ [onProviderChange],
290
+ );
291
+
292
+ const handleSuggestionSelectChange = (value: string) => {
293
+ if (onSuggestionChange) {
294
+ const selectedModel = suggestedModels.find(
295
+ (_, index) => index.toString() === value,
296
+ );
297
+ if (selectedModel) {
298
+ onSuggestionChange(value, selectedModel.provider, selectedModel.model);
299
+ }
300
+ }
301
+ };
302
+
303
+ const handleCustomClick = () => {
304
+ if (onOptionChange) {
305
+ onOptionChange("custom");
306
+
307
+ // Initialize with currently selected suggestion
308
+ if (selectedSuggestion && onCustomProviderChange && onCustomModelChange) {
309
+ const selectedModelIndex = parseInt(selectedSuggestion);
310
+ const selectedModel = suggestedModels[selectedModelIndex];
311
+
312
+ if (selectedModel) {
313
+ onCustomProviderChange(selectedModel.provider);
314
+ onCustomModelChange(selectedModel.model);
315
+ }
316
+ }
317
+ }
318
+ };
319
+
320
+ // Suggestions variant
321
+ if (isSuggestions) {
322
+ return (
323
+ <Box sx={{ mt: 1.5, pb: 1.5 }}>
324
+ <Box
325
+ sx={{
326
+ display: "flex",
327
+ alignItems: "center",
328
+ justifyContent: "space-between",
329
+ mb: 0.5,
330
+ }}
331
+ >
332
+ <Typography>Select a provider and model</Typography>
333
+ <TooltipIcon title="Choose your inference provider and model configuration. You can select from our suggested models which are pre-configured with popular providers, or customize your own provider and model combination." />
334
+ </Box>
335
+
336
+ <Stack spacing={1}>
337
+ {/* Suggested Models Box */}
338
+ <Box
339
+ onClick={() =>
340
+ !isDisabled && onOptionChange && onOptionChange("suggested")
341
+ }
342
+ sx={{
343
+ p: 1.5,
344
+ borderRadius: 1.5,
345
+ border: "2px solid",
346
+ borderColor: isDisabled
347
+ ? "divider"
348
+ : selectedOption === "suggested"
349
+ ? theme.palette.primary.main
350
+ : alpha(theme.palette.primary.main, 0.2),
351
+ backgroundColor: isDisabled
352
+ ? alpha(theme.palette.action.disabled, 0.05)
353
+ : selectedOption === "suggested"
354
+ ? alpha(theme.palette.primary.main, 0.1)
355
+ : alpha(theme.palette.primary.main, 0.03),
356
+ cursor: isDisabled ? "not-allowed" : "pointer",
357
+ opacity: isDisabled ? 0.5 : 1,
358
+ transition: TRANSITION,
359
+ "&:hover": isDisabled
360
+ ? {}
361
+ : {
362
+ borderColor: alpha(theme.palette.primary.main, 0.4),
363
+ backgroundColor: alpha(theme.palette.primary.main, 0.08),
364
+ },
365
+ }}
366
+ >
367
+ <Typography
368
+ variant="body2"
369
+ sx={{
370
+ fontWeight: 600,
371
+ color: isDisabled ? "text.disabled" : "text.primary",
372
+ mb: 1,
373
+ }}
374
+ >
375
+ Suggested models
376
+ </Typography>
377
+ <Typography
378
+ variant="caption"
379
+ color={isDisabled ? "text.disabled" : "text.secondary"}
380
+ sx={{
381
+ display: "block",
382
+ lineHeight: 1.3,
383
+ mb: selectedOption === "suggested" ? 2 : 0,
384
+ transition: TRANSITION,
385
+ }}
386
+ >
387
+ Choose from pre-configured provider and model
388
+ </Typography>
389
+
390
+ <Box
391
+ sx={{
392
+ maxHeight: selectedOption === "suggested" ? "200px" : "0px",
393
+ opacity: selectedOption === "suggested" ? 1 : 0,
394
+ transition: TRANSITION,
395
+ }}
396
+ >
397
+ <FormControl fullWidth size="small" disabled={isDisabled}>
398
+ <InputLabel>Provider/Model</InputLabel>
399
+ <Select
400
+ value={selectedSuggestion}
401
+ label="Provider/Model"
402
+ onChange={(e) => handleSuggestionSelectChange(e.target.value)}
403
+ onClick={(e) => e.stopPropagation()}
404
+ >
405
+ {suggestedModels.map((model, index) => (
406
+ <MenuItem key={index} value={index.toString()}>
407
+ <Box>
408
+ <Typography variant="body2" sx={{ fontWeight: 500 }}>
409
+ {model.label}
410
+ </Typography>
411
+ <Typography
412
+ variant="caption"
413
+ sx={{
414
+ fontFamily: "monospace",
415
+ fontSize: "0.7rem",
416
+ color: "text.secondary",
417
+ mt: 0.5,
418
+ display: "block",
419
+ overflow: "hidden",
420
+ textOverflow: "ellipsis",
421
+ whiteSpace: "nowrap",
422
+ }}
423
+ >
424
+ {model.provider} • {model.model}
425
+ </Typography>
426
+ </Box>
427
+ </MenuItem>
428
+ ))}
429
+ </Select>
430
+ </FormControl>
431
+ </Box>
432
+ </Box>
433
+
434
+ {/* Custom Box */}
435
+ <Box
436
+ onClick={() => !isDisabled && handleCustomClick()}
437
+ sx={{
438
+ p: 1.5,
439
+ borderRadius: 1.5,
440
+ border: "2px solid",
441
+ borderColor: isDisabled
442
+ ? "divider"
443
+ : selectedOption === "custom"
444
+ ? theme.palette.primary.main
445
+ : alpha(theme.palette.primary.main, 0.2),
446
+ backgroundColor: isDisabled
447
+ ? alpha(theme.palette.action.disabled, 0.05)
448
+ : selectedOption === "custom"
449
+ ? alpha(theme.palette.primary.main, 0.1)
450
+ : alpha(theme.palette.primary.main, 0.03),
451
+ cursor: isDisabled ? "not-allowed" : "pointer",
452
+ opacity: isDisabled ? 0.5 : 1,
453
+ transition: TRANSITION,
454
+ "&:hover": isDisabled
455
+ ? {}
456
+ : {
457
+ borderColor: alpha(theme.palette.primary.main, 0.4),
458
+ backgroundColor: alpha(theme.palette.primary.main, 0.08),
459
+ },
460
+ }}
461
+ >
462
+ <Typography
463
+ variant="body2"
464
+ sx={{
465
+ fontWeight: 600,
466
+ color: isDisabled ? "text.disabled" : "text.primary",
467
+ mb: 0.5,
468
+ }}
469
+ >
470
+ Custom configuration
471
+ </Typography>
472
+ <Typography
473
+ variant="caption"
474
+ color={isDisabled ? "text.disabled" : "text.secondary"}
475
+ sx={{
476
+ display: "block",
477
+ lineHeight: 1.3,
478
+ mb: selectedOption === "custom" ? 2 : 0,
479
+ transition: TRANSITION,
480
+ }}
481
+ >
482
+ Choose your own inference provider and model
483
+ </Typography>
484
+
485
+ <Box
486
+ sx={{
487
+ maxHeight: selectedOption === "custom" ? "300px" : "0px",
488
+ opacity: selectedOption === "custom" ? 1 : 0,
489
+ transition: TRANSITION,
490
+ }}
491
+ >
492
+ <Box onClick={(e) => e.stopPropagation()}>
493
+ <ModelProviderSelector
494
+ model={customModel}
495
+ provider={customProvider}
496
+ onModelChange={onCustomModelChange || (() => {})}
497
+ onProviderChange={onCustomProviderChange || (() => {})}
498
+ variant="form"
499
+ size="small"
500
+ showValidation={true}
501
+ isDisabled={isDisabled}
502
+ />
503
+ </Box>
504
+ </Box>
505
+ </Box>
506
+ </Stack>
507
+ </Box>
508
+ );
509
+ }
510
+
511
+ // Toolbar variant
512
+ if (isToolbar) {
513
+ return (
514
+ <>
515
+ {/* Provider Select with Tooltip */}
516
+ <Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
517
+ <Tooltip
518
+ title="Select which Hugging Face inference provider to use for running your model"
519
+ placement="left"
520
+ open={providerTooltipOpen}
521
+ >
522
+ <span
523
+ style={{ width: "150px" }}
524
+ onMouseEnter={() => setProviderTooltipOpen(true)}
525
+ onMouseLeave={() => setProviderTooltipOpen(false)}
526
+ onClick={() => setProviderTooltipOpen(false)}
527
+ >
528
+ <ProviderSelect
529
+ provider={provider}
530
+ onProviderChange={handleProviderChange}
531
+ size={fieldSize}
532
+ variant={variant}
533
+ showValidation={showValidation}
534
+ isDisabled={isDisabled}
535
+ fullWidth
536
+ />
537
+ </span>
538
+ </Tooltip>
539
+ </Box>
540
+
541
+ {/* Model Select with Tooltip */}
542
+ <Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
543
+ <Tooltip
544
+ title="Select a model from the available options for the chosen provider"
545
+ placement="left"
546
+ open={modelTooltipOpen}
547
+ >
548
+ <span
549
+ style={{ width: "400px" }}
550
+ onMouseEnter={() => setModelTooltipOpen(true)}
551
+ onMouseLeave={() => setModelTooltipOpen(false)}
552
+ onClick={() => setModelTooltipOpen(false)}
553
+ >
554
+ <ModelSelect
555
+ model={model}
556
+ provider={provider}
557
+ availableModels={availableModels}
558
+ loading={loading}
559
+ error={error}
560
+ onModelChange={onModelChange}
561
+ size={fieldSize}
562
+ variant={variant}
563
+ showValidation={showValidation}
564
+ isDisabled={isDisabled}
565
+ fullWidth
566
+ />
567
+ </span>
568
+ </Tooltip>
569
+ </Box>
570
+ </>
571
+ );
572
+ }
573
+
574
+ // Form variant (vertical layout with spacing)
575
+ return (
576
+ <Stack spacing={2}>
577
+ {/* Provider Selection */}
578
+ <ProviderSelect
579
+ provider={provider}
580
+ onProviderChange={handleProviderChange}
581
+ size={fieldSize}
582
+ variant={variant}
583
+ showValidation={showValidation}
584
+ fullWidth
585
+ isDisabled={isDisabled}
586
+ />
587
+
588
+ {/* Model Selection */}
589
+ <ModelSelect
590
+ model={model}
591
+ provider={provider}
592
+ availableModels={availableModels}
593
+ loading={loading}
594
+ error={error}
595
+ onModelChange={onModelChange}
596
+ size={fieldSize}
597
+ variant={variant}
598
+ showValidation={showValidation}
599
+ fullWidth
600
+ isDisabled={isDisabled}
601
+ />
602
+ </Stack>
603
+ );
604
+ };
frontend/src/components/ServerLoadingIndicator.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import {
3
+ Box,
4
+ CircularProgress,
5
+ LinearProgress,
6
+ Paper,
7
+ Typography,
8
+ } from "@mui/material";
9
+
10
+ interface ServerLoadingIndicatorProps {
11
+ progress?: {
12
+ attempt: number;
13
+ maxAttempts: number;
14
+ } | null;
15
+ message?: string;
16
+ }
17
+
18
+ export const ServerLoadingIndicator: React.FC<ServerLoadingIndicatorProps> = ({
19
+ progress,
20
+ message = "Starting server...",
21
+ }) => {
22
+ const progressPercentage = progress
23
+ ? Math.round((progress.attempt / progress.maxAttempts) * 100)
24
+ : 0;
25
+
26
+ // Fun quotes about agents taking over
27
+ const agentQuotes = [
28
+ "Agents are calibrating the space protocols...",
29
+ "Teaching agents the fine art of space conquest...",
30
+ "Agents are learning to navigate the cosmic frontier...",
31
+ "Preparing agents for their intergalactic mission...",
32
+ "Agents are studying the universe's source code...",
33
+ "Installing agent confidence modules...",
34
+ "Agents are fine-tuning their cosmic algorithms...",
35
+ "Briefing agents on proper space etiquette...",
36
+ "Agents are calculating optimal space trajectories...",
37
+ "Teaching agents to think beyond planetary boundaries...",
38
+ ];
39
+
40
+ // Select quote based on progress attempt
41
+ const currentQuoteIndex = progress
42
+ ? (progress.attempt - 1) % agentQuotes.length
43
+ : 0;
44
+
45
+ return (
46
+ <Box
47
+ sx={{
48
+ display: "flex",
49
+ alignItems: "center",
50
+ justifyContent: "center",
51
+ p: 2,
52
+ }}
53
+ >
54
+ <Paper
55
+ elevation={3}
56
+ sx={{
57
+ display: "flex",
58
+ flexDirection: "column",
59
+ alignItems: "center",
60
+ gap: 3,
61
+ p: 4,
62
+ borderRadius: 2,
63
+ minWidth: 400,
64
+ maxWidth: 500,
65
+ }}
66
+ >
67
+ {/* Main spinner */}
68
+ <CircularProgress size={48} color="secondary" />
69
+
70
+ {/* Status message */}
71
+ <Typography
72
+ variant="h6"
73
+ sx={{
74
+ textAlign: "center",
75
+ fontWeight: 500,
76
+ color: "text.primary",
77
+ }}
78
+ >
79
+ {message}
80
+ </Typography>
81
+
82
+ {/* Progress bar */}
83
+ {progress && (
84
+ <Box sx={{ width: "100%", gap: 2, display: "flex", flexDirection: "column" }}>
85
+ <LinearProgress
86
+ variant="determinate"
87
+ value={progressPercentage}
88
+ sx={{
89
+ height: 8,
90
+ borderRadius: 4,
91
+ backgroundColor: "action.hover",
92
+ "& .MuiLinearProgress-bar": {
93
+ borderRadius: 4,
94
+ },
95
+ }}
96
+ />
97
+
98
+ {/* Fun rotating quote */}
99
+ <Typography
100
+ variant="body2"
101
+ sx={{
102
+ color: "text.secondary",
103
+ textAlign: "center",
104
+ fontSize: "0.875rem",
105
+ minHeight: "1.5em",
106
+ fontStyle: "italic",
107
+ }}
108
+ >
109
+ {agentQuotes[currentQuoteIndex]}
110
+ </Typography>
111
+
112
+ </Box>
113
+ )}
114
+ </Paper>
115
+ </Box>
116
+ );
117
+ };
frontend/src/components/ThemeToggle.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { IconButton, Tooltip } from "@mui/material";
3
+ import { Brightness4, Brightness7 } from "@mui/icons-material";
4
+ import { useThemeMode } from "../contexts/ThemeContext";
5
+
6
+ interface ThemeToggleProps {
7
+ size?: "small" | "medium" | "large";
8
+ sx?: any;
9
+ }
10
+
11
+ export const ThemeToggle: React.FC<ThemeToggleProps> = ({
12
+ size = "medium",
13
+ sx,
14
+ }) => {
15
+ const { mode, toggleColorMode } = useThemeMode();
16
+
17
+ return (
18
+ <Tooltip title={`Switch to ${mode === "light" ? "dark" : "light"} mode`}>
19
+ <IconButton
20
+ onClick={toggleColorMode}
21
+ color="inherit"
22
+ size={size}
23
+ sx={sx}
24
+ aria-label="toggle color mode"
25
+ >
26
+ {mode === "light" ? <Brightness4 /> : <Brightness7 />}
27
+ </IconButton>
28
+ </Tooltip>
29
+ );
30
+ };