ruvector/docs/explorer/explorer-shell.js
rUv 2a257cdba7
Add focus and fullscreen controls to RuVector Explorer
Add controls to collapse the explorer interface and expand the animated canvas, with browser fullscreen support and a fallback view.
2026-10-14 02:38:05 -07:00

46 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// App navigation layer. Existing engine nodes and event handlers retain their identity.
const tabs=[['search','Search','Vector search and engine evidence'],['learn','Learn','Trajectory memory and feedback'],['bench','Benchmarks','Measured comparisons and reference data'],['capabilities','Capabilities','Sixteen capability demonstrations']];
const icons={search:'M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14M15 15l6 6',learn:'M12 3v18M5 7l7-4 7 4M5 17l7 4 7-4M5 7v10M19 7v10M5 7l14 10M19 7L5 17',bench:'M4 20h17M6 16V9M12 16V4M18 16v-5',capabilities:'M12 3l9 5v9l-9 5-9-5V8zM3 8l9 5 9-5M12 13v9'};
const icon=id=>`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" aria-hidden="true"><path d="${icons[id]}"/></svg>`;
const header=document.querySelector('.explorer-intro'),stage=document.getElementById('stage'),strip=document.querySelector('.strip'),learn=document.querySelector('.learn'),panels=document.querySelector('.panels'),bench=document.querySelector('.bench'),ref=document.querySelector('.ref'),lab=document.getElementById('rv-capability-lab');
const top=document.createElement('nav');top.className='rx-topnav';top.setAttribute('aria-label','Explorer sections');top.setAttribute('role','tablist');
const dock=document.createElement('nav');dock.className='rx-dock';dock.setAttribute('aria-label','App navigation');dock.setAttribute('role','tablist');
function navHTML(where){return tabs.map(([id,label,description])=>`<button id="rx-${where}-${id}" role="tab" data-rx-tab="${id}" aria-selected="false" aria-controls="rx-panel-${id}" title="${description}">${icon(id)}<span>${label}</span></button>`).join('');}
top.innerHTML=navHTML('top')+'<button class="rx-help" data-rx-help aria-label="Open Explorer guide">?</button>';dock.innerHTML=navHTML('dock');header.after(top);document.body.append(dock);
const workspace=document.createElement('main');workspace.id='rx-workspace';top.after(workspace);workspace.append(stage,strip);
const routes={};for(const [id,label]of tabs){const p=document.createElement('section');p.id='rx-panel-'+id;p.className='rx-panel';p.setAttribute('role','tabpanel');p.setAttribute('aria-label',label);p.tabIndex=-1;workspace.append(p);routes[id]=p;}
routes.search.append(panels);routes.learn.append(learn);routes.bench.append(bench,ref);routes.capabilities.append(lab);
const motif=(type)=>{const color=type==='learn'?'#7ef0cf':type==='bench'?'#7ba5ff':'#ff7a3d';let edges='',nodes='';for(let i=0;i<18;i++){const layer=Math.floor(i/6),x=40+layer*130,y=15+(i%6)*22;const px=layer?40+(layer-1)*130:20,py=layer?15+(Math.floor((i%6)/2)*2)*22:72,d=`M${px} ${py}Q${(px+x)/2} ${py} ${x} ${y}`;edges+=`<path d="${d}" stroke="${color}" opacity=".2"/><path class="rx-signal" d="${d}" stroke="${color}" pathLength="100" style="animation-delay:-${i*.2}s"/>`;nodes+=`<circle cx="${x}" cy="${y}" r="${layer?2:3}" fill="${color}" opacity=".7"/>`; }return `<svg class="rx-motif" viewBox="0 0 350 150" aria-hidden="true" fill="none">${edges}${nodes}</svg>`;};
function sectionHeader(id,kicker,title,body){const h=document.createElement('div');h.className='rx-section-head';h.innerHTML=`<div><span class="lbl">${kicker}</span><h2>${title}</h2><p>${body}</p><button class="rx-details" data-rx-info="${id}">How it works ↗</button></div>${motif(id)}`;routes[id].prepend(h);}
sectionHeader('search','RETRIEVAL / STRUCTURE / EVIDENCE','Inspect the search.','Engine metadata, the query path and recall measurements stay together.');
sectionHeader('learn','MEMORY / SEARCH BREADTH / OUTCOMES','Follow the learning loop.','Separate memory hints from controller savings. Compare recall before judging cost.');
sectionHeader('bench','SAME DATA / SAME QUERIES / MEASURED RESULTS','Compare the engines.','The benchmark runs in your browser. Published compression references remain separate.');
const tips={search:{title:'Read a search trajectory',body:'Run a query, then select Canopy, Hyper, Tree or Space. The glowing branch is the explored search path. The WASM result and the JavaScript trace are separate engines; their parameters and timings can differ.',items:['Inspect recall against the exact baseline.','Use settings to change the dataset and index structure.','Save an index only when browser persistence is available.']},learn:{title:'Read the learning evidence',body:'The trajectory memory retains past queries and answers. A controller adjusts search breadth toward a target recall. This demonstration does not modify the graph or train SONA weights.',items:['Static and learned searches share the same query.','Memory-only and controller-only trials separate their contributions.','Lower distance evaluations count as a benefit only at acceptable recall.']},bench:{title:'Compare without mixing evidence',body:'The benchmark compares engines on identical synthetic data and queries. Browser timings depend on this device and workload. The compression reference table describes separate Rust capabilities, not the browser WASM build.',items:['Report recall together with latency.','Keep JS traces and WASM results distinct.','Repeat on the deployment hardware before making a production claim.']}};
const dialog=document.createElement('dialog');dialog.className='rx-dialog';dialog.id='rx-detail-dialog';dialog.setAttribute('aria-labelledby','rx-dialog-title');dialog.innerHTML='<div class="rx-dialog-bar"><span class="lbl">RUVECTOR / EXPLORER GUIDE</span><button data-rx-close aria-label="Close detail">×</button></div><div id="rx-dialog-content"></div>';document.body.append(dialog);let returnFocus=null;
function openDialog(title,body,items=[],source=null){returnFocus=document.activeElement;const content=document.getElementById('rx-dialog-content');content.replaceChildren();const visual=document.createElement('div');visual.innerHTML=motif(active);content.append(visual);const h=document.createElement('h2');h.id='rx-dialog-title';h.textContent=title;content.append(h);const p=document.createElement('p');p.textContent=body;content.append(p);if(items.length){const ul=document.createElement('ul');items.forEach(text=>{const li=document.createElement('li');li.textContent=text;ul.append(li);});content.append(ul);}if(source){const a=document.createElement('a');a.textContent='Open source implementation ↗';a.href=source;a.target='_blank';a.rel='noopener';content.append(a);}dialog.showModal();window.dispatchEvent(new CustomEvent('ruvector:guide',{detail:{title,body,items,source,section:active,mode:lab.dataset.mode}}));dialog.querySelector('[data-rx-close]').focus();}
function closeDialog(){dialog.close();returnFocus?.focus({preventScroll:true});}
dialog.addEventListener('cancel',e=>{e.preventDefault();closeDialog();});dialog.addEventListener('click',e=>{if(e.target===dialog||e.target.closest('[data-rx-close]'))closeDialog();});
let active='search';let savedSearchView='canopy';
function setTab(id,{focus=false,history=true}={}){if(!routes[id])id='search';const previous=active;if(previous==='search'){savedSearchView=document.querySelector('#viewTabs [aria-pressed="true"]')?.dataset.view||savedSearchView;}active=id;document.body.dataset.rxSection=id;Object.entries(routes).forEach(([key,p])=>{p.hidden=key!==id;});document.querySelectorAll('[data-rx-tab]').forEach(b=>{b.setAttribute('aria-selected',String(b.dataset.rxTab===id));b.tabIndex=b.dataset.rxTab===id?0:-1;});
// The original canvas remains mounted, so engine state, export and listeners survive navigation.
stage.classList.toggle('rx-compact',id!=='search');strip.hidden=id==='capabilities';
if(id==='learn')document.querySelector('#viewTabs [data-view="learn"]')?.click();else if(previous==='learn')document.querySelector('#viewTabs [data-view="'+savedSearchView+'"]')?.click();
if(history)window.history.replaceState(null,'','#'+id);if(focus)routes[id].focus({preventScroll:true});window.dispatchEvent(new Event('resize'));}
document.addEventListener('click',e=>{const tab=e.target.closest('[data-rx-tab]');if(tab){setTab(tab.dataset.rxTab);window.scrollTo({top:0,behavior:matchMedia('(prefers-reduced-motion:reduce)').matches?'auto':'smooth'});return;}const info=e.target.closest('[data-rx-info]');if(info){const t=tips[info.dataset.rxInfo];openDialog(t.title,t.body,t.items);return;}if(e.target.closest('[data-rx-help]')){const t=tips[active]||{title:'Explore the capability atlas',body:'Each exhibit labels whether it runs a local calculation or illustrates architecture. Select a capability, adjust its controls, then open the implementation to inspect the actual engine.',items:['Computed fixtures do not imply production integration.','Use the original Search tab for measured WASM results.','Open a capability card to read its mechanism and boundary.']};openDialog(t.title,t.body,t.items);}});
// Capability cards open focused details while the scene stays selected in its own panel.
lab.addEventListener('click',e=>{const card=e.target.closest('.rvcl-card');if(!card)return;e.preventDefault();e.stopImmediatePropagation();lab.querySelector('.rvcl-tabs [data-rvcl-mode="'+card.dataset.rvclMode+'"]')?.click();openDialog(document.getElementById('rvcl-title').textContent,document.getElementById('rvcl-explain').textContent,[document.getElementById('rvcl-disclosure').textContent,document.getElementById('rvcl-evidence').textContent],document.getElementById('rvcl-source').href);},{capture:true});
// The existing project-link anchor now navigates to the atlas tab.
header.querySelector('a[href="#rv-capability-lab"]')?.addEventListener('click',e=>{e.preventDefault();setTab('capabilities');window.scrollTo({top:0,behavior:'smooth'});});
lab.addEventListener('click',e=>{const a=e.target.closest('a[href="#stage"]');if(a){e.preventDefault();setTab('search');window.scrollTo({top:0,behavior:'smooth'});}});
[top,dock].forEach(nav=>nav.addEventListener('keydown',e=>{const b=e.target.closest('[data-rx-tab]');if(!b)return;const keys=['ArrowRight','ArrowLeft','Home','End'];if(!keys.includes(e.key))return;e.preventDefault();e.stopPropagation();let i=tabs.findIndex(([id])=>id===b.dataset.rxTab);i=e.key==='Home'?0:e.key==='End'?tabs.length-1:(i+(e.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;setTab(tabs[i][0]);nav.querySelector('[data-rx-tab="'+tabs[i][0]+'"]').focus();}));
window.addEventListener('ruvector:navigate',e=>setTab(e.detail,{focus:true}));
window.addEventListener('hashchange',()=>setTab(location.hash.slice(1),{history:false}));
setTab(location.hash.slice(1)||'search',{history:false});
// Let the visualization take over the page without removing its own controls.
const focusBtn=document.getElementById('focusBtn'),fullBtn=document.getElementById('fullBtn');let focusFallback=false;
function setFocusMode(on){document.body.classList.toggle('rx-focus-mode',on);focusBtn.setAttribute('aria-pressed',String(on));focusBtn.setAttribute('aria-label',on?'Restore explorer interface':'Collapse explorer interface');focusBtn.title=on?'Restore explorer interface':'Collapse explorer interface';window.dispatchEvent(new Event('resize'));}
focusBtn.addEventListener('click',()=>setFocusMode(!document.body.classList.contains('rx-focus-mode')));
function syncFullscreen(){const on=document.fullscreenElement===stage;stage.classList.toggle('rx-fullscreen',on);if(on)focusFallback=false;fullBtn.setAttribute('aria-label',on?'Exit animation fullscreen':focusFallback?'Restore page view':'Enter animation fullscreen');fullBtn.title=on?'Exit fullscreen':focusFallback?'Restore page view':'Fullscreen animation';window.dispatchEvent(new Event('resize'));}
fullBtn.addEventListener('click',async()=>{try{if(document.fullscreenElement){await document.exitFullscreen();}else if(focusFallback){focusFallback=false;setFocusMode(false);syncFullscreen();}else if(stage.requestFullscreen){await stage.requestFullscreen();}else{focusFallback=true;setFocusMode(true);syncFullscreen();}}catch(error){focusFallback=true;setFocusMode(true);syncFullscreen();console.warn('Fullscreen unavailable; expanded the animation to fill the page instead.',error);}});
document.addEventListener('fullscreenchange',syncFullscreen);
document.addEventListener('fullscreenerror',()=>setFocusMode(true));