Xunzhuo's picture
Align Decision 1.0 Studio with the current Decision 2.0 UX
a456b58 verified
Raw History Blame Contribute Delete
44.9 kB
import { validateRequest, validateServingResult } from './contract.js?v=studio-3';
import {validateRegistry, studioExamplesForModel, exampleMode, createModelMenu, modelReadiness, modelPresentation, groupModels, orderedModels, preferredModel, statusName} from './model-menu.js?v=studio-3';
import {queuedPrediction, requestJSON} from './queue-client.js?v=studio-3';
const $ = (id) => document.getElementById(id);
const esc = (value) => String(value).replace(/[&<>"']/g, (x) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[x]));
const clone = (value) => structuredClone(value);
const display = (value) => value == null ? '' : typeof value === 'string' ? value : JSON.stringify(value, null, 2);
const pct = (p) => (p * 100).toFixed(1) + '%';
const TYPE_LABEL = {choice:'Choice', noul:'Noul', score:'Score'};
const CHEVRON = '<svg class="chevron" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="m4 6 4 4 4-4"/></svg>';
let allExamples = [], examples = [], activeExample, draft, revision = 0, status = null;
let controller = null, autoTimer, toastTimer, fleetTimer, lastResult = null, userChoseModel = false;
let models = [], selectedModel = null, modelEpoch = 0;
let contextMode='routing', statusFlight = null, statusPoll = null, statusTransportError = false;
let initRetryDelay = 1500, initWarningShown = false;
const modeWorkspaces=new Map();
const modelStatuses = new Map(), modelWorkspaces = new Map(), menuStatusVersions = new Map();
const modelInfo = () => models.find(m => m.id === selectedModel);
const modelURL = (url, model = selectedModel) => url + '?model=' + encodeURIComponent(model);
const nameOf = (model = modelInfo()) => model ? statusName(model, models) : 'the model';
// Decision 2.0 answers every question about one input in a single forward pass.
const onePass = (model = modelInfo()) => Number.parseFloat(modelPresentation(model || {}).generation) >= 2;
const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
$('run-shortcut').textContent = isMac ? '⌘ ↵' : 'Ctrl ↵';
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
$('motion-toggle').addEventListener('click', () => {
const paused = document.body.classList.toggle('motion-paused');
$('motion-toggle').setAttribute('aria-pressed', String(paused));
$('motion-toggle').setAttribute('aria-label', paused ? 'Resume animations' : 'Pause animations');
$('motion-toggle').title = paused ? 'Resume animations' : 'Pause animations';
$('motion-toggle').firstElementChild.textContent = paused ? '▷' : 'Ⅱ';
});
const chooseModel = id => {userChoseModel = true; switchModel(id).catch(e => toast(e.message));};
const modelMenu = createModelMenu({getModels:()=>models, getSelected:()=>selectedModel, getStatus:id=>modelStatuses.get(id), onSelect:chooseModel, onOpen:refreshMenuStatuses, reducedMotion});
function toast(text) { $('toast').textContent = text; $('toast').hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => $('toast').hidden = true, 4800); }
// Status regions are live: rewrite them only when the message changes.
function setRunStatus(text, kind = '') { const el = $('run-status'), className = 'run-status ' + kind; if (el.className === className && el.textContent === text) return; el.className = className; el.replaceChildren(); const led = document.createElement('span'); led.className = 'status-led'; el.append(led, document.createTextNode(text)); }
function setConnection(text, state) { const el = $('connection'), className = 'connection' + (state ? ' ' + state : ''); if (el.className === className && el.textContent === text) return; el.className = className; el.innerHTML = `<i></i><span>${esc(text)}</span>`; }
const ACTIVITY = {switching:'Connecting',submitting:'Sending',offline:'Unavailable',ready:'Ready',running:'Reading',loading:'Loading',checking:'Checking',reconnecting:'Reconnecting',queued:'Queued',complete:'Ready',busy:'Busy',error:'Error'};
function setModelActivity(phase) {
$('model-card').dataset.state = phase; $('studio-card').dataset.state = phase;
$('model-card').setAttribute('aria-busy',String(['switching','submitting','queued','running','loading','checking','reconnecting'].includes(phase)));
$('model-state').textContent = ACTIVITY[phase] || 'Unavailable';
}
function updatePass() {
document.querySelector('.pass-node').textContent = !onePass() ? 'GPU batch' : contextMode === 'batch' ? '1 pass each' : '1 pass';
}
function updateRunButton() {
const q=draft?.questions?.length||0,n=contextMode==='batch'?(draft?.contexts?.length||0):1,total=n*q,label=`Run ${total} decision${total===1?'':'s'}`;
const batchUnavailable=contextMode==='batch'&&status?.context_batch!==true;
$('run').disabled=statusTransportError||!status?.configured||!status?.loaded||status?.phase==='unavailable'||!!controller||status?.running||['running','loading'].includes(status?.phase)||batchUnavailable;
$('run').title=batchUnavailable?'Batch contexts is not ready on this model connection.':'Run the complete current request';
$('run-label').textContent=controller?`Working on ${total}…`:label;
$('scenario-summary').textContent=contextMode==='batch'?`${n} context${n===1?'':'s'} × ${q} question${q===1?'':'s'} = ${total} decisions`:contextMode==='routing'?`1 prompt · ${q} routing decision${q===1?'':'s'}`:`1 context · ${q} decision${q===1?'':'s'}`;
$('context-limit').textContent=batchUnavailable?'Connecting batch mode…':'GPU batches · Complete inputs';
}
function checkStatus() {
const epoch = modelEpoch, model = selectedModel;
if (statusFlight?.epoch === epoch && statusFlight.model === model) return statusFlight.promise;
const promise = checkStatusOnce(epoch, model).finally(() => {
if (statusFlight?.promise === promise) statusFlight = null;
});
statusFlight = {epoch, model, promise};
return promise;
}
async function checkStatusOnce(epoch, model) {
try {
const res = await fetch(modelURL('/api/status', model), {cache:'no-store'});
if (!res.ok) throw Error(); const current = await res.json();
if(epoch !== modelEpoch) return;
if(current.model !== model || current.manifest_sha256 !== modelInfo()?.manifest_sha256) throw Error('Model identity mismatch');
statusTransportError = false;
status = current; modelStatuses.set(model,current); modelMenu.updateStatuses(); updateFleet();
const readiness=modelReadiness(status), unavailable=readiness.state==='offline', connecting=readiness.state==='loading', name=nameOf();
const label = unavailable ? `${name} unavailable` : connecting ? `Connecting to ${name}…` : status.running || status.phase === 'running' ? `${name} is working` : `${name} ready${status.queued ? ' · '+status.queued+' queued' : ''}`;
setConnection(label, status.loaded ? 'connected' : unavailable ? 'offline' : 'pending');
$('offline-title').textContent = `${name} is unavailable right now.`;
$('offline-note').hidden = !unavailable; updateRunButton();
if((!controller&&!lastResult)||['switching','loading','checking','reconnecting'].includes($('model-card').dataset.state)) setRunStatus(unavailable ? `${name} unavailable · editing only` : connecting ? `Connecting to ${name}…` : status.running || status.phase === 'running' ? `${name} is working` : `${name} ready · run your questions`);
if (!controller) setModelActivity(unavailable ? 'offline' : connecting ? 'loading' : status.running || status.phase === 'running' ? 'busy' : lastResult && !$('results').classList.contains('stale') ? 'complete' : 'ready');
else if (status.backend !== 'pull_queue' && ['running','loading'].includes(status.phase)) setModelActivity(status.phase);
} catch (error) {
if(epoch !== modelEpoch) return;
statusTransportError = true;
const identityChanged = error?.message === 'Model identity mismatch';
modelStatuses.set(model, status ? {...status, loaded:false, phase:'loading'} : null);
modelMenu.updateStatuses(); updateFleet();
setConnection(identityChanged ? 'Model configuration changed · reload' : 'Connection interrupted · retrying', 'pending');
$('offline-note').hidden = true;
updateRunButton();
if (!controller) setModelActivity(identityChanged ? 'error' : 'reconnecting');
}
}
async function refreshMenuStatuses() {
// Read-only readiness discovery. Selecting a model never creates an inference job.
await Promise.allSettled(models.map(async info=>{
const version=(menuStatusVersions.get(info.id)||0)+1;menuStatusVersions.set(info.id,version);
try {
const current=await requestJSON(modelURL('/api/status',info.id),{cache:'no-store'});
if(current.model!==info.id||current.manifest_sha256!==info.manifest_sha256)throw Error('Model identity mismatch');
if(menuStatusVersions.get(info.id)===version)modelStatuses.set(info.id,current);
} catch {if(menuStatusVersions.get(info.id)===version){const previous=modelStatuses.get(info.id);modelStatuses.set(info.id,previous?{...previous,loaded:false,phase:'loading'}:null);}}
modelMenu.updateStatuses();
}));
updateFleet();
}
function scheduleFleetRefresh() {
clearTimeout(fleetTimer);
fleetTimer = setTimeout(async () => { if (!document.hidden) await refreshMenuStatuses(); scheduleFleetRefresh(); }, 60000);
}
async function loadModels() {
const result=validateRegistry(await requestJSON('/v1/models',{cache:'no-store'}));
models=result.models.map(model=>({...model,id:model.wire_id,repo_id:model.id}));
const serverDefault=result.default?models.find(model=>model.repo_id===result.default)?.id:undefined;
const kept=models.find(model=>model.id===selectedModel);
selectedModel=(kept||preferredModel(models,serverDefault)||models[0]).id;
modelMenu.identity(modelInfo(),{enabled:false});
updatePass();renderFleet();
}
// Before the first interaction, open on a ready model of the newest generation
// rather than an unavailable default. Requests never fall back to another model.
async function settleInitialModel() {
if(userChoseModel||modelReadiness(modelStatuses.get(selectedModel)).state!=='offline')return;
const ready=orderedModels(models).filter(m=>modelReadiness(modelStatuses.get(m.id)).state==='ready');
const choice=ready.find(m=>!modelPresentation(m).reasoning)||ready[0];
if(choice&&choice.id!==selectedModel)await switchModel(choice.id);
}
function saveWorkspace() {
if(draft){const saved={draft:clone(draft),text:$('state-input').value,contextMode,exampleId:activeExample.id};modelWorkspaces.set(selectedModel,saved);modeWorkspaces.set(selectedModel+'/'+contextMode,saved);}
}
function showScenario(example) { $('scenario-title').textContent = example.title; $('scenario-category').textContent = example.category; $('scenario-title').title = example.description || ''; }
function applyWorkspace(model,previous) {
const catalog=studioExamplesForModel(model,allExamples);
examples=catalog.examples;
const saved=modelWorkspaces.get(model.id);
if(saved&&examples.some(e=>e.id===saved.exampleId)) {
activeExample=examples.find(e=>e.id===saved.exampleId);draft=clone(saved.draft);
showScenario(activeExample);
contextMode=saved.contextMode||exampleMode(activeExample);$('state-input').value=saved.text;renderContextMode();renderContexts();renderExamples();renderQuestions();updateCount();dirty();
} else {
const previousExample=examples.find(e=>e.id===previous?.exampleId);
const sameMode=examples.find(e=>exampleMode(e)===(previous?.contextMode||'routing'));
loadExample(previousExample||sameMode||catalog.defaultExample);
}
}
async function switchModel(model) {
if(model===selectedModel||!models.some(m=>m.id===model))return;
saveWorkspace();const previous=modelWorkspaces.get(selectedModel);
++modelEpoch;++revision;
clearTimeout(autoTimer);
if(controller){controller.abort();controller=null;}
selectedModel=model;status=null;statusTransportError=false;lastResult=null;
$('results').classList.remove('stale');
$('result-count').textContent='—';$('results-title').textContent='Decisions';$('timing').hidden=true;$('timing').replaceChildren();
updatePass();
applyWorkspace(modelInfo(),previous);
modelMenu.identity(modelInfo(),{animate:true});
setConnection('Connecting to model','pending');
setRunStatus(`Connecting to ${nameOf()}…`);setModelActivity('switching');updateRunButton();renderFleet();
await checkStatus(); // Actual selected identity and readiness, without a cosmetic delay.
}
function renderExamples() {
$('examples').innerHTML = examples.map((e,i) => ({e,i})).filter(({e})=>exampleMode(e)===contextMode).map(({e,i}) => `<button type="button" class="example-button${e.id===activeExample?.id?' selected':''}" data-example="${i}" aria-pressed="${e.id===activeExample?.id}" title="${esc(e.description||e.category||'')}">${esc(e.title)}</button>`).join('');
const list=$('examples'),chip=list.querySelector('.selected');
if(chip&&(chip.offsetLeft<list.scrollLeft||chip.offsetLeft+chip.offsetWidth>list.scrollLeft+list.clientWidth))list.scrollLeft=chip.offsetLeft-24;
}
function loadExample(example) {
activeExample = example;
contextMode=exampleMode(example);
const states=contextMode==='batch'?example.states:[{id:'context_1',state:example.state}];
draft = {contexts:states.map(s=>({id:s.id,text:display(s.state),expanded:false})), questions: Object.entries(clone(example.questions)).map(([id,q]) => ({id,...q,expanded:false}))};
showScenario(example);
$('state-input').value = draft.contexts[0].text;
renderContextMode();renderContexts();renderExamples(); renderQuestions(); updateCount(); dirty();
}
function updateCount() { $('state-count').textContent = `${$('state-input').value.length.toLocaleString('en')} characters`; }
function renderContextMode(){
for(const mode of ['routing','single','batch'])$('context-mode-'+mode).setAttribute('aria-pressed',String(contextMode===mode));
$('single-context-editor').hidden=contextMode==='batch';$('batch-context-editor').hidden=contextMode!=='batch';
$('context-heading').textContent=contextMode==='batch'?'Contexts':contextMode==='routing'?'User prompt':'Context';$('context-count').hidden=contextMode!=='batch';
$('questions-heading').textContent=contextMode==='batch'?'Shared questions':contextMode==='routing'?'Routing decisions':'Questions';$('shared-note').hidden=contextMode!=='batch';
$('mode-description').textContent=contextMode==='batch'?'Many contexts, one set of questions.':contextMode==='routing'?'Route each request to the right model.':'One context, many decisions.';
$('workbench').dataset.mode=contextMode;
updatePass();
}
function renderContexts(){
$('context-count').textContent=draft.contexts.length;$('add-context').disabled=false;
$('context-list').innerHTML=draft.contexts.map((c,i)=>`<article class="context-item${c.expanded?' is-open':''}"><div class="context-row"><button type="button" class="context-open" data-open-context="${i}" aria-expanded="${c.expanded}" aria-controls="context-editor-${i}" aria-label="${c.expanded?'Collapse':'Edit'} context ${esc(c.id)}">${CHEVRON}<span class="context-row-id">${esc(c.id)}</span><span class="context-preview">${esc(c.text.replace(/\s+/g,' '))||'Write a context…'}</span></button><button type="button" class="icon-button" data-delete-context="${i}" aria-label="Remove context ${esc(c.id)}" ${draft.contexts.length<=1?'disabled':''}>×</button></div><div class="context-editor" id="context-editor-${i}" ${c.expanded?'':'hidden'}><div class="context-editor-head"><label class="inline-field">ID <input data-context-id="${i}" value="${esc(c.id)}" aria-label="Context ${i+1} ID" spellcheck="false"></label></div><textarea data-context-text="${i}" class="context-text" aria-label="Context ${i+1} content" spellcheck="false">${esc(c.text)}</textarea></div></article>`).join('');
updateRunButton();
}
function switchContextMode(mode){
if(!['routing','single','batch'].includes(mode))return;
if(mode===contextMode)return;saveWorkspace();const saved=modeWorkspaces.get(selectedModel+'/'+mode);
if(saved){draft=clone(saved.draft);activeExample=examples.find(e=>e.id===saved.exampleId)||activeExample;contextMode=mode;$('state-input').value=saved.text;showScenario(activeExample);renderContextMode();renderContexts();renderExamples();renderQuestions();updateCount();dirty();}
else {const example=examples.find(e=>exampleMode(e)===mode);if(example)loadExample(example);else {contextMode=mode;if(mode!=='batch'){draft.contexts=draft.contexts.slice(0,1);$('state-input').value=draft.contexts[0].text;}renderContextMode();renderContexts();renderExamples();renderQuestions();dirty();}}
}
function choicesHTML(q, index) {
if (q.type === 'noul') return `<p class="field-hint">Returns the probability of yes, from 0 to 1.</p><details class="noul-details" ${q.criteria?'open':''}><summary>Define what yes and no mean</summary>${['true','false'].map(k=>`<label class="criterion"><span class="criterion-tag">${k==='true'?'Yes':'No'}</span><input data-index="${index}" data-polarity="${k}" value="${esc(q.criteria?.[k] ? display(q.criteria[k]) : '')}" placeholder="${k==='true'?'Optional yes criterion':'Optional no criterion'}" aria-label="${k==='true'?'Yes':'No'} criterion"></label>`).join('')}</details>`;
const entries = q.type === 'choice' ? Object.entries(q.criteria) : q.criteria.map((text,i) => [String(i),text]);
return `<div class="criteria-list ${q.type}-criteria">${entries.map(([name,text],i)=>`<div class="criterion"><span class="criterion-tag">${q.type==='score'?i:String.fromCharCode(65+i%26)}</span>${q.type==='choice'?`<input class="option-name" data-index="${index}" data-option="${i}" data-field="name" value="${esc(name)}" aria-label="Option ${i+1} name">`:''}<input data-index="${index}" data-option="${i}" data-field="description" value="${esc(text===null?'':display(text))}" placeholder="${q.type==='choice'?'Description (optional)':'Describe this level'}" aria-label="${q.type==='choice'?'Option':'Level'} ${i+1} description"><button type="button" class="icon-button" data-remove-option="${i}" data-index="${index}" aria-label="Remove ${q.type==='score'?'level':'option'} ${i+1}" ${entries.length<=2?'disabled':''}>×</button></div>`).join('')}</div><div class="criteria-foot"><span>${q.type==='score'?'Ordered from lowest to highest':'Names and descriptions reach the model'}</span><button type="button" class="button-quiet small" data-add-option="${index}" ${entries.length>=(q.type==='score'?10:255)?'disabled':''}>${q.type==='score'?'Add level':'Add option'}</button></div>`;
}
function renderQuestions() {
$('question-count').textContent = draft.questions.length;
updateRunButton();
$('questions').innerHTML = draft.questions.map((q,i)=>`<article class="question-card${q.expanded?' is-open':''}">
<div class="question-head"><button type="button" class="question-toggle" data-toggle-question="${i}" aria-expanded="${!!q.expanded}" aria-controls="question-body-${i}" aria-label="${q.expanded?'Collapse':'Expand'} ${esc(q.id)}">${CHEVRON}<span class="type-tag ${q.type}">${TYPE_LABEL[q.type]}</span><span class="question-summary"><span class="question-summary-id">${esc(q.id)}</span><span class="question-summary-text">${esc(display(q.instructions))}</span></span></button><button type="button" class="icon-button" data-remove="${i}" aria-label="Remove question ${i+1}" ${draft.questions.length<=1?'disabled':''}>×</button></div>
<div class="question-body" id="question-body-${i}" ${q.expanded?'':'hidden'}>
<label class="field"><span>Question ID</span><input class="question-id" data-index="${i}" data-question-id value="${esc(q.id)}" spellcheck="false"></label>
<label class="field"><span>Instructions</span><textarea class="question-text" data-index="${i}" data-instructions rows="2" aria-label="Question ${i+1} instructions" placeholder="Required instructions">${esc(display(q.instructions))}</textarea></label>${choicesHTML(q,i)}
</div></article>`).join('');
}
function dirty() {
revision++;
if (controller) { controller.abort(); controller = null; setRunStatus('Inputs changed · previous request detached'); checkStatus(); }
if (lastResult) { $('results').classList.add('stale'); setRunStatus('Previous run · inputs have changed'); }
else renderPending();
setModelActivity(!status?.configured ? 'offline' : !status.loaded || status.phase==='loading' ? 'loading' : status.running || status.phase==='running' ? 'busy' : 'ready');
clearTimeout(autoTimer);
if ($('auto-run').checked && status?.configured) autoTimer = setTimeout(autoRun, 1000);
updateRunButton();
}
async function autoRun() { await checkStatus(); if (['running','loading'].includes(status?.phase)) { autoTimer=setTimeout(autoRun,1000); return; } run(); }
function requestBody() {
const questions = Object.create(null);
for (const q of draft.questions) {
if (!q.id.trim() || Object.hasOwn(questions, q.id)) throw Error('Every question needs a unique, nonempty ID.');
if (q.invalid) throw Error(q.invalid);
questions[q.id] = {type:q.type};
if(q.type==='noul'||(q.instructions!==null&&q.instructions!==undefined&&q.instructions!=='')) questions[q.id].instructions=q.instructions;
if (q.type!=='noul' || q.criteria && Object.keys(q.criteria).length) questions[q.id].criteria = clone(q.criteria);
}
const input=contextMode==='batch'?{states:draft.contexts.map(c=>({id:c.id,state:c.text}))}:{state:$('state-input').value};
return validateRequest({model:selectedModel,...input,questions}, selectedModel);
}
// Result rendering. `a` is null while a decision is pending, so the layout of
// every card is visible before the run and settles in place afterwards.
const shortLevel = (level) => { const text = display(level); return text.includes(':') ? text.split(':', 1)[0] : text; };
function choiceBody(q, a) {
const ids = Object.keys(q.criteria || {});
const order = a ? [...ids].sort((x, y) => a.probabilities[y] - a.probabilities[x]) : ids;
const head = a ? `<span class="decision-value">${esc(a.choice)}</span><span class="decision-meta">${pct(a.probabilities[a.choice])}</span>` : `<span class="decision-value is-empty">—</span><span class="decision-meta">${ids.length} options</span>`;
const rows = order.map((id, i) => {
const p = a ? a.probabilities[id] : 0, top = a && id === a.choice, description = display(q.criteria[id]);
return `<div class="dist-row${top ? ' is-top' : ''}" style="--p:${p};--r:${i}"><span class="label" title="${esc(description || id)}">${esc(id)}</span><span class="pct">${a ? pct(p) : '—'}</span><span class="track"><span class="fill"></span></span></div>`;
}).join('');
return `<div class="decision-answer">${head}</div><div class="dist">${rows}</div>`;
}
function noulBody(q, a) {
const p = a ? a.noul : 0, yes = a && a.noul >= 0.5;
const head = a ? `<span class="decision-value">${yes ? 'Yes' : 'No'}</span><span class="decision-meta">P(yes) ${pct(p)}</span>` : `<span class="decision-value is-empty">—</span><span class="decision-meta">Yes or no</span>`;
return `<div class="decision-answer">${head}</div><div class="gauge${a ? (yes ? ' is-yes' : ' is-no') : ''}" style="--p:${p}" role="img" aria-label="${a ? `Probability of yes ${pct(p)}` : 'Pending'}"><span class="gauge-fill"></span><span class="gauge-mid"></span></div><div class="gauge-scale" aria-hidden="true"><span>No</span><span>50%</span><span>Yes</span></div>`;
}
function scoreBody(q, a) {
const levels = q.criteria || [], n = levels.length;
const probs = levels.map((_, i) => a ? a.probabilities[String(i)] : 0);
const top = a ? probs.indexOf(Math.max(...probs)) : -1;
const head = a ? `<span class="decision-value">${a.score.toFixed(2)}<em> / ${n - 1}</em></span><span class="decision-meta">${esc(shortLevel(levels[top]))} · ${pct(probs[top])}</span>` : `<span class="decision-value is-empty">—</span><span class="decision-meta">${n} levels</span>`;
const cols = levels.map((level, i) => `<div class="level${i === top ? ' is-top' : ''}" style="--p:${probs[i]};--r:${i}" title="${esc(`${i} · ${display(level)}`)}"><span class="level-pct">${a ? pct(probs[i]) : '—'}</span><span class="level-bar"><span></span></span><span class="level-name"><b>${i}</b> ${esc(shortLevel(level))}</span></div>`).join('');
const marker = a ? `<span class="level-marker" style="--x:${((a.score + 0.5) / n * 100).toFixed(2)}%" title="Expected level ${a.score.toFixed(2)}"></span>` : '';
return `<div class="decision-answer">${head}</div><div class="levels${n > 5 ? ' is-dense' : ''}" style="--n:${n}">${cols}${marker}</div>`;
}
function decisionCard(id, q, a, i = 0) {
const body = q.type === 'noul' ? noulBody(q, a) : q.type === 'score' ? scoreBody(q, a) : choiceBody(q, a);
return `<article class="decision ${q.type}-result${a ? '' : ' is-pending'}" style="--i:${i}" aria-label="${esc(id)} ${a ? 'decision' : '(pending)'}"><div class="decision-head"><span class="decision-id" title="${esc(display(q.instructions))}">${esc(id)}</span><span class="decision-kind">${TYPE_LABEL[q.type]}</span></div><p class="decision-question">${esc(display(q.instructions))}</p>${body}</article>`;
}
function renderPending() {
if (!draft) return;
const n = contextMode === 'batch' ? draft.contexts.length : 1;
const cards = draft.questions.map((q, i) => decisionCard(q.id || `question_${i + 1}`, q, null, i)).join('');
const lede = n > 1 ? `Each of the ${n} contexts receives these ${draft.questions.length} questions.` : '';
$('results').innerHTML = `${lede ? `<p class="results-lede">${lede}</p>` : ''}<div class="decisions is-pending${draft.questions.length === 1 ? ' single-decision' : ''}">${cards}</div>`;
}
function compactDecision(a,q){
if(a.type==='noul'){const yes=a.noul>=.5;return `<span class="cell-value">${yes?'Yes':'No'}</span><span class="cell-meta">P(yes) ${pct(a.noul)}</span>`;}
if(a.type==='score'){const best=Object.entries(a.probabilities).reduce((x,y)=>y[1]>x[1]?y:x);return `<span class="cell-value">${a.score.toFixed(2)}<em> / ${q.criteria.length-1}</em></span><span class="cell-meta">Level ${esc(best[0])} · ${pct(best[1])}</span>`;}
return `<span class="cell-value">${esc(a.choice)}</span><span class="cell-meta">${pct(a.probabilities[a.choice])}</span>`;
}
function renderResult(result,payload,backend,elapsed){
validateServingResult(result,payload,modelInfo().repo_id,backend);setModelActivity('complete');lastResult=result;$('results').classList.remove('stale');
const qids=Object.keys(payload.questions),isBatch=Array.isArray(payload.states),rows=isBatch?result.results:[{id:'context',answers:result.answers,usage:result.usage}],count=rows.length*qids.length;
$('result-count').textContent=count;$('results-title').textContent='Decisions';
if(isBatch){
$('results').innerHTML=`<p class="results-lede">Shared questions, separate decisions. Open a row for every probability.</p><div class="result-table-scroll" tabindex="0" role="region" aria-label="Context decision table"><table class="result-table"><thead><tr><th scope="col">Context</th>${qids.map(id=>`<th scope="col" title="${esc(display(payload.questions[id].instructions))}"><span class="th-id">${esc(id)}</span><span class="th-kind">${TYPE_LABEL[payload.questions[id].type]}</span></th>`).join('')}</tr></thead><tbody>${rows.map((r,i)=>`<tr class="result-row" data-result-row="${i}"><th scope="row"><button type="button" class="row-toggle" aria-expanded="false" aria-controls="result-detail-${i}" aria-label="Show probabilities for ${esc(r.id)}">${CHEVRON}<span>${esc(r.id)}</span></button></th>${qids.map(id=>`<td class="decision-cell ${payload.questions[id].type}">${compactDecision(r.answers[id],payload.questions[id])}</td>`).join('')}</tr><tr id="result-detail-${i}" class="result-detail-row" hidden><td colspan="${qids.length+1}"><div class="decisions detail-grid">${qids.map((id,j)=>decisionCard(id,payload.questions[id],r.answers[id],j)).join('')}</div></td></tr>`).join('')}</tbody></table></div>`;
}else{
$('results').innerHTML=`<div class="decisions is-settled${qids.length===1?' single-decision':''}">${qids.map((id,i)=>decisionCard(id,payload.questions[id],result.answers[id],i)).join('')}</div>`;
}
setRunStatus(`${nameOf()} · ${count} decision${count===1?'':'s'} ready`);
$('timing').hidden=true;
$('timing').replaceChildren();
}
async function nativePrediction(payload, current) {
return requestJSON('/api/evaluate', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload), signal:current.signal});
}
async function run() {
if(controller || !status?.configured || !status?.loaded || status.running || ['running','loading'].includes(status?.phase)) { if(!status?.configured) $('setup-dialog').showModal(); return; }
if(contextMode==='batch'&&status.context_batch!==true){toast('Batch contexts is not ready on this model connection.');return;}
let payload; try {payload=requestBody();}catch(e){toast(e.message);return;}
clearTimeout(toastTimer);$('toast').hidden=true;
const version=revision, epoch=modelEpoch, backend=status.backend, info=modelInfo(), current=new AbortController(); controller=current; updateRunButton(); clearTimeout(autoTimer); const begin=performance.now();
setRunStatus(status.backend==='pull_queue' ? 'Sending your request…' : status.loaded ? 'Reading your context…' : 'Loading the native model…','running');
setModelActivity(status.backend==='pull_queue' ? 'submitting' : status.loaded ? 'running' : 'loading');
try {
const result = backend === 'pull_queue' ? await queuedPrediction(payload, current, info, {
onProgress: phase => {setRunStatus(phase === 'queued' ? 'Waiting for the model…' : 'Reading your context…', 'running');setModelActivity(phase);},
validateCompletion: result => validateServingResult(result,payload,info.repo_id,backend),
}) : await nativePrediction(payload, current);
if(version!==revision || epoch!==modelEpoch || current.signal.aborted) return;
if(payload.model!==selectedModel) throw Error('The selected model changed.');
renderResult(result,payload,backend,performance.now()-begin);
} catch(e) {if(e.name!=='AbortError' && version===revision){setModelActivity('error');setRunStatus('Could not complete this request','error');toast(e.message); if(!lastResult)$('results').innerHTML=`<div class="error-message" role="alert"><b>No result.</b> ${esc(e.message)}</div>`;}}
finally{if(controller===current)controller=null;updateRunButton();if(epoch===modelEpoch)await checkStatus();}
}
// Family lineup and model table: views of the same registry.
const sizeValue = (label) => { const m = /^(\d+(?:\.\d+)?)\s*([BM])$/i.exec(label || ''); return m ? Number(m[1]) / (m[2].toUpperCase() === 'M' ? 1000 : 1) : null; };
const tokenText = (n) => n % 1024 === 0 ? `${n / 1024}K` : n.toLocaleString('en');
function readinessOf(model) { return modelReadiness(modelStatuses.get(model.id)); }
function renderHero() {
const newest = groupModels(models)[0];
const generation = document.body.dataset.generation || modelPresentation(newest?.models[0] || {}).generation || '2.0';
const title = `Decision <span class="grad">${esc(generation)}</span>`;
if ($('hero-title').innerHTML !== title) $('hero-title').innerHTML = title;
$('hero-eyebrow').textContent = 'INTRODUCING';
$('hero-sub').textContent = 'Open Foundation Decision Models';
$('hero-tag').textContent = Number.parseFloat(generation) >= 2 ? 'One pass. Many decisions.' : 'One input. Many decisions.';
const sizes = newest ? newest.models.filter(m => !modelPresentation(m).reasoning).map(m => modelPresentation(m).size) : [];
$('hero-sizes').textContent = [...new Set(sizes)].sort((a, b) => (sizeValue(a) ?? 0) - (sizeValue(b) ?? 0)).join(' · ');
}
function renderLineup() {
const newest = groupModels(models)[0];
const base = newest ? newest.models.filter(m => !modelPresentation(m).reasoning) : [];
if (base.length < 2) { $('lineup').hidden = true; return; }
const sizes = base.map(m => sizeValue(modelPresentation(m).size));
const known = sizes.filter(Number.isFinite), lo = Math.log(Math.min(...known)), hi = Math.log(Math.max(...known));
const selected = modelInfo(), sp = selected && modelPresentation(selected);
$('lineup-title').textContent = newest.label;
const first = modelPresentation(base.at(-1)).size, last = modelPresentation(base[0]).size;
const distinct = new Set(base.map(m => modelPresentation(m).size)).size;
$('lineup-meta').textContent = `${base.length} ${distinct === base.length ? 'sizes' : 'models'}${known.length === base.length ? ` · ${[first, last].sort((x, y) => sizeValue(x) - sizeValue(y)).join(' – ')}` : ''}`;
const ordered = base.map((m, i) => ({m, size: sizes[i]})).sort((x, y) => (x.size ?? 0) - (y.size ?? 0));
$('lineup-stage').innerHTML = ordered.map(({m, size}) => {
const p = modelPresentation(m), ratio = Number.isFinite(size) && hi > lo ? (Math.log(size) - lo) / (hi - lo) : 0.5;
const active = sp && sp.generation === p.generation && sp.codename === p.codename && sp.size === p.size;
const r = readinessOf(m);
return `<button type="button" class="size-option" data-model="${esc(m.id)}" aria-pressed="${!!active}" data-readiness="${r.state}" style="--d:${ratio.toFixed(3)}" aria-label="${esc(p.name)}, ${r.label}"><span class="orb-wrap"><span class="orb"></span></span><span class="size-name">${esc(p.codename)}</span><span class="size-label"><span class="status-dot" aria-hidden="true"></span>${esc(p.size)}</span></button>`;
}).join('');
const reasoning = newest.models.length - base.length;
$('lineup-foot').textContent = reasoning ? `Plus ${reasoning} Reasoning variant${reasoning === 1 ? '' : 's'}. One API for every size.` : 'One API for every size.';
const facts = selected ? [sp.description, `${tokenText(selected.complete_input_tokens)} complete input`, sp.releaseLabel && `Released ${sp.releaseLabel}`].filter(Boolean) : [];
$('lineup-detail').innerHTML = selected ? `<b>${esc(sp.name)}${sp.generation ? ` <span class="lineup-generation">${esc(sp.generation)}</span>` : ''}</b><span>${esc(facts.join(' · '))}</span>` : '';
$('lineup').hidden = false;
}
function renderModelTable() {
const body = $('model-table').tBodies[0];
body.innerHTML = groupModels(models).map(group => `<tr class="group-row"><th colspan="6" scope="colgroup"><span>${esc(group.label)}</span><small>${group.models.length} model${group.models.length === 1 ? '' : 's'}</small></th></tr>` + group.models.map(m => {
const p = modelPresentation(m), r = readinessOf(m), current = m.id === selectedModel;
const compact = [p.description, `${tokenText(m.complete_input_tokens)} input`, p.releaseLabel].filter(Boolean);
return `<tr class="model-row${current ? ' is-selected' : ''}"><th scope="row"><span class="model-cell"><span class="codename">${esc(p.codename)}</span><span class="compact-only size-inline" aria-hidden="true">${esc(p.size)}</span>${p.reasoning ? '<span class="badge badge-reasoning">Reasoning</span>' : ''}</span>${p.description ? `<span class="model-desc wide-only">${esc(p.description)}</span>` : ''}<span class="model-desc compact-only" aria-hidden="true">${esc(compact.join(' · '))}</span></th><td class="num">${esc(p.size || '—')}</td><td class="num">${tokenText(m.complete_input_tokens)} tokens</td><td class="num">${esc(p.releaseLabel || '—')}</td><td><span class="readiness" data-readiness-for="${esc(m.id)}" data-readiness="${r.state}"><span class="status-dot" aria-hidden="true"></span>${r.label}</span></td><td class="row-actions"><button type="button" class="button-quiet small" data-use-model="${esc(m.id)}" aria-pressed="${current}">${current ? 'In Studio' : 'Try'}</button><a class="text-link" href="https://huggingface.co/${esc(m.repo_id)}" target="_blank" rel="noopener" aria-label="${esc(p.name)} on Hugging Face">Hub <span aria-hidden="true">↗</span></a></td></tr>`;
}).join('')).join('');
}
// Re-rendering replaces buttons, so keep keyboard focus on the same model.
function keepFocus(container, render) {
const active = document.activeElement, inside = container.contains(active);
const attr = inside && active.dataset.model ? 'data-model' : inside && active.dataset.useModel ? 'data-use-model' : null;
const key = attr && active.getAttribute(attr);
render();
if (key) container.querySelector(`[${attr}="${CSS.escape(key)}"]`)?.focus({preventScroll: true});
}
function renderFleet() { if (!models.length) return; renderHero(); keepFocus($('lineup'), renderLineup); keepFocus($('model-table'), renderModelTable); }
function updateFleet() {
for (const button of $('lineup-stage').querySelectorAll('[data-model]')) {
const model = models.find(m => m.id === button.dataset.model); if (!model) continue;
const r = readinessOf(model); button.dataset.readiness = r.state; button.setAttribute('aria-label', `${modelPresentation(model).name}, ${r.label}`);
}
for (const cell of $('model-table').querySelectorAll('[data-readiness-for]')) {
const model = models.find(m => m.id === cell.dataset.readinessFor); if (!model) continue;
const r = readinessOf(model); cell.dataset.readiness = r.state; cell.lastChild.textContent = r.label;
}
}
$('examples').addEventListener('click',e=>{const b=e.target.closest('[data-example]');if(b)loadExample(examples[Number(b.dataset.example)]);});
$('reset').onclick=()=>loadExample(activeExample);
$('context-mode-routing').onclick=()=>switchContextMode('routing');$('context-mode-single').onclick=()=>switchContextMode('single');$('context-mode-batch').onclick=()=>switchContextMode('batch');
$('shared-toggle').onclick=()=>{const closed=$('shared-questions').hidden;$('shared-questions').hidden=!closed;$('shared-toggle').setAttribute('aria-expanded',String(closed));};
$('add-context').onclick=()=>{let n=1;while(draft.contexts.some(c=>c.id==='context_'+n))n++;draft.contexts.push({id:'context_'+n,text:'',expanded:true});renderContexts();dirty();$('context-list').querySelector(`[data-context-text="${draft.contexts.length-1}"]`).focus();};
$('context-list').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;
if(b.dataset.openContext!==undefined){const i=Number(b.dataset.openContext);draft.contexts[i].expanded=!draft.contexts[i].expanded;renderContexts();$('context-list').querySelector(`[data-open-context="${i}"]`).focus();}
else if(b.dataset.deleteContext!==undefined){if(draft.contexts.length<=1)return;const i=Number(b.dataset.deleteContext);draft.contexts.splice(i,1);renderContexts();dirty();$('context-list').querySelector(`[data-open-context="${Math.min(i,draft.contexts.length-1)}"]`).focus();}
});
$('context-list').addEventListener('input',e=>{const el=e.target;
if(el.dataset.contextText!==undefined)draft.contexts[Number(el.dataset.contextText)].text=el.value;
else if(el.dataset.contextId!==undefined){const c=draft.contexts[Number(el.dataset.contextId)];c.id=el.value;el.closest('.context-item').querySelector('.context-row-id').textContent=el.value;}
else return;dirty();
});
function toggleResultRow(row){const detail=$('result-detail-'+row.dataset.resultRow),toggle=row.querySelector('.row-toggle'),open=detail.hidden;detail.hidden=!open;toggle.setAttribute('aria-expanded',String(open));}
$('results').addEventListener('click',e=>{const row=e.target.closest('[data-result-row]');if(row)toggleResultRow(row);});
$('state-input').addEventListener('input',()=>{draft.contexts[0].text=$('state-input').value;updateCount();dirty();});
$('questions').addEventListener('input',e=>{
const el=e.target,index=Number(el.dataset.index),q=draft.questions[index];if(!q)return;
if(el.hasAttribute('data-question-id')){q.id=el.value;el.closest('.question-card').querySelector('.question-summary-id').textContent=el.value||'Untitled question';}
else if(el.hasAttribute('data-instructions')){q.instructions=el.value;el.closest('.question-card').querySelector('.question-summary-text').textContent=el.value;}
else if(el.dataset.polarity){q.criteria ||= {};if(el.value)q.criteria[el.dataset.polarity]=el.value;else delete q.criteria[el.dataset.polarity];}
else if(el.dataset.option!==undefined){const i=Number(el.dataset.option);if(q.type==='score')q.criteria[i]=el.value;else{const entries=Object.entries(q.criteria);if(el.dataset.field==='name'){entries[i][0]=el.value;if(new Set(entries.map(x=>x[0])).size!==entries.length){q.invalid='Choice option names must be unique.';dirty();return;}q.invalid=null;}else entries[i][1]=el.value||null;q.criteria=Object.fromEntries(entries);}}
dirty();
});
$('questions').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;
if(b.dataset.toggleQuestion!==undefined){const i=Number(b.dataset.toggleQuestion);draft.questions[i].expanded=!draft.questions[i].expanded;renderQuestions();$('questions').querySelector(`[data-toggle-question="${i}"]`).focus();return;}
if(b.dataset.remove!==undefined){draft.questions.splice(Number(b.dataset.remove),1);renderQuestions();dirty();}
if(b.dataset.addOption!==undefined){const q=draft.questions[Number(b.dataset.addOption)];if(q.type==='score')q.criteria.push('Describe this level');else{let i=Object.keys(q.criteria).length+1;while(Object.hasOwn(q.criteria,'Option '+i))i++;q.criteria['Option '+i]=null;}renderQuestions();dirty();}
if(b.dataset.removeOption!==undefined){const q=draft.questions[Number(b.dataset.index)],i=Number(b.dataset.removeOption);if(q.type==='score')q.criteria.splice(i,1);else{const entries=Object.entries(q.criteria);entries.splice(i,1);q.criteria=Object.fromEntries(entries);}renderQuestions();dirty();}
});
$('add-toggle').onclick=()=>{const open=$('add-options').hidden;$('add-options').hidden=!open;$('add-toggle').setAttribute('aria-expanded',String(open));if(open)$('add-options').querySelector('button').focus();};
$('add-options').onclick=e=>{const button=e.target.closest('[data-add]');if(!button)return;const type=button.dataset.add;let i=draft.questions.length+1;while(draft.questions.some(q=>q.id==='question_'+i))i++;const q={id:'question_'+i,type,expanded:true,instructions:type==='choice'?'Which option best fits the context?':type==='noul'?'Is the statement supported by the context?':'How would you rate the context using these levels?'};if(type==='choice')q.criteria={'Option A':'Describe the first option','Option B':'Describe the second option'};if(type==='score')q.criteria=['Low','Medium','High'];draft.questions.push(q);$('add-options').hidden=true;$('add-toggle').setAttribute('aria-expanded','false');renderQuestions();const added=$('questions').lastElementChild;added.scrollIntoView({block:'nearest'});added.querySelector('.question-id').focus({preventScroll:true});dirty();};
document.addEventListener('pointerdown',e=>{if(!$('add-options').hidden&&!e.target.closest('.add-menu')){$('add-options').hidden=true;$('add-toggle').setAttribute('aria-expanded','false');}});
$('run').onclick=run;$('auto-run').onchange=()=>{if($('auto-run').checked)toast('Auto-run sends edits after you pause. A detached request may still finish on the server.');dirty();};
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!$('add-options').hidden){$('add-options').hidden=true;$('add-toggle').setAttribute('aria-expanded','false');$('add-toggle').focus();}if((e.metaKey||e.ctrlKey)&&e.key==='Enter'&&!document.querySelector('dialog[open]')){e.preventDefault();if(!$('run').disabled)run();}});
$('lineup-stage').addEventListener('click',e=>{const b=e.target.closest('[data-model]');if(b){chooseModel(b.dataset.model);$('studio').scrollIntoView({behavior:reducedMotion.matches?'auto':'smooth',block:'start'});}});
$('model-table').addEventListener('click',e=>{const b=e.target.closest('[data-use-model]');if(!b)return;chooseModel(b.dataset.useModel);$('studio').scrollIntoView({behavior:reducedMotion.matches?'auto':'smooth',block:'start'});});
$('setup-help').onclick=()=>$('setup-dialog').showModal();$('close-setup').onclick=()=>$('setup-dialog').close();
for(const dialog of document.querySelectorAll('dialog'))dialog.addEventListener('click',e=>{if(e.target===dialog){const r=dialog.getBoundingClientRect();if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)dialog.close();}});
async function init(){try{
await loadModels();allExamples=await requestJSON('/api/examples',{cache:'no-store'});
for(const model of models)studioExamplesForModel(model,allExamples);
applyWorkspace(modelInfo());modelMenu.identity(modelInfo());
await checkStatus();
const poll=async()=>{await checkStatus();statusPoll=setTimeout(poll,3000);};
statusPoll=setTimeout(poll,3000);
await refreshMenuStatuses();await settleInitialModel();scheduleFleetRefresh();
}catch(e){
if(!initWarningShown)toast(e.message||'Connecting to Studio…');
initWarningShown=true;
setConnection('Connecting to Studio…','pending');
setModelActivity('checking');
setTimeout(init,initRetryDelay);
initRetryDelay=Math.min(15000,Math.round(initRetryDelay*1.6));
}}
init();