Spaces:
Running
Running
Download classroom.js from Superboss1976/arff-instructor: direct link, hf CLI and curl.
- Browser
- Download file 15.7 kB
-
https://huggingface.co/spaces/Superboss1976/arff-instructor/resolve/main/classroom.js
- Command line
-
hf download hf://spaces/Superboss1976/arff-instructor/classroom.js
-
curl -L -o classroom.js https://huggingface.co/spaces/Superboss1976/arff-instructor/resolve/main/classroom.js
15.7 kB
| import {classroomCourses} from './classroom-data.js'; | |
| import {initialSession,transition,instructorCommand,suggestSection} from './classroom-state.mjs'; | |
| const $=id=>document.getElementById(id); | |
| let course=classroomCourses[0],state=initialSession(course.id),scripts=new Map(),transcripts=[],assets=[],airfield,marshaller,instructor,recognition,listening=false,speechRun=0,utterance,localNode=false,scenePaused=matchMedia('(prefers-reduced-motion: reduce)').matches,sceneGeneration=0,requestGeneration=0; | |
| const modeLabels={instructor:'INSTRUCTOR LEADS',prof:'PROF PRESENTING',paused:'PROF PAUSED',discussion:'CLASS DISCUSSION'}; | |
| const speechOK='speechSynthesis' in window && 'SpeechSynthesisUtterance' in window; | |
| const Recognition=window.SpeechRecognition||window.webkitSpeechRecognition; | |
| const key=()=>course.id+':'+state.section; | |
| const section=()=>course.sections[state.section]; | |
| const script=()=>scripts.get(key())??section().script; | |
| function dispatch(event){state=transition(state,event,course.sections.length);renderControls();} | |
| function stopVoice(){speechRun++;window.speechSynthesis?.cancel();instructor?.speak(false);utterance=null;} | |
| function stopListening(message='Microphone off. Transcript stays in this tab.'){ | |
| listening=false;try{recognition?.abort();}catch{}$('listen').textContent='Start listening';$('mic-status').textContent=message; | |
| } | |
| function renderControls(){ | |
| $('mode-pill').textContent='● '+modeLabels[state.mode];$('mode-pill').dataset.mode=state.mode; | |
| $('handover').disabled=state.mode==='prof'||state.mode==='paused';$('pause-prof').disabled=!['prof','paused'].includes(state.mode);$('pause-prof').textContent=state.mode==='paused'?'Resume Prof':'Pause Prof'; | |
| $('previous').disabled=state.section===0;$('next').disabled=state.section===course.sections.length-1; | |
| $('progress-label').textContent=`${state.presented.length} / ${course.sections.length} sections presented`; | |
| $('narration-status').textContent=({instructor:'Instructor in control',prof:'Presenting this section only',paused:'Paused — resume or take back control',discussion:'Section finished — instructor leads discussion'})[state.mode]; | |
| document.querySelectorAll('[data-section]').forEach(b=>b.setAttribute('aria-current',String(Number(b.dataset.section)===state.section))); | |
| } | |
| function render(){ | |
| $('course-status').textContent=course.status;$('step-label').textContent=`${course.code} / SECTION ${state.section+1} OF ${course.sections.length}`; | |
| $('lesson-title').textContent=section().title;$('lesson-objective').textContent=section().objective;$('scene-topic').textContent=section().title; | |
| $('scene-label').textContent=course.scene==='marshalling'?'SIGNAL STUDY · ILLUSTRATIVE RIG':'ILLUSTRATIVE APRON · NOT A SURVEYED FAUT MAP'; | |
| $('spoken').textContent=script();$('script').value=script();$('script').disabled=['prof','paused'].includes(state.mode); | |
| $('course-source').href=course.source;$('signal-source').hidden=!section().reference;if(section().reference)$('signal-source').href=section().reference; | |
| $('question').textContent=section().prompt;$('answers').replaceChildren(); | |
| section().options.forEach((label,i)=>{const b=document.createElement('button');b.textContent=label;b.classList.toggle('selected',state.answers[state.section]===i);b.onclick=()=>{dispatch({type:'ANSWER',choice:i});render();$('feedback').textContent=(i===section().answer?'Suggested discussion response. ':'Discuss this choice. ')+section().feedback;};$('answers').append(b);}); | |
| const answer=state.answers[state.section];$('feedback').textContent=answer===undefined?'':(answer===section().answer?'Suggested discussion response. ':'Discuss this choice. ')+section().feedback; | |
| marshaller?.signal(section().signal||'identify');airfield?.setTrainingMode(course.scene);if(section().route!==undefined)airfield?.moveVehicle(section().route);if(section().mission!==undefined)airfield?.setMission(section().mission); | |
| renderControls(); | |
| } | |
| async function setupScene(){ | |
| const generation=++sceneGeneration; | |
| // Keep a single WebGL apron instance alive and switch modes rather than leaking canvases. | |
| try{if(course.scene==='marshalling'){ | |
| if(airfield){$('classroom-scene').querySelector('canvas[data-airfield]')?.setAttribute('hidden','');airfield.setPaused(true);} | |
| if(!marshaller){const {createMarshaller}=await import('./marshaller.js');if(generation!==sceneGeneration)return;marshaller=createMarshaller($('classroom-scene'));$('classroom-scene').querySelector('canvas').dataset.marshaller='true';} | |
| $('classroom-scene').querySelector('canvas[data-marshaller]')?.removeAttribute('hidden');marshaller.setPaused(scenePaused);marshaller.signal(section().signal||'identify'); | |
| }else{ | |
| if(marshaller){$('classroom-scene').querySelector('canvas[data-marshaller]')?.setAttribute('hidden','');marshaller.setPaused(true);} | |
| if(!airfield){const {createAirfield}=await import('./scene.js?v=classroom-20260920');if(generation!==sceneGeneration)return;airfield=createAirfield($('classroom-scene'),()=>{},{fire:course.scene==='arff'});$('classroom-scene').querySelector('canvas').dataset.airfield='true';} | |
| $('classroom-scene').querySelector('canvas[data-airfield]')?.removeAttribute('hidden');airfield.setPaused(scenePaused);airfield.setTrainingMode(course.scene);if(section().route!==undefined)airfield.moveVehicle(section().route); | |
| } | |
| $('scene-fallback').hidden=true; | |
| }catch{$('scene-fallback').hidden=false;} | |
| } | |
| function chooseSection(index){stopVoice();stopListening();dispatch({type:'SECTION',index});render();} | |
| function chooseCourse(id){stopVoice();stopListening();requestGeneration++;course=classroomCourses.find(c=>c.id===id)||classroomCourses[0];state=initialSession(course.id);$('lesson-list').replaceChildren();$('library-results').replaceChildren();$('suggestion').replaceChildren();course.sections.forEach((s,i)=>{const b=document.createElement('button');b.dataset.section=i;const n=document.createElement('span');n.textContent=String(i+1).padStart(2,'0');const title=document.createElement('span');title.textContent=s.title;b.append(n,title);b.onclick=()=>chooseSection(i);$('lesson-list').append(b);});render();setupScene();} | |
| function present(){ | |
| stopVoice();stopListening('Microphone off while Prof presents — prevents Prof hearing his own narration.'); | |
| if(!speechOK){$('narration-status').textContent='Device narration unavailable. Use the script with instructor control.';return;} | |
| dispatch({type:'HANDOVER'});$('script').disabled=true; | |
| const run=speechRun;utterance=new SpeechSynthesisUtterance(script());utterance.lang='en-GB';utterance.rate=.93; | |
| utterance.onstart=()=>{if(run===speechRun)instructor?.speak(true);}; | |
| utterance.onend=()=>{if(run!==speechRun)return;instructor?.speak(false);dispatch({type:'NARRATION_END'});$('script').disabled=false;}; | |
| utterance.onerror=e=>{if(run!==speechRun)return;instructor?.speak(false);dispatch({type:'TAKE_BACK'});$('script').disabled=false;$('narration-status').textContent='Narration unavailable. Instructor in control; script remains visible.';}; | |
| window.speechSynthesis.speak(utterance); | |
| } | |
| function takeBack(){stopVoice();stopListening();dispatch({type:'TAKE_BACK'});$('script').disabled=false;} | |
| function togglePause(){if(state.mode==='paused'){window.speechSynthesis?.resume();instructor?.speak(true);dispatch({type:'RESUME'});}else if(state.mode==='prof'){window.speechSynthesis?.pause();instructor?.speak(false);dispatch({type:'PAUSE'});}} | |
| function follow(text,origin){ | |
| text=text.trim().slice(0,2000);if(!text)return;transcripts.push({time:new Date().toISOString(),origin,text});transcripts=transcripts.slice(-100);$('transcript').textContent=transcripts.map(t=>t.text).join('\n');$('transcript').scrollTop=$('transcript').scrollHeight; | |
| const command=instructorCommand(text); | |
| if(command&&$('voice-commands').checked){if(command==='HANDOVER')present();if(command==='PAUSE'){stopVoice();stopListening();dispatch({type:'TAKE_BACK'});$('script').disabled=false;}if(command==='TAKE_BACK')takeBack();return;} | |
| const index=suggestSection(text,course.sections);$('suggestion').replaceChildren(); | |
| if(index!==null){const p=document.createElement('p');p.textContent='Related section: '+course.sections[index].title;const b=document.createElement('button');b.textContent='Show this section';b.onclick=()=>chooseSection(index);$('suggestion').append(p,b);} | |
| else $('suggestion').textContent=command?'Voice commands are off. Use the instructor buttons or enable commands.':'Note captured. No strong section match; instructor keeps control.'; | |
| } | |
| function startListening(){ | |
| if(listening){stopListening();return;}if(!Recognition){$('mic-status').textContent='Speech recognition is unavailable in this browser. Type a note below.';return;} | |
| if(!$('mic-consent').checked){$('mic-status').textContent='Confirm microphone use with the class using the checkbox above.';return;} | |
| takeBack();recognition=new Recognition();recognition.lang='en-ZA';recognition.continuous=true;recognition.interimResults=false; | |
| recognition.onresult=e=>{if(!listening)return;for(let i=e.resultIndex;i<e.results.length;i++)if(e.results[i].isFinal)follow(e.results[i][0].transcript,'microphone');}; | |
| recognition.onerror=e=>stopListening('Listening stopped: '+e.error+'. You can type a note below.'); | |
| recognition.onend=()=>{if(listening)stopListening('Listening ended. Press Start listening when ready.');}; | |
| try{listening=true;recognition.start();$('listen').textContent='■ Stop listening';$('mic-status').textContent='● Listening through browser speech service. No audio recording is saved by this app.';}catch{stopListening('Microphone could not start. Use typed notes.');} | |
| } | |
| function showAsset(asset){const p=$('media-preview');p.replaceChildren();if(asset.kind==='text'){const t=document.createElement('p');t.textContent=asset.text.slice(0,1800);p.append(t);}else{const el=document.createElement(asset.kind);el.src=asset.url;if(asset.kind==='img')el.alt=asset.name;else el.controls=true;p.append(el);}} | |
| $('material-file').onchange=async e=>{for(const file of [...e.target.files].slice(0,20)){if(assets.length>=30)break;const kind=file.type.startsWith('image/')?'img':file.type.startsWith('video/')?'video':file.type.startsWith('audio/')?'audio':/\.(md|txt)$/i.test(file.name)?'text':null;if(!kind)continue;if(kind==='text'&&file.size>2_000_000){$('library-results').textContent='Text notes must be under 2 MB.';continue;}const asset={name:file.name,kind};if(kind==='text')asset.text=await file.text();else asset.url=URL.createObjectURL(file);assets.push(asset);const b=document.createElement('button');b.textContent=file.name;b.onclick=()=>showAsset(asset);$('materials').append(b);}e.target.value='';}; | |
| function resultCard(title,text){const a=document.createElement('article');const h=document.createElement('b');h.textContent=title;const p=document.createElement('p');p.textContent=text;a.append(h,p);return a;} | |
| $('library-form').onsubmit=async e=>{e.preventDefault();const q=$('library-query').value.trim();if(!q)return;const generation=++requestGeneration;const output=$('library-results');output.replaceChildren();const words=q.toLowerCase().match(/[a-z]{3,}/g)||[];const documents=[...course.sections.map(s=>({name:s.title,text:s.script})),...assets.filter(a=>a.kind==='text')];const matches=documents.map(d=>({...d,score:words.filter(w=>d.text.toLowerCase().includes(w)).length})).filter(d=>d.score>0).sort((a,b)=>b.score-a.score).slice(0,4);for(const d of matches)output.append(resultCard(d.name+' · draft/source note',d.text.slice(0,1400)));if(!matches.length)output.textContent='No supporting passage found in the selected course or text notes.'; | |
| if(localNode){const progress=document.createElement('p');progress.textContent='Checking the local course assistant…';output.append(progress);try{const response=await fetch('/api/prof/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question:q,course_slug:course.id}),signal:AbortSignal.timeout(45000)});const data=await response.json();if(generation!==requestGeneration)return;if(!response.ok||!data.ok)throw new Error();progress.remove();output.append(resultCard('Local Prof · '+data.mode,data.answer));for(const ref of data.references||[])output.append(resultCard(ref.module_id+' · '+ref.title,ref.summary));}catch{if(generation===requestGeneration)progress.textContent='Local assistant unavailable. The source matches above remain available.';}} | |
| }; | |
| $('handover').onclick=present;$('pause-prof').onclick=togglePause;$('takeback').onclick=takeBack;$('previous').onclick=()=>chooseSection(state.section-1);$('next').onclick=()=>chooseSection(state.section+1); | |
| $('script').oninput=()=>{scripts.set(key(),$('script').value);$('spoken').textContent=script();}; | |
| $('listen').onclick=startListening;$('mic-consent').onchange=()=>{if(!$('mic-consent').checked)stopListening();};$('clear-transcript').onclick=()=>{stopListening();transcripts=[];$('transcript').textContent='Transcript cleared.';$('suggestion').replaceChildren();}; | |
| $('transcript-form').onsubmit=e=>{e.preventDefault();follow($('typed-transcript').value,'instructor typed note');$('typed-transcript').value='';}; | |
| $('projector').onclick=()=>{document.body.classList.toggle('projector');$('projector').textContent=document.body.classList.contains('projector')?'Exit projector view':'Projector view ↗';}; | |
| $('scene-pause').onclick=()=>{scenePaused=!scenePaused;marshaller?.setPaused(scenePaused);airfield?.setPaused(scenePaused);$('scene-pause').textContent=scenePaused?'Resume animation':'Pause animation';};document.querySelectorAll('[data-view]').forEach(b=>b.onclick=()=>{(course.scene==='marshalling'?marshaller:airfield)?.view(b.dataset.view);}); | |
| $('export-session').onclick=()=>{const report={format:'AMR_CLASSROOM_PRACTICE_V1',exported_at:new Date().toISOString(),course:course.name,release_status:course.status,competency_decision:null,session:state,transcript:transcripts,instructor_scripts:Object.fromEntries(scripts),asset_names:assets.map(a=>a.name)};const url=URL.createObjectURL(new Blob([JSON.stringify(report,null,2)],{type:'application/json'}));const a=document.createElement('a');a.href=url;a.download='AM-Risk-classroom-session.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);}; | |
| document.querySelectorAll('[data-tab]').forEach(b=>b.onclick=()=>{document.querySelectorAll('[data-tab]').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));document.querySelectorAll('[data-panel]').forEach(p=>p.hidden=p.dataset.panel!==b.dataset.tab);}); | |
| for(const c of classroomCourses){const option=document.createElement('option');option.value=c.id;option.textContent=c.name;$('course-select').append(option);}$('course-select').onchange=()=>chooseCourse($('course-select').value); | |
| if(!Recognition){$('listen').disabled=true;$('mic-status').textContent='Speech recognition unavailable here. Typed classroom notes work below.';} | |
| chooseCourse(classroomCourses[0].id); | |
| import('./scene.js?v=classroom-20260920').then(({createInstructor})=>{try{instructor=createInstructor($('prof-avatar'));}catch{$('prof-avatar').textContent='Prof';}}).catch(()=>{}); | |
| fetch('/api/node/status',{signal:AbortSignal.timeout(2500)}).then(r=>{if(!r.ok)throw new Error();return r.json();}).then(s=>{localNode=s.ok===true;$('node-connection').textContent=localNode?`Local node online · ${s.local_ai.available?'model service reachable':'retrieval available; model offline'}`:'Public demonstration';}).catch(()=>{$('node-connection').textContent='Public demonstration · open the local node for private course retrieval';}); | |
| window.addEventListener('pagehide',()=>{takeBack();assets.forEach(a=>{if(a.url)URL.revokeObjectURL(a.url);});}); | |
| document.addEventListener('visibilitychange',()=>{if(document.hidden)takeBack();}); | |