soumi guria commited on
Commit Β·
f9a2deb
1
Parent(s): 28d5087
fix: serve React dashboard from FastAPI; add /training-log endpoint
Browse files- Dockerfile +23 -6
- backend/main.py +40 -0
- backend/requirements.txt +2 -1
- frontend/dist/assets/index-C3o0olYq.js +0 -0
- frontend/dist/assets/index-CV2RR57m.css +0 -1
- frontend/dist/index.html +2 -2
- frontend/src/components/Dashboard.jsx +256 -107
Dockerfile
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
|
|
|
| 5 |
COPY backend/requirements.txt .
|
| 6 |
RUN pip install uv && uv pip install --system --no-cache -r requirements.txt
|
| 7 |
|
| 8 |
-
|
| 9 |
-
COPY
|
| 10 |
-
COPY
|
| 11 |
-
COPY
|
| 12 |
-
COPY
|
| 13 |
-
COPY
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
EXPOSE 7860
|
| 16 |
|
|
|
|
| 1 |
+
# ββ Stage 1: Build React frontend ββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
FROM node:20-slim AS frontend-builder
|
| 3 |
+
|
| 4 |
+
WORKDIR /frontend
|
| 5 |
+
|
| 6 |
+
COPY frontend/package.json frontend/package-lock.json ./
|
| 7 |
+
RUN npm ci
|
| 8 |
+
|
| 9 |
+
COPY frontend/ ./
|
| 10 |
+
RUN npm run build
|
| 11 |
+
|
| 12 |
+
# ββ Stage 2: Python / FastAPI backend βββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
FROM python:3.11-slim
|
| 14 |
|
| 15 |
WORKDIR /app
|
| 16 |
|
| 17 |
+
# Python dependencies
|
| 18 |
COPY backend/requirements.txt .
|
| 19 |
RUN pip install uv && uv pip install --system --no-cache -r requirements.txt
|
| 20 |
|
| 21 |
+
# Application code
|
| 22 |
+
COPY backend/ /app/backend/
|
| 23 |
+
COPY server/ /app/server/
|
| 24 |
+
COPY grader/ /app/grader/
|
| 25 |
+
COPY models.py /app/models.py
|
| 26 |
+
COPY inference.py /app/inference.py
|
| 27 |
+
COPY openenv.yaml /app/openenv.yaml
|
| 28 |
+
|
| 29 |
+
# Built React SPA β served by FastAPI at / (assets at /assets/*)
|
| 30 |
+
COPY --from=frontend-builder /frontend/dist /app/frontend/dist
|
| 31 |
|
| 32 |
EXPOSE 7860
|
| 33 |
|
backend/main.py
CHANGED
|
@@ -11,6 +11,7 @@ Endpoints (matching openenv.yaml contract):
|
|
| 11 |
|
| 12 |
No openenv-core dependency β works on Python 3.9+.
|
| 13 |
"""
|
|
|
|
| 14 |
import os
|
| 15 |
import sys
|
| 16 |
import uuid
|
|
@@ -18,6 +19,8 @@ from typing import Dict, Any, Optional, List
|
|
| 18 |
|
| 19 |
from fastapi import FastAPI, HTTPException
|
| 20 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
|
| 21 |
from pydantic import BaseModel, Field
|
| 22 |
|
| 23 |
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
@@ -240,6 +243,43 @@ def build_app() -> FastAPI:
|
|
| 240 |
@app.get("/grade/expert", tags=["Grader"])
|
| 241 |
async def grade_expert(): return _run_grader_episode("expert")
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
return app
|
| 244 |
|
| 245 |
|
|
|
|
| 11 |
|
| 12 |
No openenv-core dependency β works on Python 3.9+.
|
| 13 |
"""
|
| 14 |
+
import json
|
| 15 |
import os
|
| 16 |
import sys
|
| 17 |
import uuid
|
|
|
|
| 19 |
|
| 20 |
from fastapi import FastAPI, HTTPException
|
| 21 |
from fastapi.middleware.cors import CORSMiddleware
|
| 22 |
+
from fastapi.responses import FileResponse
|
| 23 |
+
from fastapi.staticfiles import StaticFiles
|
| 24 |
from pydantic import BaseModel, Field
|
| 25 |
|
| 26 |
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
| 243 |
@app.get("/grade/expert", tags=["Grader"])
|
| 244 |
async def grade_expert(): return _run_grader_episode("expert")
|
| 245 |
|
| 246 |
+
# ------------------------------------------------------------------
|
| 247 |
+
# Training log β serves reward_curve.json if it exists
|
| 248 |
+
# ------------------------------------------------------------------
|
| 249 |
+
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 250 |
+
_REWARD_CURVE = os.path.join(_ROOT, "reward_curve.json")
|
| 251 |
+
|
| 252 |
+
@app.get("/training-log", tags=["Training"])
|
| 253 |
+
async def training_log():
|
| 254 |
+
if os.path.exists(_REWARD_CURVE):
|
| 255 |
+
with open(_REWARD_CURVE) as f:
|
| 256 |
+
return json.load(f)
|
| 257 |
+
return []
|
| 258 |
+
|
| 259 |
+
# ------------------------------------------------------------------
|
| 260 |
+
# React SPA β serve built frontend at / and /assets/*
|
| 261 |
+
# Only active when frontend/dist is present (i.e. inside Docker)
|
| 262 |
+
# ------------------------------------------------------------------
|
| 263 |
+
_DIST = os.path.join(_ROOT, "frontend", "dist")
|
| 264 |
+
_ASSETS = os.path.join(_DIST, "assets")
|
| 265 |
+
|
| 266 |
+
if os.path.isdir(_ASSETS):
|
| 267 |
+
app.mount("/assets", StaticFiles(directory=_ASSETS), name="assets")
|
| 268 |
+
|
| 269 |
+
if os.path.isdir(_DIST):
|
| 270 |
+
@app.get("/", include_in_schema=False)
|
| 271 |
+
async def serve_spa():
|
| 272 |
+
return FileResponse(os.path.join(_DIST, "index.html"))
|
| 273 |
+
else:
|
| 274 |
+
@app.get("/", tags=["System"])
|
| 275 |
+
async def api_root():
|
| 276 |
+
return {
|
| 277 |
+
"status": "ok",
|
| 278 |
+
"service": "Cognitive Load Manager β OpenEnv API",
|
| 279 |
+
"docs": "/docs",
|
| 280 |
+
"health": "/health",
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
return app
|
| 284 |
|
| 285 |
|
backend/requirements.txt
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
fastapi
|
| 2 |
-
uvicorn
|
| 3 |
pydantic
|
| 4 |
openai
|
| 5 |
requests
|
| 6 |
python-dotenv
|
| 7 |
openenv-core>=0.2.0
|
|
|
|
|
|
| 1 |
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
pydantic
|
| 4 |
openai
|
| 5 |
requests
|
| 6 |
python-dotenv
|
| 7 |
openenv-core>=0.2.0
|
| 8 |
+
aiofiles
|
frontend/dist/assets/index-C3o0olYq.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/assets/index-CV2RR57m.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.collapse{visibility:collapse}.sticky{position:sticky}.top-0{top:0}.top-6{top:1.5rem}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-3{margin-top:.75rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-2{height:.5rem}.h-3{height:.75rem}.h-\[calc\(100vh-6rem\)\]{height:calc(100vh - 6rem)}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-7xl{max-width:80rem}.flex-1{flex:1 1 0%}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.rounded-t-xl{border-top-left-radius:.75rem;border-top-right-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-emerald-500\/20{border-color:#10b98133}.border-emerald-500\/30{border-color:#10b9814d}.border-indigo-500\/30{border-color:#6366f14d}.border-indigo-500\/50{border-color:#6366f180}.border-red-500\/20{border-color:#ef444433}.border-slate-700{--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.border-slate-700\/50{border-color:#33415580}.border-slate-800{--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/20{background-color:#f59e0b33}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity, 1))}.bg-emerald-500\/10{background-color:#10b9811a}.bg-emerald-500\/20{background-color:#10b98133}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity, 1))}.bg-indigo-500\/10{background-color:#6366f11a}.bg-indigo-600{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity, 1))}.bg-indigo-900\/40{background-color:#312e8166}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-500\/20{background-color:#ef444433}.bg-slate-700{--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity, 1))}.bg-slate-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.bg-slate-800\/50{background-color:#1e293b80}.bg-slate-800\/80{background-color:#1e293bcc}.bg-slate-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.bg-slate-900\/50{background-color:#0f172a80}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-indigo-400{--tw-gradient-from: #818cf8 var(--tw-gradient-from-position);--tw-gradient-to: rgb(129 140 248 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-cyan-400{--tw-gradient-to: #22d3ee var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-2\.5{padding:.625rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-emerald-400{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.text-indigo-400{--tw-text-opacity: 1;color:rgb(129 140 248 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity, 1))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-transparent{color:transparent}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.shadow-\[0_0_15px_rgba\(99\,102\,241\,0\.15\)\]{--tw-shadow: 0 0 15px rgba(99,102,241,.15);--tw-shadow-colored: 0 0 15px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / .05);--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.selection\:bg-indigo-500\/30 *::-moz-selection{background-color:#6366f14d}.selection\:bg-indigo-500\/30 *::selection{background-color:#6366f14d}.selection\:bg-indigo-500\/30::-moz-selection{background-color:#6366f14d}.selection\:bg-indigo-500\/30::selection{background-color:#6366f14d}.hover\:scale-105:hover{--tw-scale-x: 1.05;--tw-scale-y: 1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-slate-500:hover{--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity, 1))}.hover\:border-slate-600:hover{--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity, 1))}.hover\:bg-emerald-500\/20:hover{background-color:#10b98133}.hover\:bg-indigo-500:hover{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity, 1))}.hover\:bg-indigo-500\/20:hover{background-color:#6366f133}.hover\:bg-slate-600:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity, 1))}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity, 1))}.active\:scale-95:active{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:hover\:bg-indigo-600:hover:disabled{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity, 1))}.disabled\:hover\:bg-slate-700:hover:disabled{--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity, 1))}@media (min-width: 1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
|
|
|
|
|
frontend/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>CLM Dashboard</title>
|
| 7 |
-
<script type="module" crossorigin src="/assets/index-
|
| 8 |
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
| 9 |
</head>
|
| 10 |
<body class="bg-slate-900 text-slate-100 font-sans">
|
| 11 |
<div id="root"></div>
|
|
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>CLM Dashboard</title>
|
| 7 |
+
<script type="module" crossorigin src="/assets/index-txBpMzf1.js"></script>
|
| 8 |
+
<link rel="stylesheet" crossorigin href="/assets/index-BvMzkwTv.css">
|
| 9 |
</head>
|
| 10 |
<body class="bg-slate-900 text-slate-100 font-sans">
|
| 11 |
<div id="root"></div>
|
frontend/src/components/Dashboard.jsx
CHANGED
|
@@ -1,12 +1,167 @@
|
|
| 1 |
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
export default function Dashboard() {
|
| 6 |
const [level, setLevel] = useState('medium');
|
| 7 |
const [sessionId, setSessionId] = useState(null);
|
| 8 |
const [obs, setObs] = useState(null);
|
| 9 |
-
const [stateData, setStateData] = useState(null);
|
| 10 |
const [logs, setLogs] = useState([]);
|
| 11 |
const [loading, setLoading] = useState(false);
|
| 12 |
const [error, setError] = useState(null);
|
|
@@ -16,8 +171,8 @@ export default function Dashboard() {
|
|
| 16 |
const fetchState = async (sid) => {
|
| 17 |
try {
|
| 18 |
const res = await fetch(`${API_BASE}/state?session_id=${sid}`);
|
| 19 |
-
if (res.ok)
|
| 20 |
-
} catch(e) { console.error(e); }
|
| 21 |
};
|
| 22 |
|
| 23 |
const handleReset = async () => {
|
|
@@ -28,15 +183,16 @@ export default function Dashboard() {
|
|
| 28 |
const res = await fetch(`${API_BASE}/reset`, {
|
| 29 |
method: 'POST',
|
| 30 |
headers: { 'Content-Type': 'application/json' },
|
| 31 |
-
body: JSON.stringify({ task_id: level })
|
| 32 |
});
|
|
|
|
| 33 |
const data = await res.json();
|
| 34 |
setSessionId(data.session_id);
|
| 35 |
setObs(data.observation);
|
| 36 |
setLogs([{ type: 'system', msg: `Episode started: ${level}` }]);
|
| 37 |
await fetchState(data.session_id);
|
| 38 |
} catch (err) {
|
| 39 |
-
setError(
|
| 40 |
} finally { setLoading(false); }
|
| 41 |
};
|
| 42 |
|
|
@@ -49,28 +205,26 @@ export default function Dashboard() {
|
|
| 49 |
const res = await fetch(`${API_BASE}/step`, {
|
| 50 |
method: 'POST',
|
| 51 |
headers: { 'Content-Type': 'application/json' },
|
| 52 |
-
body: JSON.stringify({ session_id: sessionId, action })
|
| 53 |
});
|
|
|
|
| 54 |
const data = await res.json();
|
| 55 |
setObs(data.observation);
|
| 56 |
-
setRewardHistory(prev => [...prev, {
|
| 57 |
-
step: prev.length + 1,
|
| 58 |
-
reward: data.reward
|
| 59 |
-
}]);
|
| 60 |
setLogs(prev => [...prev, {
|
| 61 |
type: data.reward >= 0 ? 'positive' : 'negative',
|
| 62 |
-
msg: `${actionType}${taskId ? ' '+taskId : ''} β reward: ${data.reward?.toFixed(3)}`,
|
| 63 |
-
reward: data.reward
|
| 64 |
}]);
|
| 65 |
if (data.done) {
|
| 66 |
setLogs(prev => [...prev, {
|
| 67 |
type: 'system',
|
| 68 |
-
msg: `DONE. Final score: ${data.info?.final_score?.toFixed(3)
|
| 69 |
}]);
|
|
|
|
| 70 |
}
|
| 71 |
await fetchState(sessionId);
|
| 72 |
setTimeout(() => {
|
| 73 |
-
if(scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 74 |
}, 50);
|
| 75 |
} catch (err) {
|
| 76 |
setError(err.message);
|
|
@@ -78,14 +232,16 @@ export default function Dashboard() {
|
|
| 78 |
};
|
| 79 |
|
| 80 |
const workers = obs?.visible_state?.workers || [];
|
| 81 |
-
const tasks
|
| 82 |
-
const
|
| 83 |
|
| 84 |
return (
|
| 85 |
-
<div style={{ minHeight: '100vh', background: '#f8fafc',
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
<div>
|
| 90 |
<h1 style={{ fontSize: 22, fontWeight: 700, color: '#0f172a', margin: 0 }}>
|
| 91 |
π§ StressTest β Cognitive Load Manager
|
|
@@ -96,20 +252,23 @@ export default function Dashboard() {
|
|
| 96 |
</div>
|
| 97 |
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
| 98 |
<select value={level} onChange={e => setLevel(e.target.value)}
|
| 99 |
-
style={{ border: '1px solid #e2e8f0', borderRadius: 8,
|
|
|
|
| 100 |
<option value="easy">Easy</option>
|
| 101 |
<option value="medium">Medium</option>
|
| 102 |
<option value="hard">Hard</option>
|
| 103 |
<option value="expert">Expert</option>
|
| 104 |
</select>
|
| 105 |
<button onClick={handleReset} disabled={loading}
|
| 106 |
-
style={{ background: '#6366f1', color: '#fff', border: 'none',
|
| 107 |
-
padding: '8px 18px', fontSize: 13, fontWeight: 600,
|
| 108 |
-
|
|
|
|
| 109 |
</button>
|
| 110 |
</div>
|
| 111 |
</div>
|
| 112 |
|
|
|
|
| 113 |
{error && (
|
| 114 |
<div style={{ background: '#fef2f2', border: '1px solid #fca5a5', borderRadius: 8,
|
| 115 |
padding: '10px 14px', marginBottom: 16, fontSize: 13, color: '#dc2626' }}>
|
|
@@ -117,33 +276,30 @@ export default function Dashboard() {
|
|
| 117 |
</div>
|
| 118 |
)}
|
| 119 |
|
| 120 |
-
{/*
|
| 121 |
{workers.length > 0 && (
|
| 122 |
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)',
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
{
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
letterSpacing: '0.08em', marginBottom: 6 }}>{m.label}</div>
|
| 135 |
-
<div style={{ fontSize: 22, fontWeight: 700, color: m.color }}>{m.value}</div>
|
| 136 |
-
</div>
|
| 137 |
-
))}
|
| 138 |
</div>
|
| 139 |
)}
|
| 140 |
|
|
|
|
| 141 |
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
|
| 142 |
-
|
| 143 |
-
{/*
|
| 144 |
-
<div style={{ background: '#fff', border: '1px solid #e2e8f0',
|
| 145 |
-
|
| 146 |
-
|
| 147 |
{tasks.length === 0 && (
|
| 148 |
<div style={{ color: '#cbd5e1', fontSize: 13, textAlign: 'center', padding: 20 }}>
|
| 149 |
Press Start to begin episode
|
|
@@ -154,87 +310,75 @@ export default function Dashboard() {
|
|
| 154 |
padding: '8px 0', borderBottom: '1px solid #f1f5f9' }}>
|
| 155 |
<div style={{ flex: 1 }}>
|
| 156 |
<div style={{ fontSize: 13, fontWeight: 500, color: '#0f172a' }}>
|
| 157 |
-
{task.task_type}
|
|
|
|
| 158 |
</div>
|
| 159 |
<div style={{ fontSize: 11, color: '#94a3b8' }}>
|
| 160 |
-
deadline: {task.deadline ?? 'β'} Β· {task.depends_on
|
|
|
|
| 161 |
</div>
|
| 162 |
-
<div style={{ height: 3, background: '#f1f5f9', borderRadius: 99,
|
|
|
|
| 163 |
<div style={{ width: `${task.progress * 100}%`, height: '100%',
|
| 164 |
-
background: task.progress >= 1 ? '#22c55e' : '#6366f1',
|
|
|
|
| 165 |
</div>
|
| 166 |
</div>
|
| 167 |
-
<span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 99,
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
| 170 |
{task.priority}
|
| 171 |
</span>
|
| 172 |
-
|
| 173 |
-
{
|
| 174 |
-
<>
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
</div>
|
| 185 |
))}
|
| 186 |
{sessionId && (
|
| 187 |
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
| 188 |
<button onClick={() => handleAction('break')}
|
| 189 |
-
style={{ flex: 1, padding:
|
| 190 |
-
background: '#f0fdf4', color: '#16a34a', fontWeight: 600,
|
| 191 |
-
|
| 192 |
-
</button>
|
| 193 |
<button onClick={() => handleAction('delay')}
|
| 194 |
-
style={{ flex: 1, padding:
|
| 195 |
-
background: '#f8fafc', color: '#64748b', fontWeight: 600,
|
| 196 |
-
|
| 197 |
-
</button>
|
| 198 |
</div>
|
| 199 |
)}
|
| 200 |
</div>
|
| 201 |
|
| 202 |
-
{/*
|
| 203 |
-
<div style={{ background: '#fff', border: '1px solid #e2e8f0',
|
| 204 |
-
|
| 205 |
-
|
| 206 |
{rewardHistory.length === 0 ? (
|
| 207 |
<div style={{ color: '#cbd5e1', fontSize: 13, textAlign: 'center', padding: 40 }}>
|
| 208 |
Rewards will appear here as the agent acts
|
| 209 |
</div>
|
| 210 |
) : (
|
| 211 |
-
<div style={{
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
<line x1="0" y1="90" x2="10000" y2="90" stroke="#f1f5f9" strokeWidth="1" />
|
| 215 |
-
{rewardHistory.map((d, i) => {
|
| 216 |
-
const x = i * 20 + 10;
|
| 217 |
-
const y = 90 - (d.reward * 70);
|
| 218 |
-
const prev = rewardHistory[i - 1];
|
| 219 |
-
return (
|
| 220 |
-
<g key={i}>
|
| 221 |
-
{prev && (
|
| 222 |
-
<line x1={(i-1)*20+10} y1={90-(prev.reward*70)} x2={x} y2={y}
|
| 223 |
-
stroke={d.reward >= 0 ? '#6366f1' : '#ef4444'} strokeWidth="2" />
|
| 224 |
-
)}
|
| 225 |
-
<circle cx={x} cy={y} r="3"
|
| 226 |
-
fill={d.reward >= 0 ? '#6366f1' : '#ef4444'} />
|
| 227 |
-
</g>
|
| 228 |
-
);
|
| 229 |
-
})}
|
| 230 |
-
</svg>
|
| 231 |
</div>
|
| 232 |
)}
|
| 233 |
{rewardHistory.length > 0 && (
|
| 234 |
<div style={{ display: 'flex', gap: 16, marginTop: 8 }}>
|
| 235 |
{[
|
| 236 |
-
{ label: 'Total', val: rewardHistory.reduce((s,d) => s+d.reward, 0).toFixed(3) },
|
| 237 |
-
{ label: 'Mean',
|
| 238 |
{ label: 'Steps', val: rewardHistory.length },
|
| 239 |
].map(s => (
|
| 240 |
<div key={s.label} style={{ textAlign: 'center' }}>
|
|
@@ -247,23 +391,28 @@ export default function Dashboard() {
|
|
| 247 |
</div>
|
| 248 |
</div>
|
| 249 |
|
| 250 |
-
{/*
|
| 251 |
-
<div style={{ background: '#fff', border: '1px solid #e2e8f0',
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
<div ref={scrollRef} style={{ height: 120, overflowY: 'auto',
|
|
|
|
| 255 |
{logs.length === 0 && (
|
| 256 |
-
<span style={{ color: '#cbd5e1' }}>No actions yet
|
| 257 |
)}
|
| 258 |
{logs.map((log, i) => (
|
| 259 |
<div key={i} style={{ padding: '2px 0',
|
| 260 |
-
color: log.type === 'positive' ? '#16a34a'
|
|
|
|
| 261 |
[{i}] {log.msg}
|
| 262 |
</div>
|
| 263 |
))}
|
| 264 |
</div>
|
| 265 |
</div>
|
| 266 |
|
|
|
|
|
|
|
|
|
|
| 267 |
</div>
|
| 268 |
);
|
| 269 |
}
|
|
|
|
| 1 |
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
|
| 3 |
+
// Empty string = relative URLs β works on HuggingFace Spaces and local dev behind a proxy.
|
| 4 |
+
// Set VITE_API_URL in .env.local only when the frontend is served separately from the backend.
|
| 5 |
+
const API_BASE = import.meta.env.VITE_API_URL || '';
|
| 6 |
+
|
| 7 |
+
// βββ tiny helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
+
|
| 9 |
+
function StatCard({ label, value, color }) {
|
| 10 |
+
return (
|
| 11 |
+
<div style={{
|
| 12 |
+
background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12,
|
| 13 |
+
padding: '14px 16px', textAlign: 'center',
|
| 14 |
+
}}>
|
| 15 |
+
<div style={{ fontSize: 10, color: '#94a3b8', textTransform: 'uppercase',
|
| 16 |
+
letterSpacing: '0.08em', marginBottom: 6 }}>{label}</div>
|
| 17 |
+
<div style={{ fontSize: 22, fontWeight: 700, color }}>{value}</div>
|
| 18 |
+
</div>
|
| 19 |
+
);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function SectionHeader({ children }) {
|
| 23 |
+
return (
|
| 24 |
+
<div style={{ fontSize: 11, fontWeight: 700, color: '#94a3b8',
|
| 25 |
+
textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 12 }}>
|
| 26 |
+
{children}
|
| 27 |
+
</div>
|
| 28 |
+
);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function RewardChart({ data, width = 300 }) {
|
| 32 |
+
if (!data.length) return null;
|
| 33 |
+
const H = 160;
|
| 34 |
+
const W = Math.max(data.length * 24, width);
|
| 35 |
+
const vals = data.map(d => d.mean ?? d.reward ?? 0);
|
| 36 |
+
const lo = Math.min(...vals);
|
| 37 |
+
const hi = Math.max(...vals);
|
| 38 |
+
const span = hi === lo ? 1 : hi - lo;
|
| 39 |
+
const toY = v => H - 16 - ((v - lo) / span) * (H - 32);
|
| 40 |
+
|
| 41 |
+
return (
|
| 42 |
+
<svg width="100%" height={H} viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none"
|
| 43 |
+
style={{ display: 'block' }}>
|
| 44 |
+
<line x1="0" y1={H / 2} x2={W} y2={H / 2} stroke="#f1f5f9" strokeWidth="1" />
|
| 45 |
+
{vals.map((v, i) => {
|
| 46 |
+
const x = i * 24 + 12;
|
| 47 |
+
const y = toY(v);
|
| 48 |
+
const prev = i > 0 ? toY(vals[i - 1]) : null;
|
| 49 |
+
return (
|
| 50 |
+
<g key={i}>
|
| 51 |
+
{prev !== null && (
|
| 52 |
+
<line x1={(i - 1) * 24 + 12} y1={prev} x2={x} y2={y}
|
| 53 |
+
stroke={v >= 0 ? '#6366f1' : '#ef4444'} strokeWidth="2" />
|
| 54 |
+
)}
|
| 55 |
+
<circle cx={x} cy={y} r="3" fill={v >= 0 ? '#6366f1' : '#ef4444'} />
|
| 56 |
+
</g>
|
| 57 |
+
);
|
| 58 |
+
})}
|
| 59 |
+
</svg>
|
| 60 |
+
);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// βββ Training Log panel βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
+
|
| 65 |
+
function TrainingLog() {
|
| 66 |
+
const [data, setData] = useState(null);
|
| 67 |
+
|
| 68 |
+
useEffect(() => {
|
| 69 |
+
fetch(`${API_BASE}/training-log`)
|
| 70 |
+
.then(r => r.ok ? r.json() : [])
|
| 71 |
+
.then(setData)
|
| 72 |
+
.catch(() => setData([]));
|
| 73 |
+
}, []);
|
| 74 |
+
|
| 75 |
+
const cardStyle = {
|
| 76 |
+
background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12, padding: 16,
|
| 77 |
+
marginBottom: 16,
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
if (data === null) {
|
| 81 |
+
return (
|
| 82 |
+
<div style={cardStyle}>
|
| 83 |
+
<SectionHeader>Training Log β Last Run</SectionHeader>
|
| 84 |
+
<div style={{ color: '#94a3b8', fontSize: 13, textAlign: 'center', padding: 24 }}>
|
| 85 |
+
Loadingβ¦
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if (!data.length) {
|
| 92 |
+
return (
|
| 93 |
+
<div style={cardStyle}>
|
| 94 |
+
<SectionHeader>Training Log β Last Run</SectionHeader>
|
| 95 |
+
<div style={{ color: '#94a3b8', fontSize: 13, textAlign: 'center', padding: 24 }}>
|
| 96 |
+
No training data yet. Run
|
| 97 |
+
<code style={{ background: '#f1f5f9', padding: '1px 6px', borderRadius: 4, fontSize: 12 }}>
|
| 98 |
+
python training_loop.py --train
|
| 99 |
+
</code>
|
| 100 |
+
to generate reward curves.
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
const means = data.map(d => d.mean);
|
| 107 |
+
const total = data.length;
|
| 108 |
+
const finalMean = means[means.length - 1];
|
| 109 |
+
const peakMean = Math.max(...means);
|
| 110 |
+
const loMean = Math.min(...means);
|
| 111 |
+
|
| 112 |
+
return (
|
| 113 |
+
<div style={cardStyle}>
|
| 114 |
+
<SectionHeader>Training Log β Last Run ({total} steps)</SectionHeader>
|
| 115 |
+
|
| 116 |
+
{/* Summary stats */}
|
| 117 |
+
<div style={{ display: 'flex', gap: 24, marginBottom: 12 }}>
|
| 118 |
+
{[
|
| 119 |
+
{ label: 'Final Mean', val: finalMean.toFixed(4), color: '#6366f1' },
|
| 120 |
+
{ label: 'Peak Mean', val: peakMean.toFixed(4), color: '#22c55e' },
|
| 121 |
+
{ label: 'Min Mean', val: loMean.toFixed(4), color: '#ef4444' },
|
| 122 |
+
{ label: 'Steps', val: total, color: '#0ea5e9' },
|
| 123 |
+
].map(s => (
|
| 124 |
+
<div key={s.label} style={{ textAlign: 'center', minWidth: 64 }}>
|
| 125 |
+
<div style={{ fontSize: 10, color: '#94a3b8', textTransform: 'uppercase' }}>{s.label}</div>
|
| 126 |
+
<div style={{ fontSize: 16, fontWeight: 700, color: s.color }}>{s.val}</div>
|
| 127 |
+
</div>
|
| 128 |
+
))}
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
{/* Reward curve chart */}
|
| 132 |
+
<div style={{ position: 'relative', overflow: 'hidden', borderRadius: 8,
|
| 133 |
+
border: '1px solid #f1f5f9', background: '#fafafa' }}>
|
| 134 |
+
<RewardChart data={data} width={600} />
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
{/* Step table β last 10 rows */}
|
| 138 |
+
<div style={{ marginTop: 12, fontFamily: 'monospace', fontSize: 11, color: '#475569' }}>
|
| 139 |
+
<div style={{ display: 'flex', gap: 0, borderBottom: '1px solid #f1f5f9',
|
| 140 |
+
paddingBottom: 4, marginBottom: 4, fontWeight: 700, color: '#94a3b8' }}>
|
| 141 |
+
{['Step', 'Mean', 'Max', 'Min'].map(h => (
|
| 142 |
+
<div key={h} style={{ flex: 1 }}>{h}</div>
|
| 143 |
+
))}
|
| 144 |
+
</div>
|
| 145 |
+
{data.slice(-10).map(d => (
|
| 146 |
+
<div key={d.step} style={{ display: 'flex', gap: 0, padding: '2px 0',
|
| 147 |
+
color: d.mean >= 0 ? '#16a34a' : '#dc2626' }}>
|
| 148 |
+
<div style={{ flex: 1 }}>{d.step}</div>
|
| 149 |
+
<div style={{ flex: 1 }}>{d.mean.toFixed(4)}</div>
|
| 150 |
+
<div style={{ flex: 1 }}>{d.max?.toFixed(4) ?? 'β'}</div>
|
| 151 |
+
<div style={{ flex: 1 }}>{d.min?.toFixed(4) ?? 'β'}</div>
|
| 152 |
+
</div>
|
| 153 |
+
))}
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// βββ Main Dashboard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 160 |
|
| 161 |
export default function Dashboard() {
|
| 162 |
const [level, setLevel] = useState('medium');
|
| 163 |
const [sessionId, setSessionId] = useState(null);
|
| 164 |
const [obs, setObs] = useState(null);
|
|
|
|
| 165 |
const [logs, setLogs] = useState([]);
|
| 166 |
const [loading, setLoading] = useState(false);
|
| 167 |
const [error, setError] = useState(null);
|
|
|
|
| 171 |
const fetchState = async (sid) => {
|
| 172 |
try {
|
| 173 |
const res = await fetch(`${API_BASE}/state?session_id=${sid}`);
|
| 174 |
+
if (res.ok) await res.json();
|
| 175 |
+
} catch (e) { console.error(e); }
|
| 176 |
};
|
| 177 |
|
| 178 |
const handleReset = async () => {
|
|
|
|
| 183 |
const res = await fetch(`${API_BASE}/reset`, {
|
| 184 |
method: 'POST',
|
| 185 |
headers: { 'Content-Type': 'application/json' },
|
| 186 |
+
body: JSON.stringify({ task_id: level }),
|
| 187 |
});
|
| 188 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 189 |
const data = await res.json();
|
| 190 |
setSessionId(data.session_id);
|
| 191 |
setObs(data.observation);
|
| 192 |
setLogs([{ type: 'system', msg: `Episode started: ${level}` }]);
|
| 193 |
await fetchState(data.session_id);
|
| 194 |
} catch (err) {
|
| 195 |
+
setError(`Cannot reach backend at "${API_BASE || window.location.origin}" β ${err.message}`);
|
| 196 |
} finally { setLoading(false); }
|
| 197 |
};
|
| 198 |
|
|
|
|
| 205 |
const res = await fetch(`${API_BASE}/step`, {
|
| 206 |
method: 'POST',
|
| 207 |
headers: { 'Content-Type': 'application/json' },
|
| 208 |
+
body: JSON.stringify({ session_id: sessionId, action }),
|
| 209 |
});
|
| 210 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 211 |
const data = await res.json();
|
| 212 |
setObs(data.observation);
|
| 213 |
+
setRewardHistory(prev => [...prev, { step: prev.length + 1, reward: data.reward }]);
|
|
|
|
|
|
|
|
|
|
| 214 |
setLogs(prev => [...prev, {
|
| 215 |
type: data.reward >= 0 ? 'positive' : 'negative',
|
| 216 |
+
msg: `${actionType}${taskId ? ' ' + taskId : ''} β reward: ${data.reward?.toFixed(3)}`,
|
|
|
|
| 217 |
}]);
|
| 218 |
if (data.done) {
|
| 219 |
setLogs(prev => [...prev, {
|
| 220 |
type: 'system',
|
| 221 |
+
msg: `DONE. Final score: ${data.info?.final_score?.toFixed(3) ?? 'N/A'}`,
|
| 222 |
}]);
|
| 223 |
+
setSessionId(null);
|
| 224 |
}
|
| 225 |
await fetchState(sessionId);
|
| 226 |
setTimeout(() => {
|
| 227 |
+
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 228 |
}, 50);
|
| 229 |
} catch (err) {
|
| 230 |
setError(err.message);
|
|
|
|
| 232 |
};
|
| 233 |
|
| 234 |
const workers = obs?.visible_state?.workers || [];
|
| 235 |
+
const tasks = obs?.tasks || [];
|
| 236 |
+
const firstW = workers[0] || {};
|
| 237 |
|
| 238 |
return (
|
| 239 |
+
<div style={{ minHeight: '100vh', background: '#f8fafc',
|
| 240 |
+
fontFamily: 'system-ui, sans-serif', padding: 24 }}>
|
| 241 |
+
|
| 242 |
+
{/* ββ Header ββ */}
|
| 243 |
+
<div style={{ display: 'flex', justifyContent: 'space-between',
|
| 244 |
+
alignItems: 'center', marginBottom: 20 }}>
|
| 245 |
<div>
|
| 246 |
<h1 style={{ fontSize: 22, fontWeight: 700, color: '#0f172a', margin: 0 }}>
|
| 247 |
π§ StressTest β Cognitive Load Manager
|
|
|
|
| 252 |
</div>
|
| 253 |
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
| 254 |
<select value={level} onChange={e => setLevel(e.target.value)}
|
| 255 |
+
style={{ border: '1px solid #e2e8f0', borderRadius: 8,
|
| 256 |
+
padding: '6px 10px', fontSize: 13 }}>
|
| 257 |
<option value="easy">Easy</option>
|
| 258 |
<option value="medium">Medium</option>
|
| 259 |
<option value="hard">Hard</option>
|
| 260 |
<option value="expert">Expert</option>
|
| 261 |
</select>
|
| 262 |
<button onClick={handleReset} disabled={loading}
|
| 263 |
+
style={{ background: '#6366f1', color: '#fff', border: 'none',
|
| 264 |
+
borderRadius: 8, padding: '8px 18px', fontSize: 13, fontWeight: 600,
|
| 265 |
+
cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? 0.7 : 1 }}>
|
| 266 |
+
{loading ? 'Loadingβ¦' : sessionId ? 'βΊ Reset' : 'βΆ Start'}
|
| 267 |
</button>
|
| 268 |
</div>
|
| 269 |
</div>
|
| 270 |
|
| 271 |
+
{/* ββ Error banner ββ */}
|
| 272 |
{error && (
|
| 273 |
<div style={{ background: '#fef2f2', border: '1px solid #fca5a5', borderRadius: 8,
|
| 274 |
padding: '10px 14px', marginBottom: 16, fontSize: 13, color: '#dc2626' }}>
|
|
|
|
| 276 |
</div>
|
| 277 |
)}
|
| 278 |
|
| 279 |
+
{/* ββ Worker metric cards ββ */}
|
| 280 |
{workers.length > 0 && (
|
| 281 |
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)',
|
| 282 |
+
gap: 12, marginBottom: 16 }}>
|
| 283 |
+
<StatCard label="Fatigue" value={firstW.fatigue_level || 'β'}
|
| 284 |
+
color={firstW.fatigue_level === 'high' ? '#ef4444'
|
| 285 |
+
: firstW.fatigue_level === 'medium' ? '#f59e0b' : '#22c55e'} />
|
| 286 |
+
<StatCard label="Stress" value={firstW.stress_level || 'β'}
|
| 287 |
+
color={firstW.stress_level === 'critical' ? '#ef4444'
|
| 288 |
+
: firstW.stress_level === 'elevated' ? '#f59e0b' : '#22c55e'} />
|
| 289 |
+
<StatCard label="Step" value={obs?.time_step ?? 'β'} color="#6366f1" />
|
| 290 |
+
<StatCard label="Tasks Done"
|
| 291 |
+
value={`${tasks.filter(t => t.progress >= 1.0).length}/${tasks.length}`}
|
| 292 |
+
color="#0ea5e9" />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
</div>
|
| 294 |
)}
|
| 295 |
|
| 296 |
+
{/* ββ Task list + Step reward chart ββ */}
|
| 297 |
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
|
| 298 |
+
|
| 299 |
+
{/* Task queue */}
|
| 300 |
+
<div style={{ background: '#fff', border: '1px solid #e2e8f0',
|
| 301 |
+
borderRadius: 12, padding: 16 }}>
|
| 302 |
+
<SectionHeader>Task Queue</SectionHeader>
|
| 303 |
{tasks.length === 0 && (
|
| 304 |
<div style={{ color: '#cbd5e1', fontSize: 13, textAlign: 'center', padding: 20 }}>
|
| 305 |
Press Start to begin episode
|
|
|
|
| 310 |
padding: '8px 0', borderBottom: '1px solid #f1f5f9' }}>
|
| 311 |
<div style={{ flex: 1 }}>
|
| 312 |
<div style={{ fontSize: 13, fontWeight: 500, color: '#0f172a' }}>
|
| 313 |
+
{task.task_type}
|
| 314 |
+
<span style={{ fontSize: 10, color: '#94a3b8' }}>#{task.id}</span>
|
| 315 |
</div>
|
| 316 |
<div style={{ fontSize: 11, color: '#94a3b8' }}>
|
| 317 |
+
deadline: {task.deadline ?? 'β'} Β· {task.depends_on
|
| 318 |
+
? `depends: ${task.depends_on}` : 'no dep'}
|
| 319 |
</div>
|
| 320 |
+
<div style={{ height: 3, background: '#f1f5f9', borderRadius: 99,
|
| 321 |
+
marginTop: 4, overflow: 'hidden' }}>
|
| 322 |
<div style={{ width: `${task.progress * 100}%`, height: '100%',
|
| 323 |
+
background: task.progress >= 1 ? '#22c55e' : '#6366f1',
|
| 324 |
+
borderRadius: 99 }} />
|
| 325 |
</div>
|
| 326 |
</div>
|
| 327 |
+
<span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 99,
|
| 328 |
+
fontWeight: 600,
|
| 329 |
+
background: task.priority === 'critical' ? '#fef2f2'
|
| 330 |
+
: task.priority === 'high' ? '#fffbeb' : '#f0fdf4',
|
| 331 |
+
color: task.priority === 'critical' ? '#dc2626'
|
| 332 |
+
: task.priority === 'high' ? '#d97706' : '#16a34a' }}>
|
| 333 |
{task.priority}
|
| 334 |
</span>
|
| 335 |
+
{sessionId && task.progress < 1.0 && (
|
| 336 |
+
<div style={{ display: 'flex', gap: 4 }}>
|
| 337 |
+
<button onClick={() => handleAction('work', task.id)}
|
| 338 |
+
style={{ fontSize: 10, padding: '3px 8px', borderRadius: 6,
|
| 339 |
+
border: '1px solid #e2e8f0', background: '#f8fafc',
|
| 340 |
+
cursor: 'pointer' }}>work</button>
|
| 341 |
+
<button onClick={() => handleAction('focus', task.id)}
|
| 342 |
+
style={{ fontSize: 10, padding: '3px 8px', borderRadius: 6,
|
| 343 |
+
border: '1px solid #6366f1', background: '#eef2ff',
|
| 344 |
+
color: '#6366f1', cursor: 'pointer' }}>focus</button>
|
| 345 |
+
</div>
|
| 346 |
+
)}
|
| 347 |
</div>
|
| 348 |
))}
|
| 349 |
{sessionId && (
|
| 350 |
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
| 351 |
<button onClick={() => handleAction('break')}
|
| 352 |
+
style={{ flex: 1, padding: 8, borderRadius: 8, border: '1px solid #e2e8f0',
|
| 353 |
+
background: '#f0fdf4', color: '#16a34a', fontWeight: 600,
|
| 354 |
+
cursor: 'pointer', fontSize: 13 }}>β Break</button>
|
|
|
|
| 355 |
<button onClick={() => handleAction('delay')}
|
| 356 |
+
style={{ flex: 1, padding: 8, borderRadius: 8, border: '1px solid #e2e8f0',
|
| 357 |
+
background: '#f8fafc', color: '#64748b', fontWeight: 600,
|
| 358 |
+
cursor: 'pointer', fontSize: 13 }}>βΈ Delay</button>
|
|
|
|
| 359 |
</div>
|
| 360 |
)}
|
| 361 |
</div>
|
| 362 |
|
| 363 |
+
{/* Step reward chart */}
|
| 364 |
+
<div style={{ background: '#fff', border: '1px solid #e2e8f0',
|
| 365 |
+
borderRadius: 12, padding: 16 }}>
|
| 366 |
+
<SectionHeader>Reward Per Step</SectionHeader>
|
| 367 |
{rewardHistory.length === 0 ? (
|
| 368 |
<div style={{ color: '#cbd5e1', fontSize: 13, textAlign: 'center', padding: 40 }}>
|
| 369 |
Rewards will appear here as the agent acts
|
| 370 |
</div>
|
| 371 |
) : (
|
| 372 |
+
<div style={{ overflow: 'hidden', borderRadius: 8,
|
| 373 |
+
border: '1px solid #f1f5f9', background: '#fafafa' }}>
|
| 374 |
+
<RewardChart data={rewardHistory} width={300} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
</div>
|
| 376 |
)}
|
| 377 |
{rewardHistory.length > 0 && (
|
| 378 |
<div style={{ display: 'flex', gap: 16, marginTop: 8 }}>
|
| 379 |
{[
|
| 380 |
+
{ label: 'Total', val: rewardHistory.reduce((s, d) => s + d.reward, 0).toFixed(3) },
|
| 381 |
+
{ label: 'Mean', val: (rewardHistory.reduce((s, d) => s + d.reward, 0) / rewardHistory.length).toFixed(3) },
|
| 382 |
{ label: 'Steps', val: rewardHistory.length },
|
| 383 |
].map(s => (
|
| 384 |
<div key={s.label} style={{ textAlign: 'center' }}>
|
|
|
|
| 391 |
</div>
|
| 392 |
</div>
|
| 393 |
|
| 394 |
+
{/* ββ Action log ββ */}
|
| 395 |
+
<div style={{ background: '#fff', border: '1px solid #e2e8f0',
|
| 396 |
+
borderRadius: 12, padding: 16, marginBottom: 16 }}>
|
| 397 |
+
<SectionHeader>Action Log</SectionHeader>
|
| 398 |
+
<div ref={scrollRef} style={{ height: 120, overflowY: 'auto',
|
| 399 |
+
fontFamily: 'monospace', fontSize: 12 }}>
|
| 400 |
{logs.length === 0 && (
|
| 401 |
+
<span style={{ color: '#cbd5e1' }}>No actions yetβ¦</span>
|
| 402 |
)}
|
| 403 |
{logs.map((log, i) => (
|
| 404 |
<div key={i} style={{ padding: '2px 0',
|
| 405 |
+
color: log.type === 'positive' ? '#16a34a'
|
| 406 |
+
: log.type === 'negative' ? '#dc2626' : '#64748b' }}>
|
| 407 |
[{i}] {log.msg}
|
| 408 |
</div>
|
| 409 |
))}
|
| 410 |
</div>
|
| 411 |
</div>
|
| 412 |
|
| 413 |
+
{/* ββ Training log (reward_curve.json from last GRPO run) ββ */}
|
| 414 |
+
<TrainingLog />
|
| 415 |
+
|
| 416 |
</div>
|
| 417 |
);
|
| 418 |
}
|