export const $=(s,root=document)=>root.querySelector(s); export const escape=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); let sid=sessionStorage.getItem('brain-lab-session');if(!sid){sid=crypto.randomUUID();sessionStorage.setItem('brain-lab-session',sid)} export const state={modules:{},anatomy:[],categories:[],orders:{}}; export async function api(path,body,method){const opt={method:method||(body?'POST':'GET'),headers:{'X-Lab-Session':sid}};if(body instanceof FormData)opt.body=body;else if(body){opt.headers['Content-Type']='application/json';opt.body=JSON.stringify(body)}const r=await fetch('/api/'+path,opt);const data=await r.json();if(!r.ok)throw Error(typeof data.detail==='string'?data.detail:JSON.stringify(data.detail));return data} let tt;export function toast(text,error=false){const el=$('#toast');el.textContent=text;el.className='visible'+(error?' error':'');clearTimeout(tt);tt=setTimeout(()=>el.className='',5000)} export async function action(button,fn,label='Working…'){const original=button.textContent;button.disabled=true;button.textContent=label;try{return await fn()}catch(e){toast(e.message,true)}finally{button.disabled=false;button.textContent=original}} export function card(title,content,badge=''){return `

${title}

${badge?`${badge}`:''}
${content}
`} export function probabilities(rows){return rows.map(c=>`
${escape(c.label)}${Math.round(c.probability*100)}%
`).join('')} export function traceHTML(){return `
ACTUAL LIF SPIKES
32 neurons · time runs left to right
MEMBRANE POTENTIAL
Draw or play to see recorded activity.
`} export function drawTrace(result){const canvas=$('#spike-chart'),m=$('#membrane-chart');if(!canvas||!m)return;const ctx=canvas.getContext('2d');ctx.clearRect(0,0,340,100);ctx.fillStyle='#f2f5ea';ctx.fillRect(0,8,340,87);if(!result.spikes?.length)return;ctx.fillStyle='#3a8065';result.spikes.forEach((row,t)=>row.forEach((s,n)=>{if(s)ctx.fillRect(5+t*330/result.spikes.length,10+n*2.6,3,2)}));const mc=m.getContext('2d');mc.clearRect(0,0,340,100);const values=result.membrane||[],threshold=result.threshold||Number($('#threshold')?.value)||1;const lo=Math.min(0,...values),hi=Math.max(threshold,...values,.1);const y=v=>85-(v-lo)/(hi-lo+1e-9)*68;mc.setLineDash([4,4]);mc.strokeStyle='#cc9f50';mc.beginPath();mc.moveTo(0,y(threshold));mc.lineTo(340,y(threshold));mc.stroke();mc.setLineDash([]);mc.strokeStyle='#3a8065';mc.lineWidth=2;mc.beginPath();values.forEach((v,i)=>i?mc.lineTo(6+i*325/(values.length-1),y(v)):mc.moveTo(6,y(v)));mc.stroke();$('#spike-count').textContent=`${result.spike_count??0} total spikes · dashed line = firing threshold`} export async function fileImage(file){if(!file?.type.startsWith('image/'))throw Error('Choose an image file.');const url=URL.createObjectURL(file);const im=await new Promise((resolve,reject)=>{const i=new Image();i.onload=()=>resolve(i);i.onerror=reject;i.src=url});const c=document.createElement('canvas');const scale=Math.min(1,768/Math.max(im.width,im.height));c.width=im.width*scale;c.height=im.height*scale;c.getContext('2d').drawImage(im,0,0,c.width,c.height);URL.revokeObjectURL(url);return c.toDataURL('image/png')} export function download(url,name){const a=document.createElement('a');a.href=url;a.download=name;a.click()} export function speak(text){if(!state.modules.broca){toast('The language-output module is off.');return}if(!('speechSynthesis'in window)){toast('Speech synthesis is unavailable in this browser.',true);return}speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(text);u.rate=.95;speechSynthesis.speak(u)} export function speechButton(button,target){let recorder=null,stream=null,timeout=null;const stop=()=>{clearTimeout(timeout);if(recorder?.state==='recording')recorder.stop();stream?.getTracks().forEach(t=>t.stop())};button.addEventListener('click',async()=>{if(recorder?.state==='recording'){stop();return}if(!state.modules.temporal||!state.modules.wernicke){toast('Switch on audio input and language understanding first.',true);return}try{stream=await navigator.mediaDevices.getUserMedia({audio:true});recorder=new MediaRecorder(stream);const chunks=[];recorder.ondataavailable=e=>chunks.push(e.data);recorder.onstop=async()=>{stream.getTracks().forEach(t=>t.stop());button.textContent='Transcribing…';button.disabled=true;try{const f=new FormData();f.append('audio',new Blob(chunks,{type:recorder.mimeType}),'hint.webm');const r=await api('speech',f);target.value=r.text;target.dispatchEvent(new Event('change'));toast('Transcribed by the hosted Whisper model.')}catch(e){toast(e.message,true)}finally{button.textContent='◉ Speak';button.disabled=false}};recorder.start();button.textContent='■ Stop recording';timeout=setTimeout(stop,10000)}catch(e){stop();toast('Microphone unavailable: '+e.message,true)}});return stop} export function activity(ids){document.dispatchEvent(new CustomEvent('brain-activity',{detail:ids}))} export function rangeHTML(label,id,value=1,min=.2,max=4,step=.1){return ``} export function wireRanges(){document.querySelectorAll('input[type=range]').forEach(e=>e.oninput=()=>{const out=$(`output[for="${e.id}"]`);if(out)out.textContent=e.value})}