"use strict"; const AXES = ["art_style", "color", "art_medium", "lighting"]; const TOKEN_STORAGE_KEY = "aamcq_token"; const EMAIL_STORAGE_KEY = "aamcq_email"; const THEME_STORAGE_KEY = "aamcq_theme"; const PASSWORD_SESSION_KEY = "aamcq_access_password"; const FIRST_SESSION_CAP = 20; const EXTRA_ROUND_CAP = 10; function setTheme(theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem(THEME_STORAGE_KEY, theme); const btn = document.getElementById("theme-toggle"); if (btn) btn.textContent = theme === "dark" ? "☾" : "☀"; } function initThemeToggle() { const current = localStorage.getItem(THEME_STORAGE_KEY) || "light"; setTheme(current); const btn = document.getElementById("theme-toggle"); btn.addEventListener("click", () => { const now = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark"; setTheme(now); }); } class HttpError extends Error { constructor(status, body) { super(`${status}: ${body}`); this.status = status; this.body = body; } } async function fetchJSON(path, init) { const resp = await fetch(path, init); if (!resp.ok) { const body = await resp.text(); throw new HttpError(resp.status, body); } return resp.json(); } async function attemptRegister(params, password) { const qs = new URLSearchParams(params); if (password) qs.set("password", password); return fetch(`/api/register?${qs.toString()}`, { method: "POST" }); } async function registerWithParams(params) { // Try with cached password (could be empty). let password = sessionStorage.getItem(PASSWORD_SESSION_KEY) || ""; let resp = await attemptRegister(params, password); while (resp.status === 403) { sessionStorage.removeItem(PASSWORD_SESSION_KEY); const entered = window.prompt( password ? "Wrong access password. Try again:" : "Enter the access password to start labeling:" ); if (entered == null) throw new Error("Access password required."); password = entered; resp = await attemptRegister(params, password); } if (resp.status === 429) { throw new HttpError(429, await resp.text()); } if (!resp.ok) { const body = await resp.text(); throw new HttpError(resp.status, body); } if (password) sessionStorage.setItem(PASSWORD_SESSION_KEY, password); const data = await resp.json(); localStorage.setItem(TOKEN_STORAGE_KEY, data.token); return data; } async function ensureToken() { const urlToken = new URL(window.location.href).searchParams.get("token"); if (urlToken) { localStorage.setItem(TOKEN_STORAGE_KEY, urlToken); return urlToken; } const stored = localStorage.getItem(TOKEN_STORAGE_KEY); if (stored) return stored; const { token } = await registerWithParams({ cap: String(FIRST_SESSION_CAP) }); return token; } function renderProfileCard(idx, profile) { const ul = document.createElement("ul"); ul.className = "profile"; for (const axis of AXES) { const li = document.createElement("li"); const key = document.createElement("span"); key.className = "axis"; key.textContent = axis.replace("_", " ") + ": "; const val = document.createElement("span"); val.className = "value"; val.textContent = profile[axis] ?? "?"; li.appendChild(key); li.appendChild(val); ul.appendChild(li); } const wrapper = document.createElement("label"); wrapper.className = "option"; const input = document.createElement("input"); input.type = "radio"; input.name = "choice"; input.value = String(idx); wrapper.appendChild(input); const badge = document.createElement("div"); badge.className = "badge"; badge.textContent = String.fromCharCode(65 + idx); wrapper.appendChild(badge); wrapper.appendChild(ul); return wrapper; } let currentItem = null; let shownAt = 0; async function loadNext(token) { const data = await fetchJSON(`/api/task?token=${encodeURIComponent(token)}`); const card = document.getElementById("card"); const submit = document.getElementById("submit"); const err = document.getElementById("error"); err.textContent = ""; if (data.done) { await renderDonePage(token, data); submit.style.display = "none"; updateProgress(data.labeled, data.cap); return; } currentItem = data; shownAt = performance.now(); document.getElementById("stimulus").src = data.image_url; document.getElementById("base-prompt").textContent = data.payload.base_prompt ? `"${data.payload.base_prompt}"` : ""; const form = document.getElementById("options"); form.innerHTML = ""; const options = data.payload.options || []; options.forEach((opt, i) => form.appendChild(renderProfileCard(i, opt))); submit.style.display = ""; submit.disabled = true; form.querySelectorAll("input[type=radio]").forEach((el) => { el.addEventListener("change", () => { submit.disabled = false; }); }); updateProgress(data.labeled, data.cap); } async function renderDonePage(token, taskData) { const card = document.getElementById("card"); const labeled = taskData.labeled ?? 0; if (taskData.reason !== "cap_reached") { // Pool is drained entirely. Thank and stop. card.innerHTML = `
All items are fully labeled (you contributed ${labeled}). Thank you!
`; appendAnnouncement(card); return; } // cap_reached: fetch detailed session status to decide UI state let status; try { status = await fetchJSON(`/api/session_status?token=${encodeURIComponent(token)}`); } catch (e) { card.innerHTML = `Session complete (${labeled} labeled). Couldn't load status: ${e.message}
`; return; } card.innerHTML = ""; const msg = document.createElement("p"); msg.className = "done"; card.appendChild(msg); if (!status.acc_pass) { // Fail state msg.innerHTML = `Low agreement rate detected.