Choose Tile Image

Search for any object, animal, food, or action to find images for your tile.

`; const win=window.open('','_blank'); if(!win){toast('⚠️','Pop-up blocked — allow pop-ups for this site');return;} win.document.write(html); win.document.close(); toast('🖨️','Print window opened!'); } // ── QR Code (simple canvas-based) ── function generateQR(text){ // Minimal QR-like visual (for demo; in production use a real QR library) const canvas=$('qrCanvas'); const ctx=canvas.getContext('2d'); const size=200; canvas.width=size;canvas.height=size; ctx.fillStyle='#fff';ctx.fillRect(0,0,size,size); ctx.fillStyle='#1A1A2E'; // Generate a deterministic pattern from the text let hash=0; for(let i=0;igrid-9)||(r>grid-9&&c<8))continue; if(((seed*(r*grid+c+1))%7)<3){ ctx.fillRect(c*cell,r*cell,cell,cell); } } } // TinkySpeak logo in center ctx.fillStyle='#fff'; const logoSize=5*cell; ctx.fillRect((size-logoSize)/2,(size-logoSize)/2,logoSize,logoSize); ctx.font='bold '+Math.floor(cell*2.5)+'px sans-serif'; ctx.fillStyle='#E8A838'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('TS',size/2,size/2); } // ── Push Modal ── function openPushModal(){ const board=getBoard(); if(!board){toast('⚠️','No board selected');return;} // Reset state $('deviceList').innerHTML=''; $('wifiPushStatus').className='push-status';$('wifiPushStatus').style.display='none'; $('cloudPushStatus').className='push-status';$('cloudPushStatus').style.display='none'; $('qrDisplay').style.display='none'; const po=$('pushOptions');if(po)po.style.display=''; $('pushModal').classList.add('open'); } function closePushModal(){$('pushModal').classList.remove('open');} // ── Push to Device: format conversion ── function boardToCompactJson(board){ return { v:1, id:board.id, t:board.name, lm:Date.now(), tiles:board.tiles.map((tile,i)=>{ const t={tid:i+1,l:tile.label,s:tile.spokenText||tile.label}; const hex=(tile.color||'#AAAAAA').replace('#',''); t.c='#FF'+hex.toUpperCase(); if(tile.emoji)t.e=tile.emoji; if(tile.category)t.cat=tile.category; return t; }) }; } function boardToDashboardJson(board){ return { id:board.id, title:board.name, columns:board.columns||3, createdBy:user?.name||'teacher', tiles:board.tiles.map((tile,i)=>{ const t={position:i,label:tile.label,spokenText:tile.spokenText||tile.label,emoji:tile.emoji||'',color:tile.color||'#E0E0E0',category:tile.category||'misc'}; if(tile.imageUrl)t.imageUrl=tile.imageUrl; return t; }) }; } // ── Push to Device: WiFi scan ── async function scanForDevices(){ const btn=$('scanBtn'); const list=$('deviceList'); const status=$('wifiPushStatus'); btn.disabled=true; $('scanBtnText').innerHTML=' Scanning network...'; list.innerHTML=''; status.className='push-status';status.style.display='none'; try{ const resp=await fetch('/api/discover'); const data=await resp.json(); if(!data.devices||data.devices.length===0){ list.innerHTML='

'+ 'No TinkySpeak devices found on this network.
'+ 'Make sure devices are on the same WiFi and TinkySpeak is open.

'; } else { list.innerHTML=data.devices.map(d=>{ const bc=d.boards?d.boards.length:0; return `
📱

${d.host}:${d.port}

${bc} board${bc!==1?'s':''} on device

`; }).join(''); list.querySelectorAll('.device-item-push').forEach(b=>{ b.addEventListener('click',e=>{ const item=e.target.closest('.device-item'); pushToDevice(item.dataset.host,parseInt(item.dataset.port),e.target); }); }); } if(data.elapsed_ms) status.className='push-status info', status.textContent='Scan completed in '+(data.elapsed_ms/1000).toFixed(1)+'s — found '+ (data.devices?data.devices.length:0)+' device(s)',status.style.display='block'; }catch(e){ status.className='push-status error'; status.textContent='Scan failed. Make sure you are running server.py (not python -m http.server).'; status.style.display='block'; } btn.disabled=false; $('scanBtnText').textContent='Scan for Devices'; } // ── Push to Device: WiFi push ── async function pushToDevice(host,port,buttonEl){ const board=getBoard();if(!board)return; const status=$('wifiPushStatus'); buttonEl.disabled=true;buttonEl.textContent='Pushing...'; const compact=boardToCompactJson(board); try{ const resp=await fetch('/api/push',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({host,port,board:compact}) }); const result=await resp.json(); if(result.success){ status.className='push-status success'; status.textContent='Board pushed to '+host+' successfully!'; status.style.display='block'; buttonEl.textContent='Sent!'; toast('📡','Board pushed to device!'); }else{ status.className='push-status error'; status.textContent='Push failed: '+(result.error||'unknown error'); status.style.display='block'; buttonEl.textContent='Retry';buttonEl.disabled=false; } }catch(e){ status.className='push-status error'; status.textContent='Push failed: '+e.message; status.style.display='block'; buttonEl.textContent='Retry';buttonEl.disabled=false; } } // ── Push to Device: Cloud push via serial ── async function pushViaCloud(){ const board=getBoard();if(!board)return; const serial=$('serialInput').value.trim(); if(!serial){$('serialInput').focus();$('serialInput').style.borderColor='#DC143C';return;} const status=$('cloudPushStatus'); const btn=$('cloudPushBtn'); btn.disabled=true;btn.textContent='Pushing...'; const dashboardBoard=boardToDashboardJson(board); try{ const resp=await fetch('/api/cloud-push',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({serialNumber:serial,board:dashboardBoard}) }); const result=await resp.json(); if(result.success){ status.className='push-status success'; status.textContent='Board queued for device '+serial+'. It will arrive when the device connects.'; status.style.display='block'; toast('☁️','Board pushed to cloud!'); }else{ status.className='push-status error'; status.textContent=result.error||'Cloud push failed.'; status.style.display='block'; } }catch(e){ status.className='push-status error'; status.textContent='Cloud push failed: '+e.message; status.style.display='block'; } btn.disabled=false;btn.textContent='Push'; } // ── Push tabs ── function initPushTabs(){ document.querySelectorAll('.push-tab').forEach(tab=>{ tab.addEventListener('click',()=>{ document.querySelectorAll('.push-tab').forEach(t=>t.classList.remove('active')); document.querySelectorAll('.push-tab-content').forEach(c=>c.classList.remove('active')); tab.classList.add('active'); const name=tab.dataset.pushTab; const id='pushTab'+name.charAt(0).toUpperCase()+name.slice(1); $(id).classList.add('active'); }); }); } // ── Settings Modal ── function openSettingsModal(){ const board=getBoard(); if(!board)return; $('settingsName').value=board.name; $('settingsCategory').value=board.category; $('settingsAudience').value=board.audience||'child'; $('settingsCols').value=board.columns; // Load AI settings $('settingsAIProvider').value=localStorage.getItem('tb_ai_provider')||''; $('settingsAIKey').value=localStorage.getItem('tb_ai_key')||''; $('settingsModal').classList.add('open'); } function closeSettingsModal(){$('settingsModal').classList.remove('open');} function saveSettings(){ const board=getBoard(); if(!board)return; board.name=$('settingsName').value||'Untitled Board'; board.category=$('settingsCategory').value; board.audience=$('settingsAudience').value; board.columns=+$('settingsCols').value; saveState(); $('boardNameInput').value=board.name; $('boardDisplayName').textContent=board.name; updateCols(board.columns); renderBoardList(); // Save AI settings const aiProvider=$('settingsAIProvider').value; const aiKey=$('settingsAIKey').value; if(aiProvider&&aiKey){localStorage.setItem('tb_ai_provider',aiProvider);localStorage.setItem('tb_ai_key',aiKey);} else{localStorage.removeItem('tb_ai_provider');localStorage.removeItem('tb_ai_key');} closeSettingsModal(); toast('✅','Settings saved!'); } function getAIConfig(){ const provider=localStorage.getItem('tb_ai_provider'); const key=localStorage.getItem('tb_ai_key'); if(provider&&key)return{provider,apiKey:key}; return null; } // ── Toast ── function toast(icon,msg){ const t=$('toast'); t.querySelector('.toast-icon').textContent=icon; t.querySelector('.toast-text').textContent=msg; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2500); } // ── Utils ── function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;} // ── Event Binding ── function bindEvents(){ // Login $('loginSubmit').addEventListener('click',handleLogin); $('loginQuick').addEventListener('click',handleQuickStart); $('loginEmail').addEventListener('keydown',e=>{if(e.key==='Enter'){$('loginPassword').focus();}}); $('loginPassword').addEventListener('keydown',e=>{if(e.key==='Enter')handleLogin();}); // New board $('newBoardBtn').addEventListener('click',()=>{ createBoard(); toast('🎨','New board created!'); }); $('emptyCreateBtn').addEventListener('click',()=>{ createBoard(); toast('🎨','Your first board — let\'s go!'); }); // Board name $('boardNameInput').addEventListener('input',e=>{ const board=getBoard(); if(board){ board.name=e.target.value||'Untitled Board'; $('boardDisplayName').textContent=board.name; saveState(); renderBoardList(); } }); // Toolbar columns document.querySelectorAll('[data-cols]').forEach(btn=>{ btn.addEventListener('click',()=>updateCols(+btn.dataset.cols)); }); // Topbar actions $('exportBtn').addEventListener('click',exportBoard); $('printBtn').addEventListener('click',printCutouts); $('pushBtn').addEventListener('click',openPushModal); $('settingsBtn').addEventListener('click',openSettingsModal); // Props panel $('propsClose').addEventListener('click',closeProps); $('propsLabel').addEventListener('input',e=>updateTileProp('label',e.target.value)); $('propsSpoken').addEventListener('input',e=>updateTileProp('spokenText',e.target.value)); $('propsEmoji').addEventListener('click',openEmojiPicker); // Image mode tabs (emoji vs image) document.querySelectorAll('.props-img-tab').forEach(tab=>{ tab.addEventListener('click',()=>{ const mode=tab.dataset.imgmode; document.querySelectorAll('.props-img-tab').forEach(t=>t.classList.remove('active')); tab.classList.add('active'); $('propsImageUpload').style.display=(mode==='image')?'block':'none'; updateTileProp('displayMode',mode); }); }); // File upload $('propsFileInput').addEventListener('change',e=>{ const file=e.target.files[0]; if(!file) return; if(file.size>500000){toast('⚠️','Image too large — keep under 500KB');return;} const reader=new FileReader(); reader.onload=ev=>{ const dataUrl=ev.target.result; updateTileProp('imageUrl',dataUrl); updateTileProp('displayMode','image'); const board=getBoard(); const tile=board?.tiles.find(t=>t.id===selectedTileId); if(tile) openProps(tile); toast('🖼','Image uploaded!'); }; reader.readAsDataURL(file); e.target.value=''; // reset so same file can be re-selected }); // Remove image $('propsRemoveImage').addEventListener('click',()=>{ updateTileProp('imageUrl',''); updateTileProp('displayMode','emoji'); const board=getBoard(); const tile=board?.tiles.find(t=>t.id===selectedTileId); if(tile) openProps(tile); toast('🗑️','Image removed'); }); // Search images (Unsplash-like via open API) $('propsImageSearch').addEventListener('click',()=>{ const board=getBoard(); const tile=board?.tiles.find(t=>t.id===selectedTileId); if(!tile) return; const query=tile.label||'object'; searchTileImages(query); }); // Emoji picker $('emojiSearch').addEventListener('input',e=>buildEmojiGrid(e.target.value)); document.addEventListener('click',e=>{ if(!e.target.closest('.emoji-picker')&&!e.target.closest('.props-emoji-preview')){ closeEmojiPicker(); } }); // Push modal $('pushClose').addEventListener('click',closePushModal); $('scanBtn').addEventListener('click',scanForDevices); $('cloudPushBtn').addEventListener('click',pushViaCloud); $('serialInput').addEventListener('keydown',e=>{if(e.key==='Enter')pushViaCloud();}); $('serialInput').addEventListener('input',()=>{$('serialInput').style.borderColor='';}); initPushTabs(); // More Options tab handlers (QR, Download, Copy) $('pushQR').addEventListener('click',()=>{ const board=getBoard(); if(!board)return; const data=JSON.stringify({n:board.name,t:board.tiles.length,id:board.id}); generateQR(data); $('pushOptions').style.display='none'; $('qrDisplay').style.display=''; }); $('pushDownload').addEventListener('click',()=>{closePushModal();exportBoard();}); $('pushCopy').addEventListener('click',()=>{ const board=getBoard(); if(!board)return; const data={version:'1.0',name:board.name,category:board.category, columns:board.columns,created:board.created,author:board.author, tiles:board.tiles.map(t=>({id:t.id,emoji:t.emoji,label:t.label, spokenText:t.spokenText,category:t.category,color:t.color,order:t.order}))}; navigator.clipboard.writeText(JSON.stringify(data,null,2)).then(()=>{ closePushModal(); toast('📋','Board data copied to clipboard!'); }); }); // Settings modal $('settingsClose').addEventListener('click',closeSettingsModal); $('settingsSave').addEventListener('click',saveSettings); // Close modals on overlay click $('pushModal').addEventListener('click',e=>{if(e.target===$('pushModal'))closePushModal();}); $('settingsModal').addEventListener('click',e=>{if(e.target===$('settingsModal'))closeSettingsModal();}); // Undo $('undoBtn').addEventListener('click',undo); document.addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key==='z'){e.preventDefault();undo();} }); // User logout $('topbarUser').addEventListener('click',()=>{ const msg = user?.authenticated ? 'Sign out? Your boards will stay saved locally.' : 'Log out? Your boards will be saved locally.'; if(confirm(msg)){ user=null; localStorage.removeItem('tb_user'); location.reload(); } }); } // ═══════════ AI BOARD ASSISTANT ═══════════ const AI_BOARDS = { zoo:[ {emoji:'🦁',label:'Lion',spokenText:'I see a lion',cat:'noun'}, {emoji:'🐘',label:'Elephant',spokenText:'I see an elephant',cat:'noun'}, {emoji:'🦒',label:'Giraffe',spokenText:'Look at the giraffe',cat:'noun'}, {emoji:'🐒',label:'Monkey',spokenText:'I see a monkey',cat:'noun'}, {emoji:'🐧',label:'Penguin',spokenText:'Look at the penguin',cat:'noun'}, {emoji:'🦜',label:'Bird',spokenText:'I see a bird',cat:'noun'}, {emoji:'👀',label:'Look',spokenText:'I want to look',cat:'verb'}, {emoji:'😊',label:'Fun',spokenText:'This is fun',cat:'adj'}, {emoji:'😰',label:'Scared',spokenText:'I feel scared',cat:'adj'}, {emoji:'🤩',label:'Wow',spokenText:'Wow that is cool',cat:'social'}, {emoji:'🚻',label:'Bathroom',spokenText:'I need the bathroom',cat:'verb'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'}, {emoji:'🍽️',label:'Hungry',spokenText:'I am hungry',cat:'adj'}, {emoji:'😴',label:'Tired',spokenText:'I am tired',cat:'adj'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'}, {emoji:'🚶',label:'Go',spokenText:'I want to go',cat:'verb'} ], morning:[ {emoji:'🌅',label:'Good Morning',spokenText:'Good morning',cat:'social'}, {emoji:'🚿',label:'Shower',spokenText:'I need to take a shower',cat:'verb'}, {emoji:'🪥',label:'Brush Teeth',spokenText:'I need to brush my teeth',cat:'verb'}, {emoji:'👕',label:'Get Dressed',spokenText:'I need to get dressed',cat:'verb'}, {emoji:'🥣',label:'Breakfast',spokenText:'I want breakfast',cat:'noun'}, {emoji:'🥛',label:'Milk',spokenText:'I want milk',cat:'noun'}, {emoji:'🥐',label:'Toast',spokenText:'I want toast',cat:'noun'}, {emoji:'🎒',label:'Backpack',spokenText:'I need my backpack',cat:'noun'}, {emoji:'👟',label:'Shoes',spokenText:'I need my shoes',cat:'noun'}, {emoji:'🚌',label:'Bus',spokenText:'The bus is coming',cat:'noun'}, {emoji:'✅',label:'Ready',spokenText:'I am ready',cat:'social'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'} ], doctor:[ {emoji:'🤕',label:'Hurts',spokenText:'Something hurts',cat:'adj'}, {emoji:'🤒',label:'Sick',spokenText:'I feel sick',cat:'adj'}, {emoji:'😰',label:'Scared',spokenText:'I feel scared',cat:'adj'}, {emoji:'👆',label:'Here',spokenText:'It hurts here',cat:'verb'}, {emoji:'🤢',label:'Nauseous',spokenText:'I feel nauseous',cat:'adj'}, {emoji:'🥵',label:'Hot',spokenText:'I feel hot',cat:'adj'}, {emoji:'🥶',label:'Cold',spokenText:'I feel cold',cat:'adj'}, {emoji:'💊',label:'Medicine',spokenText:'I need medicine',cat:'noun'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'}, {emoji:'🤗',label:'Better',spokenText:'I feel better',cat:'adj'}, {emoji:'❓',label:'What',spokenText:'What is happening',cat:'question'}, {emoji:'✋',label:'Stop',spokenText:'Please stop',cat:'neg'} ], playground:[ {emoji:'🏃',label:'Run',spokenText:'I want to run',cat:'verb'}, {emoji:'🧗',label:'Climb',spokenText:'I want to climb',cat:'verb'}, {emoji:'🎢',label:'Slide',spokenText:'I want to go on the slide',cat:'noun'}, {emoji:'🔄',label:'Swing',spokenText:'I want to swing',cat:'verb'}, {emoji:'⚽',label:'Ball',spokenText:'I want to play ball',cat:'noun'}, {emoji:'👫',label:'Together',spokenText:'Let us play together',cat:'social'}, {emoji:'🔄',label:'My Turn',spokenText:'It is my turn',cat:'social'}, {emoji:'⏳',label:'Wait',spokenText:'I am waiting',cat:'verb'}, {emoji:'🙏',label:'Please',spokenText:'Please',cat:'social'}, {emoji:'😊',label:'Fun',spokenText:'This is fun',cat:'adj'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'} ], grocery:[ {emoji:'🛒',label:'Cart',spokenText:'I want to push the cart',cat:'noun'}, {emoji:'🍎',label:'Apple',spokenText:'I want apples',cat:'noun'}, {emoji:'🍌',label:'Banana',spokenText:'I want bananas',cat:'noun'}, {emoji:'🥛',label:'Milk',spokenText:'We need milk',cat:'noun'}, {emoji:'🍞',label:'Bread',spokenText:'We need bread',cat:'noun'}, {emoji:'🧀',label:'Cheese',spokenText:'I want cheese',cat:'noun'}, {emoji:'🥕',label:'Carrots',spokenText:'I want carrots',cat:'noun'}, {emoji:'🍪',label:'Cookie',spokenText:'Can I have a cookie please',cat:'noun'}, {emoji:'👀',label:'Look',spokenText:'Look at this',cat:'verb'}, {emoji:'👆',label:'This One',spokenText:'I want this one',cat:'verb'}, {emoji:'✅',label:'Yes',spokenText:'Yes please',cat:'social'}, {emoji:'🚫',label:'No',spokenText:'No thank you',cat:'neg'} ], restaurant:[ {emoji:'🍕',label:'Pizza',spokenText:'I want pizza please',cat:'noun'}, {emoji:'🍔',label:'Burger',spokenText:'I want a burger please',cat:'noun'}, {emoji:'🍟',label:'Fries',spokenText:'I want fries',cat:'noun'}, {emoji:'🥤',label:'Drink',spokenText:'I want a drink please',cat:'noun'}, {emoji:'🧃',label:'Juice',spokenText:'I want juice',cat:'noun'}, {emoji:'🍦',label:'Ice Cream',spokenText:'I want ice cream',cat:'noun'}, {emoji:'🙏',label:'Please',spokenText:'Please',cat:'social'}, {emoji:'👍',label:'Thank You',spokenText:'Thank you',cat:'social'}, {emoji:'🚻',label:'Bathroom',spokenText:'I need the bathroom',cat:'verb'}, {emoji:'🤤',label:'Yummy',spokenText:'That is yummy',cat:'adj'}, {emoji:'🤢',label:'Not Good',spokenText:'I do not like that',cat:'neg'}, {emoji:'✅',label:'Done',spokenText:'I am done eating',cat:'social'} ], bath:[ {emoji:'🛁',label:'Bath',spokenText:'I want a bath',cat:'verb'}, {emoji:'🚿',label:'Shower',spokenText:'I want a shower',cat:'verb'}, {emoji:'🧴',label:'Soap',spokenText:'I need soap',cat:'noun'}, {emoji:'🧽',label:'Wash',spokenText:'Help me wash',cat:'verb'}, {emoji:'🪥',label:'Teeth',spokenText:'I need to brush my teeth',cat:'verb'}, {emoji:'🥶',label:'Cold',spokenText:'The water is cold',cat:'adj'}, {emoji:'🥵',label:'Hot',spokenText:'The water is too hot',cat:'adj'}, {emoji:'👍',label:'Good',spokenText:'That feels good',cat:'adj'}, {emoji:'🧸',label:'Toy',spokenText:'I want my bath toy',cat:'noun'}, {emoji:'🏁',label:'Done',spokenText:'I am done',cat:'social'}, {emoji:'👕',label:'Clothes',spokenText:'I need clothes',cat:'noun'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'} ], bedtime:[ {emoji:'🛏️',label:'Bed',spokenText:'I want to go to bed',cat:'verb'}, {emoji:'📖',label:'Story',spokenText:'Read me a story please',cat:'noun'}, {emoji:'🧸',label:'Teddy',spokenText:'I want my teddy bear',cat:'noun'}, {emoji:'💡',label:'Light Off',spokenText:'Turn the light off',cat:'verb'}, {emoji:'💡',label:'Light On',spokenText:'Turn the light on',cat:'verb'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'}, {emoji:'🤗',label:'Hug',spokenText:'I want a hug',cat:'social'}, {emoji:'😘',label:'Kiss',spokenText:'Give me a kiss',cat:'social'}, {emoji:'😰',label:'Scared',spokenText:'I am scared',cat:'adj'}, {emoji:'🌙',label:'Goodnight',spokenText:'Goodnight',cat:'social'}, {emoji:'🎵',label:'Song',spokenText:'Sing me a song',cat:'noun'}, {emoji:'🚻',label:'Bathroom',spokenText:'I need the bathroom',cat:'verb'} ], feelings:[ {emoji:'😊',label:'Happy',spokenText:'I feel happy',cat:'adj'}, {emoji:'😢',label:'Sad',spokenText:'I feel sad',cat:'adj'}, {emoji:'😠',label:'Angry',spokenText:'I feel angry',cat:'adj'}, {emoji:'😰',label:'Worried',spokenText:'I feel worried',cat:'adj'}, {emoji:'😴',label:'Tired',spokenText:'I feel tired',cat:'adj'}, {emoji:'🤩',label:'Excited',spokenText:'I feel excited',cat:'adj'}, {emoji:'😐',label:'Okay',spokenText:'I feel okay',cat:'adj'}, {emoji:'🥺',label:'Lonely',spokenText:'I feel lonely',cat:'adj'}, {emoji:'😤',label:'Frustrated',spokenText:'I feel frustrated',cat:'adj'}, {emoji:'🤗',label:'Loved',spokenText:'I feel loved',cat:'adj'}, {emoji:'🤔',label:'Confused',spokenText:'I feel confused',cat:'adj'}, {emoji:'😌',label:'Calm',spokenText:'I feel calm',cat:'adj'} ], swimming:[ {emoji:'🏊',label:'Swim',spokenText:'I want to swim',cat:'verb'}, {emoji:'💦',label:'Splash',spokenText:'I want to splash',cat:'verb'}, {emoji:'🩱',label:'Swimsuit',spokenText:'I need my swimsuit',cat:'noun'}, {emoji:'🥽',label:'Goggles',spokenText:'I need my goggles',cat:'noun'}, {emoji:'🏖️',label:'Pool',spokenText:'I want to go to the pool',cat:'noun'}, {emoji:'🧴',label:'Sunscreen',spokenText:'I need sunscreen',cat:'noun'}, {emoji:'🥶',label:'Cold',spokenText:'The water is cold',cat:'adj'}, {emoji:'😊',label:'Fun',spokenText:'This is fun',cat:'adj'}, {emoji:'🤿',label:'Dive',spokenText:'I want to dive',cat:'verb'}, {emoji:'🚫',label:'No',spokenText:'No I do not want to',cat:'neg'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'}, {emoji:'🏁',label:'Done',spokenText:'I am done swimming',cat:'social'} ], birthday:[ {emoji:'🎂',label:'Cake',spokenText:'I want cake',cat:'noun'}, {emoji:'🎈',label:'Balloon',spokenText:'I want a balloon',cat:'noun'}, {emoji:'🎁',label:'Present',spokenText:'I want to open a present',cat:'noun'}, {emoji:'🎵',label:'Song',spokenText:'Sing happy birthday',cat:'verb'}, {emoji:'🕯️',label:'Candles',spokenText:'I want to blow the candles',cat:'verb'}, {emoji:'🍕',label:'Pizza',spokenText:'I want pizza',cat:'noun'}, {emoji:'🧃',label:'Juice',spokenText:'I want juice',cat:'noun'}, {emoji:'🎮',label:'Play',spokenText:'I want to play games',cat:'verb'}, {emoji:'🤩',label:'Excited',spokenText:'I am so excited',cat:'adj'}, {emoji:'🙏',label:'Thank You',spokenText:'Thank you',cat:'social'}, {emoji:'👋',label:'Hello',spokenText:'Hello happy birthday',cat:'social'}, {emoji:'🏠',label:'Home',spokenText:'I want to go home',cat:'verb'} ], school:[ {emoji:'📖',label:'Read',spokenText:'I want to read',cat:'verb'}, {emoji:'✏️',label:'Write',spokenText:'I want to write',cat:'verb'}, {emoji:'🖍️',label:'Draw',spokenText:'I want to draw',cat:'verb'}, {emoji:'🧮',label:'Math',spokenText:'I need help with math',cat:'noun'}, {emoji:'✂️',label:'Cut',spokenText:'I need scissors',cat:'verb'}, {emoji:'🎨',label:'Art',spokenText:'I want to do art',cat:'noun'}, {emoji:'🖥️',label:'Computer',spokenText:'I want to use the computer',cat:'noun'}, {emoji:'👩‍🏫',label:'Teacher',spokenText:'I need the teacher',cat:'noun'}, {emoji:'❓',label:'Question',spokenText:'I have a question',cat:'question'}, {emoji:'🚻',label:'Bathroom',spokenText:'I need the bathroom',cat:'verb'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'}, {emoji:'✅',label:'Done',spokenText:'I am done',cat:'social'} ], travel:[ {emoji:'✈️',label:'Airplane',spokenText:'I see the airplane',cat:'noun'}, {emoji:'🧳',label:'Bag',spokenText:'I need my bag',cat:'noun'}, {emoji:'🪪',label:'Ticket',spokenText:'Where is my ticket',cat:'question'}, {emoji:'💺',label:'Seat',spokenText:'I want to sit down',cat:'verb'}, {emoji:'🍿',label:'Snack',spokenText:'I want a snack',cat:'noun'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'}, {emoji:'🚻',label:'Bathroom',spokenText:'I need the bathroom',cat:'verb'}, {emoji:'😰',label:'Scared',spokenText:'I feel scared',cat:'adj'}, {emoji:'😴',label:'Tired',spokenText:'I am tired',cat:'adj'}, {emoji:'🎵',label:'Music',spokenText:'I want to listen to music',cat:'noun'}, {emoji:'❓',label:'When',spokenText:'When are we there',cat:'question'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'} ], pets:[ {emoji:'🐕',label:'Dog',spokenText:'I want to see the dog',cat:'noun'}, {emoji:'🐈',label:'Cat',spokenText:'I want to pet the cat',cat:'noun'}, {emoji:'🐟',label:'Fish',spokenText:'I see the fish',cat:'noun'}, {emoji:'🐹',label:'Hamster',spokenText:'I want to hold the hamster',cat:'noun'}, {emoji:'🍽️',label:'Feed',spokenText:'I want to feed the pet',cat:'verb'}, {emoji:'💧',label:'Water',spokenText:'The pet needs water',cat:'verb'}, {emoji:'🚶',label:'Walk',spokenText:'I want to walk the dog',cat:'verb'}, {emoji:'🧸',label:'Toy',spokenText:'I want the pet toy',cat:'noun'}, {emoji:'🤗',label:'Gentle',spokenText:'I will be gentle',cat:'adj'}, {emoji:'😍',label:'Cute',spokenText:'The pet is so cute',cat:'adj'}, {emoji:'✅',label:'Yes',spokenText:'Yes please',cat:'social'}, {emoji:'🚫',label:'No',spokenText:'No thank you',cat:'neg'} ], sports:[ {emoji:'⚽',label:'Soccer',spokenText:'I want to play soccer',cat:'noun'}, {emoji:'🏀',label:'Basketball',spokenText:'I want to play basketball',cat:'noun'}, {emoji:'🏃',label:'Run',spokenText:'I want to run',cat:'verb'}, {emoji:'⚾',label:'Catch',spokenText:'Throw me the ball',cat:'verb'}, {emoji:'🥅',label:'Goal',spokenText:'I scored a goal',cat:'social'}, {emoji:'🔄',label:'My Turn',spokenText:'It is my turn',cat:'social'}, {emoji:'👫',label:'Team',spokenText:'I want to be on a team',cat:'social'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'}, {emoji:'😊',label:'Fun',spokenText:'This is fun',cat:'adj'}, {emoji:'😴',label:'Tired',spokenText:'I am tired',cat:'adj'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'}, {emoji:'🏁',label:'Done',spokenText:'I am done playing',cat:'social'} ], music:[ {emoji:'🎵',label:'Music',spokenText:'I want music',cat:'noun'}, {emoji:'🎤',label:'Sing',spokenText:'I want to sing',cat:'verb'}, {emoji:'🥁',label:'Drum',spokenText:'I want to play the drum',cat:'noun'}, {emoji:'🎹',label:'Piano',spokenText:'I want to play piano',cat:'noun'}, {emoji:'🎸',label:'Guitar',spokenText:'I want to play guitar',cat:'noun'}, {emoji:'💃',label:'Dance',spokenText:'I want to dance',cat:'verb'}, {emoji:'🔊',label:'Louder',spokenText:'Make it louder please',cat:'verb'}, {emoji:'🔇',label:'Quiet',spokenText:'Make it quieter please',cat:'verb'}, {emoji:'🔄',label:'Again',spokenText:'Play it again',cat:'verb'}, {emoji:'🤩',label:'Love It',spokenText:'I love this song',cat:'adj'}, {emoji:'✅',label:'Yes',spokenText:'Yes please',cat:'social'}, {emoji:'🚫',label:'No',spokenText:'No I do not like that',cat:'neg'} ], art:[ {emoji:'🎨',label:'Paint',spokenText:'I want to paint',cat:'verb'}, {emoji:'🖍️',label:'Color',spokenText:'I want to color',cat:'verb'}, {emoji:'✂️',label:'Cut',spokenText:'I want to cut',cat:'verb'}, {emoji:'🧩',label:'Glue',spokenText:'I need glue',cat:'noun'}, {emoji:'📏',label:'Ruler',spokenText:'I need a ruler',cat:'noun'}, {emoji:'🖌️',label:'Brush',spokenText:'I need a brush',cat:'noun'}, {emoji:'🔵',label:'Blue',spokenText:'I want blue',cat:'adj'}, {emoji:'🔴',label:'Red',spokenText:'I want red',cat:'adj'}, {emoji:'🌈',label:'Colors',spokenText:'I want more colors',cat:'noun'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'}, {emoji:'✅',label:'Done',spokenText:'I am done',cat:'social'}, {emoji:'🤩',label:'Pretty',spokenText:'That is pretty',cat:'adj'} ], library:[ {emoji:'📚',label:'Book',spokenText:'I want a book',cat:'noun'}, {emoji:'📖',label:'Read',spokenText:'Read to me please',cat:'verb'}, {emoji:'🤫',label:'Quiet',spokenText:'I will be quiet',cat:'social'}, {emoji:'🦕',label:'Dinosaur',spokenText:'I want a dinosaur book',cat:'noun'}, {emoji:'🐕',label:'Animals',spokenText:'I want an animal book',cat:'noun'}, {emoji:'🚀',label:'Space',spokenText:'I want a space book',cat:'noun'}, {emoji:'🧚',label:'Stories',spokenText:'I want a story book',cat:'noun'}, {emoji:'💻',label:'Computer',spokenText:'I want to use the computer',cat:'noun'}, {emoji:'👆',label:'This One',spokenText:'I want this one',cat:'verb'}, {emoji:'✅',label:'Yes',spokenText:'Yes please',cat:'social'}, {emoji:'🚫',label:'No',spokenText:'No thank you',cat:'neg'}, {emoji:'✋',label:'Help',spokenText:'I need help finding a book',cat:'question'} ], haircut:[ {emoji:'💇',label:'Haircut',spokenText:'I am getting a haircut',cat:'noun'}, {emoji:'✂️',label:'Cut',spokenText:'Cut my hair please',cat:'verb'}, {emoji:'😰',label:'Scared',spokenText:'I am scared',cat:'adj'}, {emoji:'👍',label:'Okay',spokenText:'That is okay',cat:'adj'}, {emoji:'✋',label:'Stop',spokenText:'Please stop',cat:'neg'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'}, {emoji:'🪞',label:'Mirror',spokenText:'I want to see',cat:'verb'}, {emoji:'📱',label:'Phone',spokenText:'I want my phone',cat:'noun'}, {emoji:'⏳',label:'How Long',spokenText:'How much longer',cat:'question'}, {emoji:'😊',label:'Nice',spokenText:'That looks nice',cat:'adj'}, {emoji:'🏁',label:'Done',spokenText:'Is it done yet',cat:'question'}, {emoji:'🙏',label:'Thank You',spokenText:'Thank you',cat:'social'} ], cooking:[ {emoji:'👩‍🍳',label:'Cook',spokenText:'I want to cook',cat:'verb'}, {emoji:'🥣',label:'Mix',spokenText:'I want to mix',cat:'verb'}, {emoji:'🥚',label:'Egg',spokenText:'I want to crack the egg',cat:'noun'}, {emoji:'🧈',label:'Butter',spokenText:'I need butter',cat:'noun'}, {emoji:'🍫',label:'Chocolate',spokenText:'I want chocolate',cat:'noun'}, {emoji:'🥛',label:'Milk',spokenText:'I need milk',cat:'noun'}, {emoji:'👆',label:'My Turn',spokenText:'It is my turn',cat:'social'}, {emoji:'🔥',label:'Hot',spokenText:'That is hot be careful',cat:'adj'}, {emoji:'👃',label:'Smell',spokenText:'That smells good',cat:'adj'}, {emoji:'😋',label:'Yummy',spokenText:'That is yummy',cat:'adj'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'}, {emoji:'✅',label:'Done',spokenText:'I am done',cat:'social'} ], dentist:[ {emoji:'🦷',label:'Teeth',spokenText:'My teeth hurt',cat:'noun'}, {emoji:'😰',label:'Scared',spokenText:'I am scared',cat:'adj'}, {emoji:'🤕',label:'Hurts',spokenText:'That hurts',cat:'adj'}, {emoji:'✋',label:'Stop',spokenText:'Please stop',cat:'neg'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'}, {emoji:'🪥',label:'Brush',spokenText:'I brush my teeth',cat:'verb'}, {emoji:'👄',label:'Open',spokenText:'I will open my mouth',cat:'verb'}, {emoji:'❓',label:'What',spokenText:'What are you doing',cat:'question'}, {emoji:'⏳',label:'How Long',spokenText:'How much longer',cat:'question'}, {emoji:'🤗',label:'Better',spokenText:'I feel better',cat:'adj'}, {emoji:'🙏',label:'Thank You',spokenText:'Thank you',cat:'social'}, {emoji:'👍',label:'Okay',spokenText:'That is okay',cat:'social'} ], church:[ {emoji:'⛪',label:'Church',spokenText:'We are at church',cat:'noun'}, {emoji:'🤫',label:'Quiet',spokenText:'I will be quiet',cat:'social'}, {emoji:'🎵',label:'Sing',spokenText:'I want to sing',cat:'verb'}, {emoji:'🙏',label:'Pray',spokenText:'I want to pray',cat:'verb'}, {emoji:'📖',label:'Bible',spokenText:'I want the Bible',cat:'noun'}, {emoji:'🖍️',label:'Color',spokenText:'I want to color',cat:'verb'}, {emoji:'🚻',label:'Bathroom',spokenText:'I need the bathroom',cat:'verb'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'}, {emoji:'😴',label:'Tired',spokenText:'I am tired',cat:'adj'}, {emoji:'🍪',label:'Snack',spokenText:'I want a snack',cat:'noun'}, {emoji:'⏳',label:'When Done',spokenText:'When is it done',cat:'question'}, {emoji:'🏠',label:'Home',spokenText:'I want to go home',cat:'verb'} ], camping:[ {emoji:'⛺',label:'Tent',spokenText:'I want to go in the tent',cat:'noun'}, {emoji:'🔥',label:'Fire',spokenText:'I see the fire',cat:'noun'}, {emoji:'🌭',label:'Hot Dog',spokenText:'I want a hot dog',cat:'noun'}, {emoji:'🫠',label:'Marshmallow',spokenText:'I want a marshmallow',cat:'noun'}, {emoji:'🥾',label:'Hike',spokenText:'I want to go hiking',cat:'verb'}, {emoji:'🐛',label:'Bug',spokenText:'I see a bug',cat:'noun'}, {emoji:'🌲',label:'Tree',spokenText:'Look at the trees',cat:'noun'}, {emoji:'😰',label:'Scared',spokenText:'I am scared',cat:'adj'}, {emoji:'🔦',label:'Flashlight',spokenText:'I need the flashlight',cat:'noun'}, {emoji:'😴',label:'Sleep',spokenText:'I want to sleep',cat:'verb'}, {emoji:'🚻',label:'Bathroom',spokenText:'I need the bathroom',cat:'verb'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'} ], beach:[ {emoji:'🏖️',label:'Beach',spokenText:'I love the beach',cat:'noun'}, {emoji:'🏄',label:'Waves',spokenText:'I want to play in the waves',cat:'verb'}, {emoji:'🏰',label:'Sandcastle',spokenText:'I want to build a sandcastle',cat:'verb'}, {emoji:'🐚',label:'Shell',spokenText:'I found a shell',cat:'noun'}, {emoji:'🧴',label:'Sunscreen',spokenText:'I need sunscreen',cat:'noun'}, {emoji:'🍦',label:'Ice Cream',spokenText:'I want ice cream',cat:'noun'}, {emoji:'💧',label:'Water',spokenText:'I need water',cat:'noun'}, {emoji:'🥵',label:'Hot',spokenText:'I am hot',cat:'adj'}, {emoji:'😊',label:'Fun',spokenText:'This is fun',cat:'adj'}, {emoji:'🚻',label:'Bathroom',spokenText:'I need the bathroom',cat:'verb'}, {emoji:'😴',label:'Tired',spokenText:'I am tired',cat:'adj'}, {emoji:'🏠',label:'Home',spokenText:'I want to go home',cat:'verb'} ], chores:[ {emoji:'🧹',label:'Sweep',spokenText:'I will sweep the floor',cat:'verb'}, {emoji:'🧺',label:'Laundry',spokenText:'I will do the laundry',cat:'verb'}, {emoji:'🍽️',label:'Dishes',spokenText:'I will wash the dishes',cat:'verb'}, {emoji:'🛏️',label:'Make Bed',spokenText:'I will make my bed',cat:'verb'}, {emoji:'🧸',label:'Clean Up',spokenText:'I will clean up my toys',cat:'verb'}, {emoji:'🗑️',label:'Trash',spokenText:'I will take out the trash',cat:'verb'}, {emoji:'✅',label:'Done',spokenText:'I am done',cat:'social'}, {emoji:'✋',label:'Help',spokenText:'I need help',cat:'question'}, {emoji:'⏳',label:'When',spokenText:'When can I be done',cat:'question'}, {emoji:'😤',label:'Hard',spokenText:'This is hard',cat:'adj'}, {emoji:'👍',label:'Okay',spokenText:'Okay I will do it',cat:'social'}, {emoji:'🎮',label:'Play',spokenText:'Can I play now',cat:'question'} ] }; // Keyword matching for AI const AI_KEYWORDS = { zoo:['zoo','animal','safari','field trip','aquarium','farm','animals'], morning:['morning','routine','wake','preschool','get ready','daily'], doctor:['doctor','hospital','medical','clinic','nurse','sick','checkup'], playground:['playground','recess','park','play','outside','play time','swing'], grocery:['grocery','store','shopping','supermarket','market','buy'], restaurant:['restaurant','mcdonald','eat out','dining','food court','cafeteria','ordering','lunch out'], bath:['bath','shower','wash','hygiene','clean','bathroom routine'], bedtime:['bedtime','sleep','night','bed time','night routine','goodnight'], feelings:['feeling','emotion','mood','how are you','emotional'], swimming:['swim','pool','water park','swimming lesson','beach swim'], birthday:['birthday','party','celebration','cake','present'], school:['school','classroom','class','homework','teacher','learning','lesson'], travel:['travel','airplane','airport','flight','car ride','road trip','vacation','trip'], pets:['pet','dog','cat','puppy','kitten','hamster','fish tank','animal care'], sports:['sport','soccer','basketball','baseball','football','gym','exercise','team'], music:['music','sing','song','instrument','band','drum','piano','guitar','dance'], art:['art','paint','draw','craft','color','creative','clay','sculpture'], library:['library','book','reading','story time','storytime'], haircut:['haircut','barber','hair cut','hair salon','trim'], cooking:['cook','bake','kitchen','recipe','baking','chef'], dentist:['dentist','dental','teeth cleaning','tooth','orthodontist','braces'], church:['church','temple','mosque','synagogue','worship','sunday school','religious'], camping:['camping','camp','tent','campfire','hiking','nature','outdoor','scout'], beach:['beach','ocean','sand','seashore','coast','boardwalk','seaside'], chores:['chore','clean room','tidy','housework','clean up','responsibility','task'] }; const AI_NAMES = { zoo:'Zoo Field Trip',morning:'Morning Routine',doctor:'Doctor Visit', playground:'Playground Recess',grocery:'Grocery Store',restaurant:'Restaurant', bath:'Bath Time',bedtime:'Bedtime',feelings:'Feelings Board', swimming:'Swimming',birthday:'Birthday Party',school:'School Classroom', travel:'Travel',pets:'Pets',sports:'Sports',music:'Music Time', art:'Art Time',library:'Library',haircut:'Haircut',cooking:'Cooking', dentist:'Dentist Visit',church:'Church',camping:'Camping',beach:'Beach Day', chores:'Chores' }; // ── AI Board Cache (localStorage) ── // Every AI-generated board is saved here so it can be reused without API calls. function loadAICache(){ try{ const raw=localStorage.getItem('tb_ai_cache'); if(!raw) return; const cache=JSON.parse(raw); if(!Array.isArray(cache)) return; for(const entry of cache){ if(!entry.key||!entry.tiles||!Array.isArray(entry.tiles)) continue; // Merge into AI_BOARDS if not already a built-in key if(!AI_BOARDS[entry.key]){ AI_BOARDS[entry.key]=entry.tiles; } // Merge keywords if(entry.keywords && !AI_KEYWORDS[entry.key]){ AI_KEYWORDS[entry.key]=entry.keywords; } // Merge name if(entry.name && !AI_NAMES[entry.key]){ AI_NAMES[entry.key]=entry.name; } } }catch(e){console.warn('Failed to load AI cache',e);} } function saveToAICache(key,name,tiles,prompt){ try{ const raw=localStorage.getItem('tb_ai_cache'); const cache=raw?JSON.parse(raw):[]; // Don't save duplicates if(cache.some(e=>e.key===key)) return; // Generate keywords from the prompt const words=prompt.toLowerCase().replace(/[^a-z0-9\s]/g,'').split(/\s+/).filter(w=>w.length>2); const keywords=[...new Set(words)]; cache.push({key,name,tiles,prompt,keywords,savedAt:new Date().toISOString()}); localStorage.setItem('tb_ai_cache',JSON.stringify(cache)); // Also live-merge into running state AI_KEYWORDS[key]=keywords; AI_NAMES[key]=name; }catch(e){console.warn('Failed to save AI cache',e);} } function searchAICache(prompt){ // Check if any cached board closely matches this prompt before calling API const p=prompt.toLowerCase(); try{ const raw=localStorage.getItem('tb_ai_cache'); if(!raw) return null; const cache=JSON.parse(raw); for(const entry of cache){ if(!entry.prompt) continue; // Exact or near-exact prompt match const ep=entry.prompt.toLowerCase(); if(ep===p) return entry.key; // Check if 60%+ of words overlap const pWords=p.split(/\s+/).filter(w=>w.length>2); const eWords=ep.split(/\s+/).filter(w=>w.length>2); if(pWords.length>0 && eWords.length>0){ const overlap=pWords.filter(w=>eWords.includes(w)).length; if(overlap/Math.max(pWords.length,eWords.length)>=0.6) return entry.key; } } }catch(e){} return null; } // Load cached boards on startup loadAICache(); function aiMatchBoard(prompt){ const p=prompt.toLowerCase(); let best=null,bestScore=0; for(const[key,keywords]of Object.entries(AI_KEYWORDS)){ let score=0; for(const kw of keywords){ if(p.includes(kw)) score+=kw.length; } if(score>bestScore){bestScore=score;best=key;} } return best; } function aiGenerateExplanations(key,tiles){ const explanations = { zoo:[ 'Included animal names for pointing and labeling during the visit.', 'Added "scared" and "excited" — zoo environments can be overwhelming for some children.', 'Included "help", "water", and "bathroom" for safety and basic needs.', 'Added "look" and "wow" — core verbs and social words for engagement.', 'Maintained core vocabulary balance: nouns (animals), verbs (go, look), adjectives (fun, scared), social (wow), needs (water, bathroom).' ], morning:[ 'Sequenced tiles to follow a natural morning routine order: wake → hygiene → dress → eat → leave.', 'Included breakfast choices (milk, toast) for mealtime communication.', 'Added "ready" — a powerful social word that gives the child agency.', 'Included "help" for moments when the routine gets difficult.', 'Covered all SETT Framework task areas: self-care, eating, transition, social.' ], doctor:[ 'Prioritized pain communication — "hurts," "here," body mapping.', 'Included emotional tiles (scared, nauseous) — medical visits cause anxiety.', 'Added "stop" — critical for patient autonomy and consent.', 'Included "what is happening" — reduces anxiety through understanding.', 'Covered medical AAC essentials: pain, emotion, need, autonomy, safety.' ], playground:[ 'Focused on social play vocabulary: together, my turn, wait.', 'Included physical activity verbs: run, climb, swing.', 'Added turn-taking language — essential for social skill development.', 'Included basic needs (water, help) for outdoor safety.', 'Balanced action + social + need tiles for well-rounded recess communication.' ], grocery:[ 'Included common grocery items children can identify visually.', 'Added "this one" + "look" — for pointing-based communication in aisles.', 'Included "yes/no" — for responding to parent questions about preferences.', 'Added "cookie" — respecting a child\'s right to request treats.', 'Covered choice-making (want, this one) + social (yes, no, thank you).' ], restaurant:[ 'Included popular menu items that most restaurants offer.', 'Added "please" and "thank you" — restaurant manners practice.', 'Included "bathroom" — critical for restaurant visits.', 'Added "done" — lets the child signal they\'re finished.', 'Balanced requesting + social + needs for a complete dining experience.' ], bath:[ 'Sequenced for bath routine flow: start → wash → teeth → dry → dress.', 'Included temperature feedback (hot/cold) — safety communication.', 'Added "bath toy" — making hygiene routines positive.', 'Included "done" — respecting the child\'s sense of completion.', 'Covered sensory feedback (temperature) + needs (help) + autonomy (done).' ], bedtime:[ 'Followed natural bedtime sequence: prepare → comfort → settle → sleep.', 'Included "scared" — nighttime fears are real and need expression.', 'Added "story" and "song" — bedtime comfort requests.', 'Included "light on/off" — environmental control for independence.', 'Balanced comfort-seeking (hug, teddy) + needs (water, bathroom) + social (goodnight).' ], feelings:[ 'Covered the full emotional spectrum: positive, negative, and neutral.', 'Included "confused" and "frustrated" — often missing from basic emotion boards.', 'Added "calm" — supports self-regulation vocabulary.', 'Used graduated intensity: happy → excited, sad → lonely.', 'Follows Zones of Regulation framework alignment for school use.' ], swimming:[ 'Included water activity verbs: swim, splash, dive.', 'Added safety items: goggles, sunscreen, help.', 'Covered sensory feedback (cold) and emotional response (fun).', ], birthday:[ 'Included party essentials: cake, balloons, presents, games.', 'Added social words: thank you, hello — party manners.', 'Covered food requests + emotional expression (excited).', ], school:[ 'Focused on classroom activities: read, write, draw, math, art.', 'Included help-seeking: teacher, question, help — critical for school success.', 'Added self-regulation: done, bathroom — independence skills.', ], travel:[ 'Covered travel logistics: airplane, bag, ticket, seat.', 'Included comfort needs: water, snack, bathroom, music.', 'Added emotional support: scared, tired — travel can be overwhelming.', ], pets:[ 'Included common pet types and care verbs: feed, walk, hold.', 'Added "gentle" — teaching appropriate animal interaction.', 'Covered requesting + empathy (pet needs water too).', ], sports:[ 'Focused on team play vocabulary: team, my turn, together.', 'Included common sports children play at school.', 'Added needs and feelings: water, tired, fun, help.', ], music:[ 'Included instrument choices: drum, piano, guitar.', 'Added volume control: louder, quieter — sensory management.', 'Covered participation verbs: sing, dance, play.', ], art:[ 'Focused on art activity verbs: paint, color, cut.', 'Included supply requests: brush, glue, ruler, colors.', 'Added emotional response: pretty, done — creative expression.', ], library:[ 'Included topic choices: dinosaurs, animals, space, stories.', 'Added "quiet" — library behavioral expectations.', 'Covered choosing + requesting: this one, help finding.', ], haircut:[ 'Prioritized anxiety management: scared, stop, okay.', 'Included "how long" — reducing uncertainty for the child.', 'Added "mirror" — giving visual control during the experience.', ], cooking:[ 'Focused on participation verbs: cook, mix, crack.', 'Included safety awareness: hot, be careful.', 'Added sensory words: smell, yummy — making cooking fun.', ], dentist:[ 'Prioritized pain and anxiety communication: hurts, scared, stop.', 'Included "what are you doing" — reduces fear through understanding.', 'Added cooperation words: open, okay — supporting dental care.', ], church:[ 'Included participation words: sing, pray, color.', 'Added basic needs: bathroom, water, snack, tired.', 'Covered transition: when done, home — managing long services.', ], camping:[ 'Included outdoor vocabulary: tent, fire, hike, tree.', 'Added safety needs: flashlight, scared, help.', 'Covered nature exploration + comfort needs.', ], beach:[ 'Focused on beach activities: waves, sandcastle, shell.', 'Included comfort needs: sunscreen, water, hot.', 'Added transition words: home, tired, done.', ], chores:[ 'Included common household tasks appropriate for children.', 'Added completion and negotiation: done, when, can I play.', 'Covered help-seeking and emotional expression (hard).', ] }; return explanations[key] || [ 'Generated tiles based on your request.', 'Included core vocabulary for balanced communication.', 'Added safety and needs tiles.', ]; } function aiAssessBoard(board){ if(!board || !board.tiles.length) return 'No tiles to assess. Add some tiles first!'; const cats = {}; board.tiles.forEach(t=>{cats[t.category]=(cats[t.category]||0)+1;}); const missing = []; if(!cats.verb) missing.push('⚠️ No verbs — add action words like "want," "go," "help"'); if(!cats.social) missing.push('⚠️ No social words — add "please," "thank you," "yes," "hello"'); if(!cats.question) missing.push('⚠️ No question words — add "help," "where," "what"'); if(!cats.neg) missing.push('⚠️ No negation — add "no," "stop," "don\'t want"'); if(!cats.adj) missing.push('💡 Consider adding descriptors — feelings, sizes, or qualities'); let assessment = `Board Assessment: "${esc(board.name)}"
`; assessment += `${board.tiles.length} tiles across ${Object.keys(cats).length} categories.

`; if(missing.length===0){ assessment += '✅ Great balance! This board has coverage across all Fitzgerald Key categories. '; assessment += 'It includes verbs, nouns, social words, and safety vocabulary.'; } else { assessment += 'Suggestions to improve:
'; assessment += missing.map(m=>`${m}`).join(''); } const catSummary = Object.entries(cats).map(([k,v])=>`${FK[k]?.label||k}: ${v}`).join(' · '); assessment += `

📊 Breakdown: ${catSummary}`; return assessment; } // AI Chat Engine function aiAddMessage(role,html,actions){ const chat=$('aiChat'); const msg=document.createElement('div'); msg.className='ai-msg'; const isAI=role==='ai'; msg.innerHTML=`
${isAI?'✨':'👤'}
${html}
${actions?`
${actions}
`:''}
`; chat.appendChild(msg); chat.scrollTop=chat.scrollHeight; // Bind action buttons msg.querySelectorAll('.ai-action-btn').forEach(btn=>{ btn.addEventListener('click',()=>handleAIAction(btn.dataset.action,btn.dataset.key)); }); } function aiShowTyping(){ const chat=$('aiChat'); const typing=document.createElement('div'); typing.className='ai-msg'; typing.id='aiTyping'; typing.innerHTML=`
`; chat.appendChild(typing); chat.scrollTop=chat.scrollHeight; } function aiHideTyping(){const t=$('aiTyping');if(t)t.remove();} function handleAIPrompt(prompt){ if(!prompt.trim())return; // Show user message aiAddMessage('user',esc(prompt)); $('aiInput').value=''; // Check for assessment request const p=prompt.toLowerCase(); if(p.includes('assess')||p.includes('review')||p.includes('check')||p.includes('missing')||p.includes('improve')){ const board=getBoard(); aiShowTyping(); setTimeout(()=>{ aiHideTyping(); aiAddMessage('ai',aiAssessBoard(board)); },800); return; } // Check for modification request on current board if(p.includes('remove')||p.includes('delete')||p.includes('take out')){ const board=getBoard(); if(board){ const words=p.split(/\s+/); let removed=[]; board.tiles=board.tiles.filter(t=>{ const label=t.label.toLowerCase(); if(words.some(w=>label.includes(w)&&w.length>2)){ removed.push(t.label); return false; } return true; }); board.tiles.forEach((t,i)=>t.order=i); saveState();renderTiles(); aiShowTyping(); setTimeout(()=>{ aiHideTyping(); if(removed.length){ aiAddMessage('ai',`Done! Removed ${removed.join(', ')} from the board. ${board.tiles.length} tiles remaining.`); } else { aiAddMessage('ai','I couldn\'t find those tiles to remove. Try being more specific with the tile name.'); } },500); return; } } if(p.includes('add ')&&(p.includes('tile')||!p.includes('board'))){ const board=getBoard(); if(board){ // Try to extract what to add const match=p.match(/add\s+(?:a\s+)?['""]?(\w+)['""]?/i); if(match){ const label=match[1].charAt(0).toUpperCase()+match[1].slice(1); const EMOJI_MAP={dolphin:'🐬',whale:'🐋',shark:'🦈',fish:'🐟',octopus:'🐙',turtle:'🐢',crab:'🦀', lion:'🦁',tiger:'🐯',bear:'🐻',elephant:'🐘',giraffe:'🦒',monkey:'🐒',penguin:'🐧',bird:'🦜', dog:'🐕',cat:'🐈',rabbit:'🐇',horse:'🐴',cow:'🐄',pig:'🐷',chicken:'🐔',duck:'🦆',frog:'🐸', snake:'🐍',butterfly:'🦋',bee:'🐝',ant:'🐜',spider:'🕷️',dinosaur:'🦕',dragon:'🐉',unicorn:'🦄', pizza:'🍕',burger:'🍔',sandwich:'🥪',apple:'🍎',banana:'🍌',cookie:'🍪',cake:'🎂',ice:'🍦', water:'💧',juice:'🧃',milk:'🥛',coffee:'☕',bread:'🍞',rice:'🍚',egg:'🥚',cheese:'🧀', car:'🚗',bus:'🚌',train:'🚂',airplane:'✈️',boat:'⛵',bike:'🚲',rocket:'🚀', ball:'⚽',book:'📖',music:'🎵',star:'⭐',heart:'❤️',sun:'☀️',moon:'🌙',rain:'🌧️', tree:'🌳',flower:'🌸',house:'🏠',school:'🏫',hospital:'🏥',park:'🏞️',beach:'🏖️', happy:'😊',sad:'😢',angry:'😠',scared:'😰',tired:'😴',sick:'🤒',love:'🥰', yes:'👍',no:'👎',stop:'🛑',help:'✋',please:'🙏',thanks:'🙏',sorry:'😔',hello:'👋',bye:'👋', eat:'🍽️',drink:'🥤',sleep:'😴',play:'🎮',run:'🏃',walk:'🚶',sit:'🪑',read:'📖', bathroom:'🚻',hot:'🔥',cold:'🥶',big:'🐘',small:'🐜',more:'➕',done:'✅'}; const emoji=EMOJI_MAP[label.toLowerCase()]||'🏷️'; pushUndo(); board.tiles.push({ id:'tile_'+Date.now()+'_'+Math.random().toString(36).slice(2,5), emoji:emoji,label:label,spokenText:'I want '+label.toLowerCase(), category:'noun',color:FK.noun.color,order:board.tiles.length }); saveState();renderTiles(); aiShowTyping(); setTimeout(()=>{ aiHideTyping(); aiAddMessage('ai',`Added "${label}" tile! You can click it to change the emoji and category.`); },500); return; } } } // Match to a board type (includes pre-built + cached boards) const match=aiMatchBoard(prompt); if(match && AI_BOARDS[match]){ aiShowTyping(); setTimeout(()=>{ aiHideTyping(); const tiles=AI_BOARDS[match]; const boardName=AI_NAMES[match]||match; const isCached=match.startsWith('_ai_custom_'); const explanations=aiGenerateExplanations(match); let html; if(isCached){ html=`Found a saved board: ${esc(boardName)} with ${tiles.length} tiles (no API call needed!):

`; html+=tiles.map(t=>`${t.emoji} ${t.label} — "${t.spokenText}"`).join(''); } else { html=`I've drafted a ${boardName} board with ${tiles.length} tiles. Here's my reasoning:

`; html+=explanations.map(e=>`${e}`).join(''); } html+=`
Want me to apply this to your board?`; const actions=` `; aiAddMessage('ai',html,actions); },1200); } else { // No pre-built or cached match — try real AI if configured const aiConfig=getAIConfig(); if(aiConfig){ aiShowTyping(); aiCallAPI(aiConfig,prompt); } else { // Check if there are cached boards to suggest let cacheSuggestions=''; try{ const raw=localStorage.getItem('tb_ai_cache'); if(raw){ const cache=JSON.parse(raw); if(cache.length>0){ cacheSuggestions=`

You have ${cache.length} saved board(s) from previous AI generations. Try asking for something similar!`; } } }catch(e){} aiShowTyping(); setTimeout(()=>{ aiHideTyping(); aiAddMessage('ai',`I don't have a pre-built board for "${esc(prompt)}".

` + `To generate custom boards with AI, go to Settings and add an API key (OpenAI, Gemini, or Anthropic).

` + `Or try a pre-built topic:
` + `"Build a zoo field trip board"` + `"Create a morning routine for preschool"` + `"Doctor visit board for a 5 year old"` + `"Swimming lesson" or "Birthday party"` + `"School classroom" or "Dentist visit"` + cacheSuggestions ); },600); } } } // ── AI API call ── async function aiCallAPI(config,prompt){ // Check cache first — avoid paying API credits for something we already generated const cachedKey=searchAICache(prompt); if(cachedKey && AI_BOARDS[cachedKey]){ aiHideTyping(); const tiles=AI_BOARDS[cachedKey]; const name=AI_NAMES[cachedKey]||'Saved Board'; let html=`Found a saved board matching "${esc(prompt)}" — ${name} with ${tiles.length} tiles (no API call needed!):

`; html+=tiles.map(t=>`${t.emoji} ${t.label} — "${t.spokenText}"`).join(''); html+=`
Apply these tiles to your board?`; const actions=` `; aiAddMessage('ai',html,actions); return; } try{ const resp=await fetch('/api/ai/generate',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({provider:config.provider,apiKey:config.apiKey,prompt:prompt}) }); const result=await resp.json(); aiHideTyping(); if(result.success&&result.tiles&&result.tiles.length){ // Store tiles for action buttons const key='_ai_custom_'+Date.now(); const tiles=result.tiles.map(t=>({ emoji:t.emoji,label:t.label,spokenText:t.spokenText,cat:t.category })); AI_BOARDS[key]=tiles; // Derive a board name from prompt const boardName=prompt.length>40?prompt.slice(0,37)+'...':prompt; AI_NAMES[key]=boardName; // Save to cache for reuse saveToAICache(key,boardName,tiles,prompt); let html=`Generated ${result.tiles.length} tiles for "${esc(prompt)}" (saved for reuse):

`; html+=result.tiles.map(t=>`${t.emoji} ${t.label} — "${t.spokenText}"`).join(''); html+=`
Apply these tiles to your board?`; const actions=` `; aiAddMessage('ai',html,actions); }else{ aiAddMessage('ai',`AI generation failed: ${esc(result.error||'Unknown error')}

Check your API key in Settings.`); } }catch(e){ aiHideTyping(); aiAddMessage('ai',`Failed to reach AI server: ${esc(e.message)}
Make sure server.py is running.`); } } function handleAIAction(action,key){ // Handle regenerate action (bypasses cache) if(action==='regen'){ const aiConfig=getAIConfig(); if(!aiConfig){ aiAddMessage('ai','No AI provider configured. Go to Settings to add an API key.'); return; } const btn=document.querySelector(`[data-action="regen"][data-key="${key}"]`); const prompt=btn?btn.dataset.prompt:''; if(!prompt){aiAddMessage('ai','Could not find the original prompt to regenerate.');return;} // Remove old cached entry so fresh call goes through try{ const raw=localStorage.getItem('tb_ai_cache'); if(raw){ const cache=JSON.parse(raw).filter(e=>e.key!==key); localStorage.setItem('tb_ai_cache',JSON.stringify(cache)); } }catch(e){} delete AI_BOARDS[key]; delete AI_KEYWORDS[key]; delete AI_NAMES[key]; aiShowTyping(); aiCallAPI(aiConfig,prompt); return; } const tiles=AI_BOARDS[key]; if(!tiles)return; if(action==='apply'){ const board=getBoard(); if(board){ pushUndo(); board.name=AI_NAMES[key]||board.name; board.tiles=tiles.map((t,i)=>({ id:'tile_'+Date.now()+'_'+i+'_'+Math.random().toString(36).slice(2,5), emoji:t.emoji,label:t.label,spokenText:t.spokenText, category:t.cat,color:FK[t.cat]?.color||'#AAAAAA',order:i })); saveState();renderTiles();renderBoardList(); $('boardName').value=board.name; aiAddMessage('ai','✅ Board updated! '+board.tiles.length+' tiles applied. You can now drag to reorder, click any tile to edit, or ask me to adjust.

Try: "remove penguin" or "add a dolphin" or "assess this board"'); toast('✅','Board applied!'); } } else if(action==='new'){ createBoard(AI_NAMES[key]||'New Board','custom',tiles.length>12?4:3,tiles); aiAddMessage('ai','✅ New board created! Check your sidebar — it\'s ready to customize.'); toast('✅','New board created!'); } else if(action==='fewer'){ // Reduce to 8 most essential tiles (keep variety) const essential=[]; const seen=new Set(); for(const t of tiles){ if(!seen.has(t.cat)){essential.push(t);seen.add(t.cat);} if(essential.length>=6)break; } // Fill remaining with most useful for(const t of tiles){ if(essential.length>=8)break; if(!essential.includes(t))essential.push(t); } const board=getBoard(); if(board){ pushUndo(); board.tiles=essential.map((t,i)=>({ id:'tile_'+Date.now()+'_'+i+'_'+Math.random().toString(36).slice(2,5), emoji:t.emoji,label:t.label,spokenText:t.spokenText, category:t.cat,color:FK[t.cat]?.color||'#AAAAAA',order:i })); saveState();renderTiles(); aiAddMessage('ai','✅ Reduced to '+essential.length+' essential tiles — kept one of each category for balanced communication. You can still add individual tiles by clicking "+ Add Tile" or asking me.'); toast('✅','Reduced to '+essential.length+' tiles!'); } } } // AI event bindings function bindAIEvents(){ // Board Assistant always open — no toggle $('aiSend').addEventListener('click',()=>handleAIPrompt($('aiInput').value)); $('aiInput').addEventListener('keydown',e=>{if(e.key==='Enter')handleAIPrompt($('aiInput').value);}); document.querySelectorAll('.ai-suggest-chip').forEach(chip=>{ chip.addEventListener('click',()=>{ $('aiHelper').classList.add('open'); handleAIPrompt(chip.dataset.prompt); }); }); } // ── Image Picker Events ── $('imagePickerOverlay').addEventListener('click',closeImagePicker); $('imagePickerClose').addEventListener('click',closeImagePicker); $('imagePickerSearchBtn').addEventListener('click',()=>{ const q=$('imagePickerQuery').value.trim(); if(q) runImageSearch(q); }); $('imagePickerQuery').addEventListener('keydown',e=>{ if(e.key==='Enter'){ const q=$('imagePickerQuery').value.trim(); if(q) runImageSearch(q); } }); // ── Start ── init(); bindAIEvents();