Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Image preference study</title> | |
| <style> | |
| :root { --bg:#0f1115; --fg:#e8e8ea; --mut:#9aa0a6; --acc:#4f8cff; --card:#1a1d23; --line:#262a31; } | |
| * { box-sizing:border-box; } | |
| body { margin:0; background:var(--bg); color:var(--fg); font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif; } | |
| .wrap { max-width:1000px; margin:0 auto; padding:20px 14px 72px; } | |
| h1 { font-size:22px; margin:0 0 8px; } .mut { color:var(--mut); } .center { text-align:center; } | |
| .card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:20px; margin-top:16px; } | |
| input[type=email] { width:100%; padding:12px; border-radius:8px; border:1px solid #333; background:#0c0e12; color:var(--fg); font-size:16px; } | |
| button.primary { cursor:pointer; border:0; border-radius:8px; padding:11px 16px; font-size:15px; font-weight:600; background:var(--acc); color:#fff; } | |
| button.primary:disabled { opacity:.4; cursor:default; } | |
| .bar { height:6px; background:var(--line); border-radius:6px; overflow:hidden; margin:8px 0; } | |
| .bar > div { height:100%; background:var(--acc); width:0; transition:width .2s; } | |
| .prompt { text-align:center; font-size:18px; margin:6px 0 6px; } | |
| .hint { text-align:center; color:var(--mut); margin:0 0 14px; font-size:14px; } | |
| .pair { display:grid; grid-template-columns:1fr 1fr; gap:14px; } | |
| .opt { background:#0c0e12; border:2px solid var(--line); border-radius:12px; padding:8px; text-align:center; cursor:pointer; transition:border-color .1s, transform .05s; } | |
| .opt:hover { border-color:var(--acc); } .opt:active { transform:scale(.99); } | |
| .opt img { width:100%; border-radius:8px; display:block; } | |
| .opt .tag { margin-top:6px; font-weight:700; color:var(--mut); letter-spacing:.04em; font-size:13px; } | |
| .hide { display:none; } .err { color:#ff7a7a; margin-top:10px; } | |
| table { width:100%; border-collapse:collapse; margin-top:10px; font-size:15px; } | |
| th,td { text-align:left; padding:6px; border-bottom:1px solid var(--line); } | |
| @media (max-width:560px){ .pair{ grid-template-columns:1fr; } } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>Image preference study</h1> | |
| <div id="intro" class="card"> | |
| <p>Thank you for helping! You'll see <b>pairs of AI-generated images</b> made from the same text prompt. | |
| For each pair, just <b>click the image you prefer</b> (better overall — more appealing and faithful to the prompt).</p> | |
| <p class="mut">About <span id="npairs">…</span> quick clicks, ~<span id="mins">…</span> min. Your email is only used so each person answers once; it is not shared or published.</p> | |
| <p><input id="email" type="email" placeholder="you@example.com" autocomplete="email"></p> | |
| <p><label style="cursor:pointer"><input type="checkbox" id="optrec" checked style="vertical-align:middle"> Allow my anonymized responses to be recorded for research.</label></p> | |
| <p><button class="primary" id="start">Start</button></p> | |
| <div id="introErr" class="err"></div> | |
| </div> | |
| <div id="study" class="card hide"> | |
| <div class="mut"><span id="pos">1</span> / <span id="total">0</span></div> | |
| <div class="bar"><div id="prog"></div></div> | |
| <div class="prompt">“<span id="prompt"></span>”</div> | |
| <p class="hint">Click the image you prefer — or “Cannot decide” if they look equally good.</p> | |
| <div class="pair"> | |
| <div class="opt" id="optL"><img id="imgL"><div class="tag">IMAGE 1</div></div> | |
| <div class="opt" id="optR"><img id="imgR"><div class="tag">IMAGE 2</div></div> | |
| </div> | |
| <div class="center" style="margin-top:14px"> | |
| <button class="primary" id="optC" style="background:#3a3f48">Cannot decide</button> | |
| </div> | |
| </div> | |
| <div id="done" class="card hide"> | |
| <h1 class="center">All done — thank you! 🎉</h1> | |
| <p id="recnote" class="mut center"></p> | |
| <div id="myresults"></div> | |
| <p class="mut center" style="margin-top:16px">You may close this tab.</p> | |
| </div> | |
| </div> | |
| <script> | |
| let token=null, pairs=[], i=0, votes=[], busy=false; | |
| const $ = id => document.getElementById(id); | |
| async function start(){ | |
| $("introErr").textContent=""; $("start").disabled=true; | |
| try{ | |
| const r = await fetch("/api/register",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:$("email").value, record:$("optrec").checked})}); | |
| const d = await r.json(); | |
| if(!r.ok){ $("introErr").textContent=d.detail||"Could not start."; $("start").disabled=false; return; } | |
| token=d.token; pairs=d.pairs; i=0; votes=[]; | |
| $("total").textContent=pairs.length; | |
| $("intro").classList.add("hide"); $("study").classList.remove("hide"); | |
| render(); | |
| }catch(e){ $("introErr").textContent="Network error."; $("start").disabled=false; } | |
| } | |
| function render(){ | |
| const p=pairs[i]; | |
| $("pos").textContent=i+1; $("prog").style.width=(100*i/pairs.length)+"%"; | |
| $("prompt").textContent=p.prompt; $("imgL").src=p.left; $("imgR").src=p.right; | |
| window.scrollTo(0,0); | |
| } | |
| async function choose(side){ | |
| if(busy) return; | |
| votes.push({pair_id:pairs[i].pair_id, choice:side}); | |
| i++; | |
| if(i<pairs.length){ render(); return; } | |
| busy=true; $("prog").style.width="100%"; | |
| let data; | |
| try{ | |
| const r = await fetch("/api/submit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,votes})}); | |
| data = await r.json(); | |
| if(!r.ok){ alert(data.detail||"Submit failed"); i--; votes.pop(); busy=false; return; } | |
| }catch(e){ alert("Network error on submit; please retry."); i--; votes.pop(); busy=false; return; } | |
| $("recnote").textContent = data.recorded | |
| ? "Your anonymized responses have been recorded — thank you for contributing!" | |
| : "Per your choice, your responses were NOT recorded. Here is your personal summary anyway:"; | |
| renderResults(data.results); | |
| $("study").classList.add("hide"); $("done").classList.remove("hide"); | |
| } | |
| const STYLE_LBL = {photo:"Photo", anime:"Anime", "concept-art":"Concept art", paintings:"Paintings"}; | |
| function renderResults(res){ | |
| const p=res.pref, pct=x=> x==null?"—":Math.round(100*x)+"%"; | |
| const ks=Object.keys(p.by_style); | |
| let rows=""; | |
| for(const s of ks){ | |
| const ps=p.by_style[s]||{}; | |
| rows+=`<tr><td>${STYLE_LBL[s]||s}</td><td>${pct(ps.win_rate)}</td><td>${pct(ps.tie_rate)}</td><td>${pct(ps.lose_rate)}</td></tr>`; | |
| } | |
| const table = ks.length>1 ? `<table><tr class="mut"><th>Style</th><th>Ours</th><th>No pref.</th><th>Other</th></tr>${rows}</table>` : ""; | |
| $("myresults").innerHTML = ` | |
| <p class="center">Over your <b>${p.n}</b> comparisons: you preferred <b>ours ${pct(p.win_rate)}</b>, | |
| had <b>no preference ${pct(p.tie_rate)}</b>, and preferred <b>the other ${pct(p.lose_rate)}</b> | |
| <span class="mut">(the method identity was hidden).</span></p>${table}`; | |
| } | |
| fetch("/api/info").then(r=>r.json()).then(d=>{ $("npairs").textContent=d.n_pairs; $("mins").textContent=d.est_minutes; }).catch(()=>{ $("npairs").textContent="a few dozen"; }); | |
| $("start").onclick=start; | |
| $("email").addEventListener("keydown",e=>{ if(e.key==="Enter") start(); }); | |
| $("optL").onclick=()=>choose("left"); | |
| $("optR").onclick=()=>choose("right"); | |
| $("optC").onclick=()=>choose("cannot"); | |
| document.addEventListener("keydown",e=>{ | |
| if($("study").classList.contains("hide")) return; | |
| if(e.key==="ArrowLeft"||e.key==="a"||e.key==="A") choose("left"); | |
| if(e.key==="ArrowRight"||e.key==="b"||e.key==="B") choose("right"); | |
| if(e.key==="c"||e.key==="C"||e.key==="ArrowDown"||e.key===" ") { e.preventDefault(); choose("cannot"); } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |