Spaces:
Sleeping
Sleeping
File size: 12,216 Bytes
871ff87 b8cd5c3 62a142e acdd723 083fb75 b8cd5c3 62a142e 871ff87 ea27f41 871ff87 ea27f41 871ff87 b8cd5c3 083fb75 b8cd5c3 acdd723 b8cd5c3 acdd723 b8cd5c3 acdd723 b8cd5c3 acdd723 871ff87 b8cd5c3 871ff87 acdd723 b8cd5c3 871ff87 ea27f41 b8cd5c3 ea27f41 871ff87 b8cd5c3 871ff87 b8cd5c3 f5a1931 871ff87 b8cd5c3 871ff87 b8cd5c3 f5a1931 871ff87 b8cd5c3 871ff87 b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 000a5ee b8cd5c3 871ff87 ea27f41 b8cd5c3 ea27f41 b8cd5c3 871ff87 62a142e 871ff87 ea27f41 871ff87 ea27f41 871ff87 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | "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 = `<p class='done'>All items are fully labeled (you contributed ${labeled}). Thank you!</p>`;
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 = `<p class='done'>Session complete (${labeled} labeled). Couldn't load status: ${e.message}</p>`;
return;
}
card.innerHTML = "";
const msg = document.createElement("p");
msg.className = "done";
card.appendChild(msg);
if (!status.acc_pass) {
// Fail state
msg.innerHTML =
`<strong>Low agreement rate detected.</strong><br>` +
`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 =
`<strong>Great job!</strong> Your session passed the quality check. ` +
`Submit your email to enter the lottery.`;
const form = document.createElement("form");
form.id = "email-form";
form.innerHTML =
`<input type="email" id="email-input" placeholder="your@email" required autocomplete="email" />` +
`<button type="submit">Submit email</button>`;
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 =
`<strong>Thanks, ${status.email}!</strong> You're entered in the lottery ` +
`with <strong>${entries}</strong> 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: <strong>10 × $20 Amazon gift cards</strong>, 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();
}
|