JEV-27B-Demo / play.html
uremia's picture
Update play.html
1a929c7 verified
Raw History Blame Contribute Delete
54.8 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="description" content="Explore JEV-27B in live games, text and image decisions, and Doom. Follow the model’s choices and inference time.">
<title>JEV-27B · Decision playground — AutoTrust</title>
<link rel="icon" href="/autotrust-mark.svg" type="image/svg+xml">
<style>
: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}}
/* One optional image, kept in page memory until a decision is submitted. */
.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}}
#live-games{background:#fff;border-radius:12px;overflow:hidden}.tabbar{overflow-x:auto}.tab{flex-shrink:0}.tab-tail{white-space:nowrap}@media(max-width:720px){.tabbar{gap:22px}.tab-tail{display:none}}@media(max-width:480px){.tabbar{gap:18px}.tab{gap:6px}.tab .number{display:none}}
</style>
</head>
<body>
<header class="top">
<a class="brand" href="/" aria-label="AutoTrust home"><img src="/autotrust-mark.svg" width="33" height="33" alt=""><span class="brand-name">AutoTrust</span></a>
<nav class="nav" aria-label="Main navigation"><a href="/">Demo reel</a><a class="nav-current" href="/play" aria-current="page">Decision playground</a><span class="nav-divider" aria-hidden="true"></span><a href="https://huggingface.co/autotrust/JEV-27B" target="_blank" rel="noopener noreferrer">Model ↗</a></nav>
<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>
</header>
<main>
<section class="hero" aria-labelledby="page-title"><div><p class="eyebrow">JEV-27B · Interactive demos</p><h1 id="page-title">Decision playground</h1></div><p class="hero-copy">Explore live games and decisions with text or images.<br><strong>Follow each choice and its inference time.</strong></p></section>
<div class="tabbar" role="tablist" aria-label="Playground mode">
<button class="tab" id="games-tab" type="button" role="tab" aria-selected="true" aria-controls="live-games" 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="m10 2 7 4v8l-7 4-7-4V6l7-4Zm0 8 7-4M10 10 3 6m7 4v8"/></svg>Live games</button>
<button class="tab" id="lab-tab" type="button" role="tab" aria-selected="false" aria-controls="lab" 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="M4 4h12v12H4zM7 7h6M7 10h6M7 13h3"/></svg>Decision lab</button>
<button class="tab" id="arena-tab" type="button" role="tab" aria-selected="false" aria-controls="arena" tabindex="-1"><span class="number">03</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>
<span class="tab-tail">Explore structured intelligence</span>
</div>
<section id="live-games" role="tabpanel" aria-labelledby="games-tab" tabindex="0">
<iframe id="live-games-frame" src="/arena?embed=1" title="Live game arena" style="display:block;width:100%;height:1000px;border:0;background:#fff;border-radius:12px" allow="autoplay; fullscreen"></iframe>
</section>
<section id="lab" role="tabpanel" aria-labelledby="lab-tab" tabindex="0" hidden>
<div class="lab-grid">
<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>
<form id="decision-form" class="input-body">
<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>
<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>
<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>
</div>
<div class="field"><label for="question">What should JEV decide?</label><input id="question" maxlength="1000" required autocomplete="off"></div>
<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>
<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>
</form></div>
<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>
</div>
<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>
</section>
<section id="arena" role="tabpanel" aria-labelledby="arena-tab" tabindex="0" hidden>
<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>
<div class="arena-grid"><div><div class="panel"><div class="game-screen"><img id="game-frame" alt="Current Doom arena frame" hidden><div class="game-placeholder" id="game-placeholder"><svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.3" aria-hidden="true"><circle cx="24" cy="24" r="14"/><circle cx="24" cy="24" r="5"/><path d="M24 2v12M24 34v12M2 24h12M34 24h12"/></svg><h2>Your move. Or JEV’s.</h2><p>Start an arena, try the controls, then hand the next decision to JEV.</p></div><span class="frame-badge" id="frame-badge" hidden>Simulation paused</span></div><div class="metrics" aria-label="Game metrics"><div class="metric"><span>Health</span><b id="metric-health">—</b></div><div class="metric"><span>Ammo</span><b id="metric-ammo">—</b></div><div class="metric"><span>Kills</span><b id="metric-kills">—</b></div><div class="metric"><span>Reward</span><b id="metric-reward">—</b></div><div class="metric"><span>Steps</span><b id="metric-steps">—</b></div></div></div><p class="arena-explainer"><strong>A turn-by-turn simulation.</strong> JEV reads game state; the simulation pauses between decisions.</p><p class="error" id="game-error" role="alert" hidden></p></div>
<div class="arena-sidebar"><div class="panel"><div class="panel-head"><div class="section-label"><span>01</span>Take control</div></div><div class="controls-body"><p class="controls-label">Your next move</p><div class="manual-actions" id="manual-actions"></div><p class="no-actions" id="no-actions">Start an arena to unlock its controls.</p><hr class="divider"><p class="controls-label">Let JEV decide</p><div class="agent-actions"><button class="primary" id="jev-move" type="button" disabled>JEV next move</button><button class="secondary" id="auto-play" type="button" disabled>Auto-play · 10 steps</button></div><button class="secondary stop-button" id="stop-auto" type="button" hidden>Stop after this move</button><p class="arena-note">Auto-play runs up to 10 model decisions. You can stop after the current move.</p><p class="game-status" id="game-status" role="status">No arena running.</p></div></div><div class="panel"><div class="panel-head"><div class="section-label"><span>02</span>JEV’s last decision</div></div><div class="game-result"><p class="game-decision-empty" id="game-decision-empty">Let JEV take a move to see its choice and action probabilities.</p><div id="game-decision" hidden><h3 class="picked" id="game-picked"></h3><div class="inference-timing"><span>Model inference time</span><strong id="game-latency">—</strong></div><div class="bar-list" id="game-probabilities" aria-label="Action probabilities"></div><p class="game-decision-meta" id="game-decision-meta"></p></div></div></div></div></div>
</section>
<noscript><p class="error">Enable JavaScript to use this live playground. You can still <a href="/">watch the recorded demos</a>.</p></noscript>
</main>
<footer><span><a href="https://autotrust.ai" target="_blank" rel="noopener noreferrer">autotrust.ai</a> <span aria-hidden="true"> / </span> Decisions into action.</span><div class="footer-links"><span id="footer-status">Connecting to model…</span><a href="/">Watch the demo reel ↗</a></div></footer>
<script>
'use strict';
const $=id=>document.getElementById(id);
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']}};
let decisionBusy=false,customOptions='',modelName='autotrust/JEV-27B';
const MAX_IMAGE_BYTES=5*1024*1024,IMAGE_TYPES=new Set(['image/jpeg','image/png','image/webp']);
let attachedImage=null,imageLoading=false,imageLoadVersion=0,imageReader=null,imageDragDepth=0;
let sessionId=null,gameBusy=false,gameDone=false,autoPlaying=false,stopRequested=false,actions=[],gameModel=modelName;
const tabs=[$('games-tab'),$('lab-tab'),$('arena-tab')],tabHashes=['#games','#lab','#doom'];
const liveGamesFrame=$('live-games-frame');
function setTab(index,syncHash=true){if(index<0||index>=tabs.length)index=0;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!==2&&autoPlaying){stopRequested=true;$('game-status').textContent='Stopping after the current move…'}if(syncHash&&location.hash!==tabHashes[index])history.replaceState(null,'',location.pathname+location.search+tabHashes[index])}
tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>setTab(index));tab.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight','Home','End'].includes(e.key)){e.preventDefault();const next=e.key==='Home'?0:e.key==='End'?tabs.length-1:(index+(e.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;setTab(next);tabs[next].focus()}})});
function restoreTab(){setTab(Math.max(0,tabHashes.indexOf(location.hash)))}
window.addEventListener('hashchange',restoreTab);
window.addEventListener('message',event=>{if(event.origin!==location.origin||event.source!==liveGamesFrame.contentWindow)return;const data=event.data;if(!data||data.type!=='jev-arena-size'||typeof data.height!=='number'||!Number.isFinite(data.height))return;liveGamesFrame.style.height=Math.min(6000,Math.max(320,Math.ceil(data.height)))+'px'});
restoreTab();
function showError(id,message){$(id).textContent=message||'';$(id).hidden=!message}
function serviceMessage(status,detail){if(status===429)return 'The demo is busy. Please wait a moment, then try again.';if(status===404)return 'This session has expired. Start a new arena to continue.';if(status===409)return 'This episode has finished. Reset the arena to play again.';if(status===502||status===503||status===504)return 'The model service is temporarily unavailable. Please try again shortly.';if(status===400||status===422){if(typeof detail==='string'&&!/https?:|token|key|traceback/i.test(detail))return detail.slice(0,250);return 'The request could not be accepted. Check your inputs and try again.'}return 'The request did not complete. Please try again.'}
async function api(path,body,method){const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),150000);try{const response=await fetch(path,{method:method||(body===undefined?'GET':'POST'),headers:body===undefined?{}:{'Content-Type':'application/json'},body:body===undefined?undefined:JSON.stringify(body),signal:controller.signal,cache:'no-store'});let data;try{data=await response.json()}catch{throw new Error('The service returned an unreadable response. Please try again.')}if(!response.ok){const error=new Error(serviceMessage(response.status,data.detail||data.error));error.status=response.status;throw error}return data}catch(error){if(error.name==='AbortError')throw new Error('This request took too long. Please try again shortly.');if(error instanceof TypeError)throw new Error('The demo could not reach the service. Check your connection and try again.');throw error}finally{clearTimeout(timer)}}
function connected(model){modelName=typeof model==='string'&&model?model:modelName;$('connection').dataset.status='ok';$('connection-text').textContent='Model connected';$('footer-status').textContent='Connected to JEV-27B'}
async function checkHealth(){try{const data=await api('/api/health');const online=data.status==='ok';$('connection').dataset.status=online?'ok':'unavailable';$('connection-text').textContent=online?'Model connected':'Model unavailable';$('footer-status').textContent=online?'Connected to JEV-27B':'Model temporarily unavailable';if(typeof data.model==='string'&&data.model){modelName=data.model;$('result-model').textContent=modelName}}catch{$('connection').dataset.status='unavailable';$('connection-text').textContent='Connection unavailable';$('footer-status').textContent='Connection unavailable'}}
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`:'—'}
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}}
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)})}
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:''}
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='—'}
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}
function syncImageControls(){
$('image-drop').setAttribute('aria-disabled',String(decisionBusy));
$('image-input').disabled=decisionBusy;
$('image-input').hidden=Boolean(attachedImage);
$('remove-image').disabled=decisionBusy;
$('run-decision').disabled=decisionBusy||imageLoading;
$('state').required=!attachedImage;
$('state-hint').textContent=attachedImage?'Text context is optional':'Text or image context';
$('decision-form').setAttribute('aria-busy',String(decisionBusy||imageLoading));
}
function clearImage(){
imageLoadVersion++;
if(imageReader?.readyState===1)imageReader.abort();
imageReader=null;attachedImage=null;imageLoading=false;imageDragDepth=0;
$('image-input').value='';
$('image-thumbnail').removeAttribute('src');
$('image-preview').hidden=true;$('image-picker').hidden=false;
$('image-name').textContent='';$('image-meta').textContent='';$('image-status').textContent='';
$('image-drop').classList.remove('is-dragging');showError('image-error','');syncImageControls();
}
function imageType(bytes){
if(bytes.length>=8&&[137,80,78,71,13,10,26,10].every((n,i)=>bytes[i]===n))return 'image/png';
if(bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255)return 'image/jpeg';
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';
return '';
}
function readImageData(file){
return new Promise((resolve,reject)=>{
const reader=new FileReader();imageReader=reader;
reader.onload=()=>typeof reader.result==='string'?resolve(reader.result):reject(new Error('This image could not be read. Choose the file again.'));
reader.onerror=()=>reject(new Error('This image could not be read. Choose the file again.'));
reader.onabort=()=>reject(new Error('Image selection was cancelled.'));
reader.readAsDataURL(file);
});
}
function decodeImage(dataUrl){
return new Promise((resolve,reject)=>{
const image=new Image();
image.onload=()=>{
if(image.naturalWidth>0&&image.naturalHeight>0)resolve({width:image.naturalWidth,height:image.naturalHeight});
else reject(new Error('This image could not be decoded. Choose a valid JPEG, PNG, or WebP image.'));
};
image.onerror=()=>reject(new Error('This image could not be decoded. Choose a valid JPEG, PNG, or WebP image.'));
image.src=dataUrl;
});
}
async function selectImage(files){
if(decisionBusy||!files||!files.length)return;
clearImage();resetDecision();
document.querySelectorAll('.preset').forEach(button=>button.setAttribute('aria-pressed','false'));
const version=imageLoadVersion;
imageLoading=true;$('image-status').textContent='Checking image…';syncImageControls();
try{
if(files.length!==1)throw new Error('Choose one image at a time. No image is attached.');
const file=files[0];
if(file.size>MAX_IMAGE_BYTES)throw new Error('This image is larger than 5 MiB. Choose a smaller image.');
if(!file.size)throw new Error('This file is empty. Choose a valid JPEG, PNG, or WebP image.');
if(file.type&&!IMAGE_TYPES.has(file.type))throw new Error('Unsupported image type. Choose a JPEG, PNG, or WebP image.');
const mime=imageType(new Uint8Array(await file.slice(0,12).arrayBuffer()));
if(version!==imageLoadVersion)return;
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.');
const raw=await readImageData(file);
if(version!==imageLoadVersion)return;
const dataUrl=`data:${mime};base64,${raw.slice(raw.indexOf(',')+1)}`;
const dimensions=await decodeImage(dataUrl);
if(version!==imageLoadVersion)return;
attachedImage={dataUrl,name:file.name,size:file.size,...dimensions};
$('image-thumbnail').src=dataUrl;
$('image-name').textContent=file.name;
$('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'}`;
$('image-preview').hidden=false;$('image-picker').hidden=true;
$('image-status').textContent='Image attached. You can add text for more context.';
}catch(error){
if(version!==imageLoadVersion)return;
$('image-status').textContent='No image attached.';
showError('image-error',error.message);
}finally{
if(version===imageLoadVersion){imageReader=null;imageLoading=false;syncImageControls()}
}
}
$('image-input').addEventListener('change',event=>selectImage(Array.from(event.target.files||[])));
$('remove-image').addEventListener('click',()=>{if(decisionBusy)return;clearImage();resetDecision();$('image-status').textContent='Image removed.';$('image-input').focus()});
$('image-drop').addEventListener('dragenter',event=>{event.preventDefault();if(decisionBusy)return;imageDragDepth++;$('image-drop').classList.add('is-dragging')});
$('image-drop').addEventListener('dragover',event=>{event.preventDefault();if(event.dataTransfer)event.dataTransfer.dropEffect=decisionBusy?'none':'copy'});
$('image-drop').addEventListener('dragleave',event=>{event.preventDefault();imageDragDepth=Math.max(0,imageDragDepth-1);if(!imageDragDepth)$('image-drop').classList.remove('is-dragging')});
$('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.')}});
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()}
document.querySelectorAll('.preset').forEach(button=>button.addEventListener('click',()=>applyPreset(button.dataset.preset)));
$('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'))));
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()}
$('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)}});
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 ▷'}
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()}
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()}
$('new-game').addEventListener('click',async()=>{if(gameBusy||autoPlaying)return;gameBusy=true;showError('game-error','');updateGameControls();$('game-status').textContent='Creating your arena…';try{if(sessionId){try{await api(`/api/games/${encodeURIComponent(sessionId)}`,undefined,'DELETE')}catch(error){if(error.status!==404)throw error}sessionId=null;gameDone=false;renderActions([]);$('game-frame').hidden=true;$('game-placeholder').hidden=false;$('frame-badge').hidden=true;for(const key of ['health','ammo','kills','reward','steps'])$('metric-'+key).textContent='—';$('game-decision').hidden=true;$('game-decision-empty').hidden=false}const data=await api('/api/games/new',{scenario:$('scenario').value});if(!data.session_id)throw new Error('The arena did not start. Please try again.');updateSnapshot(data);$('game-decision').hidden=true;$('game-decision-empty').hidden=false;$('game-status').textContent='Arena ready. Choose a move or let JEV decide.'}catch(error){showError('game-error',error.message);$('game-status').textContent='Could not create an arena.'}finally{gameBusy=false;updateGameControls()}});
async function runGameStep(controller,action,fromAuto=false){if(!sessionId||gameDone||gameBusy||(autoPlaying&&!fromAuto))return false;gameBusy=true;showError('game-error','');updateGameControls();if(!fromAuto)$('game-status').textContent=controller==='jev'?'JEV is choosing a move…':'Applying your move…';$('frame-badge').textContent=controller==='jev'?'JEV is deciding…':'Applying move…';try{const body={controller};if(action)body.action=action;const data=await api(`/api/games/${encodeURIComponent(sessionId)}/step`,body);updateSnapshot(data);if(gameDone)$('game-status').textContent='Episode complete. Reset the arena to play again.';else if(!fromAuto)$('game-status').textContent=controller==='jev'?'JEV’s move is complete. Your turn to choose.':'Move complete. The simulation is paused.';return true}catch(error){showError('game-error',error.message);$('game-status').textContent='Move unsuccessful. Try again or reset the arena.';if(error.status===404){sessionId=null;gameDone=true;$('frame-badge').textContent='Session expired'}else if(error.status===409){gameDone=true;$('frame-badge').textContent='Episode complete'}else $('frame-badge').textContent='Simulation paused';return false}finally{gameBusy=false;updateGameControls()}}
$('jev-move').addEventListener('click',()=>runGameStep('jev'));
$('auto-play').addEventListener('click',async()=>{if(autoPlaying||gameBusy||!sessionId||gameDone)return;autoPlaying=true;stopRequested=false;updateGameControls();let completed=0,failed=false;try{for(let i=0;i<10;i++){if(stopRequested||gameDone)break;$('game-status').textContent=`Auto-play: JEV is choosing move ${i+1} of 10…`;const success=await runGameStep('jev',undefined,true);if(!success){failed=true;break}completed++;await new Promise(resolve=>setTimeout(resolve,200))}}finally{autoPlaying=false;updateGameControls();if(gameDone)$('game-status').textContent=`Episode complete after ${completed} auto-play ${completed===1?'move':'moves'}.`;else if(!failed)$('game-status').textContent=`Auto-play ${stopRequested?'stopped':'complete'} · ${completed} ${completed===1?'move':'moves'}. The simulation is paused.`;stopRequested=false}});
$('stop-auto').addEventListener('click',()=>{stopRequested=true;$('game-status').textContent='Stopping after the current move…';updateGameControls()});
window.addEventListener('pagehide',()=>{if(sessionId)fetch(`/api/games/${encodeURIComponent(sessionId)}`,{method:'DELETE',keepalive:true}).catch(()=>{})});
applyPreset('support');checkHealth();
</script>
</body>
</html>