vedkdev/FlakyTestSleuthOpenEnvRL
0
1from __future__ import annotations2 3 4def render_home_page() -> str:5 return """6<!doctype html>7<html lang="en">8<head>9 <meta charset="utf-8" />10 <meta name="viewport" content="width=device-width, initial-scale=1" />11 <title>FlakyGym Control Center</title>12 <link rel="preconnect" href="https://fonts.googleapis.com" />13 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />14 <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />15 <style>16 :root {17 --bg-top: #edf7f1;18 --bg-bottom: #d2ead8;19 --ink: #17211d;20 --muted: #4c6359;21 --accent: #0f8b63;22 --accent-2: #e5783b;23 --panel: rgba(255, 255, 255, 0.86);24 --border: rgba(15, 139, 99, 0.22);25 --card-shadow: 0 22px 46px rgba(14, 51, 37, 0.16);26 --display: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif;27 --mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;28 }29 30 * {31 box-sizing: border-box;32 }33 34 body {35 margin: 0;36 color: var(--ink);37 font-family: var(--display);38 min-height: 100vh;39 background:40 radial-gradient(circle at 12% 16%, rgba(230, 120, 59, 0.16), transparent 42%),41 radial-gradient(circle at 86% 12%, rgba(15, 139, 99, 0.2), transparent 40%),42 linear-gradient(164deg, var(--bg-top), var(--bg-bottom));43 animation: backdropFade 700ms ease-out;44 }45 46 @keyframes backdropFade {47 from { opacity: 0; transform: translateY(4px); }48 to { opacity: 1; transform: translateY(0); }49 }50 51 .shell {52 max-width: 1100px;53 margin: 24px auto;54 padding: 0 16px 24px;55 display: grid;56 gap: 16px;57 }58 59 .hero {60 border: 1px solid var(--border);61 background: var(--panel);62 border-radius: 20px;63 box-shadow: var(--card-shadow);64 padding: 22px 22px 18px;65 animation: slideIn 500ms ease-out;66 }67 68 @keyframes slideIn {69 from { opacity: 0; transform: translateY(12px); }70 to { opacity: 1; transform: translateY(0); }71 }72 73 .eyebrow {74 display: inline-flex;75 align-items: center;76 gap: 8px;77 font-size: 12px;78 color: var(--muted);79 letter-spacing: 0.08em;80 text-transform: uppercase;81 }82 83 .dot {84 width: 10px;85 height: 10px;86 border-radius: 50%;87 background: var(--accent);88 box-shadow: 0 0 0 6px rgba(15, 139, 99, 0.15);89 }90 91 h1 {92 margin: 10px 0 8px;93 font-size: clamp(1.6rem, 2.6vw, 2.35rem);94 line-height: 1.1;95 letter-spacing: -0.02em;96 }97 98 .hero p {99 margin: 0;100 color: var(--muted);101 max-width: 760px;102 line-height: 1.5;103 }104 105 .panel-grid {106 display: grid;107 grid-template-columns: 1fr;108 gap: 16px;109 }110 111 .panel {112 border: 1px solid var(--border);113 background: var(--panel);114 border-radius: 20px;115 box-shadow: var(--card-shadow);116 padding: 18px;117 animation: slideIn 560ms ease-out;118 }119 120 .panel h2 {121 margin: 0 0 12px;122 font-size: 1.1rem;123 letter-spacing: -0.01em;124 }125 126 .brief-grid {127 display: grid;128 grid-template-columns: repeat(2, minmax(0, 1fr));129 gap: 12px;130 }131 132 .brief-card {133 border: 1px solid rgba(15, 139, 99, 0.22);134 border-radius: 12px;135 background: rgba(255, 255, 255, 0.84);136 padding: 10px;137 display: grid;138 gap: 8px;139 }140 141 .brief-card h3 {142 margin: 0;143 font-size: 0.95rem;144 letter-spacing: -0.01em;145 }146 147 .brief-card p {148 margin: 0;149 font-size: 12px;150 color: #36564a;151 line-height: 1.45;152 }153 154 .brief-list {155 margin: 0;156 padding-left: 16px;157 font-size: 12px;158 color: #2f4f43;159 line-height: 1.45;160 display: grid;161 gap: 5px;162 }163 164 .header-chips {165 display: flex;166 flex-wrap: wrap;167 gap: 6px;168 }169 170 .header-chips code {171 background: #eef8f3;172 border: 1px solid rgba(15, 139, 99, 0.26);173 border-radius: 999px;174 padding: 4px 8px;175 font: 500 11px/1.1 var(--mono);176 color: #20443a;177 white-space: nowrap;178 }179 180 .form-grid {181 display: grid;182 gap: 12px;183 grid-template-columns: repeat(2, minmax(0, 1fr));184 }185 186 .field {187 display: grid;188 gap: 6px;189 }190 191 .field.span-2 {192 grid-column: span 2;193 }194 195 label {196 font-size: 13px;197 color: var(--muted);198 }199 200 input, select {201 width: 100%;202 border: 1px solid rgba(18, 88, 63, 0.22);203 border-radius: 10px;204 padding: 10px 11px;205 font: 500 14px/1.2 var(--mono);206 color: var(--ink);207 background: rgba(255, 255, 255, 0.92);208 transition: border-color 180ms ease, box-shadow 180ms ease;209 }210 211 input:focus, select:focus {212 outline: none;213 border-color: var(--accent);214 box-shadow: 0 0 0 4px rgba(15, 139, 99, 0.14);215 }216 217 .task-picker {218 display: grid;219 gap: 8px;220 padding: 10px;221 border-radius: 12px;222 border: 1px solid rgba(15, 139, 99, 0.22);223 background: rgba(255, 255, 255, 0.85);224 }225 226 .task-picker-row {227 display: grid;228 grid-template-columns: 1fr auto;229 gap: 8px;230 }231 232 .btn-add-task {233 background: #1c6f50;234 color: #fff;235 padding: 0 14px;236 }237 238 .task-chips {239 display: flex;240 gap: 8px;241 flex-wrap: wrap;242 min-height: 32px;243 align-items: center;244 }245 246 .task-chip {247 display: inline-flex;248 align-items: center;249 gap: 8px;250 border: 1px solid rgba(20, 67, 50, 0.32);251 border-radius: 999px;252 background: #fff8dc;253 color: #2a362f;254 padding: 6px 10px;255 font: 600 12px/1 var(--display);256 }257 258 .task-chip button {259 border: 0;260 background: transparent;261 color: #4a2a12;262 padding: 0;263 border-radius: 0;264 min-width: unset;265 font: 700 12px/1 var(--display);266 line-height: 1;267 cursor: pointer;268 }269 270 .task-chip button:hover {271 transform: none;272 filter: brightness(0.92);273 }274 275 .task-empty {276 font: 500 12px/1.2 var(--mono);277 color: #52695f;278 }279 280 .field-note {281 margin: 0;282 color: #4e655b;283 font-size: 12px;284 line-height: 1.4;285 }286 287 .slider-wrap {288 display: grid;289 gap: 8px;290 }291 292 .slider-value-row {293 display: flex;294 justify-content: space-between;295 gap: 10px;296 font: 500 12px/1.3 var(--mono);297 color: #3c5950;298 }299 300 input[type="range"] {301 width: 100%;302 padding: 0;303 accent-color: var(--accent);304 cursor: pointer;305 }306 307 .eta-box {308 border: 1px solid rgba(15, 139, 99, 0.22);309 border-radius: 12px;310 background: rgba(255, 255, 255, 0.85);311 padding: 10px;312 display: grid;313 gap: 6px;314 }315 316 .eta-value {317 font: 700 17px/1 var(--display);318 letter-spacing: -0.01em;319 }320 321 .eta-warning {322 font: 500 12px/1.45 var(--mono);323 border-radius: 8px;324 padding: 6px 8px;325 display: none;326 }327 328 .eta-warning.warn {329 display: block;330 color: #752f1b;331 background: #fff0d7;332 border: 1px solid rgba(213, 114, 65, 0.45);333 }334 335 .eta-warning.ok {336 display: block;337 color: #20443a;338 background: #e8f7ef;339 border: 1px solid rgba(15, 139, 99, 0.28);340 }341 342 .actions {343 margin-top: 6px;344 display: flex;345 flex-wrap: wrap;346 gap: 10px;347 }348 349 button {350 border: 0;351 border-radius: 11px;352 font: 600 14px/1 var(--display);353 padding: 11px 14px;354 cursor: pointer;355 transition: transform 180ms ease, opacity 180ms ease, filter 180ms ease;356 }357 358 button:hover {359 transform: translateY(-1px);360 }361 362 button:disabled {363 opacity: 0.55;364 cursor: not-allowed;365 transform: none;366 }367 368 .btn-run {369 background: var(--accent);370 color: #fff;371 }372 373 .btn-stop {374 background: #f2b38f;375 color: #431d05;376 }377 378 .btn-docs {379 background: #dce8e1;380 color: #234437;381 text-decoration: none;382 display: inline-flex;383 align-items: center;384 border-radius: 11px;385 padding: 11px 14px;386 font: 600 14px/1 var(--display);387 }388 389 .status-row {390 display: grid;391 grid-template-columns: 1fr;392 gap: 10px;393 }394 395 .pill {396 display: inline-flex;397 align-items: center;398 gap: 8px;399 width: fit-content;400 border-radius: 999px;401 padding: 6px 11px;402 font: 600 13px/1 var(--display);403 background: #e1ece6;404 color: #2d4b3e;405 }406 407 .pill .status-dot {408 width: 8px;409 height: 8px;410 border-radius: 50%;411 background: #60756a;412 }413 414 .pill.running .status-dot {415 background: var(--accent);416 box-shadow: 0 0 0 7px rgba(15, 139, 99, 0.12);417 }418 419 .pill.failed .status-dot {420 background: #af4020;421 }422 423 .pill.completed .status-dot {424 background: #1d724e;425 }426 427 .pill.stopped .status-dot {428 background: var(--accent-2);429 }430 431 .meta {432 display: grid;433 grid-template-columns: repeat(2, minmax(0, 1fr));434 gap: 8px;435 font: 500 12px/1.4 var(--mono);436 color: #2f4f43;437 }438 439 .meta strong {440 color: #193428;441 }442 443 .log-wrap {444 margin-top: 8px;445 border-radius: 14px;446 border: 1px solid rgba(20, 66, 50, 0.2);447 overflow: hidden;448 background: #0f1a16;449 }450 451 .log-head {452 display: flex;453 justify-content: space-between;454 align-items: center;455 color: #b7d5c8;456 font: 500 12px/1 var(--mono);457 padding: 10px 12px;458 border-bottom: 1px solid rgba(170, 208, 193, 0.16);459 background: #13201b;460 }461 462 pre {463 margin: 0;464 padding: 12px;465 color: #d8f3e7;466 font: 400 12.5px/1.45 var(--mono);467 max-height: 360px;468 overflow: auto;469 white-space: pre-wrap;470 word-break: break-word;471 }472 473 .help {474 margin-top: 8px;475 color: #37594b;476 font-size: 12px;477 line-height: 1.45;478 }479 480 @media (max-width: 880px) {481 .form-grid {482 grid-template-columns: 1fr;483 }484 485 .brief-grid {486 grid-template-columns: 1fr;487 }488 489 .field.span-2 {490 grid-column: span 1;491 }492 493 .meta {494 grid-template-columns: 1fr;495 }496 }497 </style>498</head>499<body>500 <main class="shell">501 <section class="hero">502 <span class="eyebrow"><span class="dot"></span>FlakyGym Space</span>503 <h1>FlakyGym Control Center</h1>504 <p>This console runs flaky-test benchmark episodes and streams live logs. Use it to configure runs, estimate runtime, and review grader outcomes quickly.</p>505 </section>506 507 <section class="panel-grid">508 <div class="panel">509 <h2>Quick Brief: Dataset + Graders</h2>510 <div class="brief-grid">511 <div class="brief-card">512 <h3>Dataset: <code>dataset/py_tasks.csv</code></h3>513 <p>Each row is one flaky-test investigation task created from <code>py-data.csv</code> (repo + SHA + target test + labels + optional known fix diff).</p>514 <p class="field-note">Headers:</p>515 <div class="header-chips">516 <code>repo_url</code><code>sha</code><code>test_name</code><code>test_file</code>517 <code>category</code><code>label</code><code>status</code><code>pr_link</code>518 <code>task_types</code><code>test_code</code><code>known_fix_diff</code>519 </div>520 </div>521 522 <div class="brief-card">523 <h3>3 Graders (short)</h3>524 <ul class="brief-list">525 <li><strong>Task 1 (`classify`):</strong> exact-match flaky vs stable.</li>526 <li><strong>Task 2 (`root_cause`):</strong> category similarity matrix (partial credit allowed).</li>527 <li><strong>Task 3 (`fix_proposal`):</strong> weighted score from pattern match, patch applicability, and LLM judge.</li>528 </ul>529 </div>530 </div>531 </div>532 533 <div class="panel">534 <h2>Run Configuration</h2>535 <form id="run-form" class="form-grid">536 <div class="field span-2">537 <label for="dataset_path">Dataset Path</label>538 <input id="dataset_path" name="dataset_path" value="dataset/py_tasks.csv" />539 </div>540 541 <div class="field">542 <label for="episodes_per_task">Episodes Per Task</label>543 <div class="slider-wrap">544 <input id="episodes_per_task" name="episodes_per_task" type="range" min="1" max="100" step="1" value="1" />545 <div class="slider-value-row">546 <span><strong id="episodes_per_task_value">1</strong> episode(s)</span>547 <span>1-100</span>548 </div>549 </div>550 </div>551 552 <div class="field">553 <label for="max_steps">Max Steps</label>554 <div class="slider-wrap">555 <input id="max_steps" name="max_steps" type="range" min="1" max="100" step="1" value="20" />556 <div class="slider-value-row">557 <span><strong id="max_steps_value">20</strong> step(s)</span>558 <span>1-100</span>559 </div>560 </div>561 </div>562 563 <div class="field span-2">564 <label for="task-type-select">Task Types</label>565 <div class="task-picker">566 <div id="task-chips" class="task-chips"></div>567 <div class="task-picker-row">568 <select id="task-type-select">569 <option value="">Choose task type</option>570 </select>571 <button id="btn-add-task" class="btn-add-task" type="button">Add</button>572 </div>573 <input id="task_types" name="task_types" type="hidden" value="classify,root_cause,fix_proposal" />574 </div>575 <p class="field-note">Add from dropdown, remove with <code>x</code> on each chip.</p>576 </div>577 578 <div class="field span-2">579 <label for="benchmark_name">Benchmark Label</label>580 <input id="benchmark_name" name="benchmark_name" value="flakysleuth" />581 </div>582 583 <div class="field span-2">584 <label>Runtime ETA</label>585 <div class="eta-box">586 <div class="eta-value" id="eta-value">~09m 00s</div>587 <p class="field-note" id="eta-detail">3 task(s) × 1 episode(s) × 180s/episode</p>588 <div class="eta-warning" id="eta-warning"></div>589 </div>590 </div>591 592 <div class="field span-2">593 <label for="api_base_url">API Base URL (optional)</label>594 <input id="api_base_url" name="api_base_url" placeholder="https://api.openai.com/v1 or provider endpoint" />595 </div>596 597 <div class="field">598 <label for="model_name">Model Name (optional)</label>599 <input id="model_name" name="model_name" placeholder="gpt-4o-mini, qwen/qwen3.6-plus:free, etc." />600 </div>601 602 <div class="field">603 <label for="api_key">API Key (optional)</label>604 <input id="api_key" name="api_key" type="password" placeholder="Uses server env vars if empty" />605 </div>606 </form>607 608 <div class="actions">609 <button id="btn-run" class="btn-run" type="button">Start Inference</button>610 <button id="btn-stop" class="btn-stop" type="button">Stop Run</button>611 <a class="btn-docs" href="/docs" target="_blank" rel="noreferrer">Open API Docs</a>612 </div>613 <p class="help">Tip: if no API key is provided, <code>inference.py</code> falls back to its heuristic agent.</p>614 </div>615 616 <div class="panel">617 <h2>Run Status</h2>618 <div class="status-row">619 <div id="status-pill" class="pill"><span class="status-dot"></span><span id="status-text">idle</span></div>620 <div class="meta">621 <div><strong>Job ID:</strong> <span id="meta-job-id">-</span></div>622 <div><strong>Return Code:</strong> <span id="meta-return-code">-</span></div>623 <div><strong>Started:</strong> <span id="meta-started">-</span></div>624 <div><strong>Finished:</strong> <span id="meta-finished">-</span></div>625 </div>626 <div class="log-wrap">627 <div class="log-head">628 <span>Live Logs</span>629 <span id="log-count">0 lines</span>630 </div>631 <pre id="log-output">No run started yet.</pre>632 </div>633 <div class="help" id="summary-line"></div>634 </div>635 </div>636 </section>637 </main>638 639 <script>640 const form = document.getElementById("run-form");641 const runButton = document.getElementById("btn-run");642 const stopButton = document.getElementById("btn-stop");643 const statusPill = document.getElementById("status-pill");644 const statusText = document.getElementById("status-text");645 const jobIdEl = document.getElementById("meta-job-id");646 const returnCodeEl = document.getElementById("meta-return-code");647 const startedEl = document.getElementById("meta-started");648 const finishedEl = document.getElementById("meta-finished");649 const logEl = document.getElementById("log-output");650 const logCountEl = document.getElementById("log-count");651 const summaryEl = document.getElementById("summary-line");652 const taskInput = document.getElementById("task_types");653 const taskChipsEl = document.getElementById("task-chips");654 const taskSelectEl = document.getElementById("task-type-select");655 const taskAddButton = document.getElementById("btn-add-task");656 const episodesInput = document.getElementById("episodes_per_task");657 const episodesValueEl = document.getElementById("episodes_per_task_value");658 const maxStepsInput = document.getElementById("max_steps");659 const maxStepsValueEl = document.getElementById("max_steps_value");660 const etaValueEl = document.getElementById("eta-value");661 const etaDetailEl = document.getElementById("eta-detail");662 const etaWarningEl = document.getElementById("eta-warning");663 664 const TASK_TYPE_ORDER = ["classify", "root_cause", "fix_proposal"];665 const TASK_TYPE_LABELS = {666 classify: "Classify",667 root_cause: "Root Cause",668 fix_proposal: "Fix Proposal",669 };670 const ETA_SECONDS_PER_EPISODE = 180;671 const HACKATHON_LIMIT_SECONDS = 20 * 60;672 673 function clampInt(raw, min, max, fallback) {674 const num = Number(raw);675 if (!Number.isFinite(num)) return fallback;676 return Math.max(min, Math.min(max, Math.trunc(num)));677 }678 679 function formatDuration(totalSeconds) {680 const seconds = Math.max(0, Math.round(totalSeconds));681 const mins = Math.floor(seconds / 60);682 const secs = seconds % 60;683 if (mins >= 60) {684 const hrs = Math.floor(mins / 60);685 const remMins = mins % 60;686 return `${hrs}h ${String(remMins).padStart(2, "0")}m ${String(secs).padStart(2, "0")}s`;687 }688 return `${String(mins).padStart(2, "0")}m ${String(secs).padStart(2, "0")}s`;689 }690 691 function refreshSliderValues() {692 const episodes = clampInt(episodesInput.value, 1, 100, 1);693 const maxSteps = clampInt(maxStepsInput.value, 1, 100, 20);694 episodesInput.value = String(episodes);695 maxStepsInput.value = String(maxSteps);696 episodesValueEl.textContent = String(episodes);697 maxStepsValueEl.textContent = String(maxSteps);698 }699 700 function parseTaskTypes(raw) {701 const tokens = String(raw || "")702 .split(",")703 .map((token) => token.trim())704 .filter(Boolean);705 const unique = [];706 for (const token of tokens) {707 if (TASK_TYPE_ORDER.includes(token) && !unique.includes(token)) {708 unique.push(token);709 }710 }711 return unique;712 }713 714 let selectedTaskTypes = parseTaskTypes(taskInput.value);715 if (!selectedTaskTypes.length) {716 selectedTaskTypes = [...TASK_TYPE_ORDER];717 }718 719 function renderTaskSelect() {720 taskSelectEl.innerHTML = "";721 722 const placeholder = document.createElement("option");723 placeholder.value = "";724 placeholder.textContent = "Choose task type";725 placeholder.selected = true;726 taskSelectEl.appendChild(placeholder);727 728 for (const taskType of TASK_TYPE_ORDER) {729 if (selectedTaskTypes.includes(taskType)) continue;730 const option = document.createElement("option");731 option.value = taskType;732 option.textContent = TASK_TYPE_LABELS[taskType];733 taskSelectEl.appendChild(option);734 }735 736 const hasChoices = taskSelectEl.options.length > 1;737 taskSelectEl.disabled = !hasChoices;738 taskAddButton.disabled = !hasChoices;739 }740 741 function renderTaskChips() {742 taskChipsEl.innerHTML = "";743 744 if (!selectedTaskTypes.length) {745 const hint = document.createElement("span");746 hint.className = "task-empty";747 hint.textContent = "No task selected yet.";748 taskChipsEl.appendChild(hint);749 return;750 }751 752 for (const taskType of selectedTaskTypes) {753 const chip = document.createElement("span");754 chip.className = "task-chip";755 756 const chipText = document.createElement("span");757 chipText.textContent = TASK_TYPE_LABELS[taskType] || taskType;758 759 const chipRemove = document.createElement("button");760 chipRemove.type = "button";761 chipRemove.textContent = "x";762 chipRemove.setAttribute("aria-label", `Remove ${chipText.textContent}`);763 chipRemove.addEventListener("click", () => {764 selectedTaskTypes = selectedTaskTypes.filter((value) => value !== taskType);765 syncTaskTypes();766 });767 768 chip.appendChild(chipText);769 chip.appendChild(chipRemove);770 taskChipsEl.appendChild(chip);771 }772 }773 774 function syncTaskTypes() {775 taskInput.value = selectedTaskTypes.join(",");776 renderTaskChips();777 renderTaskSelect();778 updateRuntimeEstimate();779 }780 781 function addSelectedTaskType() {782 const selected = taskSelectEl.value.trim();783 if (!selected) return;784 if (!selectedTaskTypes.includes(selected)) {785 selectedTaskTypes.push(selected);786 }787 syncTaskTypes();788 }789 790 function updateRuntimeEstimate() {791 const episodes = clampInt(episodesInput.value, 1, 100, 1);792 const maxSteps = clampInt(maxStepsInput.value, 1, 100, 20);793 const taskCount = selectedTaskTypes.length;794 const totalEpisodes = taskCount * episodes;795 const etaSeconds = totalEpisodes * ETA_SECONDS_PER_EPISODE;796 797 etaValueEl.textContent = `~${formatDuration(etaSeconds)}`;798 etaDetailEl.textContent =799 `${taskCount} task(s) × ${episodes} episode(s) × ${ETA_SECONDS_PER_EPISODE}s/episode`;800 801 const notes = [];802 if (episodes > 2) {803 notes.push("Recommended: keep episodes per task at 1-2 for faster hackathon runs.");804 }805 if (etaSeconds > HACKATHON_LIMIT_SECONDS) {806 notes.push("Warning: ETA exceeds 20 minutes, which may violate hackathon runtime guidance.");807 }808 if (maxSteps > 20) {809 notes.push("Higher max steps can increase runtime beyond this ETA estimate.");810 }811 if (taskCount === 0) {812 notes.push("Add at least one task chip to run inference.");813 }814 815 etaWarningEl.classList.remove("warn", "ok");816 if (!notes.length) {817 etaWarningEl.textContent = "Runtime looks within limits for a quick benchmark run.";818 etaWarningEl.classList.add("ok");819 return;820 }821 822 etaWarningEl.textContent = notes.join(" ");823 if (etaSeconds > HACKATHON_LIMIT_SECONDS || episodes > 2 || taskCount === 0) {824 etaWarningEl.classList.add("warn");825 } else {826 etaWarningEl.classList.add("ok");827 }828 }829 830 function readFormPayload() {831 const episodes = clampInt(episodesInput.value, 1, 100, 1);832 const maxSteps = clampInt(maxStepsInput.value, 1, 100, 20);833 episodesInput.value = String(episodes);834 maxStepsInput.value = String(maxSteps);835 refreshSliderValues();836 updateRuntimeEstimate();837 return {838 dataset_path: form.dataset_path.value.trim(),839 episodes_per_task: episodes,840 task_types: form.task_types.value.trim(),841 max_steps: maxSteps,842 benchmark_name: form.benchmark_name.value.trim(),843 api_base_url: form.api_base_url.value.trim() || null,844 model_name: form.model_name.value.trim() || null,845 api_key: form.api_key.value.trim() || null,846 };847 }848 849 function formatTime(epoch) {850 if (!epoch) return "-";851 try {852 return new Date(epoch * 1000).toLocaleString();853 } catch (_) {854 return "-";855 }856 }857 858 function setStatus(status) {859 const normalized = (status || "idle").toLowerCase();860 statusPill.classList.remove("running", "failed", "completed", "stopped");861 if (["running", "failed", "completed", "stopped"].includes(normalized)) {862 statusPill.classList.add(normalized);863 }864 statusText.textContent = normalized;865 runButton.disabled = normalized === "running" || normalized === "starting";866 stopButton.disabled = !(normalized === "running" || normalized === "starting");867 }868 869 function renderSummary(summaries) {870 if (!Array.isArray(summaries) || summaries.length === 0) {871 summaryEl.textContent = "";872 return;873 }874 const last = summaries[summaries.length - 1];875 summaryEl.textContent = `Latest episode: success=${last.success} score=${last.score} steps=${last.steps}`;876 }877 878 function renderStatus(state) {879 setStatus(state.status || "idle");880 jobIdEl.textContent = state.job_id || "-";881 returnCodeEl.textContent = state.return_code === null || state.return_code === undefined ? "-" : String(state.return_code);882 startedEl.textContent = formatTime(state.started_at);883 finishedEl.textContent = formatTime(state.finished_at);884 885 const logs = Array.isArray(state.logs) ? state.logs : [];886 logCountEl.textContent = `${logs.length} lines`;887 logEl.textContent = logs.length ? logs.join("\\n") : "No logs yet.";888 logEl.scrollTop = logEl.scrollHeight;889 890 renderSummary(state.summaries || []);891 }892 893 async function fetchStatus() {894 try {895 const response = await fetch("/web/inference/status?tail=450", { method: "GET" });896 if (!response.ok) return;897 const state = await response.json();898 renderStatus(state);899 } catch (_) {}900 }901 902 async function startRun() {903 runButton.disabled = true;904 try {905 if (!selectedTaskTypes.length) {906 alert("Please add at least one task type.");907 return;908 }909 const response = await fetch("/web/inference/start", {910 method: "POST",911 headers: { "Content-Type": "application/json" },912 body: JSON.stringify(readFormPayload()),913 });914 915 const payload = await response.json();916 if (!response.ok) {917 const detail = typeof payload.detail === "string" ? payload.detail : "Could not start inference.";918 alert(detail);919 return;920 }921 renderStatus(payload);922 } catch (_) {923 alert("Could not start inference. Check logs and try again.");924 } finally {925 form.api_key.value = "";926 const isActive = ["running", "starting"].includes((statusText.textContent || "").toLowerCase());927 if (!isActive) {928 runButton.disabled = false;929 }930 }931 }932 933 async function stopRun() {934 stopButton.disabled = true;935 try {936 const response = await fetch("/web/inference/stop", { method: "POST" });937 if (!response.ok) return;938 const state = await response.json();939 renderStatus(state);940 } catch (_) {}941 }942 943 runButton.addEventListener("click", startRun);944 stopButton.addEventListener("click", stopRun);945 taskAddButton.addEventListener("click", addSelectedTaskType);946 taskSelectEl.addEventListener("keydown", (event) => {947 if (event.key === "Enter") {948 event.preventDefault();949 addSelectedTaskType();950 }951 });952 episodesInput.addEventListener("input", () => {953 refreshSliderValues();954 updateRuntimeEstimate();955 });956 maxStepsInput.addEventListener("input", () => {957 refreshSliderValues();958 updateRuntimeEstimate();959 });960 961 refreshSliderValues();962 syncTaskTypes();963 updateRuntimeEstimate();964 965 fetchStatus();966 window.setInterval(fetchStatus, 2200);967 </script>968</body>969</html>970"""971 