const $=id=>document.getElementById(id); let catalog,selected,session,player,polling=false,busy=false; let controlHint="Click the scene to look around."; const stored=sessionStorage.getItem('wab-session'); try{session=stored?JSON.parse(stored):null;}catch{session=null;} function save(){sessionStorage.setItem('wab-session',JSON.stringify(session));} async function api(path,options={}){ const response=await fetch(path,{...options,headers:{'Content-Type':'application/json',...(session?{Authorization:`Bearer ${session.token}`} : {}),...options.headers}}); const data=await response.json(); if(!response.ok)throw new Error(data.detail||'Unable to connect. Please try again.');return data; } function cover(title,message){$('cover').hidden=false;$('cover-title').textContent=title;$('cover-message').textContent=message;} function disconnect(){controlHint="Click the scene to look around.";player?.close();player=null;$('end').disabled=true;} function buttons(){ $('start').disabled=!catalog?.available||busy||!!session; $('start').textContent=session?'Connecting…':'Enter scene ↗'; $('end').disabled=!session||busy; } async function end(){if(session){try{await api(`/api/session/${session.id}`,{method:'DELETE'});}catch{}session=null;save();}disconnect();buttons();} function showCase(c){selected=c;$('family').textContent=c.family;$('scene').textContent=c.scene;$('answer').hidden=true;$('reveal').textContent='Reveal explanation';document.querySelectorAll('.case').forEach(el=>{const active=el.dataset.id===c.id;el.classList.toggle('active',active);el.setAttribute('aria-pressed',String(active));});cover(catalog.available?'A familiar room. A surprising detail.':'Exploration is being prepared',catalog.available?'Move closer. Change your viewpoint. Test how the world responds.':catalog.message);$('badge').textContent=catalog.available?'Ready':'Preparing';$('status').textContent=catalog.available?'Explore freely. Sessions last up to 10 minutes so everyone gets a turn.':catalog.message;buttons();} async function choose(c){if(busy)return;busy=true;buttons();await end();showCase(c);busy=false;buttons();} async function start(){if(busy||session)return;busy=true;buttons();try{session=await api('/api/session',{method:'POST',body:JSON.stringify({case_id:selected.id})});save();$('badge').textContent='Queued';cover('Your exploration is starting','Preparing a fresh Unreal scene. This may take a moment.');}catch(e){$('status').textContent=e.message;}finally{busy=false;buttons();}await poll();} async function poll(){if(!session||polling||busy)return;polling=true;const id=session.id;try{const state=await api(`/api/session/${id}/heartbeat`,{method:'POST'});if(session?.id!==id)return;session={...session,...state};save();if(state.state==='queued'){cover('A world is waiting for you',`You are number ${state.position} in the queue.`);$('badge').textContent='Queued';$('status').textContent='Keep this tab open. You can cancel with End session.';}else if(state.state==='starting'){cover('Opening the scene','Unreal is loading your challenge.');$('badge').textContent='Loading';$('status').textContent='Loading your Unreal scene…';}else if(state.state==='ready'){if(!player){const {connectPlayer}=await import('/static/player.js');if(session?.id!==id)return;$('cover').hidden=true;$('badge').textContent='Connecting';player=connectPlayer($('player'),session,(s)=>{if(s==='live'){$('badge').textContent='Live';}else if(s==='disconnected'){$('badge').textContent='Disconnected';$('status').textContent='Connection lost. End the session and try again.';}else{controlHint=s;$('status').textContent=s;}});}$('status').textContent=`${controlHint} ${Math.floor(state.seconds_left/60)}:${String(state.seconds_left%60).padStart(2,'0')} remaining · E interacts with supported objects.`;}}catch(e){if(session?.id===id){disconnect();session=null;save();cover('The session has ended','Start again to explore a fresh scene.');$('badge').textContent='Ready';$('status').textContent=e.message;buttons();}}finally{polling=false;}} $('start').onclick=start; $('end').onclick=async()=>{await end();showCase(selected);}; $('reveal').onclick=async()=>{try{const caseId=selected.id;const a=await api(`/api/answer/${caseId}`);if(selected.id!==caseId)return;$('criteria').textContent=a.criteria;$('expected').textContent=`Expected: ${a.expected}`;$('steps').textContent=`Try this: ${a.steps}`;$('answer').hidden=false;$('reveal').textContent='Explanation shown';}catch(e){$('status').textContent=e.message;}}; $('hide-answer').onclick=()=>{$('answer').hidden=true;$('reveal').textContent='Reveal explanation';}; $('fullscreen').onclick=async()=>{try{if(document.fullscreenElement)await document.exitFullscreen();else await $('viewport').requestFullscreen();}catch{$('status').textContent='Open the Space in a new tab to use fullscreen.';}}; try{catalog=await api('/api/catalog');for(const c of catalog.cases){const b=document.createElement('button');b.className='case';b.dataset.id=c.id;b.setAttribute('aria-pressed','false');const n=document.createElement('span');n.className='number';n.textContent=String(c.number).padStart(2,'0');const text=document.createElement('span');const title=document.createElement('strong');title.textContent=c.family;const scene=document.createElement('small');scene.textContent=c.scene;text.append(title,scene);b.append(n,text);b.onclick=()=>choose(c);$('cases').append(b);}showCase(catalog.cases.find(c=>c.id===session?.case_id)||catalog.cases[0]);if(!catalog.available){session=null;save();buttons();cover('Exploration is being prepared',catalog.message);}else await poll();}catch(e){cover('Unable to load the challenges','Please reload this page.');$('status').textContent=e.message;} setInterval(poll,4000);