decision1-studio / static /model-menu.js
Xunzhuo's picture
Launch Decision 1.0 studio with independent model queues and Mars banner
935d4b2 verified
Raw History Blame Contribute Delete
14.2 kB
// Registry-driven model presentation. No backend IDs, weights or readiness are invented.
const esc = value => String(value).replace(/[&<>"']/g,x=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[x]));
export function validateRegistry(result) {
if(!Array.isArray(result?.models)||!result.models.length||(result.default!==undefined&&!result.models.some(m=>m.id===result.default)))throw Error('Invalid model registry.');
const text=(v,max)=>typeof v==='string'&&v.trim()&&v.length<=max;
const ids=new Set(),wireIds=new Set();
for(const m of result.models){
if(!text(m.id,128)||!text(m.wire_id,128)||ids.has(m.id)||wireIds.has(m.wire_id)||!text(m.label,64)||!text(m.version,32)||!/^[0-9a-f]{64}$/.test(m.manifest_sha256)||!Number.isInteger(m.complete_input_tokens)||m.complete_input_tokens<1)throw Error('Invalid model identity.');
ids.add(m.id);wireIds.add(m.wire_id);
if(m.description!==undefined&&!text(m.description,240)||m.parameter_label!==undefined&&!text(m.parameter_label,32))throw Error('Invalid model description.');
if(m.example_ids!==undefined&&(!Array.isArray(m.example_ids)||!m.example_ids.length||m.example_ids.some(x=>!text(x,128))||new Set(m.example_ids).size!==m.example_ids.length))throw Error('Invalid model examples.');
if(m.default_example_id!==undefined&&!text(m.default_example_id,128))throw Error('Invalid default example.');
if(m.release_date!==undefined&&!/^\d{4}-\d{2}-\d{2}$/.test(m.release_date))throw Error('Invalid model release date.');
}
return result;
}
// Presentation facts come only from registry fields and the canonical Hub ID
// (Decision-<version>-<Codename>-<Size>[-Reasoning]).
export function modelPresentation(model) {
const repo=String(model.repo_id||model.id||'').split('/').pop();
const parsed=/^Decision-(\d+(?:\.\d+)*)-(.+?)-(\d+(?:\.\d+)?[BM])(?:-(Reasoning))?$/i.exec(repo);
const reasoning=/-reasoning$/i.test(repo)||/\breasoning\b/i.test(model.label||'');
const codename=String(model.label||parsed?.[2]||repo).replace(/[\s-]*reasoning$/i,'').trim()||repo;
const size=model.parameter_label||parsed?.[3]||'';
const generation=model.version||parsed?.[1]||'';
const name=[codename,size].filter(Boolean).join(' ')+(reasoning?' Reasoning':'');
return {codename,size,generation,reasoning,name,description:model.description||'',
release:model.release_date||'',releaseLabel:formatRelease(model.release_date)};
}
export function formatRelease(iso) {
if(typeof iso!=='string'||!/^\d{4}-\d{2}-\d{2}$/.test(iso))return '';
const date=new Date(iso+'T00:00:00Z');
return Number.isNaN(date.getTime())?'':new Intl.DateTimeFormat('en',{month:'short',day:'numeric',year:'numeric',timeZone:'UTC'}).format(date);
}
const versionParts=v=>String(v).split('.').map(x=>Number.parseInt(x,10)||0);
export function compareVersions(a,b){const x=versionParts(a),y=versionParts(b);for(let i=0;i<Math.max(x.length,y.length);i++){const d=(y[i]||0)-(x[i]||0);if(d)return d;}return 0;}
// Newest generation first; the registry's own order is kept inside a generation.
export function groupModels(models) {
const groups=new Map();
for(const model of models){const generation=modelPresentation(model).generation;if(!groups.has(generation))groups.set(generation,[]);groups.get(generation).push(model);}
return [...groups].sort((a,b)=>compareVersions(a[0],b[0])).map(([generation,items])=>({generation,label:generation?`Decision ${generation}`:'Decision',models:items}));
}
export function orderedModels(models){return groupModels(models).flatMap(group=>group.models);}
// The first base (non-Reasoning) model of the newest generation. A server
// default is honoured only inside that generation.
export function preferredModel(models,serverDefault) {
const newest=groupModels(models)[0]?.models||[];
const named=newest.find(m=>m.id===serverDefault&&!modelPresentation(m).reasoning);
return named||newest.find(m=>!modelPresentation(m).reasoning)||newest[0]||null;
}
export function statusName(model,models) {
const p=modelPresentation(model),newest=groupModels(models)[0]?.generation;
return p.generation&&newest&&p.generation!==newest?`${p.name} (${p.generation})`:p.name;
}
export function examplesForModel(model,catalog) {
if(!Array.isArray(catalog)||!catalog.length||new Set(catalog.map(e=>e.id)).size!==catalog.length)throw Error('Invalid example catalog.');
const byId=new Map(catalog.map(e=>[e.id,e]));
const examples=model.example_ids===undefined?catalog:model.example_ids.map(id=>{if(!byId.has(id))throw Error('Model example is missing.');return byId.get(id);});
if(model.default_example_id!==undefined&&!examples.some(e=>e.id===model.default_example_id))throw Error('Default example is outside this model’s catalog.');
return {examples,defaultExample:examples.find(e=>e.id===model.default_example_id)||examples[0]};
}
export const exampleMode = example => example.surface === 'routing' ? 'routing' : Array.isArray(example.states) ? 'batch' : 'single';
// These are presentation prompts only. Runtime identity and answer semantics stay unchanged.
function routingExampleForModel(example,model) {
const modelId=model.wire_id||model.id;
const variant=example.model_variants?.[modelId];
if(!variant)return example;
if(variant&&(
variant.state!==undefined&&(typeof variant.state!=='string'||!variant.state.trim())
||Object.keys(variant).some(key=>!['state','questions'].includes(key))
||Object.keys(variant.questions||{}).some(key=>!example.questions[key])
))throw Error('Invalid model-specific routing example.');
const questions=Object.fromEntries(Object.entries(example.questions).map(([key,question])=>{
const override=variant?.questions?.[key]||{};
if(Object.keys(override).some(field=>!['instructions','criteria'].includes(field)))throw Error('Invalid routing question override.');
if(override.instructions!==undefined&&(typeof override.instructions!=='string'||!override.instructions.trim()))throw Error('Invalid routing question wording.');
if(override.criteria!==undefined&&(
!question.criteria||Array.isArray(question.criteria)||!override.criteria||typeof override.criteria!=='object'
||Object.keys(override.criteria).some(candidate=>!(candidate in question.criteria)||typeof override.criteria[candidate]!=='string'||!override.criteria[candidate].trim())
))throw Error('Invalid routing choice override.');
return [key,{...question,...override,
...(override.criteria?{criteria:{...question.criteria,...override.criteria}}:{})}];
}));
return {...example,state:variant?.state||example.state,questions};
}
export function studioExamplesForModel(model,catalog) {
const modelCatalog=examplesForModel(model,catalog);
const routing=catalog.filter(example=>exampleMode(example)==='routing').map(example=>routingExampleForModel(example,model));
// Without an allowlist the catalog holds one model-tuned copy of each
// scenario per model; show each title once.
const titles=new Set();
const scenarios=modelCatalog.examples.filter(example=>exampleMode(example)!=='routing'&&(model.example_ids!==undefined||!titles.has(example.title)&&titles.add(example.title)));
return {
examples:[...routing,...scenarios],
defaultExample:routing[0]||modelCatalog.defaultExample,
};
}
export function modelReadiness(status) {
if(!status)return {label:'Checking',state:'checking'};
if(!status.configured||status.phase==='unavailable')return {label:'Unavailable',state:'offline'};
if(!status.loaded||status.phase==='loading')return {label:'Connecting',state:'loading'};
if(status.running||status.phase==='running')return {label:'Working',state:'working'};
return {label:'Ready',state:'ready'};
}
const tokenLabel=n=>n%1024===0?`${n/1024}K`:n.toLocaleString('en');
function optionHTML(m,selected) {
const p=modelPresentation(m);
const meta=[p.description,p.releaseLabel&&`Released ${p.releaseLabel}`].filter(Boolean).join(' · ');
return `<button type="button" role="option" class="model-option" data-model="${esc(m.id)}" aria-selected="${m.id===selected}" tabindex="-1">`
+`<span class="status-dot" aria-hidden="true"></span><span class="model-option-copy">`
+`<span class="model-option-title"><span class="model-option-name">${esc(p.codename)}</span>${p.size?`<span class="model-option-size">${esc(p.size)}</span>`:''}${p.reasoning?'<span class="badge badge-reasoning">Reasoning</span>':''}<span class="model-option-state"></span></span>`
+`${meta?`<span class="model-option-meta">${esc(meta)}</span>`:''}</span>`
+`<svg class="model-option-check" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="m3.5 8.5 3 3 6-7"/></svg></button>`;
}
export function createModelMenu({getModels,getSelected,getStatus,onSelect,onOpen,reducedMotion}) {
const $=id=>document.getElementById(id),trigger=$('model-trigger'),popover=$('model-popover'),options=$('model-options');
let open=false,focusIndex=0;
const buttons=()=>Array.from(options.querySelectorAll('[role=option]'));
function position(){
if(!open)return;
const r=trigger.getBoundingClientRect(),margin=12,gap=8,width=Math.min(368,innerWidth-margin*2);
const below=innerHeight-r.bottom-gap-margin,above=r.top-gap-margin,side=below>=Math.min(420,above)?'below':'above';
popover.style.width=width+'px';popover.style.maxHeight=Math.max(160,Math.min(560,side==='below'?below:above))+'px';
const height=popover.offsetHeight,left=Math.max(margin,Math.min(innerWidth-width-margin,r.right-width));
popover.style.left=left+'px';popover.style.top=(side==='below'?r.bottom+gap:r.top-gap-height)+'px';popover.dataset.side=side;
}
function focusAt(index){const list=buttons();focusIndex=(index+list.length)%list.length;list.forEach((b,i)=>b.tabIndex=i===focusIndex?0:-1);list[focusIndex]?.focus({preventScroll:true});list[focusIndex]?.scrollIntoView({block:'nearest'});}
function updateStatuses(){
for(const button of buttons()){
const status=modelReadiness(getStatus(button.dataset.model));button.dataset.readiness=status.state;button.setAttribute('aria-description',status.label);
button.querySelector('.model-option-state').textContent=status.state==='ready'||status.state==='checking'?'':status.label;
button.setAttribute('aria-selected',String(button.dataset.model===getSelected()));
}
}
function render(){
const selected=getSelected();
options.innerHTML=groupModels(getModels()).map((group,index)=>`<div class="model-group" role="group" aria-labelledby="model-group-${index}"><div class="model-group-label" id="model-group-${index}" role="presentation">${esc(group.label)}</div>${group.models.map(m=>optionHTML(m,selected)).join('')}</div>`).join('');
updateStatuses();
}
function close(returnFocus=false){open=false;popover.hidden=true;trigger.setAttribute('aria-expanded','false');if(returnFocus)trigger.focus({preventScroll:true});}
function show(index){
if(getModels().length<2||trigger.disabled)return;
render();open=true;popover.hidden=false;trigger.setAttribute('aria-expanded','true');position();
const selected=buttons().findIndex(b=>b.dataset.model===getSelected());focusAt(index??Math.max(0,selected));onOpen?.();
}
function choose(button){const id=button.dataset.model;close(true);if(id!==getSelected())onSelect(id);}
trigger.addEventListener('click',()=>open?close(true):show());
trigger.addEventListener('keydown',e=>{if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();show(e.key==='ArrowUp'?buttons().length-1:undefined);}else if(e.key==='Escape')close(true);});
options.addEventListener('click',e=>{const b=e.target.closest('[role=option]');if(b)choose(b);});
options.addEventListener('keydown',e=>{
if(['ArrowDown','ArrowUp','Home','End'].includes(e.key)){e.preventDefault();const index=e.key==='Home'?0:e.key==='End'?buttons().length-1:focusIndex+(e.key==='ArrowDown'?1:-1);focusAt(index);}
else if(e.key==='Escape'){e.preventDefault();e.stopPropagation();close(true);}
else if(e.key==='Tab')close(true);
else if(e.key==='Enter'||e.key===' '){e.preventDefault();const b=document.activeElement.closest('[role=option]');if(b)choose(b);}
});
document.addEventListener('pointerdown',e=>{if(open&&!popover.contains(e.target)&&!trigger.contains(e.target))close(false);});
document.addEventListener('focusin',e=>{if(open&&!popover.contains(e.target)&&!trigger.contains(e.target))close(false);});
addEventListener('resize',position);addEventListener('scroll',e=>{if(!popover.contains(e.target))position();},true);
function identity(model,{animate=false,enabled=true}={}){
const p=modelPresentation(model),name=$('model-name'),previous=name.textContent;
document.querySelectorAll('.model-name-ghost').forEach(e=>e.remove());name.getAnimations().forEach(a=>a.cancel());
name.textContent=p.codename;$('model-size').textContent=p.size;
$('model-version').textContent=p.generation;$('model-version').hidden=!p.generation;$('model-badge').hidden=!p.reasoning;
$('model-spec').textContent=[p.size,`Complete ${tokenLabel(model.complete_input_tokens)}`].filter(Boolean).join(' · ');
$('input-budget').textContent=`Complete ${model.complete_input_tokens.toLocaleString('en')}-token input per question. Nothing is truncated.`;
const many=getModels().length>1;
trigger.disabled=!enabled||!many;trigger.dataset.single=String(!many);trigger.setAttribute('aria-label',`${p.name}${p.generation?`, Decision ${p.generation}`:''}. ${many?'Switch decision model.':'Decision model.'}`);
if(animate&&!reducedMotion.matches&&previous!==p.codename){
const ghost=document.createElement('span');ghost.className='model-name-ghost';ghost.textContent=previous;ghost.setAttribute('aria-hidden','true');name.parentNode.append(ghost);
const a=ghost.animate([{opacity:1,transform:'translateY(0)'},{opacity:0,transform:'translateY(-6px)'}],{duration:170,easing:'ease-out'});a.onfinish=()=>ghost.remove();
name.animate([{opacity:0,transform:'translateY(5px)'},{opacity:1,transform:'translateY(0)'}],{duration:190,easing:'ease-out'});
}
updateStatuses();
}
return {identity,updateStatuses,close,isOpen:()=>open};
}