bs82's picture
deploy sd35m: 40 pairs
edb73aa verified
Raw
History Blame Contribute Delete
7.64 kB
<!doctype html>
<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>