File size: 5,913 Bytes
4155f6b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 `<div class="card"><div class="card-title"><h2>${title}</h2>${badge?`<span class="badge">${badge}</span>`:''}</div>${content}</div>`}
export function probabilities(rows){return rows.map(c=>`<div class="prob-row"><div><span>${escape(c.label)}</span><span>${Math.round(c.probability*100)}%</span></div><div class="track"><div class="fill" style="width:${c.probability*100}%"></div></div></div>`).join('')}
export function traceHTML(){return `<div class="neuron-layout"><div><span class="eyebrow">ACTUAL LIF SPIKES</span><canvas id="spike-chart" width="340" height="100"></canvas><div class="neuron-caption">32 neurons · time runs left to right</div></div><div><span class="eyebrow">MEMBRANE POTENTIAL</span><canvas id="membrane-chart" width="340" height="100"></canvas><div class="neuron-caption" id="spike-count">Draw or play to see recorded activity.</div></div></div>`}
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 `<label class="range">${label}<input type="range" id="${id}" min="${min}" max="${max}" step="${step}" value="${value}"><output for="${id}">${value}</output></label>`}
export function wireRanges(){document.querySelectorAll('input[type=range]').forEach(e=>e.oninput=()=>{const out=$(`output[for="${e.id}"]`);if(out)out.textContent=e.value})}
|