"use strict"; const $ = id => document.getElementById(id); const state = { manifest:null, episode:null, id:null, frame:0, decision:0, camera:0, mode:"observe", request:0, timer:null, videoReady:false, controller:null, cache:new Map(), path:null }; const video = $("video"); const reasons = { translation_threshold:"Translation keyframe", rotation_threshold:"Rotation keyframe", gripper_command_switch:"Gripper change", max_horizon:"Horizon keyframe", episode_end:"Final keyframe" }; const fmt = value => Number(value).toFixed(3); const clock = seconds => `${String(Math.floor(seconds/60)).padStart(2,"0")}:${String(Math.floor(seconds%60)).padStart(2,"0")}`; const text = (id,value) => { const element=$(id);if(element.textContent!==String(value))element.textContent=value; }; const narrowLibrary=window.matchMedia("(max-width:1100px)"); function setLibrary(open,{restoreFocus=true}={}) { open=Boolean(open&&narrowLibrary.matches); document.body.classList.toggle("library-open",open); $("library-toggle").setAttribute("aria-expanded",String(open)); $("library-toggle").setAttribute("aria-label",open?"Close episode library":"Open episode library"); $("library-backdrop").hidden=!open; $("episode-library").inert=narrowLibrary.matches&&!open; $("workspace").inert=open; if(open)$("search").focus(); else if(restoreFocus&&narrowLibrary.matches)$("library-toggle").focus(); } $("library-toggle").addEventListener("click",()=>setLibrary(!document.body.classList.contains("library-open"))); $("close-library").addEventListener("click",()=>setLibrary(false)); $("library-backdrop").addEventListener("click",()=>setLibrary(false)); narrowLibrary.addEventListener("change",()=>setLibrary(false,{restoreFocus:false})); setLibrary(false,{restoreFocus:false}); $("inspect-annotations").addEventListener("click",()=>{$("annotation-panel").scrollIntoView({block:"start"});$("annotation-panel").focus({preventScroll:true});}); function toast(message) { text("toast",message);$("toast").hidden=false; clearTimeout(state.timer);state.timer=setTimeout(()=>$("toast").hidden=true,2600); } function filteredEpisodes() { const query=$("search").value.trim().toLowerCase(),outcome=$("outcome-filter").value,split=$("split-filter").value; return state.manifest.episodes.filter(e => (outcome==="all"||e.source_outcome===outcome) && (split==="all"||e.split===split) && (!query||`${e.task} ${e.episode_id} ${e.lab} ${e.ordinal} ${e.recorded_date}`.toLowerCase().includes(query))); } function renderLibrary() { const list=$("episode-list"),scroll=list.scrollTop,episodes=filteredEpisodes(); list.replaceChildren();text("count",episodes.length); text("library-match",`${episodes.length} of ${state.manifest.episodes.length} episodes`); const selected=episodes.findIndex(e=>e.episode_id===state.id); $("previous-episode").disabled=selected<=0; $("next-episode").disabled=selected<0||selected>=episodes.length-1; if (!episodes.length) { const empty=document.createElement("div");empty.className="empty-list"; empty.textContent="No episodes match these filters.";list.append(empty); } episodes.forEach(e=>{ const card=document.createElement("button");card.className=`episode-card${e.episode_id===state.id?" active":""}`; card.dataset.episode=e.episode_id;card.setAttribute("aria-label",`Episode ${e.ordinal}: ${e.instruction_available?e.task:"Unlabeled task"}`); if(e.episode_id===state.id)card.setAttribute("aria-current","true"); const image=document.createElement("img");image.src=`./${e.thumbnail}`;image.alt="";image.loading="lazy"; const info=document.createElement("div"),title=document.createElement("p");title.className="card-title"; title.textContent=e.instruction_available?e.task:"Unlabeled task"; const meta=document.createElement("div");meta.className="card-meta"; const dot=document.createElement("i");dot.className=`status-dot ${e.source_outcome}`;dot.title=`Recorded ${e.source_outcome}`; const number=document.createElement("span");number.textContent=`${String(e.ordinal).padStart(3,"0")} · ${e.lab}`; const split=document.createElement("span");split.className="split-name";split.textContent=e.split; meta.append(dot,number,split);info.append(title,meta);card.append(image,info); card.addEventListener("click",()=>{setLibrary(false,{restoreFocus:false});$("workspace").focus({preventScroll:true});if(e.episode_id!==state.id)openEpisode(e.episode_id);});list.append(card); }); list.scrollTop=scroll; } async function openEpisode(id,initialFrame=0) { const request=++state.request; const previousId=state.episode?.metadata.episode_id; state.controller?.abort();state.controller=new AbortController(); video.pause();state.videoReady=false;state.id=id;state.mode="observe"; $("play").disabled=true; $("workspace").setAttribute("aria-busy","true");$("video-loading").hidden=false;$("video-error").hidden=true; renderLibrary(); try { let episode=state.cache.get(id); if(!episode){ const response=await fetch(`./episodes/${id}.json`,{signal:state.controller.signal}); if(!response.ok)throw new Error("Episode data could not be loaded. Select the episode to retry."); episode=await response.json(); state.cache.set(id,episode);if(state.cache.size>4)state.cache.delete(state.cache.keys().next().value); } if(request!==state.request)return; state.episode=episode;state.frame=Math.max(0,Math.min(episode.metadata.steps-1,initialFrame));state.decision=findDecision(state.frame); state.path=null;$("workspace").scrollTop=0; const meta=episode.metadata; text("episode-number",`EPISODE ${String(meta.ordinal).padStart(3,"0")}`);text("episode-lab",`${meta.lab} / ${meta.recorded_date}`); text("task-title",meta.instruction_available?meta.task:"Unlabeled manipulation episode"); $("task-title").classList.toggle("long-task",(meta.task||"").length>110); text("split-badge",meta.split);text("outcome-badge",`Recorded ${meta.source_outcome}`); $("outcome-badge").className=`badge ${meta.source_outcome}`; text("episode-details",`${meta.steps.toLocaleString()} frames · ${meta.turns} EEF decisions · ${clock(meta.duration_s)}`); $("scrubber").max=meta.steps-1; $("initial-loader").hidden=true;$("episode-workspace").hidden=false; video.poster="";video.src=`./${episode.video}`;video.load(); video.dataset.initialFrame=state.frame; renderMarkers();renderDecision();setCamera(state.camera);renderFrame(state.frame);renderSource(); $("workspace").setAttribute("aria-busy","false");saveLocation(); } catch(error) { if(request!==state.request)return; state.id=previousId||null;renderLibrary(); state.videoReady=Boolean(state.episode&&video.readyState>=1);$("play").disabled=!state.videoReady; toast(error.message);$("video-loading").hidden=true; if(!state.episode) { text("initial-loader",error.message); } $("workspace").setAttribute("aria-busy","false"); } } function findDecision(frame) { if(!state.episode)return 0; const events=state.episode.events; let result=0; for(let i=0;iframe)break;result=i;} return result; } function renderMarkers() { const box=$("decision-markers");box.replaceChildren(); state.episode.events.forEach((event,index)=>{ const marker=document.createElement("button");marker.className=`decision-marker${event.reason==="gripper_command_switch"?" gripper":""}`; marker.style.left=`${100*event.start/Math.max(1,state.episode.metadata.steps-1)}%`; marker.setAttribute("aria-label",`Go to EEF decision ${index+1}, frame ${event.start}`); marker.title=`Decision ${index+1} · ${reasons[event.reason]||"EEF target"} · frame ${event.start}`; marker.addEventListener("click",()=>jumpDecision(index));box.append(marker); }); } function renderDecision() { const event=state.episode.events[state.decision],args=event.action.arguments; text("decision-counter",`${String(state.decision+1).padStart(2,"0")} / ${state.episode.events.length}`); $("previous-decision").disabled=state.decision===0;$("next-decision").disabled=state.decision===state.episode.events.length-1; document.querySelectorAll(".decision-marker").forEach((m,i)=>m.classList.toggle("active",i===state.decision)); text("action-description",`${reasons[event.reason]||"EEF target"} · frames ${event.start}–${event.end}. Target sampled from the recorded command at frame ${event.end-1}.`); text("action-json",JSON.stringify(event.action,null,2)); $("prediction-panel").hidden=!event.prediction; text("prediction-json",event.prediction?JSON.stringify(event.prediction,null,2):""); updateSnapshots();renderAnnotations();renderMode(); } function renderAnnotations() { const episode=state.episode,annotation=episode.events[state.decision].generated_annotation; $("annotation-panel").hidden=!annotation; $("active-goal-strip").hidden=!annotation; if(!annotation)return; const generated=episode.generated_annotations,groups=generated.subgoals; const index=groups.findIndex(g=>g.id===annotation.subgoal.id); text("active-goal",annotation.subgoal.title); const select=$("subgoal-select");select.replaceChildren(); groups.forEach((group,i)=>{const option=document.createElement("option");option.value=i;option.textContent=`${String(i+1).padStart(2,"0")} · ${group.title}`;select.append(option);}); select.value=index;$("previous-subgoal").disabled=index<=0;$("next-subgoal").disabled=index>=groups.length-1; const track=$("subgoal-track");track.replaceChildren(); track.style.setProperty("--phases",Math.min(groups.length,14)); groups.forEach((group,i)=>{ const phase=document.createElement("button");phase.className=`subgoal-phase${i===index?" active":ijumpSubgoal(i));track.append(phase); }); text("subgoal-count",`${index+1} / ${groups.length}`); text("subgoal-title",annotation.subgoal.title);text("subgoal-intent",annotation.subgoal.intent); text("thinking-text",annotation.thinking.text);text("annotated-action",annotation.action_annotation.text); text("initial-scene",`${generated.initial_scene_review.initial_scene} Source frame ${generated.initial_scene_review.initial_scene_source_step}. This is an initial-scene memory, not a current object-state claim.`); const evidence=$("annotation-evidence");evidence.replaceChildren(); annotation.thinking.observed_facts.forEach(fact=>{const li=document.createElement("li");li.textContent=fact;evidence.append(li);}); const source=document.createElement("li");source.textContent=`Task source: ${generated.task_source}. Action description uses command-label frame ${annotation.evidence.target_command_step}; thinking excludes future feedback and episode outcome.`;evidence.append(source); const plan=$("task-plan");plan.replaceChildren(); generated.proposed_task_plan.steps.forEach(step=>{const li=document.createElement("li");li.textContent=step;plan.append(li);}); const alignment=annotation.subgoal.semantic_alignment; text("semantic-caveat",alignment==="unresolved"?"Object-level intent or multi-step alignment is unresolved. Subgoal boundaries describe motion phases, not confirmed semantic completion.":"Task intent and motion-phase boundaries are weak hypotheses; no calibrated confidence score is available."); text("next-check",annotation.next_observation_check);text("progress-text",annotation.progress_annotation.text); text("progress-check",`Suggested check: ${annotation.next_observation_check}`); } function jumpSubgoal(index) { const groups=state.episode?.generated_annotations?.subgoals;if(!groups)return; index=Math.max(0,Math.min(groups.length-1,index));jumpDecision(groups[index].start_turn); } let snapshotRequest=0; const snapshotDecoder=$("snapshot-decoder"); function waitMedia(target,event,timeout=12000){ return new Promise((resolve,reject)=>{ const done=()=>{clearTimeout(timer);target.removeEventListener(event,ready);target.removeEventListener("error",failed);}; const ready=()=>{done();resolve();},failed=()=>{done();reject(new Error("Snapshot media unavailable"));}; const timer=setTimeout(()=>{done();reject(new Error("Snapshot timeout"));},timeout); target.addEventListener(event,ready,{once:true});target.addEventListener("error",failed,{once:true}); }); } async function updateSnapshots() { const request=++snapshotRequest,episode=state.episode; if(!episode)return; const event=episode.events[state.decision],obs=state.mode==="feedback"?event.feedback.observation:event.observation; text("snapshot-label",`Video-derived snapshots · recorded frame ${obs.source_step}`); try { if(snapshotDecoder.dataset.episode!==episode.metadata.episode_id){ snapshotDecoder.dataset.episode=episode.metadata.episode_id;snapshotDecoder.src=`./${episode.video}`;snapshotDecoder.load(); } if(snapshotDecoder.readyState<1)await waitMedia(snapshotDecoder,"loadedmetadata"); if(request!==snapshotRequest)return; const target=Math.min(obs.source_step/state.manifest.fps,Math.max(0,snapshotDecoder.duration-.025)); if(Math.abs(snapshotDecoder.currentTime-target)>.001){ const seek=waitMedia(snapshotDecoder,"seeked");snapshotDecoder.currentTime=target;await seek; }else if(snapshotDecoder.readyState<2)await waitMedia(snapshotDecoder,"loadeddata"); if(request!==snapshotRequest)return; const canvas=document.createElement("canvas");canvas.width=320;canvas.height=180;const ctx=canvas.getContext("2d"); const width=snapshotDecoder.videoWidth/3,height=snapshotDecoder.videoHeight; for(let i=0;i<3;i++){ ctx.drawImage(snapshotDecoder,i*width,0,width,height,0,0,320,180); const image=$(`snapshot-${i}`);image.src=canvas.toDataURL("image/jpeg",.88);image.alt=`${state.manifest.camera_names[i]} at recorded frame ${obs.source_step}, decoded from compressed preview video`; } }catch(error){ if(request!==snapshotRequest)return; text("snapshot-label","Snapshots unavailable · use the main video or download source JPEG observations"); } } function renderMode() { const feedback=state.mode==="feedback",event=state.episode.events[state.decision]; $("observe-mode").classList.toggle("active",!feedback);$("feedback-mode").classList.toggle("active",feedback); $("observe-mode").setAttribute("aria-pressed",String(!feedback));$("feedback-mode").setAttribute("aria-pressed",String(feedback)); $("feedback-note").hidden=!feedback;$("residuals").hidden=!feedback; $("annotation-progress").hidden=!(feedback&&event.generated_annotation); text("media-type",feedback?"RECORDED FEEDBACK":"RECORDED OBSERVATION"); text("feedback-description",`Frame ${event.end}, after ${event.feedback.steps_elapsed} recorded steps. These frames follow the original dense demonstration commands. Object outcome is unverified.`); const residuals=$("residuals");residuals.replaceChildren(); if(feedback){ for(const label of [`Δ position ${(event.feedback.position_residual_m*100).toFixed(2)} cm`,`Δ angle ${(event.feedback.orientation_residual_rad*180/Math.PI).toFixed(1)}°`]){ const el=document.createElement("span");el.textContent=label;residuals.append(el); } } } function setCamera(camera) { state.camera=camera; const all=camera==="all";$("camera-window").classList.toggle("all",all); video.style.transform=all?"none":`translateX(${-Number(camera)*100/3}%)`; text("camera-label",all?"ALL THREE VIEWS":["EXTERIOR 1","EXTERIOR 2","WRIST"][camera]); document.querySelectorAll("[data-camera],[data-snapshot]").forEach(b=>{ const active=(b.dataset.camera??b.dataset.snapshot)===String(camera);b.classList.toggle("active",active);b.setAttribute("aria-pressed",String(active)); }); } function renderVector(id,axes,recorded,target,kind) { const box=$(id); if(!box.childElementCount)axes.forEach(()=>{ const row=document.createElement("div");row.className=kind; ["axis","recorded","target"].forEach(cls=>{const cell=document.createElement("span");cell.className=cls;row.append(cell);});box.append(row); }); axes.forEach((axis,i)=>[axis,fmt(recorded[i]),fmt(target[i])].forEach((value,column)=>{ const cell=box.children[i].children[column];if(cell.textContent!==value)cell.textContent=value; })); } function renderFrame(frame) { if(!state.episode)return; frame=Math.max(0,Math.min(state.episode.metadata.steps-1,frame));state.frame=frame; if(state.mode==="observe"){ const decision=findDecision(frame);if(decision!==state.decision){state.decision=decision;renderDecision();} } const recorded=state.episode.pose_trace[frame],target=state.episode.events[state.decision].action.arguments; text("stage-frame",`FRAME ${String(frame).padStart(4,"0")}`); text("frame-count",`${frame+1} / ${state.episode.metadata.steps.toLocaleString()} frames · nominal 15 fps`); text("timecode",`${clock(frame/state.episode.fps)} / ${clock(state.episode.metadata.duration_s)}`); $("scrubber").value=frame; $("scrubber").style.background=`linear-gradient(to right,#bf4c2b ${100*frame/Math.max(1,state.episode.metadata.steps-1)}%,#dde2d5 0)`; renderVector("pose-rows",["X","Y","Z"],recorded,target.target_pose.position_m,"pose-row"); renderVector("quaternion-values",["x","y","z","w"],recorded.slice(3,7),target.target_pose.quaternion_xyzw,"quaternion-row"); const grip=Math.max(0,Math.min(1,recorded[7])),goal=target.gripper_closure; text("gripper-state",grip<.1?"Open":grip>.9?"Closed":"Partial"); $("gripper-fill").style.width=`${100*grip}%`;$("gripper-target").style.left=`${100*goal}%`; text("gripper-value",grip.toFixed(2));text("gripper-target-value",goal.toFixed(2)); $("previous-frame").disabled=frame===0;$("next-frame").disabled=frame===state.episode.metadata.steps-1; renderPath(frame,target.target_pose.position_m); } function renderPath(frame,target) { const key=`${state.id}:${state.decision}`; if(state.path?.key!==key){ const trace=state.episode.pose_trace; let xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity; trace.forEach(p=>{xmin=Math.min(xmin,p[0]);xmax=Math.max(xmax,p[0]);ymin=Math.min(ymin,p[1]);ymax=Math.max(ymax,p[1]);}); xmin=Math.min(xmin,target[0]);xmax=Math.max(xmax,target[0]);ymin=Math.min(ymin,target[1]);ymax=Math.max(ymax,target[1]); const range=Math.max(xmax-xmin,ymax-ymin,.08),cx=(xmin+xmax)/2,cy=(ymin+ymax)/2; const project=p=>[130+(p[0]-cx)*120/range,79-(p[1]-cy)*120/range]; const points=trace.map(project),full=points.map(p=>p.map(v=>v.toFixed(1)).join(",")).join(" "); const goal=project(target); $("path-plot").innerHTML=`xy`; state.path={key,points,goal,serialized:points.map(p=>p.map(v=>v.toFixed(1)).join(","))}; } const {points,goal,serialized}=state.path,current=points[frame]; $("path-past").setAttribute("points",serialized.slice(0,frame+1).join(" ")); $("path-command").setAttribute("d",`M${current[0]} ${current[1]}L${goal[0]} ${goal[1]}`); $("path-current").setAttribute("cx",current[0]);$("path-current").setAttribute("cy",current[1]); } function renderSource() { const meta=state.episode.metadata,box=$("source-details");box.replaceChildren(); const pairs=[["Episode ID",meta.episode_id],["Dataset split",meta.split],["Task annotation",meta.instruction_available?meta.task_source.replaceAll("_"," "):"No task instruction available"],["Recording",`${meta.lab} / ${meta.recorded_date}`],["Source and license","See dataset NOTICE"]]; const alternatives=[...new Set((meta.instruction_variants||[]).filter(Boolean))].filter(value=>value!==meta.task); if(alternatives.length){pairs.push(["Alternative recorded wording",alternatives.join(" · ")]);pairs.push(["Wording note","Recorded alternatives can differ. Generated annotations use the primary instruction and do not resolve intended direction or object state."]);} pairs.forEach(([key,value])=>{const dt=document.createElement("dt"),dd=document.createElement("dd");dt.textContent=key;dd.textContent=value;box.append(dt,dd);}); } function seekFrame(frame,{pause=true}={}) { if(!state.episode)return; if(pause)video.pause(); frame=Math.max(0,Math.min(state.episode.metadata.steps-1,frame)); if(state.videoReady)video.currentTime=frame/state.episode.fps+.0001; else video.dataset.initialFrame=frame; renderFrame(frame);saveLocation(); } function jumpDecision(index) { if(!state.episode)return; index=Math.max(0,Math.min(state.episode.events.length-1,index));state.mode="observe";state.decision=index; renderDecision();seekFrame(state.episode.events[index].start); } function setMode(mode) { if(!state.episode)return; video.pause();state.mode=mode; const event=state.episode.events[state.decision];renderMode();updateSnapshots(); seekFrame(mode==="feedback"?event.end:event.start); } async function togglePlay() { if(!state.episode||!state.videoReady)return; if(!video.paused){video.pause();return;} if(state.mode==="feedback")setMode("observe"); if(state.frame>=state.episode.metadata.steps-1)seekFrame(0); try{await video.play();}catch{toast("Playback could not start. Try again.");} } function updatePlayIcon() { const playing=!video.paused; $("play").setAttribute("aria-label",playing?"Pause episode":"Play episode"); $("play-icon").innerHTML=playing?'':''; } function saveLocation() { if(state.id)history.replaceState(null,"",`#episode=${state.id}&frame=${state.frame}`); } video.addEventListener("loadedmetadata",()=>{ state.videoReady=true;$("play").disabled=false;video.playbackRate=Number($("speed").value); const frame=Number(video.dataset.initialFrame||0);video.currentTime=frame/state.episode.fps+.0001; renderFrame(frame);$("video-loading").hidden=true;$("video-error").hidden=true; }); video.addEventListener("canplay",()=>{$("video-loading").hidden=true;}); video.addEventListener("waiting",()=>{if(!video.paused)$("video-loading").hidden=false;}); video.addEventListener("playing",()=>{$("video-loading").hidden=true;}); video.addEventListener("error",()=>{state.videoReady=false;$("play").disabled=true;$("video-loading").hidden=true;$("video-error").hidden=false;}); video.addEventListener("play",updatePlayIcon);video.addEventListener("pause",()=>{updatePlayIcon();saveLocation();}); video.addEventListener("ended",()=>{updatePlayIcon();renderFrame(state.episode.metadata.steps-1);saveLocation();}); video.addEventListener("timeupdate",()=>{if(state.episode&&state.videoReady){const frame=Math.min(state.episode.metadata.steps-1,Math.floor(video.currentTime*state.episode.fps+.001));if(frame!==state.frame)renderFrame(frame);}}); if(video.requestVideoFrameCallback){ const callback=(now,metadata)=>{if(state.episode&&state.videoReady&&!video.paused){const frame=Math.floor(metadata.mediaTime*state.episode.fps+.001);if(frame!==state.frame)renderFrame(frame);}video.requestVideoFrameCallback(callback);}; video.requestVideoFrameCallback(callback); } $("play").addEventListener("click",togglePlay);video.addEventListener("click",togglePlay); $("previous-frame").addEventListener("click",()=>{state.mode="observe";renderMode();updateSnapshots();seekFrame(state.frame-1);}); $("next-frame").addEventListener("click",()=>{state.mode="observe";renderMode();updateSnapshots();seekFrame(state.frame+1);}); $("scrubber").addEventListener("input",()=>{state.mode="observe";renderMode();updateSnapshots();seekFrame(Number($("scrubber").value));}); $("previous-decision").addEventListener("click",()=>jumpDecision(state.decision-1));$("next-decision").addEventListener("click",()=>jumpDecision(state.decision+1)); [[-1,"previous-episode"],[1,"next-episode"]].forEach(([direction,id])=>$(id).addEventListener("click",()=>{ const episodes=filteredEpisodes(),index=episodes.findIndex(e=>e.episode_id===state.id); if(episodes[index+direction])openEpisode(episodes[index+direction].episode_id); })); $("subgoal-select").addEventListener("change",()=>jumpSubgoal(Number($("subgoal-select").value))); $("previous-subgoal").addEventListener("click",()=>jumpSubgoal(Number($("subgoal-select").value)-1)); $("next-subgoal").addEventListener("click",()=>jumpSubgoal(Number($("subgoal-select").value)+1)); $("observe-mode").addEventListener("click",()=>setMode("observe"));$("feedback-mode").addEventListener("click",()=>setMode("feedback")); $("speed").addEventListener("change",()=>{video.playbackRate=Number($("speed").value);}); $("retry-video").addEventListener("click",()=>{video.load();$("video-error").hidden=true;$("video-loading").hidden=false;}); document.querySelectorAll("[data-camera]").forEach(b=>b.addEventListener("click",()=>setCamera(b.dataset.camera==="all"?"all":Number(b.dataset.camera)))); document.querySelectorAll("[data-snapshot]").forEach(b=>b.addEventListener("click",()=>setCamera(Number(b.dataset.snapshot)))); $("fullscreen").addEventListener("click",async()=>{try{if(document.fullscreenElement)await document.exitFullscreen();else if($("stage").requestFullscreen)await $("stage").requestFullscreen();else toast("Fullscreen isn’t available in this browser.");}catch{toast("Fullscreen isn’t available in this browser.");}}); ["search","outcome-filter","split-filter"].forEach(id=>$(id).addEventListener(id==="search"?"input":"change",()=>{if(state.manifest)renderLibrary();})); $("reset-filters").addEventListener("click",()=>{$("search").value="";$("outcome-filter").value="all";$("split-filter").value="all";renderLibrary();}); $("download").addEventListener("click",()=>{ if(!state.episode)return; const blob=new Blob([JSON.stringify(state.episode,null,2)],{type:"application/json"}),url=URL.createObjectURL(blob),a=document.createElement("a"); a.href=url;a.download=`droid-${state.id}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);toast("Episode JSON downloaded."); }); document.addEventListener("keydown",event=>{ if(document.body.classList.contains("library-open")){ if(event.key==="Escape"){event.preventDefault();setLibrary(false);} if(event.key==="Tab"){ const elements=[...$("episode-library").querySelectorAll('button:not(:disabled),input,select')].filter(el=>el.offsetParent!==null); const first=elements[0],last=elements[elements.length-1]; if(event.shiftKey&&(document.activeElement===first||!$("episode-library").contains(document.activeElement))){event.preventDefault();last.focus();} else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();} } return; } if(["INPUT","SELECT","TEXTAREA","SUMMARY","BUTTON","A"].includes(event.target.tagName)||event.ctrlKey||event.metaKey||event.altKey||!state.episode)return; if(event.code==="Space"){event.preventDefault();togglePlay();} else if(event.key==="ArrowLeft"||event.key==="ArrowRight"){ event.preventDefault();state.mode="observe";renderMode();updateSnapshots();seekFrame(state.frame+(event.key==="ArrowLeft"?-1:1)); }else if(event.key.toLowerCase()==="j")jumpDecision(state.decision-1); else if(event.key.toLowerCase()==="k")jumpDecision(state.decision+1); }); async function boot() { try { const response=await fetch("./manifest.json");if(!response.ok)throw new Error("The dataset library could not be loaded."); state.manifest=await response.json(); text("dataset-summary",`${state.manifest.episodes.length} episodes · ${state.manifest.total_frames.toLocaleString()} frames · 3 cameras`); renderLibrary(); const hash=new URLSearchParams(location.hash.slice(1)),id=hash.get("episode"); const episode=state.manifest.episodes.find(e=>e.episode_id===id)||state.manifest.episodes[0]; await openEpisode(episode.episode_id,Math.max(0,Number(hash.get("frame"))||0)); }catch(error){text("initial-loader",error.message);$("workspace").setAttribute("aria-busy","false");} } boot();