import {applySharedPictionary} from './hosted-pictionary.js?v=shared-key-49';
import{featureHTML,featureView}from'./feature-view.js?v=brain-emotions-23';
import{$,api,state,escape,toast,action,card,probabilities,fileImage,speak,speechButton,activity,rangeHTML,wireRanges}from'./common.js';
export async function pictionary(root){root.innerHTML=card('A thought, in progress',`
YOUR DRAWING / THE MODEL SEES ONLY PIXELSDraw with your mouse, finger, or pen.
THE BRAIN’S BEST GUESS
I’m curious.
Add a few strokes. Let’s see what I recognize.
VISION CANDIDATES
An unfinished drawing can have many interpretations.
Visual candidates stay separate from the language model’s final guess.
1Pixels→2Decider vision→3Hint + language→4A guess
`,'LIVE RECOGNITION')+card('Inside the drawing',featureHTML(),'ILLUSTRATIVE FEATURE MAPS')+card('Recognition backend',`
Drawing pixels are sent to the selected API. Keys are sent over HTTPS to this Space and kept only in your visitor session. Qwen 3.7 Flash proposes object names and scenes freely. Decider compares those fresh hypotheses and can ask Qwen to search another interpretation. Sample drawings are examples, not an allowed vocabulary.
`,'QWEN 3.7 FLASH + DECIDER')+card('Follow the search',`
Draw anything. The brain will propose names and explore them.
There is no fixed object list. Each decision compares a small set of newly proposed hypotheses; if none fits, the search branches again. Probabilities are relative to that candidate set.
`,'OPEN VOCABULARY')+card('Watch the guesses change',`
Each completed model response will appear here.
Scores are the decision model’s returned probabilities over the declared options, not a guarantee of correctness. Spiking activity is shown in the separate arcade experiment.
`,'ACTUAL MODEL RESPONSES')+card('Class versus the brain',`
Enter your own secret prompt for automatic matching, or leave it blank for free drawing. The recognizer is never given the prompt.
Class 0 · Brain 0
`);
const canvas=$('#drawing'),ctx=canvas.getContext('2d'),features=featureView(canvas);let strokes=[],drawing=false,seq=0,timer,alive=true,lastGuess='',samples,round=null,classScore=0,brainScore=0,start=0,roundTimer,inferenceBusy=false,history=[],revision=0,baseImage=null;
function render(){revision++;ctx.fillStyle='#fff';ctx.fillRect(0,0,480,480);if(baseImage){const scale=Math.min(480/baseImage.width,480/baseImage.height);const w=baseImage.width*scale,h=baseImage.height*scale;ctx.drawImage(baseImage,(480-w)/2,(480-h)/2,w,h)}ctx.strokeStyle='#263e35';ctx.lineWidth=5;ctx.lineCap='round';ctx.lineJoin='round';for(const stroke of strokes){ctx.beginPath();stroke.forEach(([x,y],i)=>i?ctx.lineTo(x,y):ctx.moveTo(x,y));if(stroke.length===1)ctx.lineTo(stroke[0][0]+.1,stroke[0][1]+.1);ctx.stroke()}features.changed()}
function point(e){const r=canvas.getBoundingClientRect();return [(e.clientX-r.left)*480/r.width,(e.clientY-r.top)*480/r.height]}
function schedule(){clearTimeout(timer);timer=setTimeout(()=>infer(false),1200)}
async function infer(hint){if(inferenceBusy){if(hint)toast('Wait for the current visual recognition to finish.');else schedule();return}inferenceBusy=true;features.processing(true);const request=++seq,seenRevision=revision;$('#guess-info').textContent='Qwen is proposing names; Decider is evaluating them…';try{const r=await api('sketch',{image:canvas.toDataURL('image/png'),hint:hint?$('#hint').value:''});if(!alive||request!==seq)return;if(revision!==seenRevision){schedule();return}features.result(r.candidates);lastGuess=r.guess;$('#guess').textContent=r.guess||'I’m curious.';$('#guess-info').textContent=(r.hint_used?'With your hint. ':'Pixels only. ')+'Qwen 3.7 Flash → Decider · '+(r.elapsed_ms/1000).toFixed(1)+' s';$('#search-path').innerHTML=r.search_steps?.length?r.search_steps.map(step=>`
${step.pass}. ${escape(step.branch)}
${step.proposed.map(escape).join(' · ')}
${step.expand?'None fits well → explore another branch.':'Chosen: '+escape(step.guess)}
`).join(''):'Draw anything to start a new search.';$('#candidates').innerHTML=r.candidates.length?probabilities(r.candidates):'
`).join('');activity(hint?['occipital','association','wernicke','frontal']:['occipital','parietal']);if(round?.target&&!round.winner&&lastGuess.toLowerCase()===round.target){round.winner='Brain';brainScore++;$('#round-message').textContent=`The brain recognized ${round.target} in ${((performance.now()-start)/1000).toFixed(1)} seconds. Try to beat it next round!`;score()}}catch(e){if(alive&&request===seq){$('#guess-info').textContent=e.message;if(hint)toast(e.message,true)}}finally{inferenceBusy=false;if(alive)features.processing(false)}}
canvas.onpointerdown=e=>{canvas.setPointerCapture(e.pointerId);drawing=true;strokes.push([point(e)]);render();schedule()};canvas.onpointermove=e=>{if(drawing){strokes.at(-1).push(point(e));render();schedule()}};canvas.onpointerup=()=>{drawing=false;schedule()};canvas.onpointercancel=()=>drawing=false;
function clear(){seq++;strokes=[];baseImage=null;render();$('#guess').textContent='I’m curious.';$('#candidates').innerHTML='
Add a few strokes to begin.
';$('#hint').value='';lastGuess='';history=[];$('#search-path').textContent='Draw anything. The brain will propose names and explore them.';$('#guess-history').textContent='Each completed model response will appear here.';clearTimeout(timer)}
$('#clear-drawing').onclick=clear;$('#undo-stroke').onclick=()=>{strokes.pop();render();schedule()};$('#apply-hint').onclick=()=>action($('#apply-hint'),()=>infer(true),'…');$('#hint').onkeydown=e=>{if(e.key==='Enter')infer(true)};$('#say-guess').onclick=()=>speak(lastGuess||'Show me a drawing.');const stopMic=speechButton($('#hint-mic'),$('#hint'));const provider=await api('sketch/config');if(!provider.key_configured)root.prepend(root.children[2]);$('#sketch-provider').value=provider.provider;const cfgNote=cfg=>$('#provider-note').textContent=cfg.key_configured?`${cfg.global_env} available. Ready for a live model request.`:`Add ${cfg.global_env} here to use the model in this visitor session.`;cfgNote(provider);applySharedPictionary(root,provider);$('#save-provider').onclick=()=>action($('#save-provider'),async()=>{const cfg=await api('sketch/config',{provider:$('#sketch-provider').value,api_key:$('#sketch-api-key').value,save_globally:$('#global-key').checked});$('#sketch-api-key').value='';cfgNote(cfg);applySharedPictionary(root,cfg);toast(cfg.key_configured?'API key configured. The exact requested model is selected.':'The model is selected; add an API key to recognize drawings.');if(cfg.key_configured)await infer(false)});
$('#sketch-file').onchange=async e=>{try{const file=e.target.files[0];if(!file)return;clear();const url=await fileImage(file);const im=new Image();im.src=url;await im.decode();if(!alive)return;baseImage=im;render();await infer(false)}catch(err){toast(err.message,true)}};
$('#sample-select').onchange=async e=>{if(!e.target.value)return;clear();samples??=await api('samples');const ds=samples[e.target.value]?.[0];if(!ds)return;const all=ds.flatMap(s=>s[0].map((x,i)=>[x,s[1][i]]));const xs=all.map(p=>p[0]),ys=all.map(p=>p[1]);const minx=Math.min(...xs),miny=Math.min(...ys),scale=370/Math.max(Math.max(...xs)-minx,Math.max(...ys)-miny,1);strokes=ds.map(s=>s[0].map((x,i)=>[55+(x-minx)*scale,55+(s[1][i]-miny)*scale]));render();await infer(false)};
function score(){$('#round-score').textContent=`Class ${classScore} · Brain ${brainScore}`;clearInterval(roundTimer)}
$('#new-round').onclick=()=>{clear();const target=$('#round-prompt').value.trim().toLowerCase();round={target,winner:null};start=performance.now();$('#round-message').textContent=target?'Your private prompt stays in this browser. Start drawing; the recognizer receives only the pixels.':'Free drawing round. Choose any subject; score the class manually.';clearInterval(roundTimer);roundTimer=setInterval(()=>{if(round&&!round.winner&&performance.now()-start>45000){$('#round-message').textContent=round.target?`Time’s up. The prompt was ${round.target}. Start another round.`:'Free drawing round finished. Start another round.';round=null;clearInterval(roundTimer)}},1000)};$('#class-got-it').onclick=()=>{if(!round||round.winner)return toast('Start an active round first.');round.winner='Class';classScore++;$('#round-message').textContent=round.target?`The class recognized ${round.target} first. Nicely spotted.`:'The class recognized the drawing first.';score()};$('#reveal-round').onclick=()=>{if(round){$('#round-message').textContent=`Your prompt: ${round.target||'free drawing'}. The brain guessed ${lastGuess||'nothing yet'}.`;round=null;clearInterval(roundTimer)}};render();
return()=>{alive=false;features.destroy();clearTimeout(timer);clearInterval(roundTimer);stopMic()}}