XcodeAddy's picture
Add playground triage brief cards
3748f8d
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);