"use strict";
(() => {
const form = document.getElementById("tease-form");
const inputs = Array.from(document.querySelectorAll("[data-input]"));
const outputs = Array.from(document.querySelectorAll("[data-output]"));
const button = document.getElementById("submit");
const status = document.getElementById("status");
const error = document.getElementById("error");
const config = JSON.parse(document.getElementById("tease-config")?.textContent || "{}");
const preset = document.getElementById("preset");
const pin = document.getElementById("pin");
const scenarios = document.getElementById("scenarios");
const pinned = [];
let current = null, submitted = null;
const render = (output, value) => window.teaseView ? window.teaseView.render(output, value) : output.textContent = value;
const read = input => input.type === "checkbox" ? String(input.checked) : input.multiple ? JSON.stringify(Array.from(input.selectedOptions, option => Number(option.value))) : input.value;
function describe(input) {
if (input.multiple) return Array.from(input.selectedOptions, option => option.textContent).join(", ") || "None";
if (input.tagName === "SELECT") return input.selectedOptions[0]?.textContent || "";
return read(input);
}
function compare() {
if (!scenarios) return;
scenarios.replaceChildren();
const node = window.teaseView.node;
const all = pinned.map((snapshot, index) => ({snapshot, index}));
if (current && pinned.length) all.push({snapshot:current, index:null});
all.forEach(({snapshot,index}) => {
const card = node("article", undefined, scenarios); card.className = "scenario";
node("h3", index === null ? "Current result" : `Pinned ${index + 1}`, card);
const details = node("details", undefined, card); node("summary", snapshot.name || "Inputs", details);
snapshot.inputs.forEach((value,i) => node("p", `${config.inputLabels[i]}: ${value}`, details));
snapshot.values.forEach((value,i) => {
node("h4", config.outputs[i].label, card);
render(node("div", undefined, card), value);
if (config.outputs[i].numeric && pinned.length) {
const delta = Number(value) - Number(pinned[0].values[i]);
node("p", `Difference: ${delta >= 0 ? "+" : ""}${Number(delta.toPrecision(6))}`, card).className = "help";
}
});
if (index !== null) {
const remove = node("button", "Remove", card); remove.type = "button";
remove.addEventListener("click", () => { pinned.splice(index,1); compare(); if (pin) pin.disabled = !current || pinned.length >= 3; });
}
});
}
if (pin) pin.addEventListener("click", () => {
if (!current || pinned.length >= 3) return;
pinned.push(JSON.parse(JSON.stringify(current))); pin.disabled = pinned.length >= 3; compare();
});
if (preset) preset.addEventListener("change", () => {
const scenario = config.presets[Number(preset.value)];
if (preset.value === "" || !scenario) return;
scenario.inputs.forEach((value,i) => {
const input = inputs[i];
if (input.type === "checkbox") input.checked = value === "true";
else if (input.multiple) { const selected = JSON.parse(value); Array.from(input.options).forEach(option => option.selected = selected.includes(Number(option.value))); }
else input.value = value;
if (input.type === "range") input.nextElementSibling.textContent = input.value;
});
clearErrors(); status.textContent = `Loaded ${scenario.name}. Select Predict to run.`;
});
let initialized = false;
let activeRequest = null;
let nextRequest = 0;
function enable(enabled, busy = false) {
button.disabled = !enabled;
inputs.forEach(input => { input.disabled = !enabled; });
if (preset) preset.disabled = !enabled;
form.setAttribute("aria-busy", String(busy));
}
function clearErrors() {
error.textContent = "";
inputs.forEach((input, index) => {
input.removeAttribute("aria-invalid");
document.getElementById(`error-${index}`).textContent = "";
});
}
function ready(message) {
if (initialized) return;
initialized = true;
enable(!message);
status.textContent = message ? "Model unavailable." : "Ready.";
if (message) error.textContent = message;
}
function receive(response) {
if (response.request_id !== activeRequest) return;
if (activeRequest === null && response.request_id !== null) return;
activeRequest = null;
enable(!response.fatal);
clearErrors();
if (response.kind === "success") {
outputs.forEach((output, index) => render(output, response.values[index]));
current = { ...submitted, values: response.values };
if (pin) pin.disabled = pinned.length >= 3;
compare();
status.textContent = "Prediction complete.";
} else {
outputs.forEach(output => { output.textContent = ""; });
status.textContent = response.fatal ? "Model unavailable." : "Prediction not completed.";
error.textContent = response.message;
if (!response.fatal && Number.isInteger(response.field) && inputs[response.field]) {
const input = inputs[response.field];
input.setAttribute("aria-invalid", "true");
document.getElementById(`error-${response.field}`).textContent = response.message;
input.focus();
} else {
error.focus();
}
}
}
window.tease = { ready, receive };
inputs.forEach(input => {
input.addEventListener("change", () => { if (preset) preset.value = ""; });
if (input.type === "range") {
input.addEventListener("input", () => { input.nextElementSibling.textContent = input.value; });
}
});
form.addEventListener("submit", event => {
event.preventDefault();
if (!initialized || button.disabled || activeRequest !== null) return;
clearErrors();
outputs.forEach(output => { output.textContent = ""; });
submitted = { inputs: inputs.map(describe), name: preset && preset.value !== "" ? config.presets[Number(preset.value)].name : "Inputs" };
current = null; if (pin) pin.disabled = true; compare();
activeRequest = ++nextRequest;
enable(false, true);
status.textContent = "Computing…";
try {
window.ipc.postMessage(JSON.stringify({ request_id: activeRequest, inputs: inputs.map(read) }));
} catch (_) {
receive({ request_id: activeRequest, kind: "error", field: null, message: "Could not submit the prediction. Please try again." });
}
});
enable(false, true);
status.textContent = "Preparing model…";
try { window.ipc.postMessage("ready"); }
catch (_) { ready("Could not connect to the model. Restart the demo."); }
})();