Download static/science.js from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/static/science.js
- Command line
-
hf download hf://spaces/wzender/mathly/static/science.js
-
curl -L -o science.js https://huggingface.co/spaces/wzender/mathly/resolve/main/static/science.js
11 kB
| /* The science path (app/science.py): the unit ืืืืืช and its riddle skills, solved by the class together. | |
| The host opens a vote and shows its code on the board; students join with the code on their own devices and vote. | |
| Every screen asks the server for the room about once a second and a half. Votes stay hidden until the host checks; | |
| then the rocket is launched with the majority's fuel: it flies, falls back (too little fuel) or stays on the pad | |
| (too heavy). math.js opens this from the subject picker: MathlyScience.show(container, {back}). */ | |
| (()=>{ | |
| const css=document.createElement("link"); css.rel="stylesheet"; css.href=document.currentScript.src.replace("science.js","science.css"); | |
| document.head.append(css); | |
| const esc=s=>String(s??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); | |
| const fmt=s=>esc(s).replace(/\$([^$]+)\$/g,'<span class="m" dir="ltr">$1</span>'); // $โฆ$: math, set left to right | |
| const store={get:(k,s=localStorage)=>{ try{ return s.getItem(k); }catch{ return null; } }, | |
| set:(k,v,s=localStorage)=>{ try{ s.setItem(k,v); }catch{} }}; | |
| // one vote per device: a random id kept on it | |
| const VOTER=(()=>{ let id=store.get("mathly.voter"); if(!id||id.length<8){ id=(crypto.randomUUID?.()||String(Math.random()).slice(2)+Date.now()).replace(/-/g,""); store.set("mathly.voter",id); } return id; })(); | |
| const hostKey=code=>store.get(`mathly.science.host:${code}`,sessionStorage)||""; | |
| const POLL=1500; | |
| let box=null, PATH=null, ROOM=null, SHOWN="", timer=null, launched=new Set(), busy=false, msg=""; | |
| async function call(url,options={}){ | |
| const code=ROOM?.code; | |
| const r=await fetch(url,{...options,headers:{"Content-Type":"application/json",...(code?{"X-Host":hostKey(code)}:{})}}); | |
| const body=await r.json().catch(()=>({})); | |
| if(!r.ok){ const e=new Error(body.detail||`ืืฉืจืช ืขื ื ${r.status}`); e.status=r.status; throw e; } | |
| return body; | |
| } | |
| const SAY={"no vote with this code":"ืืื ืืฆืืขื ืคืชืืื ืขื ืืงืื ืืื. ืืืงื ืืช ืืงืื ืื ืกื ืฉืื.","no votes yet":"ืขืื ืืฃ ืืื ืื ืืฆืืืข.", | |
| "this riddle is no longer open for votes":"ืืืฆืืขื ืขื ืืืืื ืืืืช ืืืจ ื ืกืืจื.","this vote is full":"ืืืฆืืขื ืืืื.", | |
| "too many votes are open right now":"ืืฉ ืขืืฉืื ืืืชืจ ืืื ืืฆืืขืืช ืคืชืืืืช. ื ืกื ืฉืื ืืขืื ืืื ืืงืืช."}; | |
| const say=e=>SAY[e.message]||e.message||"ืืฉืื ืืฉืชืืฉ. ื ืกื ืฉืื."; | |
| function stop(){ clearTimeout(timer); timer=null; } | |
| const alive=()=>box&&box.isConnected&&box.querySelector(".sci"); | |
| function draw(html){ box.innerHTML=`<div class="sci">${html}</div>`; } | |
| const header=(title,sub,act='<button class="mx-btn" data-act="subjects">ืืื ืืืงืฆืืขืืช</button>')=> | |
| `<header class="mx-top"><div><h1>${esc(title)}</h1><p class="mx-sub">${esc(sub)}</p></div>${act}</header>`; | |
| async function show(container,opts={}){ | |
| if(box!==container){ container.addEventListener("click",onClick); } | |
| box=container; stop(); ROOM=null; msg=""; | |
| draw(`${header("ืืืขืื","ืืืงืฆืืขืืช ืฉืื")}<p class="mx-note">ืืืขื ืื ืืช ืืืกืืืโฆ</p>`); | |
| if(!PATH){ | |
| try{ PATH=await call("/api/science"); } | |
| catch(e){ return draw(`${header("ืืืขืื","ืืืงืฆืืขืืช ืฉืื")}<p class="mx-note">ืื ืืฆืืื ื ืืืขืื ืืช ืืืกืืื: ${esc(say(e))}</p>`); } | |
| } | |
| drawPath(); | |
| } | |
| function drawPath(){ | |
| stop(); ROOM=null; | |
| draw(`${header(PATH.subject,"ืืืงืฆืืขืืช ืฉืื")}${PATH.units.map(u=>`<section class="sci-unit"> | |
| <div class="sci-unit-head"><span aria-hidden="true">๐งฉ</span><div><h2>${esc(u.name)}</h2><p>${esc(u.description||"")}</p></div></div> | |
| ${PATH.skills.filter(s=>s.unit===u.id).map(s=>`<button class="sci-skill" data-sci="lobby" data-skill="${esc(s.id)}"> | |
| <span class="sci-skill-icon" aria-hidden="true">๐</span><span><b>${esc(s.name)}</b><small>${esc(s.description||"")}</small><small>${s.riddles} ืืืืืช ยท ืืฆืืขื ืืืชืชืืช</small></span><i aria-hidden="true">โน</i></button>`).join("")} | |
| </section>`).join("")}`); | |
| } | |
| function drawLobby(skillId){ | |
| stop(); ROOM=null; | |
| const s=PATH.skills.find(x=>x.id===skillId); | |
| draw(`${header(s.name,"ืืืืืช ยท ืืืขืื",'<button class="mx-btn" data-sci="path">ืืืจื</button>')} | |
| <section class="sci-card"><h2>ืคืชืืืช ืืฆืืขื</h2><p>ืืืืจื ืื ืืื ืฉืืฆืื ืขื ืืืื: ืคืืชืืื ืืฆืืขื, ืืืงืืืื ืงืื ืฉืืืืชื ืืฆืืจืคืช ืืืชื.</p> | |
| <button class="mx-btn solid" data-sci="open" data-skill="${esc(s.id)}"${busy?" disabled":""}>ืคืชืืืช ืืฆืืขื ืืืืชื</button></section> | |
| <section class="sci-card"><h2>ืืฆืืจืคืืช ืืืฆืืขื</h2><p>ืืงืืืื ืืช ืืงืื ืฉืืืคืืข ืขื ืืืื.</p> | |
| <form class="sci-join" data-sci-join><input name="code" inputmode="numeric" autocomplete="off" maxlength="4" pattern="[0-9]{4}" placeholder="ืงืื ืื 4 ืกืคืจืืช" aria-label="ืงืื ืืืฆืืขื" required> | |
| <button class="mx-btn solid"${busy?" disabled":""}>ืืฆืืจืคืืช</button></form></section> | |
| ${msg?`<p class="sci-msg" role="alert">${esc(msg)}</p>`:""}`); | |
| box.querySelector("[data-sci-join]").onsubmit=e=>{ e.preventDefault(); join(e.target.code.value.trim(),s.id); }; | |
| } | |
| async function openRoom(skillId){ | |
| busy=true; msg=""; drawLobby(skillId); | |
| try{ const st=await call("/api/science/rooms",{method:"POST",body:JSON.stringify({skill:skillId})}); | |
| store.set(`mathly.science.host:${st.code}`,st.host_key,sessionStorage); ROOM={code:st.code,skill:skillId}; busy=false; render(st); poll(); } | |
| catch(e){ busy=false; msg=say(e); drawLobby(skillId); } | |
| } | |
| async function join(code,skillId){ | |
| if(!/^\d{4}$/.test(code)){ msg="ืืงืื ืืื 4 ืกืคืจืืช."; return drawLobby(skillId); } | |
| busy=true; msg=""; ROOM={code,skill:skillId}; | |
| try{ const st=await call(`/api/science/rooms/${code}?voter=${VOTER}`); busy=false; render(st); poll(); } | |
| catch(e){ busy=false; ROOM=null; msg=say(e); drawLobby(skillId); } | |
| } | |
| function poll(){ | |
| stop(); | |
| timer=setTimeout(async()=>{ | |
| if(!ROOM||!alive()) return stop(); | |
| try{ render(await call(`/api/science/rooms/${ROOM.code}?voter=${VOTER}`)); } | |
| catch(e){ if(e.status===404){ msg="ืืืฆืืขื ืืกืชืืืื."; return drawLobby(ROOM.skill); } } | |
| poll(); | |
| },POLL); | |
| } | |
| async function act(url,body){ | |
| try{ const st=await call(url,{method:"POST",...(body?{body:JSON.stringify(body)}:{})}); ROOM.err=""; render(st,true); } | |
| catch(e){ ROOM.err=say(e); render(ROOM.last,true); } | |
| } | |
| const OUT={flies:["ืืืื ืืืจืื! ๐","flies"],falls:["ืืืืง ื ืืืจ ืืืืฆืข ืืืจื, ืืืืื ื ืคื ืืืืจื","falls"],stuck:["ืืืื ืืื ืืื ืืื ืื ืืื ืืฉืืืืจ","stuck"]}; | |
| function rocket(st){ | |
| const key=`${st.code}:${st.n}`, now=st.phase==="checked"&&!launched.has(key); | |
| if(now) launched.add(key); | |
| const cls=st.phase!=="checked"?"":`${now?"go":"end"}-${st.outcome}`; | |
| return `<div class="sci-stage ${cls}" aria-hidden="true"><div class="sci-sky"></div> | |
| <svg class="sci-rocket" viewBox="0 0 60 140"><g class="sci-flame"><path d="M20 112 Q30 150 40 112Z" fill="#FF8B5C"/><path d="M25 112 Q30 136 35 112Z" fill="#F7C948"/></g> | |
| <path d="M30 6 C44 22 46 50 46 80 V108 H14 V80 C14 50 16 22 30 6Z" fill="#EAF0FF"/><path d="M30 6 C38 15 42 26 44 36 H16 C18 26 22 15 30 6Z" fill="#F08A96"/> | |
| <circle cx="30" cy="56" r="8" fill="#6FB3D2" stroke="#27335F" stroke-width="3"/><path d="M14 84 L2 110 H14Z M46 84 L58 110 H46Z" fill="#F08A96"/><rect x="18" y="106" width="24" height="6" rx="2" fill="#5B6694"/></svg> | |
| <div class="sci-smoke"><i></i><i></i><i></i></div><div class="sci-pad"></div></div>`; | |
| } | |
| function render(st,force=false){ | |
| if(!ROOM||!st) return; | |
| const sig=JSON.stringify(st)+(ROOM.err||""); | |
| if(!force&&sig===SHOWN&&alive()) return; | |
| SHOWN=sig; ROOM.last=st; | |
| const done=st.phase==="checked", total=st.votes, counts=st.counts||[]; | |
| const choices=st.choices.map((text,i)=>{ | |
| const pct=done&&total?Math.round(100*counts[i]/total):0; | |
| const cls=["sci-choice",st.mine===i&&"mine",done&&i===st.picked&&"picked",done&&i===st.right&&"right",st.tie.includes(i)&&"tied"].filter(Boolean).join(" "); | |
| return `<button class="${cls}" data-sci="vote" data-choice-i="${i}"${done?" disabled":""} aria-pressed="${st.mine===i}"> | |
| ${done?`<span class="sci-bar" style="width:${pct}%"></span>`:""}<b>โฝ ${esc(text)}</b>${done?`<small>${counts[i]===1?"ืงืื ืืื":`${counts[i]} ืงืืืืช`}</small>`:st.mine===i?"<small>ืืืฆืืขื ืฉืื</small>":""}</button>`; | |
| }).join(""); | |
| const tie=st.tie.length&&!done?`<p class="sci-msg">ืชืืงื ืืื ${st.tie.map(i=>esc(st.choices[i])).join(" ืืืื ")}. ืืคืฉืจ ืืฉื ืืช ืืฆืืขื ืืืืืืง ืฉืื.</p>`:""; | |
| const result=done?`<section class="sci-result ${OUT[st.outcome][1]}" role="status"><h3>${OUT[st.outcome][0]}</h3> | |
| <p>ืืจืื ืืืจ <b>${esc(st.choices[st.picked])}</b> (${counts[st.picked]} ืืชืื ${total}).</p><p>${fmt(st.why)}</p> | |
| ${st.picked!==st.right?`<p>ืืชืฉืืื ืฉืืืืชื ืืฆืืืื: <b>${esc(st.choices[st.right])}</b></p>`:""}</section>`:""; | |
| const host=st.host?`<div class="sci-host">${done | |
| ?(st.n<st.of?'<button class="mx-btn solid" data-sci="next">ืืืืื ืืืื</button>':'<button class="mx-btn solid" data-sci="path">ืกืืื</button>') | |
| :`<button class="mx-btn solid" data-sci="check"${total?"":" disabled"}>ืืืืงื: ืฉืืืืจ ืขื ืชืฉืืืช ืืจืื</button>`}</div>`:""; | |
| draw(`${header(st.name,`ืืืื ${st.n} ืืชืื ${st.of}`,'<button class="mx-btn" data-sci="leave">ืืฆืืื ืืืืฆืืขื</button>')} | |
| <div class="sci-code${st.host?" big":""}"><span>${st.host?"ืืงืื ืืืฆืืจืคืืช":"ืงืื ืืืฆืืขื"}</span><b dir="ltr">${esc(st.code)}</b></div> | |
| <section class="sci-card sci-riddle"><h2>${esc(st.title)}</h2><p>${fmt(st.story)}</p><p class="sci-q">${esc(st.question)}</p></section> | |
| ${rocket(st)}${result} | |
| <div class="sci-choices">${choices}</div> | |
| <p class="sci-count" aria-live="polite">${done?`${total} ืืฆืืืขื`:`${total} ${total===1?"ืืฆืืืข":"ืืฆืืืขื"} ืขื ืขืืฉืื ยท ืืชืืฆืืืช ืืืืฉืคื ืืืืืงื`}</p> | |
| ${tie}${ROOM.err?`<p class="sci-msg" role="alert">${esc(ROOM.err)}</p>`:""}${host}`); | |
| } | |
| function onClick(e){ | |
| const t=e.target.closest("[data-sci]"); if(!t||!box.contains(t)) return; | |
| switch(t.dataset.sci){ | |
| case "path": return drawPath(); | |
| case "lobby": msg=""; return drawLobby(t.dataset.skill); | |
| case "open": return openRoom(t.dataset.skill); | |
| case "leave": { const s=ROOM?.skill; stop(); ROOM=null; msg=""; return drawLobby(s); } | |
| case "vote": return act(`/api/science/rooms/${ROOM.code}/vote`,{voter:VOTER,choice:+t.dataset.choiceI,n:ROOM.last.n}); | |
| case "check": return act(`/api/science/rooms/${ROOM.code}/check?voter=${VOTER}`); | |
| case "next": return act(`/api/science/rooms/${ROOM.code}/next?voter=${VOTER}`); | |
| } | |
| } | |
| window.MathlyScience={show,stop}; | |
| })(); | |