"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.
` + `Your ${labeled}-item session doesn't meet the quality threshold ` + `and can't be credited. Please try again more carefully.`; msg.classList.add("fail"); const btn = document.createElement("button"); btn.id = "retry-session"; if (status.round_number === 1) { btn.textContent = "Try again"; btn.addEventListener("click", () => { localStorage.removeItem(TOKEN_STORAGE_KEY); localStorage.removeItem(EMAIL_STORAGE_KEY); location.reload(); }); } else { btn.textContent = `Redo round ${status.round_number}`; btn.addEventListener("click", async () => { localStorage.removeItem(TOKEN_STORAGE_KEY); const email = status.email || localStorage.getItem(EMAIL_STORAGE_KEY); if (!email) { location.reload(); return; } try { await registerWithParams({ cap: String(EXTRA_ROUND_CAP), email, round: String(status.round_number), }); location.reload(); } catch (e) { document.getElementById("error").textContent = `Retry failed: ${e.message}`; } }); } card.appendChild(btn); appendAnnouncement(card); return; } // Pass state if (status.round_number === 1 && !status.email) { msg.innerHTML = `Great job! Your session passed the quality check. ` + `Submit your email to enter the lottery.`; const form = document.createElement("form"); form.id = "email-form"; form.innerHTML = `` + ``; card.appendChild(form); form.addEventListener("submit", async (ev) => { ev.preventDefault(); const email = document.getElementById("email-input").value.trim(); if (!email) return; try { await fetchJSON("/api/submit_email", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token, email }), }); localStorage.setItem(EMAIL_STORAGE_KEY, email); await renderDonePage(token, taskData); } catch (e) { document.getElementById("error").textContent = `Submit failed: ${e.message}`; } }); appendAnnouncement(card); return; } // Already on email chain: thank + offer "Label more" if not maxed const entries = status.labels_in_lottery ?? 0; msg.innerHTML = `Thanks, ${status.email}! You're entered in the lottery ` + `with ${entries} labels counted so far.`; if (status.can_extend) { const hint = document.createElement("p"); hint.className = "muted-info"; hint.textContent = `Want better odds? Label ${EXTRA_ROUND_CAP} more to add to your entry count.`; card.appendChild(hint); const btn = document.createElement("button"); btn.id = "extend-session"; btn.textContent = `Label ${EXTRA_ROUND_CAP} more`; btn.addEventListener("click", async () => { try { await registerWithParams({ cap: String(EXTRA_ROUND_CAP), email: status.email, round: String(status.round_number + 1), }); location.reload(); } catch (e) { document.getElementById("error").textContent = `Couldn't start round: ${e.message}`; } }); card.appendChild(btn); } else { const hint = document.createElement("p"); hint.className = "muted-info"; hint.textContent = "Thanks for participating!"; card.appendChild(hint); } appendAnnouncement(card); } function appendAnnouncement(card) { const div = document.createElement("div"); div.className = "announcement"; div.innerHTML = `Winning probability is proportional to labels that pass the quality check. ` + `Prize pool: 10 × $20 Amazon gift cards, drawn after ` + `we reach 2,000 labels total.`; card.appendChild(div); } function updateProgress(labeled, cap) { const el = document.getElementById("progress"); if (cap != null) { el.textContent = `${labeled ?? 0} / ${cap} done`; } else { el.textContent = `${labeled ?? 0} labeled`; } } async function submitLabel(token) { const err = document.getElementById("error"); err.textContent = ""; const chosen = document.querySelector("input[name=choice]:checked"); if (!chosen || !currentItem) return; const elapsed = (performance.now() - shownAt) / 1000; await fetchJSON("/api/label", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token, item_id: currentItem.item_id, chosen_index: Number(chosen.value), seconds: elapsed, confidence: null, }), }); await loadNext(token); } async function recoverFromInvalidToken() { // Server doesn't know this token. Wipe client state and start fresh. localStorage.removeItem(TOKEN_STORAGE_KEY); localStorage.removeItem(EMAIL_STORAGE_KEY); const { token } = await registerWithParams({ cap: String(FIRST_SESSION_CAP) }); return token; } async function main() { initThemeToggle(); let token; try { token = await ensureToken(); } catch (e) { document.getElementById("error").textContent = e.message; return; } document.getElementById("submit").addEventListener("click", async () => { try { await submitLabel(token); } catch (e) { if (e instanceof HttpError && e.status === 401) { token = await recoverFromInvalidToken(); await loadNext(token); } else { document.getElementById("error").textContent = `Submit failed: ${e.message}`; } } }); try { await loadNext(token); } catch (e) { if (e instanceof HttpError && e.status === 401) { try { token = await recoverFromInvalidToken(); await loadNext(token); } catch (e2) { document.getElementById("error").textContent = `Load failed: ${e2.message}`; } } else { document.getElementById("error").textContent = `Load failed: ${e.message}`; } } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", main); } else { main(); }