CoolFace
Apppublic

vedkdev/FlakyTestSleuthOpenEnvRL

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
ui.py971 linesDownload Raw Back to server
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