async function fetchJson(url, options = {}) { const response = await fetch(url, options); const contentType = response.headers.get("content-type") || ""; const body = contentType.includes("application/json") ? await response.json() : await response.text(); if (!response.ok) { const detail = typeof body === "object" ? body.detail || JSON.stringify(body) : body; throw new Error(`${response.status} ${response.statusText}: ${detail}`); } return body; } function safeText(value) { return value == null ? "--" : String(value); } function createBadge(text, extraClass = "") { const badge = document.createElement("span"); badge.className = extraClass ? `badge ${extraClass}` : "badge"; badge.textContent = safeText(text); return badge; } function setHealthPill(status) { const pills = document.querySelectorAll("[data-health-pill]"); pills.forEach((pill) => { pill.textContent = status === "healthy" ? "Healthy" : "Unavailable"; pill.classList.toggle("is-pending", status !== "healthy"); }); } function renderTaskCards(target, tasks) { if (!target) return; target.replaceChildren(); Object.entries(tasks).forEach(([taskId, task]) => { const article = document.createElement("article"); article.className = "task-card"; const difficultyClass = `difficulty-${safeText(task.difficulty).toLowerCase().replace(/[^a-z0-9_-]/g, "")}`; const difficulty = createBadge(task.difficulty, difficultyClass); const title = document.createElement("h3"); title.textContent = safeText(task.name); const content = document.createElement("div"); content.className = "task-card-content"; const expectedField = document.createElement("p"); expectedField.append("Expected field: "); const expectedFieldValue = document.createElement("strong"); expectedFieldValue.textContent = safeText(task.expected_field || task.output_field); expectedField.appendChild(expectedFieldValue); content.append(title, expectedField); const taskMeta = document.createElement("div"); taskMeta.className = "task-meta"; taskMeta.append( createBadge(taskId), createBadge(`${task.ticket_count || 0} incidents`), ); const taskValues = document.createElement("div"); taskValues.className = "task-values"; (task.allowed_values || task.labels || []).forEach((value) => { taskValues.appendChild(createBadge(value)); }); article.append(difficulty, content, taskMeta, taskValues); target.appendChild(article); }); } async function initHome() { const [health, metadata] = await Promise.all([ fetchJson("/health"), fetchJson("/metadata"), ]); setHealthPill(health.status); document.querySelector("[data-total-incidents]").textContent = safeText(metadata.total_tickets); document.querySelector("[data-task-count]").textContent = safeText(Object.keys(metadata.tasks).length); renderTaskCards(document.querySelector("[data-task-grid]"), metadata.tasks); } async function initStatus() { const [health, metadata, grader, schema] = await Promise.all([ fetchJson("/health"), fetchJson("/metadata"), fetchJson("/grader"), fetchJson("/schema"), ]); document.querySelector("[data-health-text]").textContent = health.status; document.querySelector("[data-total-incidents]").textContent = safeText(metadata.total_tickets); document.querySelector("[data-schema-count]").textContent = safeText(Object.keys(schema).length); renderTaskCards(document.querySelector("[data-task-grid]"), metadata.tasks); const schemaGrid = document.querySelector("[data-schema-grid]"); schemaGrid.replaceChildren(); Object.keys(schema).forEach((name) => { schemaGrid.appendChild(createBadge(name)); }); document.querySelector("[data-grader-summary]").textContent = grader.scoring; const graderList = document.querySelector("[data-grader-list]"); graderList.replaceChildren(); Object.entries(grader.tasks).forEach(([task, rule]) => { const item = document.createElement("li"); const taskName = document.createElement("strong"); taskName.textContent = task; item.append(taskName, `: ${safeText(rule)}`); graderList.appendChild(item); }); } function buildActionPayload(observation, selectedValue) { const payload = { incident_id: observation.incident_id, task_type: observation.task_type, }; payload[observation.expected_field] = selectedValue; return payload; } function createEndpointCard(endpoint) { const card = document.createElement("article"); card.className = "endpoint-card"; const header = document.createElement("div"); header.className = "endpoint-card-header"; header.append(createBadge(endpoint.method, `method-${endpoint.method.toLowerCase()}`)); const path = document.createElement("code"); path.textContent = endpoint.path; header.appendChild(path); const title = document.createElement("h3"); title.textContent = endpoint.title; const description = document.createElement("p"); description.textContent = endpoint.description; const meta = document.createElement("div"); meta.className = "endpoint-meta"; endpoint.notes.forEach((note) => { meta.appendChild(createBadge(note)); }); const link = document.createElement("a"); link.className = "button button-secondary endpoint-link"; link.href = endpoint.href; link.textContent = endpoint.linkText; card.append(header, title, description, meta, link); return card; } async function initApi() { const [health, metadata, grader, schema] = await Promise.all([ fetchJson("/health"), fetchJson("/metadata"), fetchJson("/grader"), fetchJson("/schema"), ]); document.querySelector("[data-api-health]").textContent = health.status === "healthy" ? "Healthy" : "Unavailable"; document.querySelector("[data-api-summary]").textContent = `${safeText(metadata.total_tickets)} incidents across ${Object.keys(metadata.tasks || {}).length} task families.`; const endpoints = [ { method: "GET", path: "/health", title: "Health check", description: "Fast validator ping. Must return a healthy status.", notes: ["validator", "no body"], href: "/health", linkText: "Open raw health", }, { method: "GET", path: "/metadata", title: "Environment metadata", description: "Shows name, task inventory, labels, and dataset count.", notes: ["task inventory", "reviewer-friendly"], href: "/metadata", linkText: "Open raw metadata", }, { method: "GET", path: "/schema", title: "Typed contract schemas", description: "Exposes action, observation, reward, state, and step result models.", notes: ["typed models", "OpenEnv spec"], href: "/schema", linkText: "Open raw schema", }, { method: "POST", path: "/reset", title: "Start an episode", description: "Creates a session and returns the first observation. No grading happens yet.", notes: ["returns session_id", "body optional"], href: "/playground", linkText: "Try in playground", }, { method: "POST", path: "/step?session_id=...", title: "Submit an answer", description: "Grades exactly one action and returns reward, done, correctness, and state.", notes: ["reward 0-1", "single step"], href: "/playground", linkText: "Try in playground", }, { method: "GET", path: "/state?session_id=...", title: "Read episode state", description: "Reads active or completed episode state for a known session id.", notes: ["typed state", "debugging"], href: "/playground", linkText: "Create session first", }, { method: "GET", path: "/docs", title: "Generated FastAPI docs", description: "Full OpenAPI interface generated from the running backend.", notes: ["developer docs", "OpenAPI"], href: "/docs", linkText: "Open FastAPI docs", }, { method: "GET", path: "/openapi.json", title: "Machine-readable contract", description: "Raw OpenAPI document used by tools and automated inspectors.", notes: ["JSON", "tooling"], href: "/openapi.json", linkText: "Open raw OpenAPI", }, ]; const endpointGrid = document.querySelector("[data-endpoint-grid]"); endpointGrid.replaceChildren(); endpoints.forEach((endpoint) => { endpointGrid.appendChild(createEndpointCard(endpoint)); }); const schemaGrid = document.querySelector("[data-api-schema-grid]"); schemaGrid.replaceChildren(); Object.keys(schema).forEach((name) => { schemaGrid.appendChild(createBadge(name)); }); const graderList = document.querySelector("[data-api-grader-list]"); graderList.replaceChildren(); Object.entries(grader.tasks || {}).forEach(([task, rule]) => { const item = document.createElement("li"); const taskName = document.createElement("strong"); taskName.textContent = task; item.append(taskName, `: ${safeText(rule)}`); graderList.appendChild(item); }); } async function initPlayground() { const resetForm = document.getElementById("reset-form"); const stepForm = document.getElementById("step-form"); const taskTypeInput = document.getElementById("task-type"); const ticketIdInput = document.getElementById("ticket-id"); const ticketOptions = document.getElementById("ticket-options"); const ticketHelper = document.getElementById("ticket-helper"); const expectedFieldInput = document.getElementById("expected-field"); const actionValueSelect = document.getElementById("action-value"); const stepButton = document.getElementById("step-button"); const resetButton = document.getElementById("reset-button"); const sessionIdTarget = document.getElementById("session-id"); const observationOutput = document.getElementById("observation-output"); const resultOutput = document.getElementById("result-output"); const messageTarget = document.getElementById("playground-message"); const summaryIncident = document.getElementById("summary-incident"); const summaryField = document.getElementById("summary-field"); const summaryReward = document.getElementById("summary-reward"); const summaryStatus = document.getElementById("summary-status"); const briefAlert = document.getElementById("brief-alert"); const briefTask = document.getElementById("brief-task"); const briefDifficulty = document.getElementById("brief-difficulty"); const briefExpected = document.getElementById("brief-expected"); const briefAllowedValues = document.getElementById("brief-allowed-values"); const briefContextSignals = document.getElementById("brief-context-signals"); const briefVerdict = document.getElementById("brief-verdict"); const briefAgentAnswer = document.getElementById("brief-agent-answer"); const briefGroundTruth = document.getElementById("brief-ground-truth"); const briefReward = document.getElementById("brief-reward"); const briefReason = document.getElementById("brief-reason"); let sessionId = null; let observation = null; let validTickets = []; const setOutput = (target, data) => { target.textContent = typeof data === "string" ? data : JSON.stringify(data, null, 2); }; const setMessage = (message, mode = "neutral") => { messageTarget.textContent = message; messageTarget.dataset.mode = mode; }; const setBusy = (button, isBusy, busyText, idleText) => { button.disabled = isBusy; button.textContent = isBusy ? busyText : idleText; }; const updateSummaryFromObservation = (nextObservation) => { summaryIncident.textContent = nextObservation.incident_id; summaryField.textContent = nextObservation.expected_field; summaryReward.textContent = "--"; summaryStatus.textContent = "Awaiting action"; }; const updateSummaryFromResult = (result) => { summaryReward.textContent = result.reward?.value ?? "--"; summaryStatus.textContent = result.done ? "Completed" : "In progress"; }; const formatContextValue = (value) => { if (Array.isArray(value)) return value.join(", "); if (value && typeof value === "object") return JSON.stringify(value); if (typeof value === "boolean") return value ? "true" : "false"; return safeText(value); }; const renderValueChips = (target, values) => { target.replaceChildren(); values.forEach((value) => { target.appendChild(createBadge(value)); }); }; const renderContextSignals = (target, context) => { target.replaceChildren(); Object.entries(context || {}).slice(0, 8).forEach(([key, value]) => { const chip = document.createElement("span"); chip.className = "context-chip"; const chipKey = document.createElement("strong"); chipKey.textContent = key; const chipValue = document.createElement("span"); chipValue.textContent = formatContextValue(value); chip.append(chipKey, chipValue); target.appendChild(chip); }); if (target.childElementCount === 0) { const empty = document.createElement("span"); empty.className = "context-chip"; empty.textContent = "No context signals provided."; target.appendChild(empty); } }; const resetResultBrief = () => { briefVerdict.textContent = "Waiting for step"; briefVerdict.dataset.outcome = "waiting"; briefAgentAnswer.textContent = "--"; briefGroundTruth.textContent = "--"; briefReward.textContent = "--"; briefReason.textContent = "Submit a step to see the deterministic grader explanation."; }; const updateBriefFromObservation = (resetResult) => { const nextObservation = resetResult.observation; briefAlert.textContent = nextObservation.alert_text; briefTask.textContent = resetResult.info?.task_name || nextObservation.task_description; briefDifficulty.textContent = nextObservation.difficulty; briefExpected.textContent = nextObservation.expected_field; renderValueChips(briefAllowedValues, nextObservation.allowed_values || []); renderContextSignals(briefContextSignals, nextObservation.context); resetResultBrief(); }; const updateBriefFromResult = (result) => { const correct = Boolean(result.info?.correct); const rewardValue = Number(result.reward?.value || 0); const partialCredit = !correct && rewardValue > 0; briefVerdict.textContent = correct ? "Correct triage decision" : partialCredit ? "Partial credit" : "Incorrect decision"; briefVerdict.dataset.outcome = correct ? "correct" : partialCredit ? "partial" : "incorrect"; briefAgentAnswer.textContent = safeText(result.info?.agent_answer); briefGroundTruth.textContent = safeText(result.info?.ground_truth); briefReward.textContent = safeText(result.reward?.value); briefReason.textContent = safeText(result.reward?.reason); }; const findTicket = (ticketId) => validTickets.find((ticket) => ticket.incident_id === ticketId); const syncTaskTypeFromTicket = () => { const ticket = findTicket(ticketIdInput.value.trim()); if (!ticket) return; taskTypeInput.value = ticket.task_type; ticketHelper.textContent = `${ticket.incident_id} is a ${ticket.task_type} ${ticket.difficulty} ticket.`; }; const chooseFirstTicketForTask = () => { if (!taskTypeInput.value) return; const ticket = validTickets.find((item) => item.task_type === taskTypeInput.value); if (ticket) { ticketIdInput.value = ticket.incident_id; ticketHelper.textContent = `${ticket.incident_id} selected for ${taskTypeInput.value}.`; } }; try { const ticketData = await fetchJson("/tickets"); validTickets = ticketData.tickets || []; ticketOptions.replaceChildren(); validTickets.forEach((ticket) => { const option = document.createElement("option"); option.value = safeText(ticket.incident_id); option.label = `${safeText(ticket.task_type)} / ${safeText(ticket.task_name)}`; ticketOptions.appendChild(option); }); ticketHelper.textContent = `Valid ticket range: ${validTickets[0]?.incident_id || "--"} to ${validTickets.at(-1)?.incident_id || "--"}.`; } catch (error) { ticketHelper.textContent = `Could not load ticket list: ${error.message}`; } document.querySelectorAll("[data-preset-task]").forEach((button) => { button.addEventListener("click", () => { taskTypeInput.value = button.dataset.presetTask; ticketIdInput.value = button.dataset.presetTicket; setMessage(`Preset loaded: ${button.dataset.presetTask} / ${button.dataset.presetTicket}. Click Start / Reset Environment.`, "success"); }); }); resetForm.addEventListener("submit", async (event) => { event.preventDefault(); const formData = new FormData(resetForm); const payload = {}; for (const [key, value] of formData.entries()) { if (value !== "") { payload[key] = key === "seed" ? Number(value) : value; } } const requestedTicket = payload.ticket_id; const knownTicket = requestedTicket ? findTicket(requestedTicket) : null; if (requestedTicket && validTickets.length > 0 && !knownTicket) { const message = `Ticket ${requestedTicket} does not exist. Use one of ${validTickets[0].incident_id} to ${validTickets.at(-1).incident_id}, or click a preset.`; setOutput(observationOutput, { error: message }); setMessage(message, "error"); return; } if (knownTicket && payload.task_type && payload.task_type !== knownTicket.task_type) { payload.task_type = knownTicket.task_type; taskTypeInput.value = knownTicket.task_type; ticketHelper.textContent = `Task type changed to ${knownTicket.task_type} because ${knownTicket.incident_id} belongs to that task.`; } try { setBusy(resetButton, true, "Starting...", "Start / Reset Environment"); setMessage("Reset request sent. Watch the terminal for a [RESET] log.", "neutral"); const result = await fetchJson("/reset", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); sessionId = result.info.session_id; observation = result.observation; sessionIdTarget.textContent = sessionId; expectedFieldInput.value = observation.expected_field; actionValueSelect.disabled = false; stepButton.disabled = false; actionValueSelect.replaceChildren(); observation.allowed_values.forEach((value) => { const option = document.createElement("option"); option.value = safeText(value); option.textContent = safeText(value); actionValueSelect.appendChild(option); }); setOutput(observationOutput, result); setOutput(resultOutput, "No step submitted yet."); updateSummaryFromObservation(observation); updateBriefFromObservation(result); setMessage(`Session ready for ${observation.incident_id}. Pick a value and submit the step.`, "success"); } catch (error) { setOutput(observationOutput, { error: error.message }); setMessage(error.message, "error"); } finally { setBusy(resetButton, false, "Starting...", "Start / Reset Environment"); } }); stepForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!sessionId || !observation) { setOutput(resultOutput, { error: "Start a session first." }); setMessage("Start a session before submitting a step.", "error"); return; } try { setBusy(stepButton, true, "Submitting...", "Submit Step"); setMessage("Step request sent. Watch the terminal for a [STEP] log.", "neutral"); const result = await fetchJson(`/step?session_id=${encodeURIComponent(sessionId)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(buildActionPayload(observation, actionValueSelect.value)), }); setOutput(resultOutput, result); updateSummaryFromResult(result); updateBriefFromResult(result); const reward = result.reward?.value ?? "--"; setMessage(`Step completed with reward ${reward}.`, reward === 1 ? "success" : "neutral"); } catch (error) { setOutput(resultOutput, { error: error.message }); setMessage(error.message, "error"); } finally { if (observation) { setBusy(stepButton, false, "Submitting...", "Submit Step"); } } }); ticketIdInput.addEventListener("change", syncTaskTypeFromTicket); ticketIdInput.addEventListener("blur", syncTaskTypeFromTicket); taskTypeInput.addEventListener("change", chooseFirstTicketForTask); } async function bootstrap() { const page = document.body.dataset.page; try { if (page === "home") { await initHome(); } else if (page === "status") { await initStatus(); } else if (page === "playground") { await initPlayground(); } else if (page === "api") { await initApi(); } } catch (error) { const pageShell = document.querySelector(".page-shell"); const banner = document.createElement("div"); banner.className = "floating-panel"; const title = document.createElement("strong"); title.textContent = "UI data load failed."; const detail = document.createElement("p"); detail.className = "status-helper"; detail.textContent = error.message; banner.append(title, detail); pageShell?.prepend(banner); } } window.addEventListener("DOMContentLoaded", bootstrap);