Spaces:
Running
Running
Add image input with vision-assisted JEV decisions
Browse filesAdd JPEG, PNG and WebP upload, preview and removal. Validate and normalize images server-side, then use Qwen2-VL-2B-Instruct to read the image before JEV inference. Display the vision model and image-understanding time separately from JEV decision time.
- README.md +3 -2
- THIRD_PARTY_NOTICES.md +2 -0
- app.py +84 -6
- play.html +103 -8
README.md
CHANGED
|
@@ -21,11 +21,12 @@ tags:
|
|
| 21 |
|
| 22 |
Try JEV-27B on your own inputs, or let it choose the next move in two playable Doom scenarios.
|
| 23 |
|
| 24 |
-
- **Decision lab:** multiple choice, true/false, and 0–5 scoring with option probabilities.
|
|
|
|
| 25 |
- **Doom arena:** target practice and center defense. Play manually, request one JEV move, or run ten consecutive model decisions.
|
| 26 |
- **Demo reel:** ten recorded demonstrations and six benchmark comparison bar charts.
|
| 27 |
|
| 28 |
-
Live
|
| 29 |
|
| 30 |
The deployed inference revision is `7ce153d6f7933769527939bcb92ec3f3d8292884`. The recorded reel is the latest ten-demo 5× edit (41 seconds), with the September 27, 2026 benchmark comparison. Benchmark details are in [benchmark-notes.md](benchmark-notes.md).
|
| 31 |
|
|
|
|
| 21 |
|
| 22 |
Try JEV-27B on your own inputs, or let it choose the next move in two playable Doom scenarios.
|
| 23 |
|
| 24 |
+
- **Decision lab:** text or image input for multiple choice, true/false, and 0–5 scoring, with option probabilities and timing.
|
| 25 |
+
Upload one JPEG, PNG, or WebP image up to 5 MB, add a question, and optionally add text context.
|
| 26 |
- **Doom arena:** target practice and center defense. Play manually, request one JEV move, or run ten consecutive model decisions.
|
| 27 |
- **Demo reel:** ten recorded demonstrations and six benchmark comparison bar charts.
|
| 28 |
|
| 29 |
+
Live decisions use the existing AutoTrust JEV-27B deployment. Image input uses [Qwen/Qwen2-VL-2B-Instruct](https://huggingface.co/Qwen/Qwen2-VL-2B-Instruct), pinned to revision `895c3a49bc3fa70a340399125c650a463535e71c`, to describe the image before JEV makes the decision. This is a vision-assisted pipeline, since the deployed JEV checkpoint is text-only. The interface names the vision model and shows image-understanding time separately from JEV decision time. Images are processed in memory, normalized to at most 1024 pixels per side, and are not saved by this app. The Space runs the interface and the ViZDoom simulator on CPU. JEV receives structured engine state, and the simulation pauses between decisions. Game sessions expire after ten minutes of inactivity; up to eight sessions and four simultaneous model calls are supported.
|
| 30 |
|
| 31 |
The deployed inference revision is `7ce153d6f7933769527939bcb92ec3f3d8292884`. The recorded reel is the latest ten-demo 5× edit (41 seconds), with the September 27, 2026 benchmark comparison. Benchmark details are in [benchmark-notes.md](benchmark-notes.md).
|
| 32 |
|
THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -14,6 +14,8 @@ Only the freely distributed Freedoom assets supplied with ViZDoom are used. No c
|
|
| 14 |
|
| 15 |
JEV-27B is provided by AutoTrust at https://huggingface.co/autotrust/JEV-27B. Its model card identifies the model license as Apache-2.0. This repository connects to a separately hosted deployment and contains no model weights.
|
| 16 |
|
|
|
|
|
|
|
| 17 |
## Recorded media
|
| 18 |
|
| 19 |
Demo video, AutoTrust brand assets, and benchmark graphics were supplied by the AutoTrust demo project.
|
|
|
|
| 14 |
|
| 15 |
JEV-27B is provided by AutoTrust at https://huggingface.co/autotrust/JEV-27B. Its model card identifies the model license as Apache-2.0. This repository connects to a separately hosted deployment and contains no model weights.
|
| 16 |
|
| 17 |
+
Image understanding uses Qwen/Qwen2-VL-2B-Instruct, provided by the Qwen team at https://huggingface.co/Qwen/Qwen2-VL-2B-Instruct, under Apache-2.0. The separately hosted image reader uses revision `895c3a49bc3fa70a340399125c650a463535e71c`. Its observations are passed to JEV-27B for the final decision; this is not native vision in the JEV checkpoint. No Qwen model weights are included in this repository.
|
| 18 |
+
|
| 19 |
## Recorded media
|
| 20 |
|
| 21 |
Demo video, AutoTrust brand assets, and benchmark graphics were supplied by the AutoTrust demo project.
|
app.py
CHANGED
|
@@ -2,12 +2,16 @@
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import asyncio
|
|
|
|
|
|
|
| 5 |
import contextlib
|
|
|
|
| 6 |
import math
|
| 7 |
import os
|
| 8 |
import re
|
| 9 |
import secrets
|
| 10 |
import time
|
|
|
|
| 11 |
from concurrent.futures import ThreadPoolExecutor
|
| 12 |
from contextlib import asynccontextmanager
|
| 13 |
from pathlib import Path
|
|
@@ -16,7 +20,9 @@ from urllib.parse import urlsplit
|
|
| 16 |
|
| 17 |
import httpx
|
| 18 |
from fastapi import FastAPI, HTTPException, Request
|
|
|
|
| 19 |
from fastapi.responses import FileResponse, JSONResponse
|
|
|
|
| 20 |
from pydantic import BaseModel, ConfigDict, Field, StringConstraints, model_validator
|
| 21 |
|
| 22 |
from doom_engine import DoomEpisode, SCENARIOS
|
|
@@ -27,6 +33,10 @@ MODEL = "autotrust/JEV-27B"
|
|
| 27 |
SESSION_LIMIT = 8
|
| 28 |
SESSION_TTL = 600
|
| 29 |
QUEUE_WAIT_SECONDS = 5
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
SAFE_ASSETS = {".css", ".js", ".svg", ".png", ".jpg", ".json", ".mp4", ".md"}
|
| 31 |
Option = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=180)]
|
| 32 |
|
|
@@ -34,9 +44,10 @@ Option = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, m
|
|
| 34 |
class DecisionRequest(BaseModel):
|
| 35 |
model_config = ConfigDict(extra="forbid")
|
| 36 |
kind: Literal["choice", "noul", "score"] = "choice"
|
| 37 |
-
state: Annotated[str, StringConstraints(max_length=6000)]
|
| 38 |
question: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=1000)]
|
| 39 |
options: list[Option] | None = Field(default=None, min_length=2, max_length=16)
|
|
|
|
| 40 |
|
| 41 |
@model_validator(mode="after")
|
| 42 |
def native_options(self):
|
|
@@ -148,13 +159,23 @@ async def lifespan(app: FastAPI):
|
|
| 148 |
app = FastAPI(title="JEV-27B Live Demo", lifespan=lifespan, docs_url=None, redoc_url=None)
|
| 149 |
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
@app.middleware("http")
|
| 152 |
async def basic_limits(request: Request, call_next):
|
| 153 |
if request.method in {"POST", "PUT", "PATCH"}:
|
| 154 |
body = bytearray()
|
| 155 |
async for chunk in request.stream():
|
| 156 |
body.extend(chunk)
|
| 157 |
-
if
|
|
|
|
| 158 |
return JSONResponse({"detail": "Request body is too large."}, status_code=413)
|
| 159 |
# Starlette replays the cached body when call_next constructs its Request.
|
| 160 |
request._body = bytes(body)
|
|
@@ -193,6 +214,9 @@ def clean_decision(data: dict, payload: dict) -> dict:
|
|
| 193 |
options = payload["options"]
|
| 194 |
if data.get("model") != MODEL or data.get("options") != options:
|
| 195 |
raise ValueError("Unexpected model or options")
|
|
|
|
|
|
|
|
|
|
| 196 |
probabilities = data.get("probabilities")
|
| 197 |
if not isinstance(probabilities, list) or len(probabilities) != len(options):
|
| 198 |
raise ValueError("Invalid distribution")
|
|
@@ -210,9 +234,60 @@ def clean_decision(data: dict, payload: dict) -> dict:
|
|
| 210 |
revision = data.get("revision", "")
|
| 211 |
if not isinstance(revision, str) or not re.fullmatch(r"[a-fA-F0-9]{7,64}", revision):
|
| 212 |
revision = "unavailable"
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
|
| 218 |
async def infer(payload: dict) -> dict:
|
|
@@ -243,7 +318,10 @@ async def infer(payload: dict) -> dict:
|
|
| 243 |
|
| 244 |
@app.post("/api/decide")
|
| 245 |
async def decide(payload: DecisionRequest):
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
|
| 249 |
@app.post("/api/games/new")
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import asyncio
|
| 5 |
+
import base64
|
| 6 |
+
import binascii
|
| 7 |
import contextlib
|
| 8 |
+
import io
|
| 9 |
import math
|
| 10 |
import os
|
| 11 |
import re
|
| 12 |
import secrets
|
| 13 |
import time
|
| 14 |
+
import warnings
|
| 15 |
from concurrent.futures import ThreadPoolExecutor
|
| 16 |
from contextlib import asynccontextmanager
|
| 17 |
from pathlib import Path
|
|
|
|
| 20 |
|
| 21 |
import httpx
|
| 22 |
from fastapi import FastAPI, HTTPException, Request
|
| 23 |
+
from fastapi.exceptions import RequestValidationError
|
| 24 |
from fastapi.responses import FileResponse, JSONResponse
|
| 25 |
+
from PIL import Image, ImageOps, UnidentifiedImageError
|
| 26 |
from pydantic import BaseModel, ConfigDict, Field, StringConstraints, model_validator
|
| 27 |
|
| 28 |
from doom_engine import DoomEpisode, SCENARIOS
|
|
|
|
| 33 |
SESSION_LIMIT = 8
|
| 34 |
SESSION_TTL = 600
|
| 35 |
QUEUE_WAIT_SECONDS = 5
|
| 36 |
+
MAX_IMAGE_BYTES = 5 * 1024 * 1024
|
| 37 |
+
MAX_IMAGE_BASE64 = 4 * ((MAX_IMAGE_BYTES + 2) // 3)
|
| 38 |
+
MAX_IMAGE_PIXELS = 32_000_000
|
| 39 |
+
MAX_IMAGE_EDGE = 1024
|
| 40 |
SAFE_ASSETS = {".css", ".js", ".svg", ".png", ".jpg", ".json", ".mp4", ".md"}
|
| 41 |
Option = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=180)]
|
| 42 |
|
|
|
|
| 44 |
class DecisionRequest(BaseModel):
|
| 45 |
model_config = ConfigDict(extra="forbid")
|
| 46 |
kind: Literal["choice", "noul", "score"] = "choice"
|
| 47 |
+
state: Annotated[str, StringConstraints(max_length=6000)] = ""
|
| 48 |
question: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=1000)]
|
| 49 |
options: list[Option] | None = Field(default=None, min_length=2, max_length=16)
|
| 50 |
+
image: Annotated[str, StringConstraints(max_length=MAX_IMAGE_BASE64 + 32)] | None = None
|
| 51 |
|
| 52 |
@model_validator(mode="after")
|
| 53 |
def native_options(self):
|
|
|
|
| 159 |
app = FastAPI(title="JEV-27B Live Demo", lifespan=lifespan, docs_url=None, redoc_url=None)
|
| 160 |
|
| 161 |
|
| 162 |
+
@app.exception_handler(RequestValidationError)
|
| 163 |
+
async def invalid_request(request: Request, error: RequestValidationError):
|
| 164 |
+
# Do not echo uploaded image data back in validation errors.
|
| 165 |
+
return JSONResponse({"detail": [
|
| 166 |
+
{"loc": item["loc"], "msg": item["msg"], "type": item["type"]}
|
| 167 |
+
for item in error.errors()
|
| 168 |
+
]}, status_code=422)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
@app.middleware("http")
|
| 172 |
async def basic_limits(request: Request, call_next):
|
| 173 |
if request.method in {"POST", "PUT", "PATCH"}:
|
| 174 |
body = bytearray()
|
| 175 |
async for chunk in request.stream():
|
| 176 |
body.extend(chunk)
|
| 177 |
+
limit = MAX_IMAGE_BASE64 + 40_000 if request.url.path == "/api/decide" else 40_000
|
| 178 |
+
if len(body) > limit:
|
| 179 |
return JSONResponse({"detail": "Request body is too large."}, status_code=413)
|
| 180 |
# Starlette replays the cached body when call_next constructs its Request.
|
| 181 |
request._body = bytes(body)
|
|
|
|
| 214 |
options = payload["options"]
|
| 215 |
if data.get("model") != MODEL or data.get("options") != options:
|
| 216 |
raise ValueError("Unexpected model or options")
|
| 217 |
+
image_used = bool(payload.get("image"))
|
| 218 |
+
if image_used and data.get("image_used") is not True:
|
| 219 |
+
raise ValueError("The backend did not confirm image inference")
|
| 220 |
probabilities = data.get("probabilities")
|
| 221 |
if not isinstance(probabilities, list) or len(probabilities) != len(options):
|
| 222 |
raise ValueError("Invalid distribution")
|
|
|
|
| 234 |
revision = data.get("revision", "")
|
| 235 |
if not isinstance(revision, str) or not re.fullmatch(r"[a-fA-F0-9]{7,64}", revision):
|
| 236 |
revision = "unavailable"
|
| 237 |
+
result = {"kind": payload["kind"], "options": options, "probabilities": probabilities,
|
| 238 |
+
"choice_index": index, "choice": options[index], "model": MODEL,
|
| 239 |
+
"revision": revision, "elapsed_seconds": elapsed, "image_used": image_used}
|
| 240 |
+
if image_used:
|
| 241 |
+
vision = data.get("vision")
|
| 242 |
+
if not isinstance(vision, dict):
|
| 243 |
+
raise ValueError("Missing image understanding provenance")
|
| 244 |
+
vision_model = vision.get("model")
|
| 245 |
+
vision_elapsed = vision.get("elapsed_seconds")
|
| 246 |
+
if (not isinstance(vision_model, str) or not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", vision_model)
|
| 247 |
+
or len(vision_model) > 160 or type(vision_elapsed) not in (int, float)
|
| 248 |
+
or not math.isfinite(vision_elapsed) or vision_elapsed < 0):
|
| 249 |
+
raise ValueError("Invalid image understanding provenance")
|
| 250 |
+
result["vision"] = {"model": vision_model, "elapsed_seconds": vision_elapsed}
|
| 251 |
+
return result
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def prepare_image(data_url: str) -> str:
|
| 255 |
+
"""Decode and normalize a user image without fetching URLs or storing files."""
|
| 256 |
+
header, separator, encoded = data_url.partition(",")
|
| 257 |
+
formats = {"data:image/jpeg;base64": "JPEG", "data:image/png;base64": "PNG",
|
| 258 |
+
"data:image/webp;base64": "WEBP"}
|
| 259 |
+
if not separator or header not in formats:
|
| 260 |
+
raise HTTPException(422, "Upload a JPEG, PNG, or WebP image.")
|
| 261 |
+
try:
|
| 262 |
+
raw = base64.b64decode(encoded, validate=True)
|
| 263 |
+
except (ValueError, binascii.Error):
|
| 264 |
+
raise HTTPException(422, "The image could not be read. Choose another file.") from None
|
| 265 |
+
if not raw or len(raw) > MAX_IMAGE_BYTES:
|
| 266 |
+
raise HTTPException(413, "Choose an image no larger than 5 MB.")
|
| 267 |
+
try:
|
| 268 |
+
with warnings.catch_warnings():
|
| 269 |
+
warnings.simplefilter("error", Image.DecompressionBombWarning)
|
| 270 |
+
with Image.open(io.BytesIO(raw)) as probe:
|
| 271 |
+
if probe.format != formats[header] or getattr(probe, "is_animated", False):
|
| 272 |
+
raise ValueError("Unsupported image format")
|
| 273 |
+
if probe.width * probe.height > MAX_IMAGE_PIXELS:
|
| 274 |
+
raise ValueError("Image dimensions are too large")
|
| 275 |
+
probe.verify()
|
| 276 |
+
with Image.open(io.BytesIO(raw)) as source:
|
| 277 |
+
upright = ImageOps.exif_transpose(source)
|
| 278 |
+
upright.thumbnail((MAX_IMAGE_EDGE, MAX_IMAGE_EDGE), Image.Resampling.LANCZOS)
|
| 279 |
+
if "A" in upright.getbands() or "transparency" in upright.info:
|
| 280 |
+
rgba = upright.convert("RGBA")
|
| 281 |
+
normalized = Image.new("RGB", rgba.size, "white")
|
| 282 |
+
normalized.paste(rgba, mask=rgba.getchannel("A"))
|
| 283 |
+
else:
|
| 284 |
+
normalized = upright.convert("RGB")
|
| 285 |
+
output = io.BytesIO()
|
| 286 |
+
normalized.save(output, format="JPEG", quality=90)
|
| 287 |
+
return "data:image/jpeg;base64," + base64.b64encode(output.getvalue()).decode("ascii")
|
| 288 |
+
except (UnidentifiedImageError, OSError, ValueError, SyntaxError,
|
| 289 |
+
Image.DecompressionBombError, Image.DecompressionBombWarning):
|
| 290 |
+
raise HTTPException(422, "Use a valid, still JPEG, PNG, or WebP image up to 32 megapixels.") from None
|
| 291 |
|
| 292 |
|
| 293 |
async def infer(payload: dict) -> dict:
|
|
|
|
| 318 |
|
| 319 |
@app.post("/api/decide")
|
| 320 |
async def decide(payload: DecisionRequest):
|
| 321 |
+
data = payload.model_dump(exclude_none=True)
|
| 322 |
+
if payload.image is not None:
|
| 323 |
+
data["image"] = await asyncio.to_thread(prepare_image, payload.image)
|
| 324 |
+
return await infer(data)
|
| 325 |
|
| 326 |
|
| 327 |
@app.post("/api/games/new")
|
play.html
CHANGED
|
@@ -9,6 +9,9 @@
|
|
| 9 |
<link rel="icon" href="/autotrust-mark.svg" type="image/svg+xml">
|
| 10 |
<style>
|
| 11 |
:root{color-scheme:dark;--bg:#122E27;--surface:#18362F;--deep:#0E251F;--ivory:#FAF6EC;--gold:#E4C96A;--muted:#A9B9AF;--line:rgba(250,246,236,.14);--soft:rgba(250,246,236,.045);--red:#F3B4A1}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ivory);font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:14px;line-height:1.55}button,input,textarea,select{font:inherit}button,a,input,textarea,select{-webkit-tap-highlight-color:transparent}a{color:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.42}button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--gold);outline-offset:4px}button:disabled:hover{transform:none}button{transition:background .15s,border-color .15s}main,.top,footer{max-width:1224px;width:calc(100% - 80px);margin:auto}[hidden]{display:none!important}.top{height:86px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);gap:25px}.brand{display:flex;gap:10px;align-items:center;text-decoration:none;flex:none}.brand img{height:33px;width:33px}.brand-name{font-family:Georgia,serif;font-size:21px;letter-spacing:-.3px}.nav{display:flex;align-items:center;gap:26px;font-size:12px;color:var(--muted)}.nav a{text-decoration:none}.nav a:hover{color:var(--gold)}.nav .nav-current{color:var(--ivory)}.nav-divider{height:18px;border-left:1px solid var(--line)}.connection{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--muted);white-space:nowrap}.dot{height:6px;width:6px;border-radius:50%;background:#90A197;flex:none}.connection[data-status=ok] .dot{background:var(--gold);box-shadow:0 0 0 4px rgba(228,201,106,.07)}.connection[data-status=unavailable] .dot{background:var(--red)}.connection[data-status=unavailable]{color:var(--red)}.hero{padding:48px 0 32px;display:flex;align-items:flex-end;justify-content:space-between;gap:42px}.eyebrow{font-size:10px;text-transform:uppercase;letter-spacing:.2em;color:var(--gold);margin:0 0 15px;display:flex;gap:12px;align-items:center}.eyebrow:before{content:"";height:1px;width:23px;background:var(--gold)}h1,h2,h3,p{margin-top:0}h1{font-family:Georgia,"Times New Roman",serif;font-weight:400;letter-spacing:-2.2px;line-height:1.05;font-size:clamp(38px,4.5vw,60px);margin:0}h1 em{color:var(--gold);font-weight:400}.hero-copy{max-width:320px;font-size:13px;color:var(--muted);margin:0 0 3px;line-height:1.8}.hero-copy strong{font-weight:500;color:var(--ivory)}.tabbar{display:flex;border-bottom:1px solid var(--line);gap:34px;margin:0 0 26px}.tab{display:flex;gap:10px;align-items:center;border:0;background:none;color:var(--muted);padding:17px 0;position:relative;font-size:13px}.tab .number{font-size:10px;letter-spacing:.04em;opacity:.65}.tab[aria-selected=true]{color:var(--gold)}.tab[aria-selected=true]:after{content:"";height:2px;background:var(--gold);position:absolute;bottom:-1px;left:0;right:0}.tab svg{width:16px;height:16px}.tab-tail{margin:auto 0 auto auto;color:var(--muted);font-size:10px;letter-spacing:.1em;text-transform:uppercase}.lab-grid{display:grid;grid-template-columns:1.07fr 1fr;gap:20px}.panel{border:1px solid var(--line);border-radius:12px;background:var(--surface);overflow:hidden}.panel-head{padding:21px 25px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:12px}.section-label{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;gap:9px}.section-label span{color:var(--gold);font-family:ui-monospace,monospace}.panel-head .small{font-size:10px;color:var(--muted)}.input-body{padding:22px 25px 24px}.preset-label{font-size:11px;color:var(--muted);margin-bottom:9px}.presets{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:24px}.preset{background:var(--soft);border:1px solid var(--line);border-radius:6px;color:var(--muted);padding:7px 10px;font-size:11px}.preset:hover,.preset[aria-pressed=true]{color:var(--gold);border-color:rgba(228,201,106,.45);background:rgba(228,201,106,.06)}label{display:block;font-size:11px;color:var(--ivory);margin:0 0 8px}.label-line{display:flex;align-items:baseline;justify-content:space-between;gap:10px}.label-line span{color:var(--muted);font-size:10px}.field{margin:0 0 19px}.field:last-of-type{margin-bottom:21px}input,textarea,select{width:100%;border:1px solid rgba(250,246,236,.2);background:var(--deep);color:var(--ivory);border-radius:6px;padding:11px 12px;font-size:12px;line-height:1.65}textarea{resize:vertical;min-height:85px}textarea[readonly]{color:var(--muted)}input::placeholder,textarea::placeholder{color:#91A298}select{padding:10px 12px;appearance:auto}.form-row{display:grid;grid-template-columns:1fr 1.16fr;gap:16px;align-items:start}.helper{font-size:10px;color:var(--muted);line-height:1.6;margin:7px 0 0}.primary,.secondary,.action-button{border-radius:6px;padding:11px 15px;font-size:12px;border:1px solid var(--line);background:var(--soft);color:var(--ivory);display:inline-flex;align-items:center;justify-content:center;gap:8px}.primary{background:var(--gold);color:var(--bg);border-color:var(--gold);font-weight:650}.primary:hover:not(:disabled){background:#EFDA8C}.secondary:hover:not(:disabled),.action-button:hover:not(:disabled){border-color:rgba(228,201,106,.55);background:rgba(228,201,106,.06);color:var(--gold)}.primary svg,.secondary svg{width:14px;height:14px}.run-row{display:flex;align-items:center;justify-content:space-between;gap:16px}.run-row .primary{min-width:146px}.run-caption{font-size:10px;color:var(--muted);text-align:right;line-height:1.6;max-width:150px}.error{padding:12px 14px;background:rgba(243,180,161,.08);border:1px solid rgba(243,180,161,.25);border-radius:6px;color:var(--red);font-size:12px;margin:15px 0 0}.output-panel{display:flex;flex-direction:column;min-width:0}.output-body{padding:28px 25px;flex:1;display:flex;flex-direction:column}.empty-output{display:flex;align-items:center;justify-content:center;flex:1;min-height:280px;flex-direction:column;text-align:center}.signal-mark{height:78px;width:78px;position:relative;display:flex;align-items:center;justify-content:center;margin-bottom:27px}.signal-mark:before,.signal-mark:after{position:absolute;content:"";inset:0;border:1px solid rgba(228,201,106,.25);transform:rotate(45deg);border-radius:12px}.signal-mark:after{inset:14px;border-color:rgba(228,201,106,.5);border-radius:6px}.signal-mark svg{height:25px;width:25px;color:var(--gold)}.empty-output h2{font-family:Georgia,serif;font-size:26px;font-weight:400;letter-spacing:-.5px;margin-bottom:10px}.empty-output p{max-width:275px;font-size:12px;color:var(--muted);line-height:1.8;margin:0}.output-foot{border-top:1px solid var(--line);padding:15px 25px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:10px;color:var(--muted)}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.result-label{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:10px}.picked{font-family:Georgia,serif;color:var(--gold);font-size:31px;font-weight:400;letter-spacing:-.6px;line-height:1.3;overflow-wrap:anywhere;margin-bottom:28px}.bar-list{display:flex;flex-direction:column;gap:19px}.bar-row{min-width:0}.bar-top{display:flex;justify-content:space-between;gap:16px;font-size:11px;margin-bottom:7px}.bar-name{overflow-wrap:anywhere;color:var(--muted)}.bar-row.selected .bar-name{color:var(--ivory)}.bar-value{font-family:ui-monospace,monospace;flex:none;color:var(--muted)}.bar-row.selected .bar-value{color:var(--gold)}.bar-track{height:5px;background:rgba(250,246,236,.08);border-radius:3px;overflow:hidden}.bar-fill{height:100%;width:0;background:#718D7F;border-radius:3px;transition:width .35s ease}.bar-row.selected .bar-fill{background:var(--gold)}.result-note{font-size:10px;color:var(--muted);line-height:1.7;margin:25px 0 0}.loading .signal-mark{animation:signal-breathe 1.4s ease-in-out infinite}.loading .signal-mark:after{background:rgba(228,201,106,.08)}.below-lab{padding:23px 3px 0;display:grid;grid-template-columns:repeat(3,1fr);gap:35px}.how-item{font-size:11px;color:var(--muted);line-height:1.75;display:flex;gap:12px}.how-item>span{font-family:Georgia,serif;color:var(--gold);font-size:17px;margin-top:-2px}.how-item p{margin:0}.how-item strong{color:var(--ivory);display:block;font-weight:500;margin-bottom:3px}.arena-toolbar{display:flex;justify-content:space-between;align-items:center;gap:20px;margin:0 0 17px}.scenario-wrap{display:flex;align-items:center;gap:13px}.scenario-wrap label{margin:0;font-size:11px;color:var(--muted)}.scenario-wrap select{min-width:180px;padding:8px 10px}.arena-toolbar .primary{padding:10px 17px}.arena-grid{display:grid;grid-template-columns:minmax(0,1.8fr) minmax(265px,1fr);gap:20px}.game-screen{position:relative;aspect-ratio:4/3;background:#0A1915;display:flex;align-items:center;justify-content:center;border-bottom:1px solid var(--line);overflow:hidden}.game-screen img{width:100%;height:100%;object-fit:contain;image-rendering:pixelated;display:block}.game-placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;padding:32px;background-image:linear-gradient(rgba(228,201,106,.035) 1px,transparent 1px),linear-gradient(90deg,rgba(228,201,106,.035) 1px,transparent 1px);background-size:36px 36px}.game-placeholder svg{width:43px;height:43px;color:var(--gold);opacity:.7;margin-bottom:21px}.game-placeholder h2{font-family:Georgia,serif;font-weight:400;font-size:27px;letter-spacing:-.5px;margin:0 0 9px}.game-placeholder p{font-size:12px;color:var(--muted);max-width:235px;margin:0;line-height:1.8}.frame-badge{position:absolute;top:14px;left:14px;border:1px solid rgba(250,246,236,.2);color:var(--ivory);background:rgba(10,25,21,.85);padding:5px 8px;border-radius:4px;font-size:9px;text-transform:uppercase;letter-spacing:.09em}.metrics{display:grid;grid-template-columns:repeat(5,1fr);padding:17px 10px}.metric{text-align:center;border-right:1px solid var(--line)}.metric:last-child{border:0}.metric span{display:block;font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:5px}.metric b{font-weight:500;font-size:18px;font-family:ui-monospace,monospace}.arena-sidebar{display:flex;flex-direction:column;gap:17px}.controls-body{padding:19px 20px}.controls-label{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);margin:0 0 10px}.manual-actions{display:grid;grid-template-columns:1fr 1fr;gap:7px}.action-button{font-size:11px;padding:10px 8px;min-height:39px}.no-actions{font-size:11px;color:var(--muted);margin:0;padding:5px 0 13px}.divider{border:0;border-top:1px solid var(--line);margin:21px 0}.agent-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.agent-actions .primary,.agent-actions .secondary{padding:10px 8px;font-size:11px}.stop-button{width:100%;margin-top:9px;border-color:rgba(243,180,161,.4);color:var(--red)}.arena-note{font-size:10px;color:var(--muted);line-height:1.8;margin:13px 0 0}.game-status{font-size:11px;color:var(--muted);margin:16px 0 0;min-height:18px}.game-result{padding:19px 20px}.game-result .picked{font-size:23px;margin-bottom:19px}.game-result .bar-list{gap:12px}.game-result .bar-top{font-size:10px}.game-result .bar-track{height:4px}.game-decision-empty{font-size:12px;line-height:1.8;color:var(--muted);margin:0}.inference-timing{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 16px;margin:0 0 24px;border:1px solid rgba(228,201,106,.3);border-radius:6px;background:rgba(228,201,106,.055)}.inference-timing span{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}.inference-timing strong{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:23px;font-weight:500;white-space:nowrap;color:var(--gold)}.game-result .inference-timing{padding:12px;margin-bottom:20px;gap:10px}.game-result .inference-timing strong{font-size:21px}.game-decision-meta{font-size:10px;color:var(--muted);margin:19px 0 0}.arena-explainer{margin:16px 0 0;color:var(--muted);font-size:11px}.arena-explainer strong{font-weight:500;color:var(--ivory)}footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--line);margin-top:43px;padding:22px 0 31px;color:var(--muted);font-size:10px}footer a{text-decoration:none;color:var(--gold)}.footer-links{display:flex;gap:20px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@keyframes signal-breathe{50%{opacity:.45;transform:scale(.93)}}@media(min-width:1400px){.hero{padding-top:55px}}@media(max-width:980px){main,.top,footer{width:calc(100% - 48px)}.hero{gap:24px}.hero-copy{max-width:260px;font-size:12px}.nav{gap:19px}.form-row{grid-template-columns:1fr}.lab-grid{grid-template-columns:1.05fr 1fr}.input-body,.output-body{padding:22px 20px}.panel-head{padding:20px}.output-foot{padding:15px 20px}.arena-grid{grid-template-columns:minmax(0,1.6fr) minmax(245px,1fr)}.game-screen{aspect-ratio:1/1}.agent-actions{grid-template-columns:1fr}.below-lab{gap:22px}.nav-divider{display:none}.top>.connection{display:none}}@media(max-width:720px){.hero{display:block;padding-top:36px}.hero-copy{max-width:430px;margin:19px 0 0;font-size:13px}.lab-grid,.arena-grid{grid-template-columns:1fr}.form-row{grid-template-columns:1fr 1.16fr}.empty-output{min-height:245px}.output-panel{min-height:350px}.arena-sidebar{display:grid;grid-template-columns:1fr 1fr;align-items:start}.game-screen{aspect-ratio:4/3}.agent-actions{grid-template-columns:1fr}.below-lab{gap:17px}.how-item{display:block}.how-item>span{display:block;margin:0 0 6px}.tab-tail{font-size:9px}.nav{gap:18px}}@media(max-width:480px){main,.top,footer{width:calc(100% - 32px)}.top{height:73px;gap:12px}.brand-name{font-size:19px}.brand img{width:29px;height:29px}.nav{font-size:11px;gap:15px}.nav .nav-current{display:none}h1{font-size:41px;letter-spacing:-1.7px}.hero{padding:31px 0 22px}.hero-copy{font-size:12px}.tabbar{gap:24px;margin-bottom:20px}.tab-tail{display:none}.tab{font-size:12px}.panel-head{padding:17px 18px}.input-body,.output-body{padding:20px 18px}.form-row{grid-template-columns:1fr}.presets{gap:6px}.preset{padding:7px 9px;font-size:10px}.below-lab{grid-template-columns:1fr;gap:16px;padding-top:22px}.how-item{display:flex;gap:12px}.how-item>span{flex:0 0 15px}.how-item strong{display:inline;margin-right:5px}.scenario-wrap{display:block}.scenario-wrap label{margin-bottom:5px;font-size:10px}.scenario-wrap select{min-width:0;font-size:11px;max-width:185px}.arena-toolbar{gap:10px;align-items:flex-end}.arena-toolbar .primary{font-size:11px;padding:11px 13px;white-space:nowrap}.arena-sidebar{display:flex}.agent-actions{grid-template-columns:1fr 1fr}.metrics{padding:14px 4px}.metric b{font-size:15px}.metric span{font-size:8px;letter-spacing:.03em}footer{align-items:flex-start;flex-direction:column;gap:10px;margin-top:32px}.run-caption{font-size:9px}.output-foot{padding:15px 18px}.picked{font-size:28px}}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
|
|
|
|
|
|
|
|
|
| 12 |
</style>
|
| 13 |
</head>
|
| 14 |
<body>
|
|
@@ -18,7 +21,7 @@
|
|
| 18 |
<div class="connection" id="connection" data-status="checking" role="status"><span class="dot" aria-hidden="true"></span><span id="connection-text">Checking model…</span></div>
|
| 19 |
</header>
|
| 20 |
<main>
|
| 21 |
-
<section class="hero" aria-labelledby="page-title"><div><p class="eyebrow">JEV-27B · Live playground</p><h1 id="page-title">A decision. Then <em>action.</em></h1></div><p class="hero-copy">Give JEV
|
| 22 |
<div class="tabbar" role="tablist" aria-label="Playground mode">
|
| 23 |
<button class="tab" id="lab-tab" type="button" role="tab" aria-selected="true" aria-controls="lab" tabindex="0"><span class="number">01</span><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M4 4h12v12H4zM7 7h6M7 10h6M7 13h3"/></svg>Decision lab</button>
|
| 24 |
<button class="tab" id="arena-tab" type="button" role="tab" aria-selected="false" aria-controls="arena" tabindex="-1"><span class="number">02</span><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M7 4h6l4 4v7l-3 1-3-3H9l-3 3-3-1V8zM7 7v5M4.5 9.5h5M13 8.5h.01M15 11h.01"/></svg>Doom arena</button>
|
|
@@ -29,14 +32,16 @@
|
|
| 29 |
<div class="panel"><div class="panel-head"><div class="section-label"><span>01</span>Set the situation</div><span class="small">Your input</span></div>
|
| 30 |
<form id="decision-form" class="input-body">
|
| 31 |
<div class="preset-label">Start with an example</div><div class="presets" aria-label="Example situations"><button class="preset" type="button" data-preset="support" aria-pressed="true">Support ticket</button><button class="preset" type="button" data-preset="route" aria-pressed="false">Choose a route</button><button class="preset" type="button" data-preset="fact" aria-pressed="false">Factual check</button></div>
|
| 32 |
-
<div class="field"><div class="label-line"><label for="state">Situation</label><span>
|
|
|
|
|
|
|
| 33 |
<div class="field"><label for="question">What should JEV decide?</label><input id="question" maxlength="1000" required autocomplete="off"></div>
|
| 34 |
<div class="form-row"><div class="field"><label for="kind">Decision type</label><select id="kind"><option value="choice">Choose an option</option><option value="noul">True or false</option><option value="score">Score from 0 to 5</option></select><p class="helper" id="kind-help">Compare the choices you provide.</p></div><div class="field"><div class="label-line"><label for="options">Options</label><span id="options-label">One per line</span></div><textarea id="options" rows="3" maxlength="8000" required spellcheck="false"></textarea></div></div>
|
| 35 |
<div class="run-row"><button class="primary" id="run-decision" type="submit">Run JEV <svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 9h11M10 5l4 4-4 4"/></svg></button><span class="run-caption">Each run calls the<br>connected JEV-27B model.</span></div><p class="error" id="decision-error" role="alert" hidden></p>
|
| 36 |
</form></div>
|
| 37 |
-
<div class="panel output-panel"><div class="panel-head"><div class="section-label"><span>02</span>Read the decision</div><span class="small" id="result-status">Ready when you are</span></div><div class="output-body"><div class="empty-output" id="empty-output"><div class="signal-mark" aria-hidden="true"><svg viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M7 14h14M14 7v14"/><circle cx="14" cy="14" r="10"/></svg></div><h2 id="empty-title">Give a choice some context.</h2><p id="empty-copy">Run an example or write your own situation. JEV’s answer and option probabilities will appear here.</p></div><div id="decision-result" aria-live="polite" hidden><p class="result-label">JEV’s choice</p><h2 class="picked" id="picked-answer"></h2><div class="inference-timing"><span>
|
| 38 |
</div>
|
| 39 |
-
<div class="below-lab"><div class="how-item"><span>1</span><p><strong>Describe the situation.</strong>
|
| 40 |
</section>
|
| 41 |
<section id="arena" role="tabpanel" aria-labelledby="arena-tab" tabindex="0" hidden>
|
| 42 |
<div class="arena-toolbar"><div class="scenario-wrap"><label for="scenario">Choose an arena</label><select id="scenario"><option value="basic">Target practice</option><option value="defend_the_center">Center defense</option></select></div><button class="primary" type="button" id="new-game">Start arena <svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M5 3l9 6-9 6z"/></svg></button></div>
|
|
@@ -51,6 +56,8 @@
|
|
| 51 |
const $=id=>document.getElementById(id);
|
| 52 |
const presets={support:{kind:'choice',state:'A customer was charged twice for a single subscription renewal. They included two transaction IDs for the same date and amount. Their account is active, and they need help correcting the duplicate charge.',question:'Which team should handle this ticket?',options:['Billing support','Technical support','Sales']},route:{kind:'choice',state:'A delivery must arrive within 25 minutes. Route A takes 18 minutes but crosses a bridge closed for repairs. Route B takes 23 minutes and is open. Route C takes 32 minutes and is open.',question:'Which route should the driver take?',options:['Route A','Route B','Route C']},fact:{kind:'noul',state:'The museum is open Tuesday through Sunday, from 10:00 to 18:00. It is closed every Monday. Today is Monday.',question:'Is the museum open today at 14:00?',options:['false','true']}};
|
| 53 |
let decisionBusy=false,customOptions='',modelName='autotrust/JEV-27B';
|
|
|
|
|
|
|
| 54 |
let sessionId=null,gameBusy=false,gameDone=false,autoPlaying=false,stopRequested=false,actions=[],gameModel=modelName;
|
| 55 |
const tabs=[$('lab-tab'),$('arena-tab')];
|
| 56 |
function setTab(index){tabs.forEach((tab,i)=>{tab.setAttribute('aria-selected',String(i===index));tab.tabIndex=i===index?0:-1;$(tab.getAttribute('aria-controls')).hidden=i!==index});if(index!==1&&autoPlaying){stopRequested=true;$('game-status').textContent='Stopping after the current move…'}}
|
|
@@ -63,13 +70,101 @@ async function checkHealth(){try{const data=await api('/api/health');const onlin
|
|
| 63 |
function formatTime(seconds){const n=seconds;return typeof n==='number'&&Number.isFinite(n)&&n>=0?n<1?`${Math.round(n*1000)} ms`:`${n.toFixed(2)} s`:'—'}
|
| 64 |
function decisionData(data,fallbackOptions=[]){const optionList=Array.isArray(data.options)?data.options:fallbackOptions;const labels=optionList.map(option=>typeof option==='object'?String(option.label??option.id??''):String(option));let probabilities=data.probabilities;if(!Array.isArray(probabilities)&&probabilities&&typeof probabilities==='object')probabilities=optionList.map(option=>probabilities[typeof option==='object'?option.id:option]);if(!Array.isArray(probabilities))probabilities=[];const rawChoice=data.choice??data.answer??data.selected;let selectedIndex=-1;if(typeof rawChoice==='number'&&Number.isInteger(rawChoice)&&rawChoice>=0&&rawChoice<labels.length)selectedIndex=rawChoice;else selectedIndex=optionList.findIndex((option,index)=>String(typeof option==='object'?option.id:option)===String(rawChoice)||labels[index]===String(rawChoice));if(selectedIndex<0&&Number.isInteger(data.choice_index))selectedIndex=data.choice_index;const choiceLabel=selectedIndex>=0&&selectedIndex<labels.length?labels[selectedIndex]:rawChoice==null?'No decision returned':String(rawChoice);return {labels,probabilities,selectedIndex,choiceLabel}}
|
| 65 |
function renderBars(container,result){container.replaceChildren();result.labels.forEach((label,index)=>{const row=document.createElement('div');row.className='bar-row'+(index===result.selectedIndex?' selected':'');const top=document.createElement('div');top.className='bar-top';const name=document.createElement('span');name.className='bar-name';name.textContent=label;const value=document.createElement('span');value.className='bar-value';const p=Number(result.probabilities[index]),valid=Number.isFinite(p)&&p>=0&&p<=1;value.textContent=valid?`${(p*100).toFixed(1)}%`:'—';const track=document.createElement('div');track.className='bar-track';const fill=document.createElement('div');fill.className='bar-fill';fill.style.width=valid?`${p*100}%`:'0%';track.append(fill);top.append(name,value);row.append(top,track);container.append(row)})}
|
| 66 |
-
function
|
|
|
|
| 67 |
function updateKind(){const kind=$('kind').value;$('options').readOnly=kind!=='choice';$('options-label').textContent=kind==='choice'?'One per line':'Fixed choices';$('kind-help').textContent=kind==='choice'?'Compare the choices you provide.':kind==='noul'?'Judge whether the statement is true.':'Use your question to define what the score means.';if(kind==='noul')$('options').value='false\ntrue';else if(kind==='score')$('options').value='0\n1\n2\n3\n4\n5';else $('options').value=customOptions}
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
document.querySelectorAll('.preset').forEach(button=>button.addEventListener('click',()=>applyPreset(button.dataset.preset)));
|
| 70 |
$('kind').addEventListener('change',()=>{updateKind();resetDecision()});$('options').addEventListener('input',()=>{if($('kind').value==='choice')customOptions=$('options').value});['state','question','options','kind'].forEach(id=>$(id).addEventListener('input',()=>document.querySelectorAll('.preset').forEach(button=>button.setAttribute('aria-pressed','false'))));
|
| 71 |
-
function setDecisionBusy(busy){decisionBusy=busy;document.querySelectorAll('#decision-form button,#decision-form input,#decision-form textarea,#decision-form select').forEach(element=>element.disabled=busy);$('run-decision').textContent=busy?'JEV is deciding…':'Run JEV →';
|
| 72 |
-
$('decision-form').addEventListener('submit',async event=>{event.preventDefault();if(decisionBusy)return;const kind=$('kind').value,state=$('state').value.trim(),question=$('question').value.trim(),options=$('options').value.split('\n').map(option=>option.trim()).filter(Boolean);if(!
|
| 73 |
function updateGameControls(){const disabled=gameBusy||autoPlaying||!sessionId||gameDone;$('new-game').disabled=gameBusy||autoPlaying;$('scenario').disabled=gameBusy||autoPlaying;$('jev-move').disabled=disabled;$('auto-play').disabled=disabled;document.querySelectorAll('#manual-actions button').forEach(button=>button.disabled=disabled);$('stop-auto').hidden=!autoPlaying;$('stop-auto').disabled=stopRequested;$('auto-play').textContent=autoPlaying?'Auto-play running…':'Auto-play · 10 steps';$('new-game').textContent=sessionId?'Reset arena ↻':'Start arena ▷'}
|
| 74 |
function renderActions(nextActions){if(!Array.isArray(nextActions))return;actions=nextActions;$('manual-actions').replaceChildren();for(const action of actions){if(!action||typeof action.id!=='string')continue;const button=document.createElement('button');button.type='button';button.className='action-button';button.textContent=String(action.label||action.id);button.title=String(action.description||action.label||action.id);button.addEventListener('click',()=>runGameStep('human',action.id));$('manual-actions').append(button)}$('no-actions').hidden=actions.length>0;updateGameControls()}
|
| 75 |
function updateSnapshot(data){if(typeof data.session_id==='string')sessionId=data.session_id;gameDone=Boolean(data.done);if(typeof data.frame==='string'&&data.frame){const frame=data.frame.startsWith('data:image/png;base64,')?data.frame:`data:image/png;base64,${data.frame}`;$('game-frame').src=frame;$('game-frame').hidden=false;$('game-placeholder').hidden=true;$('game-frame').alt=`Doom ${data.scenario==='defend_the_center'?'center defense':'target practice'}; step ${data.metrics?.steps??'unknown'}`}if(data.actions)renderActions(data.actions);for(const key of ['health','ammo','kills','reward','steps']){const value=data.metrics?.[key];$('metric-'+key).textContent=typeof value==='number'&&Number.isFinite(value)?Number.isInteger(value)?String(value):value.toFixed(1):'—'}$('frame-badge').hidden=!sessionId;$('frame-badge').textContent=gameDone?'Episode complete':'Simulation paused';if(data.decision){const decision=data.decision;connected(decision.model);const results=decisionData(decision,actions.map(action=>action.id));const labels=results.labels.map(label=>actions.find(action=>action.id===label||action.description===label)?.label||label);const selectedId=results.choiceLabel;results.labels=labels;results.choiceLabel=actions.find(action=>action.id===selectedId||action.description===selectedId)?.label||selectedId;$('game-picked').textContent=results.choiceLabel;renderBars($('game-probabilities'),results);$('game-decision-empty').hidden=true;$('game-decision').hidden=false;gameModel=typeof decision.model==='string'?decision.model:typeof data.model==='string'?data.model:modelName;$('game-latency').textContent=formatTime(decision.elapsed_seconds);$('game-decision-meta').textContent=`${gameModel} · Timing reported by the model server`;}updateGameControls()}
|
|
|
|
| 9 |
<link rel="icon" href="/autotrust-mark.svg" type="image/svg+xml">
|
| 10 |
<style>
|
| 11 |
:root{color-scheme:dark;--bg:#122E27;--surface:#18362F;--deep:#0E251F;--ivory:#FAF6EC;--gold:#E4C96A;--muted:#A9B9AF;--line:rgba(250,246,236,.14);--soft:rgba(250,246,236,.045);--red:#F3B4A1}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ivory);font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:14px;line-height:1.55}button,input,textarea,select{font:inherit}button,a,input,textarea,select{-webkit-tap-highlight-color:transparent}a{color:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.42}button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--gold);outline-offset:4px}button:disabled:hover{transform:none}button{transition:background .15s,border-color .15s}main,.top,footer{max-width:1224px;width:calc(100% - 80px);margin:auto}[hidden]{display:none!important}.top{height:86px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);gap:25px}.brand{display:flex;gap:10px;align-items:center;text-decoration:none;flex:none}.brand img{height:33px;width:33px}.brand-name{font-family:Georgia,serif;font-size:21px;letter-spacing:-.3px}.nav{display:flex;align-items:center;gap:26px;font-size:12px;color:var(--muted)}.nav a{text-decoration:none}.nav a:hover{color:var(--gold)}.nav .nav-current{color:var(--ivory)}.nav-divider{height:18px;border-left:1px solid var(--line)}.connection{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--muted);white-space:nowrap}.dot{height:6px;width:6px;border-radius:50%;background:#90A197;flex:none}.connection[data-status=ok] .dot{background:var(--gold);box-shadow:0 0 0 4px rgba(228,201,106,.07)}.connection[data-status=unavailable] .dot{background:var(--red)}.connection[data-status=unavailable]{color:var(--red)}.hero{padding:48px 0 32px;display:flex;align-items:flex-end;justify-content:space-between;gap:42px}.eyebrow{font-size:10px;text-transform:uppercase;letter-spacing:.2em;color:var(--gold);margin:0 0 15px;display:flex;gap:12px;align-items:center}.eyebrow:before{content:"";height:1px;width:23px;background:var(--gold)}h1,h2,h3,p{margin-top:0}h1{font-family:Georgia,"Times New Roman",serif;font-weight:400;letter-spacing:-2.2px;line-height:1.05;font-size:clamp(38px,4.5vw,60px);margin:0}h1 em{color:var(--gold);font-weight:400}.hero-copy{max-width:320px;font-size:13px;color:var(--muted);margin:0 0 3px;line-height:1.8}.hero-copy strong{font-weight:500;color:var(--ivory)}.tabbar{display:flex;border-bottom:1px solid var(--line);gap:34px;margin:0 0 26px}.tab{display:flex;gap:10px;align-items:center;border:0;background:none;color:var(--muted);padding:17px 0;position:relative;font-size:13px}.tab .number{font-size:10px;letter-spacing:.04em;opacity:.65}.tab[aria-selected=true]{color:var(--gold)}.tab[aria-selected=true]:after{content:"";height:2px;background:var(--gold);position:absolute;bottom:-1px;left:0;right:0}.tab svg{width:16px;height:16px}.tab-tail{margin:auto 0 auto auto;color:var(--muted);font-size:10px;letter-spacing:.1em;text-transform:uppercase}.lab-grid{display:grid;grid-template-columns:1.07fr 1fr;gap:20px}.panel{border:1px solid var(--line);border-radius:12px;background:var(--surface);overflow:hidden}.panel-head{padding:21px 25px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:12px}.section-label{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;gap:9px}.section-label span{color:var(--gold);font-family:ui-monospace,monospace}.panel-head .small{font-size:10px;color:var(--muted)}.input-body{padding:22px 25px 24px}.preset-label{font-size:11px;color:var(--muted);margin-bottom:9px}.presets{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:24px}.preset{background:var(--soft);border:1px solid var(--line);border-radius:6px;color:var(--muted);padding:7px 10px;font-size:11px}.preset:hover,.preset[aria-pressed=true]{color:var(--gold);border-color:rgba(228,201,106,.45);background:rgba(228,201,106,.06)}label{display:block;font-size:11px;color:var(--ivory);margin:0 0 8px}.label-line{display:flex;align-items:baseline;justify-content:space-between;gap:10px}.label-line span{color:var(--muted);font-size:10px}.field{margin:0 0 19px}.field:last-of-type{margin-bottom:21px}input,textarea,select{width:100%;border:1px solid rgba(250,246,236,.2);background:var(--deep);color:var(--ivory);border-radius:6px;padding:11px 12px;font-size:12px;line-height:1.65}textarea{resize:vertical;min-height:85px}textarea[readonly]{color:var(--muted)}input::placeholder,textarea::placeholder{color:#91A298}select{padding:10px 12px;appearance:auto}.form-row{display:grid;grid-template-columns:1fr 1.16fr;gap:16px;align-items:start}.helper{font-size:10px;color:var(--muted);line-height:1.6;margin:7px 0 0}.primary,.secondary,.action-button{border-radius:6px;padding:11px 15px;font-size:12px;border:1px solid var(--line);background:var(--soft);color:var(--ivory);display:inline-flex;align-items:center;justify-content:center;gap:8px}.primary{background:var(--gold);color:var(--bg);border-color:var(--gold);font-weight:650}.primary:hover:not(:disabled){background:#EFDA8C}.secondary:hover:not(:disabled),.action-button:hover:not(:disabled){border-color:rgba(228,201,106,.55);background:rgba(228,201,106,.06);color:var(--gold)}.primary svg,.secondary svg{width:14px;height:14px}.run-row{display:flex;align-items:center;justify-content:space-between;gap:16px}.run-row .primary{min-width:146px}.run-caption{font-size:10px;color:var(--muted);text-align:right;line-height:1.6;max-width:150px}.error{padding:12px 14px;background:rgba(243,180,161,.08);border:1px solid rgba(243,180,161,.25);border-radius:6px;color:var(--red);font-size:12px;margin:15px 0 0}.output-panel{display:flex;flex-direction:column;min-width:0}.output-body{padding:28px 25px;flex:1;display:flex;flex-direction:column}.empty-output{display:flex;align-items:center;justify-content:center;flex:1;min-height:280px;flex-direction:column;text-align:center}.signal-mark{height:78px;width:78px;position:relative;display:flex;align-items:center;justify-content:center;margin-bottom:27px}.signal-mark:before,.signal-mark:after{position:absolute;content:"";inset:0;border:1px solid rgba(228,201,106,.25);transform:rotate(45deg);border-radius:12px}.signal-mark:after{inset:14px;border-color:rgba(228,201,106,.5);border-radius:6px}.signal-mark svg{height:25px;width:25px;color:var(--gold)}.empty-output h2{font-family:Georgia,serif;font-size:26px;font-weight:400;letter-spacing:-.5px;margin-bottom:10px}.empty-output p{max-width:275px;font-size:12px;color:var(--muted);line-height:1.8;margin:0}.output-foot{border-top:1px solid var(--line);padding:15px 25px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:10px;color:var(--muted)}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.result-label{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:10px}.picked{font-family:Georgia,serif;color:var(--gold);font-size:31px;font-weight:400;letter-spacing:-.6px;line-height:1.3;overflow-wrap:anywhere;margin-bottom:28px}.bar-list{display:flex;flex-direction:column;gap:19px}.bar-row{min-width:0}.bar-top{display:flex;justify-content:space-between;gap:16px;font-size:11px;margin-bottom:7px}.bar-name{overflow-wrap:anywhere;color:var(--muted)}.bar-row.selected .bar-name{color:var(--ivory)}.bar-value{font-family:ui-monospace,monospace;flex:none;color:var(--muted)}.bar-row.selected .bar-value{color:var(--gold)}.bar-track{height:5px;background:rgba(250,246,236,.08);border-radius:3px;overflow:hidden}.bar-fill{height:100%;width:0;background:#718D7F;border-radius:3px;transition:width .35s ease}.bar-row.selected .bar-fill{background:var(--gold)}.result-note{font-size:10px;color:var(--muted);line-height:1.7;margin:25px 0 0}.loading .signal-mark{animation:signal-breathe 1.4s ease-in-out infinite}.loading .signal-mark:after{background:rgba(228,201,106,.08)}.below-lab{padding:23px 3px 0;display:grid;grid-template-columns:repeat(3,1fr);gap:35px}.how-item{font-size:11px;color:var(--muted);line-height:1.75;display:flex;gap:12px}.how-item>span{font-family:Georgia,serif;color:var(--gold);font-size:17px;margin-top:-2px}.how-item p{margin:0}.how-item strong{color:var(--ivory);display:block;font-weight:500;margin-bottom:3px}.arena-toolbar{display:flex;justify-content:space-between;align-items:center;gap:20px;margin:0 0 17px}.scenario-wrap{display:flex;align-items:center;gap:13px}.scenario-wrap label{margin:0;font-size:11px;color:var(--muted)}.scenario-wrap select{min-width:180px;padding:8px 10px}.arena-toolbar .primary{padding:10px 17px}.arena-grid{display:grid;grid-template-columns:minmax(0,1.8fr) minmax(265px,1fr);gap:20px}.game-screen{position:relative;aspect-ratio:4/3;background:#0A1915;display:flex;align-items:center;justify-content:center;border-bottom:1px solid var(--line);overflow:hidden}.game-screen img{width:100%;height:100%;object-fit:contain;image-rendering:pixelated;display:block}.game-placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;padding:32px;background-image:linear-gradient(rgba(228,201,106,.035) 1px,transparent 1px),linear-gradient(90deg,rgba(228,201,106,.035) 1px,transparent 1px);background-size:36px 36px}.game-placeholder svg{width:43px;height:43px;color:var(--gold);opacity:.7;margin-bottom:21px}.game-placeholder h2{font-family:Georgia,serif;font-weight:400;font-size:27px;letter-spacing:-.5px;margin:0 0 9px}.game-placeholder p{font-size:12px;color:var(--muted);max-width:235px;margin:0;line-height:1.8}.frame-badge{position:absolute;top:14px;left:14px;border:1px solid rgba(250,246,236,.2);color:var(--ivory);background:rgba(10,25,21,.85);padding:5px 8px;border-radius:4px;font-size:9px;text-transform:uppercase;letter-spacing:.09em}.metrics{display:grid;grid-template-columns:repeat(5,1fr);padding:17px 10px}.metric{text-align:center;border-right:1px solid var(--line)}.metric:last-child{border:0}.metric span{display:block;font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:5px}.metric b{font-weight:500;font-size:18px;font-family:ui-monospace,monospace}.arena-sidebar{display:flex;flex-direction:column;gap:17px}.controls-body{padding:19px 20px}.controls-label{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);margin:0 0 10px}.manual-actions{display:grid;grid-template-columns:1fr 1fr;gap:7px}.action-button{font-size:11px;padding:10px 8px;min-height:39px}.no-actions{font-size:11px;color:var(--muted);margin:0;padding:5px 0 13px}.divider{border:0;border-top:1px solid var(--line);margin:21px 0}.agent-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.agent-actions .primary,.agent-actions .secondary{padding:10px 8px;font-size:11px}.stop-button{width:100%;margin-top:9px;border-color:rgba(243,180,161,.4);color:var(--red)}.arena-note{font-size:10px;color:var(--muted);line-height:1.8;margin:13px 0 0}.game-status{font-size:11px;color:var(--muted);margin:16px 0 0;min-height:18px}.game-result{padding:19px 20px}.game-result .picked{font-size:23px;margin-bottom:19px}.game-result .bar-list{gap:12px}.game-result .bar-top{font-size:10px}.game-result .bar-track{height:4px}.game-decision-empty{font-size:12px;line-height:1.8;color:var(--muted);margin:0}.inference-timing{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 16px;margin:0 0 24px;border:1px solid rgba(228,201,106,.3);border-radius:6px;background:rgba(228,201,106,.055)}.inference-timing span{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}.inference-timing strong{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:23px;font-weight:500;white-space:nowrap;color:var(--gold)}.game-result .inference-timing{padding:12px;margin-bottom:20px;gap:10px}.game-result .inference-timing strong{font-size:21px}.game-decision-meta{font-size:10px;color:var(--muted);margin:19px 0 0}.arena-explainer{margin:16px 0 0;color:var(--muted);font-size:11px}.arena-explainer strong{font-weight:500;color:var(--ivory)}footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--line);margin-top:43px;padding:22px 0 31px;color:var(--muted);font-size:10px}footer a{text-decoration:none;color:var(--gold)}.footer-links{display:flex;gap:20px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@keyframes signal-breathe{50%{opacity:.45;transform:scale(.93)}}@media(min-width:1400px){.hero{padding-top:55px}}@media(max-width:980px){main,.top,footer{width:calc(100% - 48px)}.hero{gap:24px}.hero-copy{max-width:260px;font-size:12px}.nav{gap:19px}.form-row{grid-template-columns:1fr}.lab-grid{grid-template-columns:1.05fr 1fr}.input-body,.output-body{padding:22px 20px}.panel-head{padding:20px}.output-foot{padding:15px 20px}.arena-grid{grid-template-columns:minmax(0,1.6fr) minmax(245px,1fr)}.game-screen{aspect-ratio:1/1}.agent-actions{grid-template-columns:1fr}.below-lab{gap:22px}.nav-divider{display:none}.top>.connection{display:none}}@media(max-width:720px){.hero{display:block;padding-top:36px}.hero-copy{max-width:430px;margin:19px 0 0;font-size:13px}.lab-grid,.arena-grid{grid-template-columns:1fr}.form-row{grid-template-columns:1fr 1.16fr}.empty-output{min-height:245px}.output-panel{min-height:350px}.arena-sidebar{display:grid;grid-template-columns:1fr 1fr;align-items:start}.game-screen{aspect-ratio:4/3}.agent-actions{grid-template-columns:1fr}.below-lab{gap:17px}.how-item{display:block}.how-item>span{display:block;margin:0 0 6px}.tab-tail{font-size:9px}.nav{gap:18px}}@media(max-width:480px){main,.top,footer{width:calc(100% - 32px)}.top{height:73px;gap:12px}.brand-name{font-size:19px}.brand img{width:29px;height:29px}.nav{font-size:11px;gap:15px}.nav .nav-current{display:none}h1{font-size:41px;letter-spacing:-1.7px}.hero{padding:31px 0 22px}.hero-copy{font-size:12px}.tabbar{gap:24px;margin-bottom:20px}.tab-tail{display:none}.tab{font-size:12px}.panel-head{padding:17px 18px}.input-body,.output-body{padding:20px 18px}.form-row{grid-template-columns:1fr}.presets{gap:6px}.preset{padding:7px 9px;font-size:10px}.below-lab{grid-template-columns:1fr;gap:16px;padding-top:22px}.how-item{display:flex;gap:12px}.how-item>span{flex:0 0 15px}.how-item strong{display:inline;margin-right:5px}.scenario-wrap{display:block}.scenario-wrap label{margin-bottom:5px;font-size:10px}.scenario-wrap select{min-width:0;font-size:11px;max-width:185px}.arena-toolbar{gap:10px;align-items:flex-end}.arena-toolbar .primary{font-size:11px;padding:11px 13px;white-space:nowrap}.arena-sidebar{display:flex}.agent-actions{grid-template-columns:1fr 1fr}.metrics{padding:14px 4px}.metric b{font-size:15px}.metric span{font-size:8px;letter-spacing:.03em}footer{align-items:flex-start;flex-direction:column;gap:10px;margin-top:32px}.run-caption{font-size:9px}.output-foot{padding:15px 18px}.picked{font-size:28px}}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
| 12 |
+
|
| 13 |
+
/* One optional image, kept in page memory until a decision is submitted. */
|
| 14 |
+
.vision-details{margin:-12px 0 24px;padding:0 2px 14px;border-bottom:1px solid var(--line)}.vision-timing{display:flex;align-items:center;justify-content:space-between;gap:15px;font-size:11px;color:var(--muted)}.vision-timing strong{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;font-weight:500;color:var(--ivory);white-space:nowrap}.vision-model{font-size:9px;color:var(--muted);margin:5px 0 0;overflow-wrap:anywhere}.image-field{margin-top:15px}.image-drop{position:relative;border:1px dashed rgba(250,246,236,.28);border-radius:7px;background:var(--deep);transition:border-color .15s,background .15s}.image-drop:focus-within{outline:2px solid var(--gold);outline-offset:4px}.image-drop.is-dragging{border-color:var(--gold);background:rgba(228,201,106,.08)}.image-drop[aria-disabled=true]{opacity:.5}.image-picker{display:flex;align-items:center;justify-content:center;gap:11px;min-height:83px;padding:15px 17px;margin:0;cursor:pointer;color:var(--muted);font-size:11px;line-height:1.6;text-align:left}.image-picker svg{width:24px;height:24px;flex:none;color:var(--gold)}.image-picker strong{font-weight:500;color:var(--gold)}.image-picker small{display:block;font-size:10px;color:var(--muted);margin-top:2px}.image-drop[aria-disabled=true] .image-picker{cursor:default}.image-preview{display:flex;align-items:center;gap:13px;padding:13px;min-width:0}.image-thumbnail{display:block;flex:0 0 74px;width:74px;height:74px;object-fit:contain;border-radius:4px;background:rgba(250,246,236,.04);border:1px solid var(--line)}.image-details{flex:1;min-width:0}.image-name{color:var(--ivory);font-size:11px;line-height:1.5;overflow-wrap:anywhere;margin:0 0 5px}.image-meta{font-size:10px;color:var(--muted);line-height:1.5;margin:0}.image-remove{border:1px solid var(--line);border-radius:5px;background:transparent;color:var(--muted);font-size:10px;padding:6px 9px;margin-top:9px}.image-remove:hover:not(:disabled){color:var(--ivory);border-color:var(--ivory)}.image-status{min-height:0;font-size:10px;color:var(--gold);margin:7px 0 0;line-height:1.6}.image-status:empty{display:none}.image-error{margin-top:9px;padding:10px 12px;font-size:11px}@media(max-width:480px){.image-picker{padding:14px 12px;gap:10px}.image-preview{padding:11px;gap:11px}.image-thumbnail{width:64px;height:64px;flex-basis:64px}}
|
| 15 |
</style>
|
| 16 |
</head>
|
| 17 |
<body>
|
|
|
|
| 21 |
<div class="connection" id="connection" data-status="checking" role="status"><span class="dot" aria-hidden="true"></span><span id="connection-text">Checking model…</span></div>
|
| 22 |
</header>
|
| 23 |
<main>
|
| 24 |
+
<section class="hero" aria-labelledby="page-title"><div><p class="eyebrow">JEV-27B · Live playground</p><h1 id="page-title">A decision. Then <em>action.</em></h1></div><p class="hero-copy">Give JEV text or an image, and a set of choices.<br><strong>See what it picks. Watch what happens.</strong><br>Real inference, one decision at a time.</p></section>
|
| 25 |
<div class="tabbar" role="tablist" aria-label="Playground mode">
|
| 26 |
<button class="tab" id="lab-tab" type="button" role="tab" aria-selected="true" aria-controls="lab" tabindex="0"><span class="number">01</span><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M4 4h12v12H4zM7 7h6M7 10h6M7 13h3"/></svg>Decision lab</button>
|
| 27 |
<button class="tab" id="arena-tab" type="button" role="tab" aria-selected="false" aria-controls="arena" tabindex="-1"><span class="number">02</span><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M7 4h6l4 4v7l-3 1-3-3H9l-3 3-3-1V8zM7 7v5M4.5 9.5h5M13 8.5h.01M15 11h.01"/></svg>Doom arena</button>
|
|
|
|
| 32 |
<div class="panel"><div class="panel-head"><div class="section-label"><span>01</span>Set the situation</div><span class="small">Your input</span></div>
|
| 33 |
<form id="decision-form" class="input-body">
|
| 34 |
<div class="preset-label">Start with an example</div><div class="presets" aria-label="Example situations"><button class="preset" type="button" data-preset="support" aria-pressed="true">Support ticket</button><button class="preset" type="button" data-preset="route" aria-pressed="false">Choose a route</button><button class="preset" type="button" data-preset="fact" aria-pressed="false">Factual check</button></div>
|
| 35 |
+
<div class="field"><div class="label-line"><label for="state">Situation</label><span id="state-hint">Text or image context</span></div><textarea id="state" rows="4" maxlength="6000" required spellcheck="false" placeholder="Describe the situation, or attach an image below." aria-describedby="state-hint"></textarea>
|
| 36 |
+
<div class="image-field"><div class="label-line"><label for="image-input">Image</label><span>Optional · one image</span></div><div class="image-drop" id="image-drop" role="group" aria-label="Image attachment" aria-disabled="false"><input id="image-input" class="sr-only" type="file" accept="image/jpeg,image/png,image/webp" aria-label="Choose an image" aria-describedby="image-help image-status image-error"><label class="image-picker" id="image-picker" for="image-input"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m4 17 5-5 4 4 3-3 5 5"/></svg><span><strong>Choose an image</strong> or drop it here<small>Add a photo, screenshot, or visual question.</small></span></label><div class="image-preview" id="image-preview" hidden><img class="image-thumbnail" id="image-thumbnail" alt="Preview of the attached image"><div class="image-details"><p class="image-name" id="image-name"></p><p class="image-meta" id="image-meta"></p><button class="image-remove" id="remove-image" type="button" aria-label="Remove attached image">Remove image</button></div></div></div><p class="helper" id="image-help">JPEG, PNG or WebP · up to 5 MiB. Text is optional with an image. Images are read by a vision model, then JEV makes the decision.</p><p class="image-status" id="image-status" role="status" aria-live="polite"></p><p class="error image-error" id="image-error" role="alert" hidden></p></div>
|
| 37 |
+
</div>
|
| 38 |
<div class="field"><label for="question">What should JEV decide?</label><input id="question" maxlength="1000" required autocomplete="off"></div>
|
| 39 |
<div class="form-row"><div class="field"><label for="kind">Decision type</label><select id="kind"><option value="choice">Choose an option</option><option value="noul">True or false</option><option value="score">Score from 0 to 5</option></select><p class="helper" id="kind-help">Compare the choices you provide.</p></div><div class="field"><div class="label-line"><label for="options">Options</label><span id="options-label">One per line</span></div><textarea id="options" rows="3" maxlength="8000" required spellcheck="false"></textarea></div></div>
|
| 40 |
<div class="run-row"><button class="primary" id="run-decision" type="submit">Run JEV <svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 9h11M10 5l4 4-4 4"/></svg></button><span class="run-caption">Each run calls the<br>connected JEV-27B model.</span></div><p class="error" id="decision-error" role="alert" hidden></p>
|
| 41 |
</form></div>
|
| 42 |
+
<div class="panel output-panel"><div class="panel-head"><div class="section-label"><span>02</span>Read the decision</div><span class="small" id="result-status">Ready when you are</span></div><div class="output-body"><div class="empty-output" id="empty-output"><div class="signal-mark" aria-hidden="true"><svg viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M7 14h14M14 7v14"/><circle cx="14" cy="14" r="10"/></svg></div><h2 id="empty-title">Give a choice some context.</h2><p id="empty-copy">Run an example or write your own situation. JEV’s answer and option probabilities will appear here.</p></div><div id="decision-result" aria-live="polite" hidden><p class="result-label">JEV’s choice</p><h2 class="picked" id="picked-answer"></h2><div class="inference-timing"><span>JEV inference time</span><strong id="result-latency">—</strong></div><div class="vision-details" id="vision-details" hidden><div class="vision-timing"><span>Image understanding</span><strong id="vision-latency">—</strong></div><p class="vision-model mono" id="vision-model"></p></div><div class="bar-list" id="probabilities" aria-label="Option probabilities"></div><p class="result-note">Probabilities compare the provided options. They are not a guarantee that an answer is correct.</p></div></div><div class="output-foot"><span class="mono" id="result-model">autotrust/JEV-27B</span><span>Timing reported by the model server</span></div></div>
|
| 43 |
</div>
|
| 44 |
+
<div class="below-lab"><div class="how-item"><span>1</span><p><strong>Describe the situation.</strong>Write the context or attach an image. Add the details that matter.</p></div><div class="how-item"><span>2</span><p><strong>Define the decision.</strong>Choose an option, check a statement, or assign a score.</p></div><div class="how-item"><span>3</span><p><strong>Inspect the result.</strong>Compare option probabilities. Edit the context and try again.</p></div></div>
|
| 45 |
</section>
|
| 46 |
<section id="arena" role="tabpanel" aria-labelledby="arena-tab" tabindex="0" hidden>
|
| 47 |
<div class="arena-toolbar"><div class="scenario-wrap"><label for="scenario">Choose an arena</label><select id="scenario"><option value="basic">Target practice</option><option value="defend_the_center">Center defense</option></select></div><button class="primary" type="button" id="new-game">Start arena <svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M5 3l9 6-9 6z"/></svg></button></div>
|
|
|
|
| 56 |
const $=id=>document.getElementById(id);
|
| 57 |
const presets={support:{kind:'choice',state:'A customer was charged twice for a single subscription renewal. They included two transaction IDs for the same date and amount. Their account is active, and they need help correcting the duplicate charge.',question:'Which team should handle this ticket?',options:['Billing support','Technical support','Sales']},route:{kind:'choice',state:'A delivery must arrive within 25 minutes. Route A takes 18 minutes but crosses a bridge closed for repairs. Route B takes 23 minutes and is open. Route C takes 32 minutes and is open.',question:'Which route should the driver take?',options:['Route A','Route B','Route C']},fact:{kind:'noul',state:'The museum is open Tuesday through Sunday, from 10:00 to 18:00. It is closed every Monday. Today is Monday.',question:'Is the museum open today at 14:00?',options:['false','true']}};
|
| 58 |
let decisionBusy=false,customOptions='',modelName='autotrust/JEV-27B';
|
| 59 |
+
const MAX_IMAGE_BYTES=5*1024*1024,IMAGE_TYPES=new Set(['image/jpeg','image/png','image/webp']);
|
| 60 |
+
let attachedImage=null,imageLoading=false,imageLoadVersion=0,imageReader=null,imageDragDepth=0;
|
| 61 |
let sessionId=null,gameBusy=false,gameDone=false,autoPlaying=false,stopRequested=false,actions=[],gameModel=modelName;
|
| 62 |
const tabs=[$('lab-tab'),$('arena-tab')];
|
| 63 |
function setTab(index){tabs.forEach((tab,i)=>{tab.setAttribute('aria-selected',String(i===index));tab.tabIndex=i===index?0:-1;$(tab.getAttribute('aria-controls')).hidden=i!==index});if(index!==1&&autoPlaying){stopRequested=true;$('game-status').textContent='Stopping after the current move…'}}
|
|
|
|
| 70 |
function formatTime(seconds){const n=seconds;return typeof n==='number'&&Number.isFinite(n)&&n>=0?n<1?`${Math.round(n*1000)} ms`:`${n.toFixed(2)} s`:'—'}
|
| 71 |
function decisionData(data,fallbackOptions=[]){const optionList=Array.isArray(data.options)?data.options:fallbackOptions;const labels=optionList.map(option=>typeof option==='object'?String(option.label??option.id??''):String(option));let probabilities=data.probabilities;if(!Array.isArray(probabilities)&&probabilities&&typeof probabilities==='object')probabilities=optionList.map(option=>probabilities[typeof option==='object'?option.id:option]);if(!Array.isArray(probabilities))probabilities=[];const rawChoice=data.choice??data.answer??data.selected;let selectedIndex=-1;if(typeof rawChoice==='number'&&Number.isInteger(rawChoice)&&rawChoice>=0&&rawChoice<labels.length)selectedIndex=rawChoice;else selectedIndex=optionList.findIndex((option,index)=>String(typeof option==='object'?option.id:option)===String(rawChoice)||labels[index]===String(rawChoice));if(selectedIndex<0&&Number.isInteger(data.choice_index))selectedIndex=data.choice_index;const choiceLabel=selectedIndex>=0&&selectedIndex<labels.length?labels[selectedIndex]:rawChoice==null?'No decision returned':String(rawChoice);return {labels,probabilities,selectedIndex,choiceLabel}}
|
| 72 |
function renderBars(container,result){container.replaceChildren();result.labels.forEach((label,index)=>{const row=document.createElement('div');row.className='bar-row'+(index===result.selectedIndex?' selected':'');const top=document.createElement('div');top.className='bar-top';const name=document.createElement('span');name.className='bar-name';name.textContent=label;const value=document.createElement('span');value.className='bar-value';const p=Number(result.probabilities[index]),valid=Number.isFinite(p)&&p>=0&&p<=1;value.textContent=valid?`${(p*100).toFixed(1)}%`:'—';const track=document.createElement('div');track.className='bar-track';const fill=document.createElement('div');fill.className='bar-fill';fill.style.width=valid?`${p*100}%`:'0%';track.append(fill);top.append(name,value);row.append(top,track);container.append(row)})}
|
| 73 |
+
function renderVision(vision){const present=vision&&typeof vision==='object'&&!Array.isArray(vision);$('vision-details').hidden=!present;$('vision-latency').textContent=present?formatTime(vision.elapsed_seconds):'—';$('vision-model').textContent=present&&typeof vision.model==='string'?vision.model:''}
|
| 74 |
+
function resetDecision(){renderVision(null);showError('decision-error','');$('decision-result').hidden=true;$('empty-output').hidden=false;$('empty-output').classList.remove('loading');$('empty-title').textContent='Give a choice some context.';$('empty-copy').textContent='Run an example or write your own situation. JEV’s answer and option probabilities will appear here.';$('result-status').textContent='Ready when you are';$('result-latency').textContent='—'}
|
| 75 |
function updateKind(){const kind=$('kind').value;$('options').readOnly=kind!=='choice';$('options-label').textContent=kind==='choice'?'One per line':'Fixed choices';$('kind-help').textContent=kind==='choice'?'Compare the choices you provide.':kind==='noul'?'Judge whether the statement is true.':'Use your question to define what the score means.';if(kind==='noul')$('options').value='false\ntrue';else if(kind==='score')$('options').value='0\n1\n2\n3\n4\n5';else $('options').value=customOptions}
|
| 76 |
+
|
| 77 |
+
function syncImageControls(){
|
| 78 |
+
$('image-drop').setAttribute('aria-disabled',String(decisionBusy));
|
| 79 |
+
$('image-input').disabled=decisionBusy;
|
| 80 |
+
$('image-input').hidden=Boolean(attachedImage);
|
| 81 |
+
$('remove-image').disabled=decisionBusy;
|
| 82 |
+
$('run-decision').disabled=decisionBusy||imageLoading;
|
| 83 |
+
$('state').required=!attachedImage;
|
| 84 |
+
$('state-hint').textContent=attachedImage?'Text context is optional':'Text or image context';
|
| 85 |
+
$('decision-form').setAttribute('aria-busy',String(decisionBusy||imageLoading));
|
| 86 |
+
}
|
| 87 |
+
function clearImage(){
|
| 88 |
+
imageLoadVersion++;
|
| 89 |
+
if(imageReader?.readyState===1)imageReader.abort();
|
| 90 |
+
imageReader=null;attachedImage=null;imageLoading=false;imageDragDepth=0;
|
| 91 |
+
$('image-input').value='';
|
| 92 |
+
$('image-thumbnail').removeAttribute('src');
|
| 93 |
+
$('image-preview').hidden=true;$('image-picker').hidden=false;
|
| 94 |
+
$('image-name').textContent='';$('image-meta').textContent='';$('image-status').textContent='';
|
| 95 |
+
$('image-drop').classList.remove('is-dragging');showError('image-error','');syncImageControls();
|
| 96 |
+
}
|
| 97 |
+
function imageType(bytes){
|
| 98 |
+
if(bytes.length>=8&&[137,80,78,71,13,10,26,10].every((n,i)=>bytes[i]===n))return 'image/png';
|
| 99 |
+
if(bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255)return 'image/jpeg';
|
| 100 |
+
if(bytes.length>=12&&[82,73,70,70].every((n,i)=>bytes[i]===n)&&[87,69,66,80].every((n,i)=>bytes[i+8]===n))return 'image/webp';
|
| 101 |
+
return '';
|
| 102 |
+
}
|
| 103 |
+
function readImageData(file){
|
| 104 |
+
return new Promise((resolve,reject)=>{
|
| 105 |
+
const reader=new FileReader();imageReader=reader;
|
| 106 |
+
reader.onload=()=>typeof reader.result==='string'?resolve(reader.result):reject(new Error('This image could not be read. Choose the file again.'));
|
| 107 |
+
reader.onerror=()=>reject(new Error('This image could not be read. Choose the file again.'));
|
| 108 |
+
reader.onabort=()=>reject(new Error('Image selection was cancelled.'));
|
| 109 |
+
reader.readAsDataURL(file);
|
| 110 |
+
});
|
| 111 |
+
}
|
| 112 |
+
function decodeImage(dataUrl){
|
| 113 |
+
return new Promise((resolve,reject)=>{
|
| 114 |
+
const image=new Image();
|
| 115 |
+
image.onload=()=>{
|
| 116 |
+
if(image.naturalWidth>0&&image.naturalHeight>0)resolve({width:image.naturalWidth,height:image.naturalHeight});
|
| 117 |
+
else reject(new Error('This image could not be decoded. Choose a valid JPEG, PNG, or WebP image.'));
|
| 118 |
+
};
|
| 119 |
+
image.onerror=()=>reject(new Error('This image could not be decoded. Choose a valid JPEG, PNG, or WebP image.'));
|
| 120 |
+
image.src=dataUrl;
|
| 121 |
+
});
|
| 122 |
+
}
|
| 123 |
+
async function selectImage(files){
|
| 124 |
+
if(decisionBusy||!files||!files.length)return;
|
| 125 |
+
clearImage();resetDecision();
|
| 126 |
+
document.querySelectorAll('.preset').forEach(button=>button.setAttribute('aria-pressed','false'));
|
| 127 |
+
const version=imageLoadVersion;
|
| 128 |
+
imageLoading=true;$('image-status').textContent='Checking image…';syncImageControls();
|
| 129 |
+
try{
|
| 130 |
+
if(files.length!==1)throw new Error('Choose one image at a time. No image is attached.');
|
| 131 |
+
const file=files[0];
|
| 132 |
+
if(file.size>MAX_IMAGE_BYTES)throw new Error('This image is larger than 5 MiB. Choose a smaller image.');
|
| 133 |
+
if(!file.size)throw new Error('This file is empty. Choose a valid JPEG, PNG, or WebP image.');
|
| 134 |
+
if(file.type&&!IMAGE_TYPES.has(file.type))throw new Error('Unsupported image type. Choose a JPEG, PNG, or WebP image.');
|
| 135 |
+
const mime=imageType(new Uint8Array(await file.slice(0,12).arrayBuffer()));
|
| 136 |
+
if(version!==imageLoadVersion)return;
|
| 137 |
+
if(!mime||(file.type&&file.type!==mime))throw new Error('The file content is not a matching JPEG, PNG, or WebP image. Choose another file.');
|
| 138 |
+
const raw=await readImageData(file);
|
| 139 |
+
if(version!==imageLoadVersion)return;
|
| 140 |
+
const dataUrl=`data:${mime};base64,${raw.slice(raw.indexOf(',')+1)}`;
|
| 141 |
+
const dimensions=await decodeImage(dataUrl);
|
| 142 |
+
if(version!==imageLoadVersion)return;
|
| 143 |
+
attachedImage={dataUrl,name:file.name,size:file.size,...dimensions};
|
| 144 |
+
$('image-thumbnail').src=dataUrl;
|
| 145 |
+
$('image-name').textContent=file.name;
|
| 146 |
+
$('image-meta').textContent=`${dimensions.width} × ${dimensions.height} · ${file.size>=1024*1024?(file.size/(1024*1024)).toFixed(1)+' MiB':Math.max(1,Math.ceil(file.size/1024))+' KiB'}`;
|
| 147 |
+
$('image-preview').hidden=false;$('image-picker').hidden=true;
|
| 148 |
+
$('image-status').textContent='Image attached. You can add text for more context.';
|
| 149 |
+
}catch(error){
|
| 150 |
+
if(version!==imageLoadVersion)return;
|
| 151 |
+
$('image-status').textContent='No image attached.';
|
| 152 |
+
showError('image-error',error.message);
|
| 153 |
+
}finally{
|
| 154 |
+
if(version===imageLoadVersion){imageReader=null;imageLoading=false;syncImageControls()}
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
$('image-input').addEventListener('change',event=>selectImage(Array.from(event.target.files||[])));
|
| 158 |
+
$('remove-image').addEventListener('click',()=>{if(decisionBusy)return;clearImage();resetDecision();$('image-status').textContent='Image removed.';$('image-input').focus()});
|
| 159 |
+
$('image-drop').addEventListener('dragenter',event=>{event.preventDefault();if(decisionBusy)return;imageDragDepth++;$('image-drop').classList.add('is-dragging')});
|
| 160 |
+
$('image-drop').addEventListener('dragover',event=>{event.preventDefault();if(event.dataTransfer)event.dataTransfer.dropEffect=decisionBusy?'none':'copy'});
|
| 161 |
+
$('image-drop').addEventListener('dragleave',event=>{event.preventDefault();imageDragDepth=Math.max(0,imageDragDepth-1);if(!imageDragDepth)$('image-drop').classList.remove('is-dragging')});
|
| 162 |
+
$('image-drop').addEventListener('drop',event=>{event.preventDefault();imageDragDepth=0;$('image-drop').classList.remove('is-dragging');if(decisionBusy)return;const files=Array.from(event.dataTransfer?.files||[]);if(files.length)selectImage(files);else{clearImage();resetDecision();$('image-status').textContent='No image attached.';showError('image-error','Drop an image file from your device, or use Choose an image.')}});
|
| 163 |
+
function applyPreset(name){if(decisionBusy)return;clearImage();const p=presets[name];$('state').value=p.state;$('question').value=p.question;$('kind').value=p.kind;if(p.kind==='choice')customOptions=p.options.join('\n');updateKind();document.querySelectorAll('.preset').forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.preset===name)));resetDecision()}
|
| 164 |
document.querySelectorAll('.preset').forEach(button=>button.addEventListener('click',()=>applyPreset(button.dataset.preset)));
|
| 165 |
$('kind').addEventListener('change',()=>{updateKind();resetDecision()});$('options').addEventListener('input',()=>{if($('kind').value==='choice')customOptions=$('options').value});['state','question','options','kind'].forEach(id=>$(id).addEventListener('input',()=>document.querySelectorAll('.preset').forEach(button=>button.setAttribute('aria-pressed','false'))));
|
| 166 |
+
function setDecisionBusy(busy){decisionBusy=busy;document.querySelectorAll('#decision-form button,#decision-form input,#decision-form textarea,#decision-form select').forEach(element=>element.disabled=busy);$('run-decision').textContent=busy?'JEV is deciding…':'Run JEV →';syncImageControls()}
|
| 167 |
+
$('decision-form').addEventListener('submit',async event=>{event.preventDefault();if(decisionBusy||imageLoading)return;const kind=$('kind').value,state=$('state').value.trim(),question=$('question').value.trim(),options=$('options').value.split('\n').map(option=>option.trim()).filter(Boolean);if(!question){showError('decision-error','Add a question for JEV first.');return}if(!state&&!attachedImage){showError('decision-error','Describe a situation or attach an image first.');return}if(options.length<2||options.length>16){showError('decision-error','Provide between 2 and 16 options, one per line.');return}if(options.some(option=>option.length>180)){showError('decision-error','Keep each option to 180 characters or fewer.');return}if(new Set(options).size!==options.length){showError('decision-error','Give each option a different name.');return}showError('decision-error','');setDecisionBusy(true);$('decision-result').hidden=true;$('empty-output').hidden=false;$('empty-output').classList.add('loading');$('empty-title').textContent='Weighing the options.';$('empty-copy').textContent=attachedImage?'Reading your image, then JEV will compare the available choices.':'JEV is reading your situation and comparing the available choices.';$('result-status').textContent='Inference in progress';$('result-latency').textContent='—';try{const payload={kind,state,question,options};if(attachedImage)payload.image=attachedImage.dataUrl;const data=await api('/api/decide',payload);const result=decisionData(data,options);if(!result.labels.length||result.choiceLabel==='No decision returned')throw new Error('The model did not return a valid decision. Please try again.');connected(data.model);renderVision(data.vision);$('picked-answer').textContent=result.choiceLabel;renderBars($('probabilities'),result);$('empty-output').hidden=true;$('decision-result').hidden=false;$('result-status').textContent='Decision received';$('result-latency').textContent=formatTime(data.elapsed_seconds);$('result-model').textContent=typeof data.model==='string'?data.model:modelName}catch(error){showError('decision-error',error.message);$('empty-title').textContent='The decision did not arrive.';$('empty-copy').textContent='Your inputs are still here. Try again when the service is ready.';$('result-status').textContent='Request unsuccessful';checkHealth()}finally{$('empty-output').classList.remove('loading');setDecisionBusy(false)}});
|
| 168 |
function updateGameControls(){const disabled=gameBusy||autoPlaying||!sessionId||gameDone;$('new-game').disabled=gameBusy||autoPlaying;$('scenario').disabled=gameBusy||autoPlaying;$('jev-move').disabled=disabled;$('auto-play').disabled=disabled;document.querySelectorAll('#manual-actions button').forEach(button=>button.disabled=disabled);$('stop-auto').hidden=!autoPlaying;$('stop-auto').disabled=stopRequested;$('auto-play').textContent=autoPlaying?'Auto-play running…':'Auto-play · 10 steps';$('new-game').textContent=sessionId?'Reset arena ↻':'Start arena ▷'}
|
| 169 |
function renderActions(nextActions){if(!Array.isArray(nextActions))return;actions=nextActions;$('manual-actions').replaceChildren();for(const action of actions){if(!action||typeof action.id!=='string')continue;const button=document.createElement('button');button.type='button';button.className='action-button';button.textContent=String(action.label||action.id);button.title=String(action.description||action.label||action.id);button.addEventListener('click',()=>runGameStep('human',action.id));$('manual-actions').append(button)}$('no-actions').hidden=actions.length>0;updateGameControls()}
|
| 170 |
function updateSnapshot(data){if(typeof data.session_id==='string')sessionId=data.session_id;gameDone=Boolean(data.done);if(typeof data.frame==='string'&&data.frame){const frame=data.frame.startsWith('data:image/png;base64,')?data.frame:`data:image/png;base64,${data.frame}`;$('game-frame').src=frame;$('game-frame').hidden=false;$('game-placeholder').hidden=true;$('game-frame').alt=`Doom ${data.scenario==='defend_the_center'?'center defense':'target practice'}; step ${data.metrics?.steps??'unknown'}`}if(data.actions)renderActions(data.actions);for(const key of ['health','ammo','kills','reward','steps']){const value=data.metrics?.[key];$('metric-'+key).textContent=typeof value==='number'&&Number.isFinite(value)?Number.isInteger(value)?String(value):value.toFixed(1):'—'}$('frame-badge').hidden=!sessionId;$('frame-badge').textContent=gameDone?'Episode complete':'Simulation paused';if(data.decision){const decision=data.decision;connected(decision.model);const results=decisionData(decision,actions.map(action=>action.id));const labels=results.labels.map(label=>actions.find(action=>action.id===label||action.description===label)?.label||label);const selectedId=results.choiceLabel;results.labels=labels;results.choiceLabel=actions.find(action=>action.id===selectedId||action.description===selectedId)?.label||selectedId;$('game-picked').textContent=results.choiceLabel;renderBars($('game-probabilities'),results);$('game-decision-empty').hidden=true;$('game-decision').hidden=false;gameModel=typeof decision.model==='string'?decision.model:typeof data.model==='string'?data.model:modelName;$('game-latency').textContent=formatTime(decision.elapsed_seconds);$('game-decision-meta').textContent=`${gameModel} · Timing reported by the model server`;}updateGameControls()}
|