Pierre Andrews
commited on
Commit
·
f52d137
0
Parent(s):
Initial commit
Browse files> Co-authored-by: Pierre Andrews <[email protected]>
> Co-authored-by: Adrien <[email protected]>
> Co-authored-by: Avijit Ghosh <[email protected]>
> Co-authored-by: Clémentine Fourrier <[email protected]>
> Co-authored-by: Maxime Lecanu <[email protected]>
> Co-authored-by: thibaud frere <[email protected]>
> Co-authored-by: Kunal Mukesh Malkan <[email protected]>
> Co-authored-by: Romain Froger <[email protected]>
This view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +15 -0
- .gitattributes +44 -0
- .gitignore +6 -0
- .pre-commit-config.yaml +27 -0
- Dockerfile +125 -0
- Dockerfile.dev +13 -0
- LICENSE +21 -0
- README.md +14 -0
- app.py +603 -0
- backend/are.py +167 -0
- backend/cleanup.py +20 -0
- backend/globals.py +13 -0
- backend/iframe.py +81 -0
- backend/session.py +33 -0
- blog_assets/demo_base.mov +3 -0
- blog_assets/demo_robot_short.mp4 +3 -0
- blog_assets/demo_traces.mov +3 -0
- blog_assets/fig12_calls_tokens_vs_score_pareto_frontier.png +3 -0
- blog_assets/fig1_budget_scaling_curves.png +3 -0
- blog_assets/fig2_structure_of_are.png +3 -0
- blog_assets/fig9_gaia2_scores_per_capability.png +3 -0
- blog_assets/thumbnail_mare_gaia2.png +3 -0
- demo_mcp_file.json +33 -0
- docker-compose.yml +91 -0
- frontend/.gitignore +23 -0
- frontend/.prettierignore +3 -0
- frontend/.prettierrc +1 -0
- frontend/Dockerfile.dev +18 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +52 -0
- frontend/public/config.json +5 -0
- frontend/public/demo-mcp.json +32 -0
- frontend/public/favicon.ico +0 -0
- frontend/public/index.html +43 -0
- frontend/public/logo192.png +0 -0
- frontend/public/logo512.png +0 -0
- frontend/public/manifest.json +25 -0
- frontend/public/robots.txt +3 -0
- frontend/src/App.css +4 -0
- frontend/src/App.test.tsx +9 -0
- frontend/src/App.tsx +178 -0
- frontend/src/components/DemoView.tsx +321 -0
- frontend/src/components/DevModeLoginButton.tsx +75 -0
- frontend/src/components/HuggingFaceLoginButton.tsx +145 -0
- frontend/src/components/IframeDisplay.tsx +48 -0
- frontend/src/components/InitialForm.tsx +563 -0
- frontend/src/components/LoginButton.tsx +30 -0
- frontend/src/components/ModelProviderSelector.tsx +604 -0
- frontend/src/components/ServerLoadingIndicator.tsx +117 -0
- 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:[email protected]/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
|
blog_assets/fig1_budget_scaling_curves.png
ADDED
|
Git LFS Details
|
blog_assets/fig2_structure_of_are.png
ADDED
|
Git LFS Details
|
blog_assets/fig9_gaia2_scores_per_capability.png
ADDED
|
Git LFS Details
|
blog_assets/thumbnail_mare_gaia2.png
ADDED
|
|
Git LFS Details
|
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: "[email protected]",
|
| 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 |
+
};
|